diff --git a/.changeset/opinionated-mdx-rebuild.md b/.changeset/opinionated-mdx-rebuild.md
new file mode 100644
index 0000000..fcc25dc
--- /dev/null
+++ b/.changeset/opinionated-mdx-rebuild.md
@@ -0,0 +1,26 @@
+---
+"@fulldotdev/sitex": minor
+---
+
+Rebuild Sitex around MDX-only pages and a framework-owned document head.
+
+Breaking changes:
+
+- Pages are MDX files only. TSX files in `src/pages` fail the build with a migration error; move page markup into layouts and blocks. Dynamic `[param]` routes and the `paths` export are removed.
+- The `site` config is required: `sitex({ site: { name, url } })`. Optional fields: `description`, `titleTemplate`, `locale`, `image`, `favicon`, `twitter`, `organization`.
+- Frontmatter follows a validated schema: `layout`, `title`, and `description` are required. `type` (`webpage` | `article` | `faq`) drives JSON-LD structured data; `image`, `canonical`, and `index` are optional overrides. Articles require `publishedAt`; FAQ pages require `questions`.
+- Apps do not provide `index.html`. Layouts render the full document (``/`
`/``) and Sitex parses the rendered page to add missing head defaults (charset, viewport, canonical, og:url, robots, favicon, generator) and hoist JSON-LD into the head.
+- `src/styles/global.css` is required and automatically imported as the global stylesheet entry. Tailwind is not bundled by Sitex; apps that use Tailwind install `tailwindcss` and `@tailwindcss/vite` themselves.
+- `MarkdownLayoutProps` is renamed to `LayoutProps` and now includes the page schema fields. `PageContext`, `StaticPath`, and `StaticPaths` are removed.
+- `sitex:pages` has a stable, statically declared `Page` type; generated `.sitex/pages.d.ts` files are no longer written (legacy ones are cleaned up automatically).
+- The `src/pages/examples/**` exemption is removed; everything under `src/pages` is a page.
+- The main entry no longer exports UI components (`Layout`, `Section`, `Typography`, `Price`, and friends). They moved to the Sitex shadcn registry; install them with `shadcn add` from `https://sitex.full.dev/r/{name}.json`. `@fulldotdev/sitex` now exports types only, plus the `/plugin` and `/island` entries.
+- Globals moved to `src/globals/index.yaml` as the root locale file. The root locale name comes from the new `site.locale` option (default `"en"`); additional files like `nl.yaml` define locale variants served under `/nl` route prefixes and are exposed through the `locales` export of `sitex:globals`. A single named file like `en.yaml` still works as the root locale.
+- Static HTML is rendered in-process during `vp build` through a dedicated SSR build environment; Sitex no longer boots a second Vite server after the client build.
+
+New:
+
+- Link prefetching is on by default: speculation rules let supporting browsers prerender on hover, with a small hover-prefetch fallback module for others. Opt out with `prefetch: false` or `data-no-prefetch`.
+- `vp build` writes `sitemap.xml` (respects `index: false` and `canonical` overrides, `lastmod` from `updatedAt`/`publishedAt`) and a default `robots.txt` when the app ships none.
+- Path-alias imports (like `@/components/alert`) now resolve inside MDX pages on newer Vite resolvers.
+- Frontmatter validation errors are targeted per field, and MDX pages are typechecked against their layout props via `.sitex/typecheck`.
diff --git a/.gitignore b/.gitignore
index 0e4ecde..910882e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,6 @@ coverage/
# Local Netlify folder
.netlify
+
+# generated shadcn registry
+apps/docs/public/r/
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 4557048..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# AGENTS.md
-
-- SiteX is a Vite-based React framework for static content sites.
-- The repo follows the Vite+ monorepo shape: docs app in `apps/docs`, package in `packages/sitex`.
-- Docs routes live in `apps/docs/src/pages` and render to static HTML by default.
-- Client interactivity uses imported islands with `client:load`, `client:only`, `client:visible`, `client:idle`, or `client:media`.
-- Keep routes thin: routes choose layouts, layouts compose blocks, blocks use UI.
-- Do not add new rendering modes, generated routes, content collections, or new directives casually.
diff --git a/CONTEXT.md b/CONTEXT.md
deleted file mode 100644
index 016365a..0000000
--- a/CONTEXT.md
+++ /dev/null
@@ -1,224 +0,0 @@
-# Sitex
-
-Sitex is an experimental framework for building static sites from React route modules with narrowly scoped client interactivity.
-
-## Language
-
-**Static route**:
-A URL that Sitex can render to static HTML ahead of serving a request. A static route may be authored directly as a local route file today, and may later be generated from data without becoming an SSR route.
-_Avoid_: SSR route, request route
-
-**Local route**:
-A static route represented by one concrete route file in the app. This is the current v0 routing target because it keeps routing explicit and simple.
-_Avoid_: dynamic route
-
-**Route module**:
-A file discovered by Sitex routing. A local route module default-exports a page component; a future generated static route module may default-export `definePages(...)`.
-_Avoid_: page
-
-**Page component**:
-The React component exported by a local route module to render one static route.
-_Avoid_: route, entry
-
-**Generated static route**:
-A static route produced from one route module plus one or more data entries. This is a possible future routing shape for external or local data sources, but not the current v0 default.
-_Avoid_: dynamic route, SSR route
-
-**Generated static route module**:
-A future route module that explicitly exports `definePages(...)` to produce multiple static routes. Generated static route modules should be signaled by their API shape, not by catch-all filename syntax.
-_Avoid_: catch-all route, `[...page]`
-
-**Route entry**:
-One data item used by a future generated static route module to produce one static route.
-_Avoid_: page, route
-
-**App-provided data**:
-Data loaded or declared by the app and passed into Sitex route APIs. Sitex core should not require a built-in content collection model.
-_Avoid_: Sitex content collection
-
-**Recommended app structure**:
-An app-owned organization where route modules stay thin, layouts compose page chrome, blocks define reusable page sections, and UI primitives hold low-level design-system components. Sitex recommends this structure but does not require or interpret it.
-_Avoid_: framework layout system
-
-**Reference app**:
-An app that both documents Sitex and proves the recommended app structure against real static routes and islands. The docs app is the current reference app.
-_Avoid_: component showcase
-
-**Repository shape**:
-The repo follows Vite+'s monorepo shape: `packages/sitex` contains the framework package and `apps/docs` contains the docs/reference app.
-_Avoid_: mixing package and app concerns in the repository root
-
-**Framework import**:
-Published apps should import Sitex through package-style public imports such as `@fulldotdev/sitex/plugin`. The in-repo docs app may import package source directly when dogfooding unpublished changes.
-_Avoid_: exposing framework internals as app-facing public APIs
-
-**Hidden island runtime**:
-The React runtime code Sitex uses under the hood to mark, load, hydrate, and client-render islands. App authors should not need to import this directly.
-_Avoid_: React helpers
-
-**Public surface**:
-The current public surface should be the Sitex Vite plugin and JSX client directives. Other runtime pieces should stay hidden behind compiler/plugin behavior.
-_Avoid_: direct island imports, document helper imports
-
-**Router export**:
-Route rendering internals are framework-owned virtual module code. Apps should not import route rendering helpers directly.
-_Avoid_: public router helpers
-
-**TypeScript config export**:
-`@fulldotdev/sitex/tsconfig` may remain as a convenience for framework-required compiler settings and JSX directive types. It should not become a broad app preference bundle.
-_Avoid_: app style config
-
-**Directive validation**:
-TypeScript may broadly allow `client:*` attributes, but the compiler is responsible for rejecting invalid directive placement such as HTML elements or local components.
-_Avoid_: type-only enforcement
-
-**Package build**:
-Sitex is published as `@fulldotdev/sitex` from `packages/sitex`. Public app-facing imports should use exported package entrypoints, while the in-repo docs app may import package source directly when dogfooding unpublished changes.
-_Avoid_: app imports from unexported internals
-
-**Production build**:
-The public production build command should be `vp build`. Any custom Sitex build script is experiment scaffolding; cleanup should keep static route rendering and asset emission inside the Sitex Vite plugin.
-_Avoid_: separate Sitex build command
-
-**Development server**:
-The public development command should be `vp dev`. Sitex development behavior should come from the Vite plugin rather than a custom dev server command.
-_Avoid_: separate Sitex dev command
-
-**Document shell**:
-The app-owned HTML structure for a page, including ``, ``, and ``. Sitex supplies small document primitives, but the app decides the shell through its own layouts.
-_Avoid_: framework document, global app root
-
-**Root document shell**:
-The ordinary TSX document markup returned by each route render through the app's root layout. Sitex should not require Sitex-specific document components or validate more than asset injection needs.
-_Avoid_: page fragment
-
-**Document metadata**:
-Plain TSX rendered inside the app-owned document shell, such as `` and `` tags. Sitex should not require helper components for ordinary metadata.
-_Avoid_: metadata API
-
-**Build asset injection**:
-Framework-owned insertion of production asset tags such as stylesheet and island client scripts. Sitex should not require app-placed `HeadContent` or `Scripts` helper components for this.
-_Avoid_: document helper components
-
-**Style entry**:
-An app-owned CSS import that Vite includes in the module graph. Sitex should support normal Vite CSS imports wherever the app chooses to place them.
-_Avoid_: required global stylesheet component
-
-**CSS API**:
-Sitex should not expose a CSS-specific API. Styling should flow through normal Vite CSS imports and Sitex should ensure those assets appear in static output.
-_Avoid_: stylesheet helper, styles config
-
-**Island**:
-An imported React component that opts into browser interactivity inside otherwise static HTML.
-_Avoid_: app root, hydrated page
-
-**Island root**:
-The imported React component that carries a `client:*` directive. An island root cannot be an HTML element or a component declared locally inside the same route module.
-_Avoid_: intrinsic island, inline island
-
-**Rendering mode**:
-The way Sitex decides where a page or component is rendered. Current page modes are static by default and explicit request-time rendering with `export const render = "server"`. Current island modes are static plus client rendering with `client:load`, `client:visible`, `client:idle`, or `client:media`, and browser-only rendering with `client:only`.
-_Avoid_: hydration mode
-
-**Static rendering**:
-The default rendering mode. Static rendering produces HTML ahead of serving requests and does not attach browser interactivity.
-_Avoid_: server rendering
-
-**Client rendering**:
-Browser rendering used for interactivity. `client:load`, `client:visible`, `client:idle`, and `client:media` combine static rendering with client rendering, while `client:only` uses client rendering only.
-_Avoid_: hydration
-
-**Server rendering**:
-Request-time page rendering on a server. Sitex supports this only through explicit route opt-in with `export const render = "server"`.
-_Avoid_: static rendering
-
-**Server directive**:
-A possible future directive for request-time server rendering behavior. Server directives are reserved conceptually but are not part of the current Sitex API.
-_Avoid_: current API
-
-**Client-loaded island**:
-An island marked with `client:load`. A client-loaded island is statically rendered first and then client-rendered for interactivity.
-_Avoid_: browser-only island
-
-**Lazy island**:
-An island marked with `client:visible`, `client:idle`, or `client:media`. A lazy island is statically rendered first and then client-rendered after the chosen browser condition is met.
-_Avoid_: separate route, server island
-
-**Static children**:
-Children passed through an island boundary as static slot HTML. Static children stay visible but do not become live React children in the parent island's client tree.
-_Avoid_: included children, live React children
-
-**Static slot model**:
-The Astro-like model for island children. Children are rendered as static slot HTML, and any nested `client:*` components inside them are independent islands rather than part of the parent island's live React tree.
-_Avoid_: live subtree hydration
-
-**Independent island**:
-An island that client-renders separately from any surrounding island. Static children may contain independent islands, but those islands do not share a React tree, context, or state with the surrounding island.
-_Avoid_: included island, subtree island
-
-**Island props**:
-Data passed from static rendering into an island root. Island props must be JSON-serializable data; behavior should live inside the island root component.
-_Avoid_: function props, React element props
-
-**Prop serialization**:
-Sitex v0 should use a simple JSON-only serialization boundary for island props. Astro-like support for `Date`, `Map`, `Set`, `BigInt`, and similar values is a possible later enhancement, not cleanup scope.
-_Avoid_: rich serializer
-
-**Invalid island prop**:
-A value that cannot safely cross the static-to-client boundary, such as a function, symbol, React element, cyclic object, or ambiguous dropped value. Invalid island props should cause a hard framework error.
-_Avoid_: stringified function, silently dropped prop
-
-**Prop serialization error**:
-A targeted Sitex error raised when an island prop cannot be represented by the v0 JSON-only serializer.
-_Avoid_: raw `JSON.stringify` failure
-
-**Browser-only island**:
-An island marked with `client:only`. A browser-only island has no initial static render; it is invisible until the browser renders it.
-_Avoid_: hydrated island, fallback island
-
-**Simplicity principle**:
-Sitex should keep the current experiment extremely small and explicit, even when that postpones useful framework conveniences.
-_Avoid_: complete framework
-
-## Flagged Ambiguities
-
-**Hydration naming**:
-The code currently uses `hydration` names for the island runtime, but the preferred product language is rendering modes: static rendering, client rendering, and future server rendering. Do not change the code during the grilling session; revisit this in the later cleanup/refactor.
-
-**Document helpers**:
-The current `HeadContent` and `Scripts` helpers leak build asset concerns into the app-owned document shell. Cleanup should remove them and replace them with framework-owned build asset injection.
-
-**Document validation**:
-Sitex should not validate document structure beyond what it needs for asset injection. Missing insertion points should produce targeted errors only when injection cannot proceed.
-
-**Nested island transform**:
-Static children should be allowed to contain independent islands. The current compiler may miss literal nested `client:*` directives inside the same transformed JSX subtree; revisit this in the later cleanup/refactor.
-
-**Subtree island API**:
-The earlier `children:include` experiment made a parent island and its children one React tree. It may never be needed because shared client state can be modeled by creating a larger island root component.
-
-**Shared client tree**:
-A set of interactive components that need shared React state or context. A shared client tree should be composed inside one island root component rather than through a subtree hydration directive.
-_Avoid_: `children:include`
-
-## Example Dialogue
-
-Developer: "Should this service page be a server route?"
-
-Domain expert: "No. It should be a static route unless it actually needs request-time data."
-
-Developer: "Where do page metadata and scripts belong?"
-
-Domain expert: "In the app-owned document shell. Sitex can provide primitives, but the app's layouts own the actual document structure."
-
-Developer: "This sidebar needs search and theme toggling. Is the whole page an island?"
-
-Domain expert: "No. The sidebar can be an island, while the page body remains static children."
-
-Developer: "Should `client:only` include fallback children?"
-
-Domain expert: "No. A browser-only island has no initial static render; it appears when the browser renders it."
-
-Developer: "Filters and results need shared React state. Should I use `children:include`?"
-
-Domain expert: "No. Make a `Dashboard` island root that renders the provider, filters, and results internally."
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a8d54fb..f9e389d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-SiteX is experimental. Keep changes small, explicit, and aligned with the current scope.
+Sitex is experimental. Keep changes small, explicit, and aligned with the current scope.
## Local setup
@@ -20,7 +20,7 @@ pnpm ready
## Scope
-- Keep the public API small: the Vite plugin, TypeScript config, and client directives.
-- Prefer static routes and explicit TSX files.
-- Do not add SSR, generated routes, content collections, or new client directives without first documenting the decision.
-- Preserve the reference app as a simple content-site example.
+- Keep the public API small: the Vite plugin, the type exports, the TypeScript config, and the `client:*` directives.
+- Pages are static MDX files rendered through app layouts. Document the decision before adding rendering modes, generated routes, or content collections.
+- UI components live in `packages/ui` and ship through the shadcn registry, not npm.
+- The docs app in `apps/docs` is the reference app; keep it a simple content site.
diff --git a/apps/docs/components.json b/apps/docs/components.json
index fc691b6..d9441da 100644
--- a/apps/docs/components.json
+++ b/apps/docs/components.json
@@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
- "css": "src/styles/globals.css",
+ "css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
diff --git a/apps/docs/package.json b/apps/docs/package.json
index f6f429f..316a3ac 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -10,29 +10,30 @@
},
"dependencies": {
"@base-ui/react": "catalog:",
- "@fontsource-variable/geist": "^5.2.9",
+ "@fontsource-variable/geist": "catalog:",
"@fulldotdev/sitex": "workspace:*",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "lucide-react": "^1.16.0",
- "react": "^19.1.1",
- "react-dom": "^19.1.1",
- "@shikijs/core": "^4.1.0",
- "@shikijs/engine-javascript": "^4.1.0",
- "@shikijs/langs": "^4.1.0",
- "@shikijs/themes": "^4.1.0",
- "tailwind-merge": "^3.6.0",
- "tw-animate-css": "^1.4.0"
+ "@shikijs/core": "catalog:",
+ "@shikijs/engine-javascript": "catalog:",
+ "@shikijs/langs": "catalog:",
+ "@shikijs/themes": "catalog:",
+ "class-variance-authority": "catalog:",
+ "clsx": "catalog:",
+ "lucide-react": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "schema-dts": "catalog:",
+ "tailwind-merge": "catalog:",
+ "tw-animate-css": "catalog:"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.1.13",
- "@types/react": "^19.1.12",
- "@types/react-dom": "^19.1.9",
- "@vitejs/plugin-react": "^5.0.2",
- "shadcn": "^4.8.3",
- "tailwindcss": "^4.1.13",
- "typescript": "^5.9.2",
- "vite": "^8.0.16",
- "vite-plus": "^0.1.24"
+ "@tailwindcss/vite": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "@vitejs/plugin-react": "catalog:",
+ "shadcn": "catalog:",
+ "tailwindcss": "catalog:",
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vite-plus": "catalog:"
}
}
diff --git a/apps/docs/public/favicon.svg b/apps/docs/public/favicon.svg
index 2cd3557..64b8581 100644
--- a/apps/docs/public/favicon.svg
+++ b/apps/docs/public/favicon.svg
@@ -1,7 +1,4 @@
diff --git a/apps/docs/src/assets/.gitkeep b/apps/docs/src/assets/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/apps/docs/src/assets/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/apps/docs/src/components/blocks/sidebar-1.tsx b/apps/docs/src/components/blocks/sidebar-1.tsx
index ee2d5c3..56dad6f 100644
--- a/apps/docs/src/components/blocks/sidebar-1.tsx
+++ b/apps/docs/src/components/blocks/sidebar-1.tsx
@@ -2,25 +2,11 @@
import { type ReactNode } from "react"
-import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react"
+import { ExternalLinkIcon } from "lucide-react"
+import { ModeToggle } from "@/components/mode-toggle"
import { ThemeProvider } from "@/components/theme-provider"
-import {
- Breadcrumb,
- BreadcrumbEllipsis,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
import { Header, HeaderContainer, HeaderGroup } from "@/components/ui/header"
import { Logo, LogoImage, LogoText } from "@/components/ui/logo"
import { Separator } from "@/components/ui/separator"
@@ -37,35 +23,30 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
-import { ThemeToggle } from "@/components/ui/theme-toggle"
import { cn } from "@/lib/utils"
type NavigationGroup = {
label: string
href?: string
- links?: {
+ links?: readonly {
label: string
href: string
}[]
}
+type SidebarSection = {
+ label: string
+ href: string
+ navigation: readonly NavigationGroup[]
+}
+
type Sidebar1Props = {
className?: string
logo: {
label: string
href: string
}
- breadcrumb: {
- items: {
- label: string
- href: string
- }[]
- menu: {
- label: string
- href: string
- }[]
- }
- navigation: NavigationGroup[]
+ sections: readonly SidebarSection[]
githubRepo: string
path: string
children?: ReactNode
@@ -77,24 +58,36 @@ function normalizePath(path: string) {
return path.replace(/\/$/, "")
}
-function toDomId(value: string) {
- return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "")
+function findActiveSection(
+ sections: readonly SidebarSection[],
+ currentPath: string
+) {
+ const matches = sections.filter((section) => {
+ const sectionPath = normalizePath(section.href)
+
+ return (
+ currentPath === sectionPath || currentPath.startsWith(`${sectionPath}/`)
+ )
+ })
+
+ return matches.sort((a, b) => b.href.length - a.href.length)[0] ?? sections[0]
}
function Sidebar1({
className,
logo,
- breadcrumb,
- navigation,
+ sections,
githubRepo,
path,
children,
}: Sidebar1Props) {
const currentPath = normalizePath(path)
+ const activeSection = findActiveSection(sections, currentPath)
+ const navigation = activeSection?.navigation ?? []
return (
-
+ Sitex
+ {sections.length > 1 ? (
+
- {children}
-
- )
-}
diff --git a/apps/docs/src/lib/globals.ts b/apps/docs/src/lib/globals.ts
new file mode 100644
index 0000000..03f826a
--- /dev/null
+++ b/apps/docs/src/lib/globals.ts
@@ -0,0 +1,40 @@
+import globals from "sitex:globals"
+
+export type NavigationLink = {
+ href: string
+ label: string
+}
+
+export type NavigationGroup = {
+ label: string
+ links: readonly NavigationLink[]
+}
+
+export type SidebarSection = {
+ label: string
+ href: string
+ navigation: readonly NavigationGroup[]
+}
+
+export type DocsGlobals = {
+ name: string
+ logo: {
+ label: string
+ href: string
+ }
+ header: {
+ githubRepo: string
+ navigation: readonly NavigationLink[]
+ }
+ sidebar: {
+ sections: readonly SidebarSection[]
+ }
+}
+
+const docsGlobals = globals as DocsGlobals
+
+export { docsGlobals as globals }
+
+export const docsNavigation = docsGlobals.sidebar.sections.flatMap((section) =>
+ section.navigation.flatMap((group) => group.links)
+)
diff --git a/apps/docs/src/pages/docs/components.mdx b/apps/docs/src/pages/docs/components.mdx
new file mode 100644
index 0000000..fea49fb
--- /dev/null
+++ b/apps/docs/src/pages/docs/components.mdx
@@ -0,0 +1,58 @@
+---
+layout: "doc"
+title: "Components"
+description: "Static rendering by default, client directives, and the pre-built component registry."
+---
+
+Components are plain React. Everything renders to static HTML at build time, and Sitex follows each page's module graph to include exactly the CSS that page imports. Pages ship zero JavaScript unless a component opts in.
+
+## Client Directives
+
+Imported React components opt into browser rendering with a `client:*` directive. Directives live in TSX (layouts, blocks, components), never in MDX bodies, and never on HTML elements or same-file components.
+
+```tsx
+import Search from "@/components/search"
+
+export default function Header() {
+ return
+}
+```
+
+1. `client:load` — hydrates immediately. For controls that must work right away.
+2. `client:idle` — hydrates when the browser is idle. For page chrome like sidebars.
+3. `client:visible` — hydrates when scrolled into view. For below-the-fold widgets.
+4. `client:media="(min-width: 64rem)"` — hydrates when the media query matches.
+5. `client:only` — skips static rendering, renders in the browser only.
+
+Island props must be JSON-serializable — functions, symbols, React elements, and cyclic values throw at build time. Children pass through as static HTML; a nested `client:*` component inside them becomes its own independent island. Components that share client state belong inside one island root.
+
+## Pre-Built Components
+
+Sitex ships components for the pieces content sites keep needing — sections, typography, code blocks, a table of contents, a price display. They follow the [shadcn/ui](https://ui.shadcn.com) conventions (Tailwind, `cn` from `@/lib/utils`, your theme tokens) and install from the Sitex registry: the source lands in `src/components/ui`, where you own and edit it. No npm package, no version to track, and they compose with regular shadcn/ui components.
+
+Initialize shadcn once (creates `components.json` and `src/lib/utils.ts`), then add what you need:
+
+```bash
+pnpm dlx shadcn@latest init
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/section.json
+```
+
+| Component | Description |
+| ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| [`banner`](/docs/components/banner) | Dismissable site-wide announcement banner. |
+| [`code`](/docs/components/code) | Code block with shiki highlighting and a copy button; drop-in MDX `pre` replacement. |
+| [`gradient`](/docs/components/gradient) | Decorative gradient surface for hero and section backgrounds. |
+| [`header`](/docs/components/header) | Site header with container and grouping primitives. |
+| [`layout`](/docs/components/layout) | Document primitives for Sitex layouts: html, head with SEO meta and JSON-LD, body, and main. |
+| [`marquee`](/docs/components/marquee) | Continuously scrolling strip for logos and quotes. |
+| [`price`](/docs/components/price) | Locale-aware price display with sale and discount formatting. |
+| [`rating`](/docs/components/rating) | Star rating display. |
+| [`section`](/docs/components/section) | Page section with consistent vertical rhythm and container. |
+| [`theme-toggle`](/docs/components/theme-toggle) | Light/dark theme provider and toggle button. |
+| [`tile`](/docs/components/tile) | Card-like tile and tile group for feature grids. |
+| [`toc`](/docs/components/toc) | Table of contents navigation for page headings. |
+| [`typography`](/docs/components/typography) | Prose typography primitives for MDX content. |
+
+## Prefetch
+
+Navigation is prefetched by default: Sitex injects speculation rules so supporting browsers prerender links on hover (instant navigation), plus a small hover-prefetch script for the rest. Disable it with `sitex({ prefetch: false })`, or per link tree with a `data-no-prefetch` attribute.
diff --git a/apps/docs/src/pages/docs/components/banner.mdx b/apps/docs/src/pages/docs/components/banner.mdx
new file mode 100644
index 0000000..c8a5a80
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/banner.mdx
@@ -0,0 +1,38 @@
+---
+layout: "doc"
+title: "Banner"
+description: "Dismissable site-wide announcement banner."
+---
+
+A site-wide announcement bar with an optional close button and session-based dismissal. Use it for launch notices, promos, or maintenance messages at the top of a layout.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/banner.json
+```
+
+## Usage
+
+```tsx
+import { Banner, BannerContainer } from "@/components/ui/banner"
+
+export function SiteBanner() {
+ return (
+
+
+
SiteX 0.3 is out — read the release notes.
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `Banner` renders an `aside` with two variants: `default` (full-width, inverted colors) and `floating` (rounded, bordered, centered).
+2. `storageKey` makes dismissal persist for the session: closing stores a flag in `sessionStorage` and the banner stays hidden on later renders.
+3. `BannerContainer` centers the content and renders the close button; pass `showClose={false}` to hide it, or `onClose` to intercept the click (call `event.preventDefault()` to keep the banner open).
+4. `bannerVariants` is exported for reusing the variant classes elsewhere.
+
+The dismiss behavior and `sessionStorage` check are client-side, so hydrate the banner as an island with a Sitex client directive where you use it in a layout or block, e.g. ``.
diff --git a/apps/docs/src/pages/docs/components/code.mdx b/apps/docs/src/pages/docs/components/code.mdx
new file mode 100644
index 0000000..a5fb272
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/code.mdx
@@ -0,0 +1,43 @@
+---
+layout: "doc"
+title: "Code"
+description: "Code block with shiki syntax highlighting and a copy button; drop-in MDX pre replacement."
+---
+
+A syntax-highlighted code block powered by shiki, with light/dark themes and a copy button. It is designed as the `pre` replacement for MDX code fences, and also works standalone.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/code.json
+```
+
+## Usage
+
+Wire it up as the MDX `pre` component in `vite.config.ts`:
+
+```ts
+import { sitex } from "sitex"
+
+export default defineConfig({
+ plugins: [sitex({ mdx: { components: { pre: "@/components/ui/code" } } })],
+})
+```
+
+Every fenced code block in your MDX pages now renders through this component. For direct use, import the named `Code` export:
+
+```tsx
+import { Code } from "@/components/ui/code"
+
+export function InstallSnippet() {
+ return
+}
+```
+
+Key details:
+
+1. The default export is a drop-in `pre` replacement that reads the code text and `language-*` class from the MDX-generated `pre > code` element.
+2. The named `Code` export takes `code`, an optional `lang`, and `className`.
+3. Supported languages: `bash`, `css`, `html`, `json`, `markdown`, `mdx`, `ts`, `tsx`, `yaml`; anything else falls back to plain `text`.
+4. Highlighting uses the github-light and github-dark themes and switches with your site's dark mode.
+5. The copy button (`code-copy-button.tsx`, installed with this item) carries its own `client:load` directive, so it hydrates as its own island automatically — no directive needed where you use `Code`.
diff --git a/apps/docs/src/pages/docs/components/gradient.mdx b/apps/docs/src/pages/docs/components/gradient.mdx
new file mode 100644
index 0000000..caee776
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/gradient.mdx
@@ -0,0 +1,40 @@
+---
+layout: "doc"
+title: "Gradient"
+description: "Decorative gradient surface for hero and section backgrounds."
+---
+
+An absolutely positioned, mask-based gradient overlay that fades the `primary` color out from an edge or the center. Use it inside a `relative` container to give heroes and sections a soft background glow.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/gradient.json
+```
+
+## Usage
+
+```tsx
+import { Gradient } from "@/components/ui/gradient"
+
+export function Hero() {
+ return (
+
+
+
+
Build sites fast
+
+
+ )
+}
+```
+
+Key details:
+
+1. `variant` is `radial` (default) or `linear`, implemented with CSS mask images.
+2. `direction` sets the gradient origin: `left`, `right`, `top`, `bottom` (default), or `center`.
+3. The element fills its nearest positioned ancestor (`absolute inset-0`), is `pointer-events-none`, and is hidden from assistive tech with `aria-hidden`.
+4. It renders at 50% opacity with the `primary` background color; override both via `className`.
+5. `gradientVariants` is exported for reusing the variant classes elsewhere.
+
+The component is purely decorative and static, so it needs no client directive.
diff --git a/apps/docs/src/pages/docs/components/header.mdx b/apps/docs/src/pages/docs/components/header.mdx
new file mode 100644
index 0000000..87b6b00
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/header.mdx
@@ -0,0 +1,44 @@
+---
+layout: "doc"
+title: "Header"
+description: "Site header with container and grouping primitives."
+---
+
+Structural primitives for a site header: the header bar itself, a centered container, and groups for clustering navigation, logos, and actions.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/header.json
+```
+
+## Usage
+
+```tsx
+import { Header, HeaderContainer, HeaderGroup } from "@/components/ui/header"
+
+export function SiteHeader() {
+ return (
+
+
+
+ Acme
+
+
+ Docs
+ Pricing
+
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `Header` renders a `header` element with two variants: `default` (plain full-width bar) and `floating` (rounded, bordered, centered card that respects the `--container` CSS variable).
+2. `HeaderContainer` centers content with a `max-w-7xl` container and horizontal padding.
+3. `HeaderGroup` is a flex row for clustering items; use `className="ml-auto"` to push a group to the far side.
+4. `headerVariants` is exported for reusing the variant classes elsewhere.
+
+All parts are static and render without a client directive; add one only if you place interactive islands inside.
diff --git a/apps/docs/src/pages/docs/components/layout.mdx b/apps/docs/src/pages/docs/components/layout.mdx
new file mode 100644
index 0000000..f4fee52
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/layout.mdx
@@ -0,0 +1,58 @@
+---
+layout: "doc"
+title: "Layout"
+description: "Document primitives for Sitex layouts: html, head with SEO meta and JSON-LD, body, and main."
+---
+
+Document primitives for building Sitex layouts: `Layout` renders the `html` element, `LayoutHead` the `head` with SEO meta tags and JSON-LD, plus `LayoutBody` and `LayoutMain` for the page shell.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/layout.json
+```
+
+## Usage
+
+```tsx
+import {
+ Layout,
+ LayoutBody,
+ LayoutHead,
+ LayoutMain,
+} from "@/components/ui/layout"
+
+export default function BaseLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ {children}
+
+
+ )
+}
+```
+
+Key details:
+
+1. `Layout`, `LayoutBody`, and `LayoutMain` are thin wrappers over `html`, `body`, and `main` with base styling; all standard element props pass through.
+2. `LayoutHead` turns props into meta tags: `title`, `description`, `name` (as `og:site_name`), `image` (string or object with `src`, `alt`, `width`, `height`, `type`), `openGraphType` (alias `type`), and `twitterCard` (`summary` or `summary_large_image`).
+3. For `openGraphType="article"`, `publishedAt` and `updatedAt` emit `article:published_time` and `article:modified_time`.
+4. `jsonLd` accepts a single `schema-dts` object (`Graph` or `WithContext`) or an array, serialized into a `script type="application/ld+json"` tag.
+5. Children of `LayoutHead` render after the generated tags, so you can add extra `link` or `meta` elements. `LayoutHeadProps` and `LayoutJsonLd` types are exported.
diff --git a/apps/docs/src/pages/docs/components/marquee.mdx b/apps/docs/src/pages/docs/components/marquee.mdx
new file mode 100644
index 0000000..6f8d121
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/marquee.mdx
@@ -0,0 +1,39 @@
+---
+layout: "doc"
+title: "Marquee"
+description: "Continuously scrolling strip for logos and quotes."
+---
+
+A CSS-animated strip that scrolls its children in a continuous loop. Use it for logo walls, testimonial tickers, or any row of items you want to drift across the page.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/marquee.json
+```
+
+## Usage
+
+```tsx
+import { Marquee } from "@/components/ui/marquee"
+
+export function LogoMarquee() {
+ return (
+
+ )
+}
+```
+
+Key props:
+
+1. `direction` sets the scroll axis and heading: `"left"` (default), `"right"`, `"top"`, or `"bottom"`. Vertical directions stack the children in a column.
+2. `duration` (or the legacy `time` prop, default `30`) controls one loop's length; a number is treated as seconds, a string is passed through as a CSS duration.
+3. `gap` (default `"1rem"`) spaces the items and is also used to offset the loop seam.
+4. `infinite` (default `true`) duplicates the children in an `aria-hidden` copy so the loop is seamless; set it to `false` to animate a single pass without duplication.
+5. `pauseOnHover` pauses the animation while the pointer is over the marquee, and `variant` picks the edge treatment: `"gradient"` (default, fades the edges with a mask) or `"solid"`.
+
+The animation is pure CSS (it also respects `prefers-reduced-motion`), so no client directive is needed. `marqueeVariants` is exported for reusing the variant classes elsewhere.
diff --git a/apps/docs/src/pages/docs/components/price.mdx b/apps/docs/src/pages/docs/components/price.mdx
new file mode 100644
index 0000000..d5f82ad
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/price.mdx
@@ -0,0 +1,41 @@
+---
+layout: "doc"
+title: "Price"
+description: "Locale-aware price display with sale and discount formatting."
+---
+
+A price display that formats amounts with `Intl.NumberFormat` and handles compare-at pricing with a strikethrough and a discount badge. Use it on product cards, pricing tables, and checkout summaries.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/price.json
+```
+
+## Usage
+
+```tsx
+import { Price, PriceUnit, PriceValue } from "@/components/ui/price"
+
+export function ProductPrice() {
+ return (
+
+
+ per month
+
+ )
+}
+```
+
+Key parts:
+
+1. `Price` is a flex wrapper that lays out its children; `PriceUnit` renders a muted suffix such as a billing period.
+2. `PriceValue` formats `price` and optional `compareAt`. Both accept a `PriceValueType`: a number, a numeric string, a range string like `"10 - 20"`, or a `[min, max]` tuple (ranges render as `"$10.00 - $20.00"`). It renders nothing when `price` is empty.
+3. `currency` (default `"USD"`) and `locale` (default `"en-US"`) drive the currency formatting.
+4. When `compareAt` is higher than `price`, the original price is shown struck through with a "Save …" badge; `discountFormat` switches the badge between `"percentage"` (default) and `"amount"`, and `showDiscount={false}` hides it. The `variant` prop styles the main value: `"default"` or `"sale"` (muted with line-through).
+5. The helpers `formatPriceValue`, `formatPriceDiscount`, and `parseSinglePriceValue` are exported for formatting prices outside the component, along with `priceValueVariants`.
diff --git a/apps/docs/src/pages/docs/components/rating.mdx b/apps/docs/src/pages/docs/components/rating.mdx
new file mode 100644
index 0000000..f34e4c1
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/rating.mdx
@@ -0,0 +1,36 @@
+---
+layout: "doc"
+title: "Rating"
+description: "Star rating display."
+---
+
+A read-only row of five stars that fills to match a numeric rating, including fractional values. Use it for review scores on product cards and testimonials.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/rating.json
+```
+
+## Usage
+
+```tsx
+import { Rating } from "@/components/ui/rating"
+
+export function ReviewScore() {
+ return (
+
+
+ 4.5 out of 5
+
+ )
+}
+```
+
+Key points:
+
+1. `Rating` is the only export; it renders a `div` and accepts all standard `div` props.
+2. `value` (default `5`) sets how many of the five stars are filled; fractions like `4.5` render a partially filled star.
+3. The stars scale with the surrounding font size (they are sized in `em`), so set `className="text-sm"` or similar to resize them; filled stars use the `primary` color and empty stars a muted track.
+
+It is purely presentational with no state or handlers, so no client directive is needed.
diff --git a/apps/docs/src/pages/docs/components/section.mdx b/apps/docs/src/pages/docs/components/section.mdx
new file mode 100644
index 0000000..4e773be
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/section.mdx
@@ -0,0 +1,39 @@
+---
+layout: "doc"
+title: "Section"
+description: "Page section with consistent vertical rhythm and container."
+---
+
+A `section` element with consistent vertical padding plus a centered container for its content. Use it as the outer shell of every page block so sections share the same rhythm and max width.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/section.json
+```
+
+## Usage
+
+```tsx
+import { Section, SectionContainer } from "@/components/ui/section"
+
+export function FeatureSection() {
+ return (
+
+
+
Features
+
Everything you need to ship a marketing site.
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `Section` renders a `section` with vertical padding, column flex layout, and a scroll margin so anchor links land cleanly below a fixed header.
+2. The `variant` prop picks the shell: `"default"` (full-width on the page background) or `"floating"` (a centered, rounded, bordered card with a shadow, capped at the `--container` width).
+3. `SectionContainer` centers the content, caps it at `max-w-7xl`, and adds horizontal padding and an 8-unit column gap.
+4. `sectionVariants` is exported for reusing the variant classes elsewhere.
+
+Both parts are static wrappers around their children, so no client directive is needed.
diff --git a/apps/docs/src/pages/docs/components/theme-toggle.mdx b/apps/docs/src/pages/docs/components/theme-toggle.mdx
new file mode 100644
index 0000000..b16f3c4
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/theme-toggle.mdx
@@ -0,0 +1,36 @@
+---
+layout: "doc"
+title: "Theme Toggle"
+description: "Light/dark theme provider and toggle button."
+---
+
+A theme provider that applies a `light` or `dark` class to the document root, plus a button that switches between the two. Use it to add a persistent light/dark mode switch to a layout header.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/theme-toggle.json
+```
+
+## Usage
+
+```tsx
+import { ThemeProvider, ThemeToggle } from "@/components/ui/theme-toggle"
+
+export function HeaderThemeToggle() {
+ return (
+
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `ThemeProvider` holds the current theme in React state, writes it to `localStorage` under `storageKey` (default `"theme"`), and toggles the `light`/`dark` class on `document.documentElement`. `defaultTheme` accepts `"light"`, `"dark"`, or `"system"` (default), where `system` follows `prefers-color-scheme`.
+2. `ThemeToggle` renders an icon button (sun/moon) that flips between `light` and `dark` on click. It accepts all `button` props and sets `aria-pressed` and `data-state` based on the active theme.
+3. `useTheme` returns `{ theme, setTheme }` from the provider context for building custom controls.
+4. The `Theme` type (`"dark" | "light" | "system"`) is exported for typing your own code.
+
+Theme state lives entirely on the client, so hydrate the whole tree as an island with a Sitex client directive where you use it in a layout, e.g. ``. The `ThemeToggle` must render inside the same island as its `ThemeProvider`, since the context does not cross island boundaries.
diff --git a/apps/docs/src/pages/docs/components/tile.mdx b/apps/docs/src/pages/docs/components/tile.mdx
new file mode 100644
index 0000000..abc568f
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/tile.mdx
@@ -0,0 +1,45 @@
+---
+layout: "doc"
+title: "Tile"
+description: "Card-like tile and tile group for feature grids."
+---
+
+A card-like tile with a responsive group wrapper for laying out feature grids, link collections, or image galleries. Tiles can render as links, and the group supports grid and masonry layouts.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/tile.json
+```
+
+## Usage
+
+```tsx
+import { Tile, TileContent, TileGroup } from "@/components/ui/tile"
+
+export function FeatureGrid() {
+ return (
+
+
+
+
Pages
+
File-based routing for MDX and TSX pages.
+
+
+
+
+
Layouts
+
Shared wrappers with SEO baked in.
+
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `Tile` has four variants: `default` (transparent border), `inset` (negative horizontal margin), `outline` (bordered), and `muted` (filled background). Passing `href` renders an `a` instead of a `div` and enables a hover state.
+2. `TileGroup` wraps tiles with `variant="default"` (auto-fit grid) or `variant="masonry"` (CSS columns), and `size` `sm`, `default`, or `lg` controlling the minimum column width (240px, 300px, or 400px).
+3. `TileContent` adds the inner padding and vertical flex layout for text content.
+4. `tileVariants` and `tileGroupVariants` are exported for reusing the variant classes elsewhere.
diff --git a/apps/docs/src/pages/docs/components/toc.mdx b/apps/docs/src/pages/docs/components/toc.mdx
new file mode 100644
index 0000000..2c9c9fe
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/toc.mdx
@@ -0,0 +1,52 @@
+---
+layout: "doc"
+title: "Toc"
+description: "Table of contents navigation for page headings."
+---
+
+A table of contents navigation for the headings on the current page. Use it in a doc layout sidebar so readers can jump to any section.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/toc.json
+```
+
+## Usage
+
+Sitex passes the extracted MDX headings to layouts via the `headings` prop, so a doc layout can map them straight into the menu:
+
+```tsx
+import type { LayoutProps } from "@fulldotdev/sitex"
+
+import {
+ Toc,
+ TocMenu,
+ TocMenuItem,
+ TocMenuLink,
+ TocTitle,
+} from "@/components/ui/toc"
+
+export default function DocLayout({ headings }: LayoutProps) {
+ return (
+
+ On this page
+
+ {headings?.map((heading) => (
+
+
+ {heading.label}
+
+
+ ))}
+
+
+ )
+}
+```
+
+Key parts:
+
+1. `Toc` renders the outer `nav`, `TocTitle` the heading (a `p` by default, changeable via `as`), and `TocMenu`/`TocMenuItem` the `ul`/`li` structure.
+2. `TocMenuLink` indents by `depth` (default `2`; `3` and deeper get increasing left padding), matching the `depth` of each Sitex heading.
+3. Pass `isActive` (or `active`) to `TocMenuLink` to highlight the current section; it sets `data-active` and `aria-current="location"`.
diff --git a/apps/docs/src/pages/docs/components/typography.mdx b/apps/docs/src/pages/docs/components/typography.mdx
new file mode 100644
index 0000000..d550619
--- /dev/null
+++ b/apps/docs/src/pages/docs/components/typography.mdx
@@ -0,0 +1,51 @@
+---
+layout: "doc"
+title: "Typography"
+description: "Prose typography primitives for MDX content."
+---
+
+Styled typography primitives for headings, paragraphs, lists, tables, and inline elements. Use them as MDX component overrides or directly wherever you render prose.
+
+## Install
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/typography.json
+```
+
+## Usage
+
+```tsx
+import {
+ TypographyH1,
+ TypographyInlineCode,
+ TypographyLead,
+ TypographyList,
+ TypographyListItem,
+ TypographyP,
+} from "@/components/ui/typography"
+
+export function Intro() {
+ return (
+ <>
+ Getting started
+ Build static sites with MDX and React.
+
+ Install with pnpm add and
+ create your first page.
+
+
+ Install the package.
+ Add a page.
+
+ >
+ )
+}
+```
+
+Key parts:
+
+1. Headings: `TypographyH1` through `TypographyH4`, each with a `size` prop (`sm`, `default`, `lg`) that steps the font size up or down one level.
+2. Text: `TypographyP`, `TypographyLead` (larger muted intro text), `TypographyBlockquote`, `TypographyA` (underlined link), and `TypographyInlineCode`.
+3. Lists: `TypographyList` renders a `ul` by default or an `ol` via `as="ol"`; use `TypographyListItem` for items.
+4. Tables: `TypographyTable` (wrapped in a scrollable bordered container, styleable via `containerClassName`) with `TypographyTableRow`, `TypographyTableHead`, and `TypographyTableCell`; the cells respect the `align` attribute.
+5. `Typography` is a generic wrapper (`div` by default, changeable via `as`) with a `size` prop, useful as a prose container.
diff --git a/apps/docs/src/pages/docs/content.mdx b/apps/docs/src/pages/docs/content.mdx
deleted file mode 100644
index 7a9d43a..0000000
--- a/apps/docs/src/pages/docs/content.mdx
+++ /dev/null
@@ -1,113 +0,0 @@
----
-layout: "doc"
-title: "Pages API"
-description: "Share route metadata with typed getPages and getPage helpers."
-order: 6
----
-
-## Overview
-
-Static route files can export JSON page data. Sitex exposes that data through `sitex:pages`, so indexes, cards, sidebars, and metadata can use route-owned values.
-
-Dynamic routes with bracket params are not included.
-
-## data export
-
-Add `export const data` to a static TSX route.
-
-```tsx
-type PostData = {
- title: string
- description: string
- publishedAt: string
-}
-
-export const data = {
- title: "Building content sites with React",
- description: "How route-owned content keeps pages local.",
- publishedAt: "2026-06-03",
-} satisfies PostData
-
-export default function PostPage() {
- return
{data.title}
-}
-```
-
-MDX pages use frontmatter as page data.
-
-## getPages
-
-Use `getPages` to read all discovered page data, or pass a slash-prefixed path prefix.
-
-```tsx
-import { getPages } from "sitex:pages"
-
-export default async function BlogPage() {
- const posts = await getPages("/blog")
-
- return (
-
-
-
- ))}
-
- )
-}
-```
-
-`page.path` follows the configured trailing-slash style.
-
-## getPage
-
-Use `getPage` when you know the route path.
-
-```tsx
-import { getPage } from "sitex:pages"
-
-export default async function FeaturedPost() {
- const post = await getPage("/blog/building-content-sites")
-
- if (!post) return null
-
- return {post.title}
-}
-```
-
-The return value is `Page | undefined`.
-
-## TypeScript
-
-SiteX writes generated `sitex:pages` types from discovered page data.
-
-```ts
-type Page = {
- path: string
- title: string
- description?: string
- publishedAt?: string
-}
-```
-
-Use `satisfies` when a group of pages should share one data shape.
-
-## Limits
-
-`data` can use imported values, constants, and helper functions, but the final value must be JSON-serializable.
-
-```ts
-import { defaultAuthor } from "@/data/authors"
-
-export const data = {
- title: "Building content sites with React",
- author: defaultAuthor.name,
- tags: ["react", "content"],
-}
-```
-
-Use strings, numbers, booleans, arrays, plain objects, and `null`. Do not use JSX, functions, class instances, dates, maps, sets, or cyclic values.
-
-Because route modules run during discovery, top-level page code must be build-safe. Avoid browser-only globals, network side effects, and request-time state.
diff --git a/apps/docs/src/pages/docs/deployment.mdx b/apps/docs/src/pages/docs/deployment.mdx
deleted file mode 100644
index 2d59585..0000000
--- a/apps/docs/src/pages/docs/deployment.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-layout: "doc"
-title: "Deployment"
-description: "Deploy static SiteX sites and sites with server routes."
-order: 10
----
-
-## Overview
-
-Static-only SiteX sites deploy like normal Vite sites.
-
-If any route exports `render = "server"`, deploy to a host that can run server routes. Netlify, Vercel, and Cloudflare are detected automatically.
-
-## Static Sites
-
-```bash
-pnpm build
-```
-
-Publish the generated output directory for your app.
-
-## Server Routes
-
-Server routes run at request time and are not written as static HTML files.
-
-```tsx
-export const render = "server"
-```
diff --git a/apps/docs/src/pages/docs/folder-structure.mdx b/apps/docs/src/pages/docs/folder-structure.mdx
deleted file mode 100644
index 6f2533a..0000000
--- a/apps/docs/src/pages/docs/folder-structure.mdx
+++ /dev/null
@@ -1,50 +0,0 @@
----
-layout: "doc"
-title: "Folder structure"
-description: "Required folders and recommended project shape."
-order: 11
----
-
-```text
-src/
- pages/
- index.tsx
- docs/
- index.mdx
- layouts/
- base.tsx
- doc.tsx
- post.tsx
- components/
- blocks/
- hero.tsx
- sidebar.tsx
- ui/
- button.tsx
- typography.tsx
-```
-
-## Required Folders
-
-Sitex requires `src/pages`. TSX and MDX files in this folder become routes.
-
-MDX pages also use `src/layouts`.
-
-1. `layout: "doc"` uses `src/layouts/doc.tsx`.
-2. `layout: "post"` uses `src/layouts/post.tsx`.
-
-## Recommended Shape
-
-Keep routes thin. Routes choose layouts and provide page content.
-
-Use `src/layouts/base.tsx` for shared metadata, navigation, and site chrome.
-
-Use `src/components` for reusable implementation pieces. This folder is a convention, not a Sitex requirement.
-
-## Blocks
-
-Place blocks in `src/components/blocks`. Blocks are reusable page sections, such as hero sections, feature grids, article frames, and sidebars.
-
-## UI
-
-Place UI components in `src/components/ui`. UI components are low-level design system pieces, such as buttons, typography, inputs, breadcrumbs, and sidebar primitives.
diff --git a/apps/docs/src/pages/docs/index.mdx b/apps/docs/src/pages/docs/index.mdx
index 427d669..429007d 100644
--- a/apps/docs/src/pages/docs/index.mdx
+++ b/apps/docs/src/pages/docs/index.mdx
@@ -1,34 +1,19 @@
---
layout: "doc"
title: "Introduction"
-description: "A simpler, Vite-based React framework for building fast websites with local data."
-order: 1
+description: "An opinionated, Vite-based React framework for building fast content sites."
---
-## Overview
+Sitex is an opinionated React framework for content sites. Pages are MDX documents with typed frontmatter. Structure, chrome, and design live in TSX layouts and components. Every page renders to static HTML at build time; components opt into browser JavaScript with `client:*` directives.
-Sitex renders React routes to fast static HTML by default, while island architecture lets you add browser interactivity exactly where a page needs it.
+The framework handles routing, asset loading, island scripts, head defaults, sitemap, robots.txt, and favicon. Everything else is plain React.
-Routes are local TSX files. Sitex maps them to URLs, keeps routing explicit and file-based. Read more in [Page routing](/docs/page-routing).
+## The Model
-Imported components can opt into client rendering with directives like `client:load`, `client:visible`, and `client:idle`. Read more in [Island rendering](/docs/island-rendering).
+1. `src/pages/**/*.mdx` — routable content documents with typed frontmatter.
+2. `src/layouts/**/*.tsx` — the full document around a page, selected by the `layout` field.
+3. `src/components/**/*.tsx` — blocks, UI primitives, and islands.
-Static routes, server routes, route-level CSS, and island assets are handled by Vite's module graph. Read more in [Rendering and assets](/docs/rendering-and-assets).
+The split is strict on purpose: because every route is a content document flowing through the same pipeline, metadata is always complete and pages stay simple enough that anyone — or any agent — can add one without touching React.
-## Use cases
-
-Sitex is intended for simple content-based websites like landing pages, marketing sites, and documentation. It is especially useful when that content site belongs to a larger product that also has an app-like dashboard built with something like Next.js or TanStack.
-
-In that setup, the content site can reuse the same React components and design system as the app, but render them statically instead of dynamically.
-
-We like building with the React ecosystem. We want to use React components, Vite, TypeScript, Shadcn/ui, and the rest of the ecosystem while still producing extremely fast websites.
-
-## Sitex versus Astro
-
-We love Astro. Fulldev sponsors Astro monthly, and Sitex is mostly inspired by Astro's static-first model and island architecture.
-
-Astro already proves that this model works. Sitex is not trying to replace Astro; it is an experiment around one specific difference.
-
-In Astro, client directives are placed on framework components from inside Astro components. That can make it harder to build the whole website with React components, and you often end up with duplicate components in both `.astro` and `.tsx` syntax to work around that.
-
-Because of this, at Fulldev we've even built [a UI library for Astro](https://ui.full.dev) to work around that limitation. Sitex is the experiment in the other direction: keep the Astro-inspired static output and island architecture, but let client directives be used from TSX so the app feels like a normal React and Vite project.
+Start with [Installation](/docs/installation).
diff --git a/apps/docs/src/pages/docs/installation.mdx b/apps/docs/src/pages/docs/installation.mdx
index c038504..a79c19e 100644
--- a/apps/docs/src/pages/docs/installation.mdx
+++ b/apps/docs/src/pages/docs/installation.mdx
@@ -1,71 +1,31 @@
---
layout: "doc"
title: "Installation"
-description: "Install SiteX and add the Vite and TypeScript configuration."
-order: 2
+description: "Install Sitex and create your first page."
---
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
-
-
- Installing with an AI agent?
-
- Share this page URL with your agent and ask it to follow the SiteX
- installation instructions exactly.
-
-
-
-## AI Agent Install
-
-Give your agent this instruction from inside the project you want to set up:
-
-```text
-Read https://sitex.full.dev/docs/installation and install SiteX in this project.
-
-Follow the manual install steps on that page: install the package, add the Vite plugin,
-extend the SiteX TypeScript config, and create the first page.
-```
-
-## Manual Install
-
-Install SiteX, React, Vite+, and the Vite packages your app needs.
+## Install
```bash
pnpm add @fulldotdev/sitex react react-dom vite vite-plus @vitejs/plugin-react
pnpm add -D typescript @types/react @types/react-dom
```
-## Vite Config
-
-Add the SiteX plugin to `vite.config.ts`. Set `appType` to `custom` so Vite does not expect a normal single-page app fallback.
-
```ts
+// vite.config.ts
import { defineConfig } from "vite-plus"
import { sitex } from "@fulldotdev/sitex/plugin"
import react from "@vitejs/plugin-react"
export default defineConfig({
- appType: "custom",
plugins: [react(), sitex()],
})
```
-SiteX writes slashless URLs by default. Pass `sitex({ trailingSlash: true })` if you want directory index output and trailing-slash paths.
-
-## TypeScript Config
-
-Extend the SiteX TypeScript config so JSX client directives like `client:load`, `client:only`, `client:visible`, `client:idle`, and `client:media` are accepted.
-
```json
+// tsconfig.json
{
"extends": "@fulldotdev/sitex/tsconfig",
- "include": [
- "src/**/*",
- ".sitex/**/*.d.ts",
- ".sitex/typecheck/**/*.tsx",
- "vite.config.ts"
- ],
- "exclude": ["dist"],
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
@@ -74,45 +34,81 @@ Extend the SiteX TypeScript config so JSX client directives like `client:load`,
}
```
+The Sitex tsconfig brings the includes and the generated `.sitex` types. The `@/*` alias maps imports to `src/`; Vite picks it up from the tsconfig automatically. Add `.sitex` to `.gitignore` — Sitex regenerates it on every run.
+
+Builds need an absolute site URL for canonical, sitemap, and robots output. Deploy platforms provide one automatically (Netlify `URL`, `VERCEL_PROJECT_PRODUCTION_URL`, `SITE_URL`, and similar); otherwise pass `sitex({ site: { url: "https://example.com" } })`.
+
+## Project Shape
+
+```text
+src/
+ components/ # blocks, UI primitives, islands (convention, not required)
+ globals/
+ index.yaml # shared chrome content, imported via sitex:globals
+ layouts/ # TSX layouts, selected by the layout frontmatter field
+ default.tsx
+ pages/ # MDX pages only — every .mdx file is a route
+ index.mdx
+ index.css # global stylesheet, imported by the base layout
+```
+
## First Page
-Create `src/pages/index.tsx`.
+Create these four files and the site runs:
+
+```css
+/* src/index.css */
+body {
+ font-family: system-ui, sans-serif;
+}
+```
+
+```yaml
+# src/globals/index.yaml
+name: My Site
+```
```tsx
-export default function HomePage() {
+// src/layouts/default.tsx
+import type { LayoutProps } from "@fulldotdev/sitex"
+import globals from "sitex:globals"
+
+import "@/index.css"
+
+export default function DefaultLayout({ title, children }: LayoutProps) {
return (
-
+
- My SiteX site
-
+ {title}
-
Hello from SiteX
+ {globals.name}
+
+
{title}
+ {children}
+
)
}
```
-`src/pages/index.tsx` becomes `/`. Read [Routing](/docs/page-routing) for file-to-URL behavior, or [MDX pages](/docs/mdx) for prose pages with `src/layouts`.
+```mdx
+---
+layout: "default"
+title: "My Site"
+description: "My first Sitex page."
+---
-## Build Behavior
+Hello from Sitex.
+```
-SiteX runs through Vite+.
+## Commands
```bash
-vp dev
-vp build
-vp preview
+vp dev # dev server with the same rendering pipeline as the build
+vp build # static build
+vp preview # serve the built output
```
-`vp dev` starts the development server. `vp build` writes one HTML file per static route, includes production CSS, and adds the island client script only when a page needs client rendering. `vp preview` serves the built output locally.
-
-## Next Steps
-
-1. Read [Routing](/docs/page-routing) for the route types.
-2. Read [TSX pages](/docs/tsx-pages) for static, dynamic, and server routes.
-3. Read [MDX pages](/docs/mdx) for prose pages with layouts.
-4. Read [Pages API](/docs/content) for indexes, cards, and navigation from route metadata.
-5. Read [Component rendering](/docs/island-rendering) for browser interactivity.
-6. Read [Deployment](/docs/deployment) before deploying server routes.
+`vp build` writes to `dist/`: one HTML file per page (slashless URLs by default; `trailingSlash: true` for directory indexes), `sitemap.xml` (excludes `noindex` pages, `lastmod` from `updatedAt`/`publishedAt`), `robots.txt`, and a generated `/favicon.svg` (configure via the `favicon` plugin option, disable with `favicon: false`). Files in `public/` always win over generated ones. Deploy `dist/` to any static host.
diff --git a/apps/docs/src/pages/docs/island-rendering.mdx b/apps/docs/src/pages/docs/island-rendering.mdx
deleted file mode 100644
index a210e59..0000000
--- a/apps/docs/src/pages/docs/island-rendering.mdx
+++ /dev/null
@@ -1,89 +0,0 @@
----
-layout: "doc"
-title: "Component rendering"
-description: "Add browser interactivity to imported React components."
-order: 9
----
-
-## Overview
-
-SiteX renders components statically unless an imported component uses a client directive.
-
-Client directives only work on imported React components. Do not put them on HTML elements or components declared in the same file.
-
-## Choosing Directives
-
-Use the least eager directive that still matches the interaction.
-
-1. Use `client:load` for controls that must be interactive as soon as possible.
-2. Use `client:idle` for page chrome that is already useful as static HTML, such as a sidebar or theme-aware shell.
-3. Use `client:visible` for below-the-fold widgets and repeated controls, such as copy buttons in long documentation pages.
-4. Use `client:media` for viewport-specific UI that should hydrate only when a media query matches.
-
-## `client:load`
-
-Hydrates immediately.
-
-```tsx
-import Search from "@/components/search"
-
-export default function Page() {
- return
-}
-```
-
-## `client:only`
-
-Renders only in the browser.
-
-```tsx
-import Search from "@/components/search"
-
-export default function Page() {
- return
-}
-```
-
-## `client:visible`
-
-Hydrates when the component enters the viewport.
-
-```tsx
-import CopyButton from "@/components/copy-button"
-
-export default function Page() {
- return
-}
-```
-
-## `client:idle`
-
-Hydrates after the browser has idle time.
-
-```tsx
-import Sidebar from "@/components/sidebar"
-
-export default function Page() {
- return
-}
-```
-
-## `client:media`
-
-Hydrates after a media query matches.
-
-```tsx
-import DesktopNav from "@/components/desktop-nav"
-
-export default function Page() {
- return
-}
-```
-
-## Boundaries
-
-Island roots must be imported React components.
-
-Props must be JSON-serializable data. Functions, symbols, cyclic values, and React elements cannot cross the static-to-client boundary as props.
-
-Children are rendered as static slot HTML. If those children contain another island, it renders independently with its own client boundary.
diff --git a/apps/docs/src/pages/docs/layouts.mdx b/apps/docs/src/pages/docs/layouts.mdx
new file mode 100644
index 0000000..a19646f
--- /dev/null
+++ b/apps/docs/src/pages/docs/layouts.mdx
@@ -0,0 +1,90 @@
+---
+layout: "doc"
+title: "Layouts"
+description: "TSX layouts with typed props, globals, head defaults, and MDX components."
+---
+
+A layout is a TSX file in `src/layouts` that renders the full document — ``, ``, and ``. A page selects it with the `layout` frontmatter field: `layout: "doc"` renders with `src/layouts/doc.tsx`.
+
+## Props
+
+A layout receives one props object: the page frontmatter plus Sitex-provided `path`, `url`, `locale`, `headings`, and `children`. Type extra frontmatter with `LayoutProps`:
+
+```tsx
+import type { LayoutProps } from "@fulldotdev/sitex"
+
+type PostData = { badge?: string }
+
+export default function PostLayout({
+ title,
+ badge,
+ headings,
+ children,
+}: LayoutProps) {
+ return (
+
+
+ {title}
+
+
+
+
+
{title}
+ {badge && {badge}}
+ {children}
+
+
+
+ )
+}
+```
+
+Sitex writes one typecheck file per page to `.sitex/typecheck`, so `tsc` reports frontmatter that does not match the layout props.
+
+## Globals
+
+Shared chrome content — name, logo, navigation, footer links — lives in `src/globals/index.yaml` and is imported with `sitex:globals` (fully typed from the YAML):
+
+```tsx
+import globals from "sitex:globals"
+```
+
+`index.yaml` serves the root locale; its language tag comes from the `site.locale` plugin option (default `"en"`) and sets ``. Files like `nl.yaml` define locale variants served under `/nl` routes, available via the `locales` export and the layout `locale` prop.
+
+## Head Defaults
+
+The layout owns the ``. After rendering, Sitex adds only what is missing:
+
+1. canonical link and `og:url` from the site URL + the page path (`canonical` frontmatter overrides)
+2. `` from the page locale
+3. charset and viewport
+4. robots `noindex` when the page sets `noindex: true`
+5. favicon link and generator meta
+
+For JSON-LD, render a `script type="application/ld+json"` anywhere in the layout — Sitex hoists it into the head. The `layout` component from [pre-built components](/docs/components) wires title, description, Open Graph, Twitter, and JSON-LD props for you.
+
+## MDX Components
+
+Override Markdown elements globally in the plugin config, or per layout with an `mdxComponents` export. Layout entries win. Use this for elements that need more than CSS — the main case is code blocks with syntax highlighting and a copy button, which the registry ships as the [`code` component](/docs/components/code):
+
+```bash
+pnpm dlx shadcn@latest add https://sitex.full.dev/r/code.json
+```
+
+```ts
+// vite.config.ts
+sitex({ mdx: { components: { pre: "@/components/ui/code" } } })
+```
+
+```tsx
+// src/layouts/doc.tsx — per-layout override, wins over the config
+import CodePre from "@/components/ui/code"
+
+export const mdxComponents = { pre: CodePre }
+```
diff --git a/apps/docs/src/pages/docs/mdx-components.mdx b/apps/docs/src/pages/docs/mdx-components.mdx
deleted file mode 100644
index 168d7c6..0000000
--- a/apps/docs/src/pages/docs/mdx-components.mdx
+++ /dev/null
@@ -1,75 +0,0 @@
----
-layout: "doc"
-title: "MDX components"
-description: "Customize Markdown elements rendered inside MDX pages."
-order: 7
----
-
-## Overview
-
-Configure shared MDX components in `vite.config.ts`.
-
-```ts
-import { defineConfig } from "vite-plus"
-import { sitex } from "@fulldotdev/sitex/plugin"
-import react from "@vitejs/plugin-react"
-
-export default defineConfig({
- appType: "custom",
- plugins: [
- react(),
- sitex({
- mdx: {
- components: {
- pre: "@/components/mdx/pre",
- a: "@/components/mdx/link",
- },
- },
- }),
- ],
-})
-```
-
-Each key is an MDX element name. Each value is an import path to a default-exported React component.
-
-## Component Example
-
-```tsx
-import type { ComponentProps } from "react"
-
-export default function MdxLink(props: ComponentProps<"a">) {
- return
-}
-```
-
-```tsx
-import type { ComponentProps } from "react"
-
-export default function MdxPre(props: ComponentProps<"pre">) {
- return
-}
-```
-
-## Layout Overrides
-
-A layout can override components for its MDX pages.
-
-```tsx
-import MdxPre from "@/components/mdx/pre"
-
-export const mdxComponents = {
- pre: MdxPre,
-}
-
-export default function DocsLayout({ children }) {
- return {children}
-}
-```
-
-Layout components override shared components from `vite.config.ts`.
-
-## Component Names
-
-Use normal MDX tag names such as `pre`, `code`, `a`, `h2`, `table`, `img`, or a custom component name like `Callout`.
-
-Component names must start with a letter and may contain letters, numbers, and dashes.
diff --git a/apps/docs/src/pages/docs/mdx.mdx b/apps/docs/src/pages/docs/mdx.mdx
deleted file mode 100644
index 634f762..0000000
--- a/apps/docs/src/pages/docs/mdx.mdx
+++ /dev/null
@@ -1,101 +0,0 @@
----
-layout: "doc"
-title: "MDX pages"
-description: "Create prose pages with Markdown, React components, and typed layouts."
-order: 5
----
-
-## Overview
-
-Use `.mdx` files for docs, articles, changelogs, and other prose-heavy pages. MDX pages live in `src/pages` and require YAML frontmatter with a `layout` value.
-
-```mdx
----
-layout: post
-title: Building with SiteX
-description: How route-owned content keeps pages local.
----
-
-# Building with SiteX
-
-This body is passed to the layout as `children`.
-```
-
-`layout: post` uses `src/layouts/post.tsx`.
-
-## Layouts
-
-An MDX layout receives frontmatter fields and `children`.
-
-```tsx
-import type { MarkdownLayoutProps } from "@fulldotdev/sitex"
-
-type Props = MarkdownLayoutProps<{
- title: string
- description: string
-}>
-
-export default function PostLayout({ title, description, children }: Props) {
- return (
-
-
{title}
-
{description}
- {children}
-
- )
-}
-```
-
-The `layout` field selects the layout. It is not passed to the layout component.
-
-## Page Data
-
-MDX frontmatter is available through [Pages API](/docs/content).
-
-Reserved keys:
-
-1. `path` and `children` cannot be defined in page data.
-2. `headings` cannot be defined in MDX frontmatter.
-3. `layout` is included in page data but not passed to the layout component.
-
-## Path And Headings
-
-Layouts can receive the route path and document headings.
-
-```tsx
-import type { MarkdownLayoutProps } from "@fulldotdev/sitex"
-
-type Props = MarkdownLayoutProps<{
- title: string
-}>
-
-export default function DocsLayout({ title, path, headings, children }: Props) {
- return (
-
-
{title}
-
- {children}
-
- )
-}
-```
-
-Use `headings` for table-of-contents UI. Use `path` for navigation, breadcrumbs, and canonical links.
-
-## React Components
-
-Import React components directly in MDX.
-
-```mdx
-import { Alert } from "@/components/ui/alert"
-
-Use MDX for prose, TSX for heavily custom pages.
-```
-
-Use [MDX components](/docs/mdx-components) to customize Markdown elements across pages.
diff --git a/apps/docs/src/pages/docs/page-routing.mdx b/apps/docs/src/pages/docs/page-routing.mdx
deleted file mode 100644
index 59e1725..0000000
--- a/apps/docs/src/pages/docs/page-routing.mdx
+++ /dev/null
@@ -1,43 +0,0 @@
----
-layout: "doc"
-title: "Routing"
-description: "Choose TSX or MDX routes and understand how files map to URLs."
-order: 3
----
-
-## Overview
-
-TSX and MDX files in `src/pages` become routes. Routes are static by default.
-
-## Route Types
-
-Use TSX for component-heavy pages, dynamic routes, and server routes. Use MDX for prose-heavy pages.
-
-1. [TSX pages](/docs/tsx-pages) export a React component from `.tsx`.
-2. [MDX pages](/docs/mdx) use Markdown, frontmatter, and a layout.
-3. [Pages API](/docs/content) lets route metadata power indexes, cards, and navigation.
-
-## File Mapping
-
-Files map to URLs by their path inside `src/pages`.
-
-1. `src/pages/index.tsx` becomes `/`.
-2. `src/pages/docs/index.mdx` becomes `/docs`.
-3. `src/pages/about.tsx` becomes `/about`.
-4. `src/pages/docs/routing.mdx` becomes `/docs/routing`.
-
-## Static Routes
-
-Static routes are rendered during build and deployed as HTML.
-
-## Dynamic Routes
-
-A bracket segment becomes a route parameter. Dynamic static routes use TSX and export `paths`.
-
-Read [TSX pages](/docs/tsx-pages) for a full dynamic route example.
-
-## Server Routes
-
-Export `render = "server"` from a TSX route when a page needs request-time data.
-
-Read [Page rendering](/docs/rendering-and-assets) for how static and server routes are rendered, and [Deployment](/docs/deployment) before deploying server routes.
diff --git a/apps/docs/src/pages/docs/pages.mdx b/apps/docs/src/pages/docs/pages.mdx
new file mode 100644
index 0000000..62f0b79
--- /dev/null
+++ b/apps/docs/src/pages/docs/pages.mdx
@@ -0,0 +1,54 @@
+---
+layout: "doc"
+title: "Pages"
+description: "MDX pages with typed frontmatter and the pages API."
+---
+
+Every page is an MDX file in `src/pages`. The file path is the route: `index.mdx` is `/`, `docs/pages.mdx` is `/docs/pages`. Routes are static — no dynamic segments, no TSX pages.
+
+## Frontmatter
+
+`layout`, `title`, and `description` are required. Everything else is optional:
+
+```yaml
+layout: "doc" # renders with src/layouts/doc.tsx; "blog/post" nests
+title: "Pages"
+description: "Create pages with MDX files."
+type: "webpage" # webpage (default) | article | faq
+image: "/covers/pages.png" # social image path or absolute URL
+canonical: "https://example.com/original" # canonical override; excludes page from sitemap
+noindex: true # robots noindex + removed from sitemap
+```
+
+`type: article` requires `publishedAt` (`YYYY-MM-DD`) and allows `updatedAt` and `author`. `type: faq` requires `questions` as a list of `{ question, answer }`.
+
+Any other field passes through to the layout as a prop and to the pages API. `path`, `url`, `locale`, `headings`, and `children` are reserved.
+
+## MDX Body
+
+The body is regular Markdown and can import React components. A fully custom page is one block component in an otherwise empty body:
+
+```mdx
+---
+layout: "base"
+title: "Pricing"
+description: "Simple pricing for teams."
+---
+
+import PricingPage from "@/components/blocks/pricing-page"
+
+
+```
+
+## Pages API
+
+Read frontmatter across the site from layouts and components with `sitex:pages` — for indexes, cards, and navigation. Both helpers are async; async components are fine because everything renders at build time.
+
+```tsx
+import { getPage, getPages } from "sitex:pages"
+
+const posts = await getPages("/blog") // all pages under /blog
+const post = await getPage("/blog/hello") // Page | undefined
+```
+
+`Page` is a stable type: the frontmatter schema plus `path` and `headings`, with extra fields typed as `JsonValue`.
diff --git a/apps/docs/src/pages/docs/rendering-and-assets.mdx b/apps/docs/src/pages/docs/rendering-and-assets.mdx
deleted file mode 100644
index ee2259d..0000000
--- a/apps/docs/src/pages/docs/rendering-and-assets.mdx
+++ /dev/null
@@ -1,87 +0,0 @@
----
-layout: "doc"
-title: "Page rendering"
-description: "How SiteX renders static pages, dynamic pages, server pages, and route CSS."
-order: 8
----
-
-## Overview
-
-Sitex renders pages to HTML at build time unless a route opts into server rendering.
-
-## Static Routes
-
-Static routes are the default.
-
-```tsx
-export default function AboutPage() {
- return (
-
-
- About
-
-
-