Skip to content
Back to skills

Compose Page

ASecurity

Use when authoring or editing a marketing page or blog post in this repo — locating the right MDX file under artifacts/web/content/, choosing Blocks from the catalog, writing frontmatter, composing Blocks with literal props, and validating before viewing. Use whenever asked to add a page, edit copy, add or rearrange a section, or change what a page contains.

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 22, 2026
ai-agentsjavascriptgojavareactexpressapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 25, 2026

npx -y skills add robotostudio/turbo-start-web-agent --skill compose-page --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Compose Page?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Compose Page
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robotostudio-compose-page/badge)](https://www.skillsdirectory.com/skills/robotostudio-compose-page)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: compose-page
description: Use when authoring or editing a marketing page or blog post in this repo — locating the right MDX file under artifacts/web/content/, choosing Blocks from the catalog, writing frontmatter, composing Blocks with literal props, and validating before viewing. Use whenever asked to add a page, edit copy, add or rearrange a section, or change what a page contains.
---

# Compose a page

Content in this repo is MDX that compiles to a static React tree — never
executable code. You are composing pre-built **Blocks**, not writing
components. Every rule below is enforced at build time by a remark plugin
(`artifacts/web/src/lib/content/remark-content-lockdown.ts`); breaking one
fails `pnpm content:check` with a message naming the exact rule and the file
position. That failing build is your feedback channel — read the message, it
tells you what to do instead.

## 1. Locate the file

- Site pages: `artifacts/web/content/pages/*.mdx` (e.g. `home.mdx`,
  `about.mdx`, `contact.mdx`, `privacy.mdx`, `terms.mdx`, `style-guide.mdx`,
  `blocks-gallery.mdx`). A new page is a new file here.
- Blog posts: `artifacts/web/content/blog/*.mdx` (e.g.
  `introducing-harbour.mdx`, `composing-pages.mdx`).
- The site name, header nav, footer, and site-wide announcement bar are
  **not** a page — they're YAML at
  `artifacts/web/content/settings/site.yml`, `navigation.yml`, `footer.yml`,
  `announcement.yml` and `overscroll.yml`, not covered by this skill. `site.yml` is where the
  site's name and description live; the header, the browser tab title, and
  the blog index all read from it, so a request like "change the site name"
  is one edit there and never a change to a `.tsx` file. The announcement bar is chrome rendered above the header on every
  page (`enabled: true`/`false` there turns it on or off); it is a different
  thing from the `Banner` Block below, which is in-page content for a single
  page and can never render above the header.
- The URL slug is derived automatically from the file path (`pages/about.mdx`
  → `/about`) — there is no `slug` frontmatter field to set.

## 2. Choose Blocks

Read `artifacts/web/src/lib/blocks/catalog.json` — a generated, machine-readable
reference (`pnpm catalog` regenerates it; do not hand-edit it). Its shape:

```json
{
  "blockCount": 19,
  "blocks": {
    "FeatureGrid": { "schema": { "properties": { "...": "..." }, "required": ["title", "features"], "description": "..." } }
  }
}
```

Each key is a Block's exact tag name (PascalCase) and its JSON Schema —
`properties` are the props you may pass, `required` are the ones you must,
and nested `description` fields on URL-shaped props spell out the allowed
scheme. Today's 19 Blocks: `Banner`, `Hero`, `CTA`, `Comparison`,
`FeatureGrid`, `FeatureSplit`, `FeatureRows`, `PreviewStage`, `ImageCards`,
`Gallery`, `PostGrid`, `Faq`, `Testimonial`, `FeaturedQuote`, `LogoCloud`,
`Team`, `Stats`, `Newsletter`, `Pricing`. For a
worked example of every Block with real props, read
`artifacts/web/content/pages/blocks-gallery.mdx` or
`home.mdx` — both are live composed pages, not documentation.

**Read each Block's `description` before choosing it.** It says what the Block
is for, and for the ledger grids it says the count that fills cleanly: FeatureGrid,
Team and ImageCards want a multiple of 3, LogoCloud a multiple of 6, FeatureRows
exactly 3, Comparison 5 or 6 rows, Gallery exactly 8 images, Pricing 1 to 4
plans. FeatureGrid and Team close a short last row with empty ruled cells; the
other grids do not, so an off count leaves a visible gap.

If the Block you
need doesn't exist, that's a code change (new schema + component), not a
content edit — see the README's "Adding a Block" section — and is out of
scope for this skill.

## 3. Write frontmatter

Every page needs:

```yaml
---
title: My Page Title       # string, max 200 chars
description: One line.     # string, max 300 chars
draft: true                # new pages start true; omit for existing pages you're editing
---
```

`draft` defaults to `false` and excludes an entry from production builds and
the sitemap (still visible on preview deployments) — always start a **new**
page with `draft: true` and flip it once the client approves. `noindex: true`
(default `false`) publishes a page but keeps it out of the sitemap and asks
search engines not to index it — used on `blocks-gallery.mdx`, `privacy.mdx`
and `terms.mdx` today. Blog
posts additionally require `pubDate` (ISO date, e.g. `2026-08-17`) and
`category` (string), with optional `excerpt` (max 300 chars) and `cover` (must
be a full `https://*.public.blob.vercel-storage.com/...` URL).

A blog post's side panel ("On this page") is **derived, never authored**: it
is built at build time from the post's top-level `##` headings, so there is no
list to write or keep in step. Two consequences when writing a post: use `##`
for the sections a reader might jump to (`###` and below stay out of the
panel), and a post with no `##` headings renders full-width with no panel at
all, which is the right shape for a short note.

## 4. Compose Blocks with literal props

```mdx
<FeatureGrid
  eyebrow="What you get"
  title="A template that stays out of your way once the client signs off."
  lede="Six pieces, each one doing a single job well."
  features={[
    { title: "Block system", body: "Compose pages from pre-built sections." },
    { title: "One token file", body: "Rebrand by editing CSS variables." },
    { title: "Schemas that catch mistakes", body: "A bad edit fails the build." },
  ]}
/>
```

**Eyebrows.** Most Blocks take an optional `eyebrow`: the small-caps label with
a pink tick above the title. Use one when it tells the reader something the
title does not ("Pricing" above "One template, three ways to license it"), and
leave it out when it would only repeat the title. `LogoCloud` has no title, so
its `eyebrow` is required.

**Hero.** `variant` is `showcase` (the default, and what home uses: a lede, a
prompt card on the texture, no headline), `centered` or `left`. The `title` is
optional in every variant.

**A worked page: home.** `home.mdx` runs Hero (showcase), LogoCloud,
FeatureRows, Stats, PreviewStage, FeaturedQuote, Faq, PostGrid, CTA. It opens on
the product, proves it (logos, numbers, the preview), lets a customer speak,
answers objections, shows it is alive (the blog), and closes on one call to
action. Most of its sections carry an eyebrow. Borrow the shape for a landing
page rather than stacking Blocks at random.

Hard constraints — breaking any of these fails `content:check` with the
lockdown's own hint: *"Content MDX is compose-only: assemble Blocks from
src/lib/blocks/schemas.ts and pass props as literal values. See README.md."*

- No `import` or `export` statements — Blocks are already in scope.
- No raw HTML elements (`<div>`, `<a>`, …) — only capitalised Block tag names.
  No hyphenated, namespaced (`:`), or dotted (`.`) tag names either.
- Props are literal values only: strings, numbers, booleans, and arrays/objects
  written as expression attributes — `primary={{ label: "…", href: "/…" }}`,
  never a quoted JSON string. No function calls, member access, bare
  identifiers, spreads (`{...x}`), or operators beyond unary `+ - !`.
- No prop name may start with `on` (treated as an event handler by name alone,
  regardless of what it holds) — rename it, e.g. `onboardingSteps` → `steps`.
- No `dangerouslySetInnerHTML` or `srcDoc` props.
- Any prop named `href`, `src`, `action`, `formAction`, `poster`, `url`, `to`,
  `cite`, `ping`, `srcSet`, or ending in `url`/`href`/`src` is treated as a
  URL: it must be a plain literal string (no interpolation) starting with
  `/`, `#`, `?`, `.`, or an `http(s)`/`mailto`/`tel` scheme —
  `javascript:`, `data:`, and protocol-relative `//host` are all rejected.

## 5. Validate, then view

```sh
pnpm content:check   # velite --strict — frontmatter schema + the lockdown above
pnpm build            # also catches bad Block *props* (Zod schema mismatches, not just syntax)
```

If `content:check` passes, the MDX itself is legal; `pnpm build` renders every
page and is what catches, e.g., a required prop you forgot. Then view the
page: `pnpm dev` serves at `http://localhost:3000` by default (respects
`$PORT`) — check whether a dev server is already running in this workspace
before starting a second one. A green build is necessary but not sufficient:
click through the actual page before calling the change done.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.