Tutorials

Building a Zine Catalog

Creating a complete cataloging system using all four ZineCore2 profiles

This tutorial guides you through building a complete zine cataloging system from scratch. You'll learn how to plan your catalog, set up the structure, import data, and maintain it over time.

What You'll Build

A complete cataloging system that tracks:

  • Zines (the intellectual works)
  • Creators (people, collectives, organizations)
  • Holdings (physical copies you own)
  • Repository (your collection itself)

Our Scenario

You're starting a community zine library called "Radical Reads" in Chicago. You have:

  • 50 zines to catalog
  • A mix of perzines, political zines, and art zines
  • Multiple copies of some zines
  • A mix of creators (individuals and collectives)
  • Plans to lend zines and allow photocopying

Phase 1: Planning

Step 1: Define Your Repository

First, document your library itself:

{
  "repo_id": "radical-reads-chicago",
  "repository_name": "Radical Reads",
  "repository_kind": "Library",
  "description": "Community-run lending library focused on radical and DIY zines, with emphasis on Chicago voices and social justice themes.",
  "location": {
    "city": "Chicago",
    "region": "Illinois",
    "country": "United States"
  },
  "website": ["https://radicalreads.example.org"],
  "email": "[email protected]",
  "specializations": [
    "Chicago zine culture",
    "Social justice zines",
    "DIY and punk zines",
    "Feminist and queer zines"
  ]
}

Save this as repository.json.

Step 2: Choose Your ID Schemes

Decide how you'll assign IDs:

Repository IDs:

  • One repository: radical-reads-chicago

Zine IDs:

  • Pattern: zine-[number] (e.g., zine-001, zine-002)
  • Alternative: [creator-lastname]-[title-slug] (e.g., chen-mental-health)

Agent IDs:

  • Pattern: agent-[number] or [name-slug] (e.g., agent-001 or riley-chen)

Holding IDs:

  • Pattern: hold-[zine-id]-[copy-num] (e.g., hold-001-1, hold-001-2)
  • Or use barcodes if you have them

Step 3: Create a Cataloging Workflow

  1. Examine the zine
  2. Create or find creator record (AgentCore2)
  3. Create zine record (ZineCore2)
  4. Create holding record for your copy (HoldingCore2)
  5. Validate all records
  6. Add to your system

Phase 2: Creating Records

Example 1: Simple Perzine

Physical zine in hand: "Bike Dreams" by Alex Gomez, 2023, 20 pages, freely duplicatable

Step 1: Creator

{
  "agent_id": "alex-gomez",
  "display_name": "Alex Gomez",
  "agent_kind": "Person",
  "location": "Chicago, Illinois, United States"
}

Save as agents/alex-gomez.json.

Step 2: Zine

{
  "zine_id": "zine-001",
  "title": "Bike Dreams",
  "creator": ["alex-gomez"],
  "subject": ["travel", "diy"],
  "format": "print",
  "date": "2023",
  "genre": ["perzine"],
  "abstract": "Personal zine about bike touring through the Midwest, with tips for budget travel and bike maintenance.",
  "number_of_pages": 20,
  "physical_dimensions": "5.5 x 8.5 inches",
  "binding_features": ["stapled"],
  "language": ["eng"],
  "place_of_publication": "Chicago, Illinois, United States",
  "rights": "freely-duplicatable"
}

Save as zines/zine-001.json.

Step 3: Holding

{
  "holding_id": "hold-001-1",
  "zine_id": "zine-001",
  "repository_id": "radical-reads-chicago",
  "call_number": "TRAVEL-001",
  "copy_number": "1",
  "condition_note": "Excellent condition",
  "location_details": "Cabinet A, Shelf 2",
  "access_status": "Loanable",
  "distro_status": "Can duplicate"
}

Save as holdings/hold-001-1.json.

Example 2: Collective Zine

Zine details: "Our Voices" by Chicago Zine Collective, 2024, compilation of 10 contributors

Step 1: Creator (Collective)

{
  "agent_id": "chicago-zine-collective",
  "display_name": "Chicago Zine Collective",
  "agent_kind": "Collective",
  "biography": "Volunteer-run collective that creates compilation zines featuring Chicago zinesters. Active since 2020.",
  "location": "Chicago, Illinois, United States",
  "active_dates": "2020-present",
  "website": ["https://chicagozines.example.org"]
}

Step 2: Zine

{
  "zine_id": "zine-002",
  "title": "Our Voices",
  "series_title": ["Chicago Zine Collective Annual"],
  "issue_designation": "Volume 4",
  "creator": ["chicago-zine-collective"],
  "subject": ["art", "poetry", "activism"],
  "format": "print",
  "date": "2024",
  "genre": ["compilation-zine", "literary-zine"],
  "abstract": "Annual compilation zine featuring writing, art, and comics from 10 Chicago-based zinesters.",
  "number_of_pages": 48,
  "physical_dimensions": "8.5 x 11 inches",
  "binding_features": ["saddle-stitched"],
  "language": ["eng"],
  "place_of_publication": "Chicago, Illinois, United States",
  "publisher": ["chicago-zine-collective"],
  "rights": "cc-by-nc-sa",
  "table_of_contents": [
    "Introduction by the Collective",
    "Poetry by 5 contributors",
    "Comics by 3 contributors",
    "Essays by 2 contributors"
  ]
}

Step 3: Holding

{
  "holding_id": "hold-002-1",
  "zine_id": "zine-002",
  "repository_id": "radical-reads-chicago",
  "call_number": "COMP-001",
  "copy_number": "1 of 2",
  "condition_note": "Excellent condition, brand new",
  "location_details": "New arrivals shelf",
  "access_status": "Loanable",
  "distro_status": "Can duplicate",
  "public_notes": "Donated by the collective, March 2024"
}

Phase 3: Organizing Your Files

Create a directory structure:

radical-reads-catalog/
├── repository.json
├── agents/
│   ├── alex-gomez.json
│   ├── chicago-zine-collective.json
│   └── ...
├── zines/
│   ├── zine-001.json
│   ├── zine-002.json
│   └── ...
└── holdings/
    ├── hold-001-1.json
    ├── hold-002-1.json
    └── ...

Phase 4: Bulk Import Preparation

For your 50 zines, you might start with a spreadsheet:

Example Spreadsheet Columns

zine_idtitlecreator_namedatesubjectgenrepagesrightsconditionlocation
zine-001Bike DreamsAlex Gomez2023travel;diyperzine20freely-duplicatableexcellentCabinet A
zine-002Our VoicesChicago Zine Collective2024art;poetry;activismcompilation-zine;literary-zine48cc-by-nc-saexcellentNew arrivals

Convert to JSON

Write a script to convert CSV to JSON:

import csv
import json

# Read spreadsheet
with open('zines.csv', 'r') as f:
    reader = csv.DictReader(f)
    zines = list(reader)

# Convert each row to ZineCore2 JSON
for row in zines:
    zine_record = {
        "zine_id": row['zine_id'],
        "title": row['title'],
        "creator": [row['creator_name']],  # Will need to match to agent_id later
        "subject": row['subject'].split(';'),
        "genre": row['genre'].split(';'),
        "format": "print",
        "date": row['date'],
        "number_of_pages": int(row['pages']),
        "rights": row['rights']
    }

    # Save to file
    with open(f"zines/{row['zine_id']}.json", 'w') as outfile:
        json.dump(zine_record, outfile, indent=2)

    # Also create holding record
    holding_record = {
        "holding_id": f"hold-{row['zine_id']}-1",
        "zine_id": row['zine_id'],
        "repository_id": "radical-reads-chicago",
        "call_number": row['zine_id'].upper(),
        "condition_note": row['condition'],
        "location_details": row['location'],
        "access_status": "Loanable",
        "distro_status": "Can duplicate"
    }

    with open(f"holdings/hold-{row['zine_id']}-1.json", 'w') as outfile:
        json.dump(holding_record, outfile, indent=2)

Phase 5: Using the Reference Implementation

Setup Django Server

Follow the installation guide:

# Clone repository
git clone https://github.com/zpublishing/zinecore2-api
cd zinecore2-api

# Setup environment
./onboarding.sh

# Start server
./start.sh

Import Your Records via API

import requests
import json
import glob

API_BASE = "http://localhost:8000/api"
TOKEN = "your-auth-token-here"
headers = {"Authorization": f"Token {TOKEN}"}

# 1. Create repository
with open('repository.json') as f:
    repo = json.load(f)
    response = requests.post(f"{API_BASE}/repositories/", json=repo, headers=headers)
    print(f"Created repository: {response.status_code}")

# 2. Create agents
for agent_file in glob.glob('agents/*.json'):
    with open(agent_file) as f:
        agent = json.load(f)
        response = requests.post(f"{API_BASE}/agents/", json=agent, headers=headers)
        print(f"Created agent {agent['agent_id']}: {response.status_code}")

# 3. Create zines
for zine_file in glob.glob('zines/*.json'):
    with open(zine_file) as f:
        zine = json.load(f)
        response = requests.post(f"{API_BASE}/zines/", json=zine, headers=headers)
        print(f"Created zine {zine['zine_id']}: {response.status_code}")

# 4. Create holdings
for holding_file in glob.glob('holdings/*.json'):
    with open(holding_file) as f:
        holding = json.load(f)
        response = requests.post(f"{API_BASE}/holdings/", json=holding, headers=headers)
        print(f"Created holding {holding['holding_id']}: {response.status_code}")

Phase 6: Maintaining Your Catalog

Adding New Zines

  1. Examine zine
  2. Check if creator exists (search API or your files)
  3. Create new agent if needed
  4. Create zine record
  5. Create holding record
  6. Validate and submit

Updating Records

When you discover new information:

# Update a zine record
zine_id = "zine-001"
updates = {
    "abstract": "Updated description with more detail...",
    "table_of_contents": ["Section 1", "Section 2"]
}

response = requests.patch(
    f"{API_BASE}/zines/{zine_id}/",
    json=updates,
    headers=headers
)

Tracking Circulation

Add notes to holdings when zines are loaned:

{
  "holding_id": "hold-001-1",
  "public_notes": "Checked out to member #42 on 2024-03-15. Returned 2024-03-22."
}

Or use your library system's circulation module if available.

Handling Damaged or Lost Items

Update the holding:

# Mark as damaged
updates = {
    "condition_note": "Damaged: water damage on cover. Pages intact but wrinkled.",
    "access_status": "Reference"  # Change from Loanable to Reference
}

response = requests.patch(
    f"{API_BASE}/holdings/hold-001-1/",
    json=updates,
    headers=headers
)

# Or mark as lost
updates = {
    "condition_note": "Lost as of 2024-03-20. Last seen on loan.",
    "access_status": "Restricted"
}

Phase 7: Public Access

Building a Search Interface

Create a simple search page:

// Fetch all zines
async function searchZines(query) {
    const response = await fetch(
        `http://localhost:8000/api/zines/?search=${query}`
    );
    const data = await response.json();
    return data.results;
}

// Display results
async function displayResults() {
    const query = document.getElementById('search').value;
    const zines = await searchZines(query);

    const resultsDiv = document.getElementById('results');
    resultsDiv.innerHTML = zines.map(zine => `
        <div class="zine">
            <h3>${zine.title}</h3>
            <p>By ${zine.creator.join(', ')}</p>
            <p>${zine.abstract}</p>
            <p>Subjects: ${zine.subject.join(', ')}</p>
        </div>
    `).join('');
}

Export Options

Provide downloads in multiple formats:

# Export to CSV for patrons
import csv

response = requests.get(f"{API_BASE}/zines/", headers={"Accept": "text/csv"})
with open('catalog.csv', 'w') as f:
    f.write(response.text)

# Export JSON-LD for archives
response = requests.get(f"{API_BASE}/zines/", headers={"Accept": "application/ld+json"})
with open('catalog.jsonld', 'w') as f:
    f.write(response.text)

Best Practices

Consistency

  • Use consistent naming patterns for IDs
  • Always fill out the same fields for similar zines
  • Use vocabulary terms consistently

Documentation

  • Keep a README.md explaining your cataloging decisions
  • Document your ID schemes
  • Note any local practices

Backup

# Backup all JSON files
tar -czf backup-$(date +%Y%m%d).tar.gz zines/ agents/ holdings/ repository.json

# Or backup database if using reference implementation
pg_dump zinecore > backup-$(date +%Y%m%d).sql

Quality Control

  • Validate records regularly
  • Check for duplicates
  • Review records quarterly for updates

Scaling Up

For Larger Collections

  • Use UUIDs instead of sequential IDs
  • Implement batch import scripts
  • Consider dedicated database instead of JSON files
  • Use the reference implementation API
  • Set up automated backups

For Multiple Staff

  • Create cataloging guidelines document
  • Use version control (git) for JSON files
  • Implement review workflow
  • Train staff on vocabulary terms

Common Challenges

Challenge: Multiple Creators

Solution: Create agent records for each, list all in creator array

Challenge: Unknown Information

Solution: Use "unknown" for rights, leave optional fields empty

Challenge: Inconsistent Naming

Solution: Use alternative_names in AgentCore2 to link variants

Challenge: Hard to Categorize

Solution: Use public_notes to explain your decisions

Next Steps

Resources

You now have a complete cataloging system! Happy cataloging! 📚✨

Copyright ©2026 ZineCore2 Contributors,