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
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)
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)
2. Prebuild pipeline changes (in @stackwright/build-scripts)
Extend
stackwright-prebuildto detect and compile collections:content/for subdirectories that are NOTpages/— treat each as a collection.yaml/.ymlfilespublic/stackwright-content/collections/<name>/<slug>.json_index.jsonas a sorted manifest containing:slug, plus any fields declared asindexFieldsin a_collection.yamlconfig file (see below)Optional
_collection.yamlconfig per collectionIf
_collection.yamlis absent, all top-level scalar fields are included in the manifest.3. TypeScript type generation
Extend prebuild to emit a generated types file:
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:Default registration:
registerCollectionProvider(new FileCollectionProvider())should be called in the user's_app.tsx/layout.tsxalongsideregisterNextJSComponents(). 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):
During build, S3 provider can be used in
getStaticProps/generateStaticParamsto 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-s3as 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
FileCollectionProviderandS3CollectionProviderlive in:@stackwright/nextjs(alongside other Next.js-specific adapters), or@stackwright/collectionspackageRecommendation: new
@stackwright/collectionspackage. Keeps collections framework-agnostic at the provider level; a future SvelteKit adapter could use the same providers.Acceptance criteria
CollectionProviderinterface exported from@stackwright/coreregisterCollectionProvider()/getCollectionProvider()implementedcontent/<name>/directories and compiles topublic/stackwright-content/collections/_index.jsonmanifest generated with configurable fields_collection.yamlconfig supported (sort, indexFields)FileCollectionProviderimplemented and testedS3CollectionProviderimplemented with@aws-sdk/client-s3as peer depregisterCollectionProvider(new FileCollectionProvider())posts/collection tohellostackwrightnextdemonstrating the full pipelineFileCollectionProviderlist/get/filter/sort