Content Shape
The InvertContent interface — the normalized shape all adapters produce.
Content Shape
All content in Invert normalizes to a single InvertContent interface defined in src/adapters/interface.ts. Adapters are responsible for mapping their source format to this shape. The renderer and router only depend on the required fields.
InvertContent
interface InvertContent {
// Required
id: string; // Unique identifier (adapter-determined)
slug: string; // URL-friendly identifier — used in routing
title: string; // Display title
body: string; // HTML string — adapters convert markdown, etc.
contentType: string; // e.g. "posts", "pages", "docs"
// Optional
date?: string; // ISO 8601 date string
modified?: string; // ISO 8601 date string
author?: string; // Author name or identifier
excerpt?: string; // Short summary
featuredImage?: string; // URL or relative path
taxonomies?: Record<string, string[]>; // e.g. { tags: ["astro"] }
meta?: Record<string, unknown>; // Arbitrary pass-through data
}
Content types are strings
There is no structural distinction between a "post" and a "page" and a "recipe". A content type is a string. Directory structure can optionally mirror content types (e.g. content/posts/) but this is convention, not architecture. The system does not enforce schemas per content type — that's the adapter's job if it wants to.
Routing
Content is routed by contentType and slug:
/posts/my-post→{ contentType: "posts", slug: "my-post" }/docs/getting-started→{ contentType: "docs", slug: "getting-started" }/pages/about→{ contentType: "pages", slug: "about" }
Listing pages at /{type}/ show all content of that type.
The body field
The body field must be an HTML string. Adapters that read Markdown must convert it to HTML before returning it. The ContentBody component renders it with set:html.
If your adapter reads from an API that returns Markdown, convert it with remark/rehype before returning. If it returns HTML directly (e.g. WordPress REST API), pass it through as-is.
Using meta
The meta field is an escape hatch for data that doesn't fit the standard shape. Templates can read from content.meta for source-specific fields without breaking the interface contract.
{
"id": "my-post",
"slug": "my-post",
"title": "My Post",
"contentType": "posts",
"body": "<p>...</p>",
"meta": {
"wordpressId": 42,
"acf": { "custom_field": "value" }
}
}