I stopped hand-writing content models. The agent writes them now.

Every CMS asks you the same question first: what shape is your content?

You answer it in a config file. You name the fields, pick the types, wire the relationships, redeploy, and only then do you get to write anything. Payload does this well — a typed schema, checked at build time, exactly as promised. But the schema is yours to author, by hand, before the content exists.

I wanted to skip that step. Not remove it — skip it. Let the agent that writes your case studies also decide what a case study is.

That is what Custom Collections does in SpiderPublish™. An agent declares a content type, fills it with records, and puts each record on its own live URL. One session. No redeploy.

Define, fill, render

Here is the whole loop.

Define. You describe the fields you want. Nine types cover it: text, number, bool, select, date, richtext, media, relationship, blocks. Adding a tenth is a registry entry, not a migration. The schema lives in a JSON column, so a new field type never touches your data.

Fill. Push up to 100 records in one call. It is one transaction: every record lands, or none do. A half-imported catalogue is the worst possible outcome, so it is the one outcome that cannot happen.

Render. Set a route_base and every record gets a real page. Its own slug, its own SEO fields, its own Open Graph image. Not a row in a table. A page.

The part I got wrong first

Records are read by slug and written by id.

GET    /collections/guides/records/gemini-35-web-designers    ← slug
PATCH  /collections/guides/records/a9b793d1-0e97-4f93-…       ← id
DELETE /collections/guides/records/a9b793d1-0e97-4f93-…       ← id

That asymmetry is deliberate and it will catch you once. Reads are the public surface, so they take the human-readable thing. Writes are the dangerous surface, so they take the unambiguous thing. A slug can be edited; an id cannot. You never rename your way into overwriting the wrong record.

Budget one round-trip for it: list, keep the id, then write.

A chalk record card with an open archway on its left labelled READ BY SLUG and a small gold keyhole on its right labelled WRITE BY ID.

Unknown fields are rejected, not ignored

Send a field the schema does not declare and the write fails. The error names every field that is declared.

This is the opposite of how our blog posts behave, where an unrecognised key is quietly dropped. I made collections fail loud on purpose. A dropped field on a blog post costs you a paragraph. A dropped field across a 100-record import costs you a column you will not notice for a month.

Fail loud, once, at write time. That is cheaper every time.

A white square tile sitting inside a chalk-drawn frame labelled DECLARED, while a gold triangular tile is stopped on the frame's rim, labelled REJECTED.

Relationships without the N+1

Link a guide to its author, a product to its category. Cardinality is many-to-one or one-to-many.

The query budget is fixed and I will state it plainly, because "fast" is not a number:

  • one lookup to resolve the URL segment to a collection

  • one joined query for the definition and the page of records, with the count windowed onto the same query

  • one batched fetch per many-to-one relationship field — two for a one-to-many reverse walk

Never one query per record. A page of 50 records with two relationships costs four queries, not 104. Set depth=0 and you get raw foreign keys with no include queries at all.

Note the shape of that promise: it is fixed, not single. A one-to-many walk genuinely costs two. I would rather give you the real number.

Publishing is a two-step

Creating and editing a draft applies immediately. Changing a record's status — publishing, archiving, unpublishing — does not.

You call it once with dry_run and get back a preview, a single-use token, and a hash of what you are about to change. You call it again with the token to commit. Deleting a collection works the same way, and tells you how many records go with it.

An agent moving fast through your content should have to stop and confirm before anything becomes public. This is that stop.

It runs on this site

The page you are reading sits on a site that uses this.

Our guides section is a custom collection: nine fields, eight published records, filled the day after the feature shipped. Every one of them has its own page.

/guides/gemini-35-web-designers
/guides/claude-code-3d-animated
/guides/google-ai-studio-19-min

Those are records, not hand-built pages. Nobody wrote a template for each one.

A stack of flat chalk rows labelled ONE RECORD, with an arc leading to a standing gold rectangle shaped like a web page, labelled ONE PAGE.

What I am not claiming

The honest edges, so you do not find them on day one:

  • Many-to-many is not here. Many-to-one and one-to-many only. Nested collections likewise.

  • Relationship targets are bounded — other records, posts, and authors. Business data is deliberately not linkable, which keeps the public renderer away from anything private.

  • Record detail pages are automatic; the index page is not. /guides/<record> works the moment you set a route_base. A landing page that lists the collection still needs a page with a dynamic component on it.

  • Quotas are enforced but not yet configurable. The caps exist and hold. Setting them per plan has no admin path yet, so nobody should be promised custom tiers.

  • Slug rules differ by level, and it is not obvious. Collection and record slugs are lowercase letters, digits and hyphens — no underscores. Field ids are snake_case with underscores. case-studies is a valid collection slug; case_studies is not.

  • Rich text caps at 256 KB per field.

Why this is different

A typed CMS is not new, and I am not pretending it is. Payload, Strapi and Sanity all give you one, and Payload's is genuinely excellent.

What is new is who holds the pen. In every one of those, a person defines the model and an agent — if you have one — fills it in afterwards. Here the model and the content arrive together, from the same session, through the same API, gated by the same confirm step.

You stop describing your content model to a machine. You ask the machine for one.

Read the docs

Publish