| name | camox-layout |
| description | How to create and edit Camox layout definition files. Use this skill whenever the user wants to create a new layout for their Camox website, modify an existing layout, wrap pages in shared structure (navbar + footer), customize meta titles or OG images, or asks about the layout definition API. Trigger on mentions of layouts, page wrappers, page templates, shared page structure, navbar/footer placement, or any request to define how pages are structured — even if they don't say 'layout' explicitly. |
Creating Camox Layout Definitions
A layout wraps pages in shared structure — a navbar at the top, a footer at the bottom, consistent styling. Each page in the CMS is assigned a layout. This skill covers creating layout definitions — the template that describes which blocks surround page content and how to render them.
After defining the layout: load camox-cli
A definition only adds a new layout to the catalog — it doesn't apply it to any page, and existing pages keep whatever layout they were already assigned. If the user wanted "the about page to use the new marketing layout" or "all docs pages to use this layout", the second half of that work is reassigning pages, which happens through the CLI. Once the layout is defined and the dev server has picked it up, load the camox-cli skill to update the relevant pages' layout assignments. Don't stop after the definition unless the user explicitly only wanted to set up the layout for future pages.
Quick Start
A layout file lives in the app's src/layouts/ folder, is a .tsx file, and exports layout:
import { createLayout } from "camox/createLayout";
import { block as navbarBlock } from "../blocks/navbar";
import { block as footerBlock } from "../blocks/footer";
import { block as heroBlock } from "../blocks/hero";
const myLayout = createLayout({
id: "my-layout",
title: "My Layout",
description: "When to use this layout",
blocks: {
before: [navbarBlock],
after: [footerBlock],
initial: [heroBlock],
},
component: MyLayoutComponent,
buildMetaTitle: ({ pageMetaTitle, projectName }) => `${pageMetaTitle} | ${projectName}`,
});
function MyLayoutComponent({ children }: { children: React.ReactNode }) {
return (
<main className="flex min-h-screen flex-col">
<myLayout.BeforeBlocks />
<div className="flex-1">{children}</div>
<myLayout.AfterBlocks />
</main>
);
}
export { myLayout as layout };
The createLayout options
| Option | Required | Description |
|---|
id | yes | Unique kebab-case identifier. Must match the filename without extension. |
title | yes | Display name shown in the CMS UI. |
description | yes | Tells the CMS user (or AI agent) when to pick this layout. Write it as guidance — explain what kind of pages this layout suits. |
blocks | yes | An object with before, after, and optional initial arrays. before blocks (must be layoutOnly: true) render above the page content, after blocks (must be layoutOnly: true) render below it, and initial blocks (must NOT be layoutOnly: true) pre-populate the homepage. See Block Placement and Initial Blocks below. |
component | yes | A named React function component that renders the layout shell. Receives { children } — the page content. |
buildMetaTitle | yes | A function that builds the <title> tag. Receives { pageMetaTitle, projectName, pageFullPath } and returns a string. |
buildOgImage | no | A function that returns a JSX element for generating Open Graph images. Receives { title, description, projectName }. |
Block Placement — before, after, and initial
The blocks object groups every block that belongs to this layout into three slots:
before: rendered above the page content (navbars, banners, announcements)
after: rendered below the page content (footers, cookie bars)
initial (optional): pre-populated on the homepage when a project is first created — see the dedicated section below
The blocks in before and after must be marked with layoutOnly: true in their block definition — the createLayout type rejects non-layout-only blocks in those slots, since layout blocks shouldn't appear in the page-content "add block" picker. Conversely, initial accepts only blocks where layoutOnly is omitted or false — it's for page content, not chrome.
blocks: {
before: [navbarBlock, announcementBlock],
after: [footerBlock],
}
You can have multiple blocks in either group, or leave one empty:
blocks: {
before: [navbarBlock],
after: [],
}
The Layout Component
The component is a named React function that receives { children } and renders the overall page structure. Inside it, use the layout constant's two slot components — BeforeBlocks and AfterBlocks — to place all of the before and after blocks, in the order you declared them in the blocks config.
function MyLayoutComponent({ children }: { children: React.ReactNode }) {
return (
<main className="flex min-h-screen flex-col">
<myLayout.BeforeBlocks />
<div className="flex-1">{children}</div>
<myLayout.AfterBlocks />
</main>
);
}
You don't list individual blocks in the component — <myLayout.BeforeBlocks /> renders every block in blocks.before in order, and <myLayout.AfterBlocks /> does the same for blocks.after. To reorder or add blocks, edit the blocks config; the component doesn't need to change.
The component controls the HTML structure — you decide how to wrap and position the BeforeBlocks / AfterBlocks slots and the page content. Use Tailwind CSS for styling.
Meta Title — buildMetaTitle
Controls how the browser tab title is built. Receives three parameters:
pageMetaTitle — the title set on the individual page
projectName — the site/project name
pageFullPath — the full URL path of the page
Common patterns:
buildMetaTitle: ({ pageMetaTitle, projectName }) =>
`${pageMetaTitle} | ${projectName}`,
buildMetaTitle: ({ pageMetaTitle, projectName }) =>
`${projectName} | ${pageMetaTitle}`,
buildMetaTitle: ({ pageMetaTitle }) => pageMetaTitle,
Initial Blocks — blocks.initial (optional)
When a project is first set up, Camox creates a homepage automatically. The blocks.initial option lets you specify which blocks (and in what order) should be pre-populated on that page. Each block is created with its default content from the block definition.
import { block as heroBlock } from "../blocks/hero";
import { block as statisticsBlock } from "../blocks/statistics";
const myLayout = createLayout({
blocks: {
before: [navbarBlock],
after: [footerBlock],
initial: [heroBlock, statisticsBlock],
},
});
- Only non-layout blocks. Blocks here must NOT be
layoutOnly: true — the type system enforces it. Layout blocks (before/after) are created separately as part of the layout itself.
- At most one layout per app can define
blocks.initial. If multiple layouts define it, the app will crash with a clear error at startup. This is enforced in createApp.
- Order matters. The blocks appear on the homepage in the order listed.
- If no layout defines
blocks.initial, Camox still creates an empty homepage using the first available layout (with its layout blocks like navbar/footer), but no page blocks are added.
OG Image — buildOgImage (optional)
Generates an Open Graph image (the preview shown when sharing links on social media). The function returns a JSX element that gets rendered as a 1200x630 image.
The JSX here uses inline styles only (no Tailwind) because it's rendered by an image generation engine, not a browser. Use display: "flex" for layout.
buildOgImage: ({ title, description, projectName }) => (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
width: "100%",
height: "100%",
backgroundColor: "#09090b",
padding: "60px 80px",
fontFamily: "sans-serif",
}}
>
{projectName && (
<div style={{ fontSize: 24, color: "#a1a1aa", marginBottom: 24 }}>
{projectName}
</div>
)}
<div
style={{
fontSize: 64,
fontWeight: 700,
color: "#fafafa",
lineHeight: 1.2,
marginBottom: 24,
}}
>
{title}
</div>
{description && (
<div style={{ fontSize: 28, color: "#a1a1aa", lineHeight: 1.5 }}>
{description}
</div>
)}
</div>
),
Rules and Conventions
- File = one layout. One
.tsx file per layout in src/layouts/. The id must match the filename (without .tsx).
- Named export as
layout. Always: export { myVar as layout }. Not a default export.
- Named function component. Use
function MyComponent(), not an arrow function. Reference it in createLayout before its declaration is fine (hoisting).
- Import path is
"camox/createLayout". The createLayout function comes from this import.
- Import blocks from
"../blocks/filename". Layout blocks are imported from the blocks directory. Import the named block export.
- Use Tailwind CSS for the component. Style the layout shell with Tailwind utility classes. The OG image function uses inline styles instead.
- Render groups, not individual blocks. The component places
<layout.BeforeBlocks /> and <layout.AfterBlocks /> — each renders every block in its group, in declared order. Don't reference blocks individually by name.
buildMetaTitle is required. Every layout must define how page titles are constructed.
- Description guides layout selection. Write the
description to help CMS users choose the right layout for their page — explain what types of pages it's suited for.
- Layout blocks must use
layoutOnly: true. Blocks placed in blocks.before or blocks.after must be defined with layoutOnly: true (enforced by the createLayout type). Blocks placed in blocks.initial must NOT be layoutOnly: true.