Skip to content

feat(collections): implement CollectionProvider interface, FileCollectionProvider, and S3CollectionProvider #85

@perasperaactual

Description

@perasperaactual

Overview

Implement the Collections system — a first-class abstraction for content that exists as a set of entries (blog posts, changelog entries, team members, case studies) rather than as individual pages.

This is a foundational piece. The UI content types that consume collections (collection_listing, collection_entry, collection_featured) are a follow-on ticket.

Architecture (Option A — confirmed)

content/
  pages/
    about/content.yaml          ← existing page pattern (unchanged)
  posts/                        ← collection: folder name = collection name
    my-first-post.yaml
    second-post.yaml

[prebuild]
  ↓
public/stackwright-content/
  collections/
    posts/_index.json           ← sorted manifest (slug, title, date, excerpt, tags)
    posts/my-first-post.json    ← full entry
    posts/second-post.json

FileCollectionProvider.list('posts') reads _index.json.
FileCollectionProvider.get('posts', 'my-first-post') reads the individual file.
No filesystem access at render time.

1. CollectionProvider interface (in @stackwright/core)

export interface CollectionEntry {
  slug: string;
  [key: string]: unknown;
}

export interface CollectionListOptions {
  limit?: number;
  page?: number;
  sort?: string;       // field name, prefix with '-' for descending
  filter?: Record<string, unknown>;
}

export interface CollectionProvider {
  list(collection: string, opts?: CollectionListOptions): Promise<CollectionEntry[]>;
  get(collection: string, slug: string): Promise<CollectionEntry | null>;
  collections(): Promise<string[]>;  // list available collection names
}

// Registration (follows existing pattern)
export function registerCollectionProvider(provider: CollectionProvider): void;
export function getCollectionProvider(): CollectionProvider;

2. Prebuild pipeline changes (in @stackwright/build-scripts)

Extend stackwright-prebuild to detect and compile collections:

  • Scan content/ for subdirectories that are NOT pages/ — treat each as a collection
  • For each collection directory, read all .yaml / .yml files
  • Validate each entry against its collection's Zod schema (if registered; fall through gracefully if not)
  • Write individual entry JSON files to public/stackwright-content/collections/<name>/<slug>.json
  • Write _index.json as a sorted manifest containing: slug, plus any fields declared as indexFields in a _collection.yaml config file (see below)

Optional _collection.yaml config per collection

# content/posts/_collection.yaml
schema: post          # references a registered Zod schema (optional)
sort: -date           # default sort field
indexFields:          # fields to include in _index.json manifest
  - title
  - date
  - author
  - excerpt
  - tags
  - coverImage

If _collection.yaml is absent, all top-level scalar fields are included in the manifest.

3. TypeScript type generation

Extend prebuild to emit a generated types file:

// auto-generated: public/stackwright-content/collections/posts.d.ts
export interface PostEntry {
  slug: string;
  title: string;
  date: string;        // ISO 8601
  author: string;
  excerpt?: string;
  tags?: string[];
  content?: ContentItem[];
}

Generated from the union of all entry fields found in the collection (or from the registered Zod schema if present).

4. FileCollectionProvider (in @stackwright/nextjs or new @stackwright/collections package)

Reads from public/stackwright-content/collections/ at render time:

export class FileCollectionProvider implements CollectionProvider {
  async list(collection, opts) {
    // reads _index.json, applies limit/page/sort/filter
  }
  async get(collection, slug) {
    // reads <slug>.json
  }
  async collections() {
    // reads directory listing of public/stackwright-content/collections/
  }
}

Default registration: registerCollectionProvider(new FileCollectionProvider()) should be called in the user's _app.tsx / layout.tsx alongside registerNextJSComponents(). Scaffold template should include this.

5. S3CollectionProvider (in @stackwright/collections or same package)

Reads from an S3 bucket instead of local filesystem. Build-time or request-time (ISR-compatible):

export class S3CollectionProvider implements CollectionProvider {
  constructor(config: { bucket: string; prefix?: string; region?: string }) {}
  // same interface as FileCollectionProvider, reads from S3
}

During build, S3 provider can be used in getStaticProps / generateStaticParams to fetch collection entries from S3 rather than local files. For ISR use cases, the same provider works at request time.

Note: S3 provider requires @aws-sdk/client-s3 as a peer dependency. Document this clearly — do not add it as a direct dependency to avoid bloating installs for users who don't need S3.

Package placement question

Decide before implementation: does FileCollectionProvider and S3CollectionProvider live in:

  • @stackwright/nextjs (alongside other Next.js-specific adapters), or
  • A new @stackwright/collections package

Recommendation: new @stackwright/collections package. Keeps collections framework-agnostic at the provider level; a future SvelteKit adapter could use the same providers.

Acceptance criteria

  • CollectionProvider interface exported from @stackwright/core
  • registerCollectionProvider() / getCollectionProvider() implemented
  • Prebuild detects content/<name>/ directories and compiles to public/stackwright-content/collections/
  • _index.json manifest generated with configurable fields
  • _collection.yaml config supported (sort, indexFields)
  • TypeScript types auto-generated from collection contents
  • FileCollectionProvider implemented and tested
  • S3CollectionProvider implemented with @aws-sdk/client-s3 as peer dep
  • Scaffold template updated to call registerCollectionProvider(new FileCollectionProvider())
  • Example: add a posts/ collection to hellostackwrightnext demonstrating the full pipeline
  • Unit tests for prebuild collection compilation
  • Unit tests for FileCollectionProvider list/get/filter/sort

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:laterPlanned but not yet committed

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions