Skip to content

blueprints

Arnel Robles edited this page Sep 26, 2026 · 2 revisions

Content type blueprints

Every new site used to start from an empty schema, and the first hour was retyping the same content types. A blueprint is a named set of content type definitions that one call creates in a tenant.

Listing and applying

GET  /api/content-types/blueprints
POST /api/content-types/blueprints/{name}

Both are gated on manage_content_types, the capability that creates a content type, because that is what applying one does. The list returns each blueprint's name, description, the type names it creates, whether it is built in, and any errors found in its file. The apply creates every type in the caller's tenant and returns their ids and names.

Applying is additive and all or nothing. If any type the blueprint declares already exists in the tenant, the whole apply is refused with 409 and the refusal names the types that clash. Nothing is replaced, ever. A partial apply would leave references pointing at a type whose fields are not the ones the blueprint assumed, so one clash refuses the set. Types that the blueprint does not mention are left alone, so applying blog to a tenant that already has a newsletter type works.

Types are created the way POST /api/content-types creates one: name normalized, document sourced, the sourcing decision recorded against the name. A name that was decided event sourced before is refused rather than recreated, for the reason event-sourced-content-types.md gives.

The audit log records contenttype.blueprint_applied with the names created.

The built-in six

Every addressable type has a Slug field of type slug, which is what makes /api/public/{type}/{slug} exist, and every type is publicly deliverable. The site type is the exception to the slug: it is a singleton, one entry per tenant, read from the list. Fields that are for the team and not the public are marked Sensitive (masked on the way out) or Hidden (removed).

Blueprint Types Notes
blog post, category, author, page Post has a markdown body, excerpt, cover image URL, published date, author and category references, tags. Author email is Sensitive. Page can nest under a parent page and has an optional HideTitle bool, which tells the renderer to leave the page's title off.
events event, venue, speaker Event has starts and ends, a venue reference and a geopoint location, so filter[Location][near] works on delivery. Venue contact details are Sensitive.
portfolio project, client Project has a client reference, a gallery array, a live URL and a testimonial. Client contact name and email are Sensitive, internal notes are Hidden.
docs article, section Article has a markdown body, a required section reference and an order within it. Section can nest under a parent section.
site site A singleton holding the site's identity, theme and chrome, read by the renderer from /api/public/site. The theme and chrome fields are JSON; site-settings.md gives their shapes.
devsite page, post, category, author, doc, package, release, contributor, up-for-grabs, milestone A product site: the blog types under their own names, plus a flat doc type carrying its own section, order and parent fields for a documentation tree, and five types meant to be filled by collection syncs (#794) rather than typed by hand: package (NuGet), release and contributor (GitHub), up-for-grabs (open issues), milestone (open GitHub milestones, one per product). The shape barakocms.com runs on (BaryoDev/barakoCMS#959); see below for wiring it into a site's Collections setting. Its page has the same optional HideTitle as blog.

The blueprints carry no SEO fields. Run POST /api/content-types/{name}/seo-fields on the types a frontend renders as pages; see seo-fields.md.

Every body field above is markdown, deliberately. The richtext type is still valid and is still accepted, and neither type is sanitised: both store and return the string that was saved. The difference is what a consumer does with it. Markdown is normally rendered by something that drops raw HTML, which makes an untrusted body harmless. Richtext exists to be rendered as HTML, so a blueprint that chose it would hand anyone who can edit content a script tag on every page that shows it. Choose richtext only where the deployment sanitises on its own side.

There is no media content type in the core, so an image is a url field. If a deployment models media as content, a custom blueprint can reference it instead.

Wiring devsite into a renderer

Applying devsite only creates the types. A renderer reads them as lists and detail pages through the site's Collections setting, in the shape site-settings.md documents. post, author and category need no entry: their field names already match barakoPress's own defaults, the same way applying blog needs none. doc, package, release, contributor, up-for-grabs and milestone do, because a renderer has no default for a type it did not name:

{
  "doc": {
    "type": "doc",
    "route": "/docs",
    "fields": { "title": "Title", "slug": "Slug", "summary": "Summary", "body": "Body" },
    "tree": { "section": "Section", "order": "Order", "parent": "ParentDoc", "product": "Product", "editPath": "EditPath" }
  },
  "package": {
    "type": "package",
    "route": "/modules",
    "fields": { "title": "Name", "slug": "Slug", "summary": "Summary", "image": "IconUrl", "url": "Url" }
  },
  "release": {
    "type": "release",
    "route": "/changelog",
    "fields": { "title": "Title", "slug": "Slug", "body": "Body", "date": "PublishedAt", "url": "Url" },
    "sort": "-PublishedAt"
  },
  "contributor": {
    "type": "contributor",
    "fields": { "title": "Name", "slug": "Slug", "photo": "Photo", "url": "ProfileUrl" },
    "index": false
  },
  "up-for-grabs": {
    "type": "up-for-grabs",
    "fields": { "title": "Title", "slug": "Slug", "summary": "Summary", "tags": "Tags", "url": "Url" },
    "index": false
  },
  "milestone": {
    "type": "milestone",
    "fields": { "title": "Version", "slug": "Slug", "summary": "Description", "url": "Url" },
    "index": false
  }
}

release and contributor both feed the changelog page, contributor and up-for-grabs both feed the community page, and milestone feeds the roadmap page grouped by product. None of those three pages is one collection's index: each composes from blocks that draw on one or two collections, the way any page of blocks does. index: false on contributor, up-for-grabs and milestone keeps them out of a bare listing that no site has ever linked to, while leaving them fully readable by key for that composition.

Every field named above is a field the collection sync in collection-syncs.md can write to directly: package.Downloads, release.PublishedAt, contributor.Contributions, up-for-grabs.RepositoryName and milestone.Product/Open/Closed are ordinary data fields with no renderer role, there for the sync to fill and for a custom block to read, but not required by the shapes above. milestone has no date field: the roadmap page on barakocms.com shows a version, a description, an open and a closed count and a link, grouped by product, and says why in its own copy: "There are no dates here on purpose... a date would be a guess presented as a commitment." Reading what the page actually renders, rather than guessing at a shape, is why this type has no PublishedAt.

Custom blueprints

Set Blueprints:Path to a directory. Every *.json file in it is listed alongside the built-ins and applies the same way. The setting is unset by default, and the directory is read on every list and apply, so a file dropped in is visible without a restart. At most 100 files are read; past that the list says so under problems.

A file is one object:

{
  "name": "agency",
  "description": "Case studies and the people who wrote them.",
  "contentTypes": [
    {
      "name": "case-study",
      "displayName": "Case study",
      "isPubliclyDeliverable": true,
      "fields": [
        { "name": "Title", "displayName": "Title", "type": "string", "isRequired": true },
        { "name": "Slug", "displayName": "Slug", "type": "slug", "isRequired": true },
        { "name": "Lead", "displayName": "Lead", "type": "reference", "referenceType": "consultant" }
      ]
    },
    {
      "name": "consultant",
      "displayName": "Consultant",
      "fields": [
        { "name": "Name", "displayName": "Name", "type": "string", "isRequired": true },
        { "name": "Rate", "displayName": "Day rate", "type": "money", "sensitivity": "Hidden" }
      ]
    }
  ]
}

The entries under contentTypes are the same shape the Portability import accepts, so a file can be assembled from GET /api/portability/export by keeping the types and dropping the contents.

A file is validated when it is listed, with the validator the create endpoint runs, plus three rules of its own:

  • the name is lower-case letters, digits and hyphens, and a name a built-in already uses is refused;
  • a reference must point at a type declared in the same blueprint, so applying on an empty tenant produces a schema that works;
  • a property the type does not have is an error rather than ignored, so a misspelt sensitivity cannot leave a field Public that the author marked Hidden.

An invalid file still appears in the list, with its problems under errors, and applying it is a 400 repeating them. A file that does not parse is listed under its file name with the parse error.


Generated from docs/blueprints.md by scripts/wiki-sync.sh. Edit the doc in the repository, not this page.

Clone this wiki locally