How 30-seconds-of-code Implements Astro Collections with a Custom Model Layer

The 30-seconds-of-code repository bypasses Astro's native defineCollection API entirely, instead using a custom JavaScript model layer that transforms YAML definitions into static JSON for the build process.

The 30-seconds-of-code project is a widely-used open-source snippet library built with Astro. While the framework offers a built-in content collections system, this project implements a unique approach to Astro collections that decouples content hierarchy and pagination logic from Astro's type-safe constraints, enabling more flexible content relationships.

Why the Project Avoids Astro's Native defineCollection

Rather than using Astro's schema-based content collections, 30-seconds-of-code maintains its own collection system outside the framework. This custom architecture allows the project to handle complex hierarchy relationships, parent-child connections, and featured collection logic that would be difficult to model within Astro's native defineCollection constraints.

Defining Collections in YAML

Collection metadata originates in YAML files stored in content/collections/*.yaml. These files define essential properties including the collection slug, title, description, and hierarchical relationships.

For example, the repository includes definitions for specialized collections like webdev.yaml and main-listing.yaml in this directory. Each YAML file specifies whether a collection appears at the top level, its parent collection (if any), and other configuration data that drives the site's navigation structure.

Loading Configuration and File Discovery

The system discovers these YAML files through a glob pattern defined in src/lib/contentUtils/config.js. This configuration specifies the path ${contentDir}/collections/**/*.yaml, ensuring the build process locates all collection definitions at build time without manual registration.

This decoupled approach means adding a new collection requires only creating a new YAML file in the correct directory, with no changes needed to the Astro configuration or TypeScript definitions.

The Collection Model Class

At the heart of the system is the Collection class defined in src/models/collection.js. This JavaScript model handles hierarchy traversal, pagination calculations, and page generation through computed getters.

The class extends ContentModel and implements methods to find parent and child collections, calculate page counts based on settings.presentation.cardsPerPage, and generate page objects for static site generation:

// src/models/collection.js
export default class Collection extends ContentModel {
  constructor(data) {
    super(data);
    this.id = data.id;
    this.title = data.title;
    this.topLevel = data.topLevel || false;
    this.parentId = data.parentId;
  }

  // Hierarchy traversal
  get parent() { return Collection.find(this.parentId); }
  get children() { return Collection.where({ parentId: this.id }); }

  // Pagination logic generates pages array
  get pages() {
    if (this.isCollections) return this.collectionsPages;
    const pagination = { 
      pageCount: this.pageCount, 
      itemCount: this.listedSnippets.length, 
      itemType: 'articles' 
    };
    return Array.from({ length: this.pageCount }, (_, i) => {
      const pageNumber = i + 1;
      return Page.from(this, {
        pageNumber,
        items: this.listedSnippets.slice(
          i * settings.presentation.cardsPerPage,
          (i + 1) * settings.presentation.cardsPerPage
        ),
        ...pagination,
        largeImages: false,
        singleColumn: this.isUpdateLogs,
      });
    });
  }
}

Generating Static JSON for Astro Consumption

The bridge between the custom model layer and Astro occurs in src/lib/astroContent.js. Here, the generateCollectionPages static method walks every Collection instance, calls the pages getter, and serializes each page to JSON:

// src/lib/astroContent.js
static generateCollectionPages() {
  const pages = Collection.all.reduce((acc, collection) => {
    collection.pages.forEach(page => {
      acc[page.key] = page.serialize;   // One JSON file per collection page
    });
    return acc;
  }, {});
  fs.writeJson(settings.paths.out.collections, pages, ...this.outputParams);
}

This process outputs to settings.paths.out.collections, creating a static JSON file for every page of every collection. Astro consumes these pre-computed JSON files at build time, eliminating the need for runtime database queries or complex logic in the template layer.

Rendering Collections in Astro Routes

The dynamic route src/astro/pages/[lang]/[...listing].astro receives the serialized collection data as props. This component renders the collection header, optional sub-links (displayed as chips), and a PreviewList of articles:

---
// src/astro/pages/[lang]/[...listing].astro
const {
  slug,
  pagination = null,
  collection,
  collectionItems,
  largeImages = false,
  singleColumn = false,
  pageDescription,
  structuredData,
} = Astro.props;
---

<Layout title={structuredData ? structuredData.name : collection.title}
        description={pageDescription}
        logoSrc={collection.cover ? collection.cover : undefined}
        structuredData={structuredData}
        canonical={slug}>

  <main slot="main-content">
    <Hero title={collection.title}
          description={collection.content}
          cover={collection.cover}
          coverSrcset={collection.coverSrcset} />

    {collection.sublinks.length ? 
      <Chips items={collection.sublinks} /> : 
      <div aria-hidden="true" data-area-gap />}

    <PreviewList contentItems={collectionItems}
                 largeImages={largeImages}
                 singleColumn={singleColumn}>
      {pagination ? <Pagination pagination={pagination} slot="bottom-nav" /> : null}
    </PreviewList>
  </main>
</Layout>

Collections are surfaced to users through the site's navigation components. In src/astro/components/Header.astro, the link to browse all collections points to /collections/p/1, which renders the first page of the collections index:

<!-- src/astro/components/Header.astro -->
<a href="/collections/p/1" data-nav-action="collections">Collections</a>

Additionally, keyboard navigation is implemented in src/astro/scripts/hotkeys.js, where pressing the j key triggers navigation to the collections page, providing power users with rapid access to the content hierarchy.

Summary

  • 30-seconds-of-code implements Astro collections through a custom model layer rather than using Astro's native defineCollection API.
  • Collection definitions live in content/collections/*.yaml, with metadata describing hierarchy, pagination, and display properties.
  • The Collection class in src/models/collection.js handles complex relationships, pagination calculations, and page generation.
  • Static JSON generation in src/lib/astroContent.js pre-computes all collection pages at build time for optimal performance.
  • Astro templates consume this JSON through dynamic routes like [...listing].astro, receiving pre-processed props including collection, collectionItems, and pagination.

Frequently Asked Questions

Why does 30-seconds-of-code avoid Astro's defineCollection API?

The project requires complex hierarchical relationships between collections (parent-child connections) and custom pagination logic that would be difficult to implement within Astro's schema-based content collections. The custom model layer in src/models/collection.js provides greater flexibility for these specific domain requirements.

How does the custom Collection class handle pagination?

The Collection class calculates pagination through the pages getter, which slices the listedSnippets array according to settings.presentation.cardsPerPage. It returns an array of Page instances, each containing the subset of items for that specific page number, along with metadata like pageCount and itemType.

Where are new collection definitions added?

New collections are added by creating YAML files in the content/collections/ directory. The glob pattern in src/lib/contentUtils/config.js automatically discovers these files at build time, meaning no registration in Astro's configuration is required to add new collections to the site.

How does Astro receive the collection data at runtime?

Astro does not query the collection data at runtime. Instead, src/lib/astroContent.js generates static JSON files during the build process, which Astro consumes as props in dynamic routes like src/astro/pages/[lang]/[...listing].astro. This static generation approach ensures fast page loads and eliminates runtime dependencies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →