ZineCore2 is a Dublin Core–based metadata application profile for zines, designed to be practical for zinesters, community libraries, and union catalogs. This guide explains how to implement ZineCore2 in a typical stack:
- Django (canonical catalog API)
- Typesense (search index)
- Nuxt / TypeScript (frontend)
- JSON / JSON-LD for interchange
ZineCore2 consists of four main artifacts:
-
DCAP narrative (
docs/zinecore2-profile.md)
Human-readable definition of elements, definitions, and examples. -
DC TAP table (
profiles/zinecore2-tap.csv)
Machine-readable application profile: shape, properties, cardinality, and value constraints. -
JSON-LD context (
contexts/zinecore2-context.jsonld)
Maps JSON keys (e.g.title,series_title) to IRIs (e.g.dcterms:title,dcterms:isPartOf). -
JSON Schema (
schemas/zinecore2.schema.json)
Validates JSON records and drives code generation (TypeScript types, forms, etc.).
Everything else (Django models, serializers, Typesense schemas, UI forms) should be treated as implementation details that conform to these artifacts.
Create a Zine model that mirrors the ZineCore2 fields. For example:
class Zine(models.Model):
id = models.CharField(primary_key=True, max_length=64)
title = models.CharField(max_length=512)
series_title = ArrayField(models.CharField(max_length=255), blank=True, default=list)
issue_designation = models.CharField(max_length=255, blank=True, null=True)
edition_statement = ArrayField(models.CharField(max_length=255), blank=True, default=list)
alternative_title = ArrayField(models.CharField(max_length=255), blank=True, default=list)
creator = ArrayField(models.CharField(max_length=255))
contributor = ArrayField(models.CharField(max_length=255), blank=True, default=list)
subject = ArrayField(models.CharField(max_length=255))
genre = ArrayField(models.CharField(max_length=255))
abstract = models.TextField(blank=True)
table_of_contents = models.TextField(blank=True)
public_notes = ArrayField(models.TextField(), blank=True, default=list)
publisher = ArrayField(models.CharField(max_length=255), blank=True, default=list)
date = ArrayField(models.CharField(max_length=64)) # allow "Spring 2010", "c. 1996"
physical_dimensions = models.CharField(max_length=255, blank=True)
number_of_pages = models.CharField(max_length=64, blank=True)
format = ArrayField(models.CharField(max_length=255), blank=True, default=list)
binding_features = ArrayField(models.CharField(max_length=255), blank=True, default=list)
language = ArrayField(models.CharField(max_length=16))
place_of_publication = ArrayField(models.CharField(max_length=255), blank=True, default=list)
coverage = ArrayField(models.CharField(max_length=255), blank=True, default=list)
source = ArrayField(models.CharField(max_length=255), blank=True, default=list)
relation = ArrayField(models.CharField(max_length=255), blank=True, default=list)
rights = ArrayField(models.CharField(max_length=255))
identifier = ArrayField(models.CharField(max_length=255), blank=True, default=list)You can also normalize creators, subjects, and genres into separate tables and use M2M relations if you want authority control.
Use DRF serializers that:
- Match the ZineCore2 JSON naming and structure (arrays, nulls).
- Optionally validate against the JSON Schema using
jsonschemaorajvon the backend.
class ZineSerializer(serializers.ModelSerializer):
class Meta:
model = Zine
fields = "__all__"For stricter validation, you can plug the JSON Schema into a custom validator before saving.
- Import from CSV/MARC: map incoming fields to ZineCore2 keys via a crosswalk.
- Export:
/api/zines/:id→ canonical ZineCore2 JSON (schema-conformant).- Optionally
/api/zines/:id.jsonld→ same JSON plus@contextand@idfor Linked Data.
Create a Typesense zines collection that indexes key ZineCore2 fields:
title(string)creator(string[])series_title(string[])issue_designation(string)subject(string[])genre(string[])date(string[])language(string[])place_of_publication(string[])rights(string[])
Store the full ZineCore2 JSON either:
- As a
zinenested object/field (if supported), or - As your ground truth in Django and just send a subset into Typesense.
On reindex:
- Fetch canonical JSON from Django.
- Validate against
zinecore2.schema.json. - Transform into a Typesense document and upsert.
Use the ZineCore2 TypeScript interface:
import type { ZineCore2Zine } from '~/types/zinecore2';
async function fetchZine(id: string): Promise<ZineCore2Zine> {
const { data } = await useFetch(`/api/zines/${id}`);
return data.value as ZineCore2Zine;
}All components dealing with zine records can now be fully typed.
For create/edit forms:
- Base the form schema on the JSON Schema:
- Required fields: mark as required in UI.
- Arrays: render tag/“chips” inputs.
subject,genre,rights: populate from your vocab picklists (Anchor-based subjects, ZineCore2 genres, rights statements).
- Enforce simple constraints client-side:
languagestring pattern for ISO codes.- At least one
creator,subject,genre,date,language,rights.
You can also generate a simple form schema from the JSON Schema with a library, or hand-write it.
Use a consistent pattern when rendering ZineCore2 data:
- Title + series/issue:
Mutate Zine #3 (Mutate Zine, No. 3)whenseries_titleandissue_designationpresent.
- Subline:
Perzine · 2009 · English · Portland, OR, USA.
- Badges:
rightsas license/rights badges.genreas chips.subjectas clickable filters.
ZineCore2 leaves vocabularies open but RECOMMENDS:
- Subjects: Anchor Archive–inspired subject thesaurus (flattened for your use).
- Genres: Short controlled list (perzine, fanzine, art zine, etc.).
- Rights: Finite list including “All rights reserved”, “Anti-copyright”, “Copyleft”, “Please copy”, and CC licenses.
In your system:
- Store vocabularies in their own tables (or JSON files) and reference them in UI.
- Use
valueConstraintlabels from the DC TAP (AnchorArchiveSubjects,ZineCore2Genres, etc.) as keys to wire these together.
To claim ZineCore2 conformance:
-
Record structure
- Every zine issue record MUST validate against
zinecore2.schema.json.
- Every zine issue record MUST validate against
-
Required fields
- Every record MUST include:
title,creator,subject,genre,date,language,rights.
- Every record MUST include:
-
Semantics
series_titleandissue_designationSHOULD be used for serial zines.- Each issue SHOULD be a separate record; series-level discovery is built by grouping on
series_titleandissue_designation.
-
Interchange
- Systems exchanging zine metadata SHOULD use ZineCore2 JSON (optionally JSON‑LD with the provided context).
- Add ZineCore2 JSON Schema, TAP CSV, and JSON‑LD context to your repo.
- Align your Django
Zinemodel and serializers with the ZineCore2 fields. - Use the JSON Schema to validate data at ingest and before export.
- Index key fields into Typesense for search/browse.
- Use the TypeScript interface in Nuxt for type-safe API access and forms.
- Document in your README that your API/web app is ZineCore2-compliant.
ZineCore2 then becomes the stable contract between creators, zine libraries, and your software: everyone speaks the same shape, regardless of backend details.