A page is a JSON file that lists its sections in order. Each section names an
island variant and holds the values for that variant’s fields. This guide
describes the current @blockbite/astro implementation.
The page file
{
"version": 1,
"template": "default",
"title": "Home",
"sections": [
{
"island": "hero/simple",
"fields": {
"heading": "Build an island once, publish it anywhere",
"intro": "One component and one fields.yaml per variant."
}
},
{
"island": "editorial-grid/three-up",
"fields": {
"eyebrow": "Island collections",
"title": "Give visitors something to interact with",
"entries": [
{ "title": "Let the work lead", "meta": "Gallery" },
{ "title": "Turn browsing into discovery", "meta": "Collection" }
]
}
}
]
}
islandis the variant’sfamily/variantpath.fieldscontains exactly the keys that variant’sfields.yamldeclares, no more and no fewer. An empty string or an empty list is a valid value.- Repeater rows are plain objects, and their order in the file is the order on the page.
- The same variant can appear more than once with different values.
- Nothing in the file is a generated ID, so it reads well and diffs cleanly.
Validate pages with a content collection
Pages load through a native Astro content collection. loadIslands() reads your
island folders, and createPageSchema() turns them into the collection schema:
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { createPageSchema } from '@blockbite/astro/content';
import { loadIslands } from '@blockbite/astro/islands';
const { islands } = await loadIslands(new URL('./islands/', import.meta.url));
export const collections = {
pages: defineCollection({
loader: glob({ pattern: '*.json', base: './src/content/pages' }),
schema: createPageSchema({ islands, templates: ['default'] }),
}),
};
A page fails to load when a section names an unknown island, misses a declared field, adds a field the variant does not declare, or uses the wrong value type. The error points to the exact section, field and row.
Bind sections to a native route
For Desktop editing, import each island explicitly and spread its section’s fields directly into the call. For the page JSON above:
---
// src/pages/index.astro
import { getEntry } from 'astro:content';
import type { IslandProps } from '@blockbite/astro/islands';
import Hero from '../islands/hero/simple/Island.astro';
import EditorialGrid from '../islands/editorial-grid/three-up/Island.astro';
const page = await getEntry('pages', 'home');
if (!page) throw new Error('Missing src/content/pages/home.json');
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{page.data.title}</title>
</head>
<body>
<main>
<Hero {...(page.data.sections[0].fields as IslandProps<'hero/simple'>)} />
<EditorialGrid {...(page.data.sections[1].fields as IslandProps<'editorial-grid/three-up'>)} />
</main>
</body>
</html>
@blockbite/astro does not add routes or a section renderer. This page is your
own Astro code, so you decide layouts, URLs and which pages exist.
Keep the component calls aligned with the section paths and order in the JSON.
The type assertions give each call its declared prop type; content validation
still comes from createPageSchema(). You can move the document shell into
your existing layout component.
This explicit shape lets Desktop connect a rendered section to its values.
Dynamic registries and sections.map() may render in Astro while remaining
read-only in the visual editor.
Rename a field
A field’s key is its identity, so a rename touches three places in one change:
- The key in
fields.yaml, for examplesummarytodescription. - Every page section that uses that variant.
- The component that reads the prop.
Until all three match, page validation or astro check fails. Astro caches
validated content, so run npx astro sync --force after the change to check
every page against the new declaration. Changing only a label needs no
content changes.
See the island file reference for every rule and limit.