Skip to main content
SiteShi p

Reference

Site Architecture Reference

The engine handles file routing, render-time context, storage adapters, and frontmatter typing. You write the templates and content; the engine wires up the rest.

File-based website using LiquidJS templating and Tailwind CSS (compiled by the engine on your machine and served at /tailwind.css). Think Jekyll + WordPress template hierarchy + Next.js dynamic routes, rendered at the edge.

Workspace layout

Path Purpose Editable
pages/ Routable pages. .html or .md files map to URLs. Subdirectories become path segments. [param] segments capture dynamic params. yes
pages/index.html Homepage. yes
pages/{dir}/index.html Collection archive page (e.g. pages/blog/index.html). yes
pages/{dir}/{slug}.md Collection entry (e.g. a blog post). yes
templates/ Layouts. templates/layout.html is the base. templates/{dir}/single.html and templates/{dir}/archive.html are collection-specific. yes
templates/components/ Partials (Liquid {% render %} targets). yes
data/ Site-wide data files (.json for structured records, .yaml for config, .csv for tabular). Exposed as {{ data.* }} in templates. yes
data/site.yaml Site config (see SiteConfig). yes
data/forms/{id}.json Form definitions (see FormConfig). yes
theme.css Tailwind @theme CSS variables and site-wide styling. Optional. yes
assets/ Static assets copied as-is on export. yes
assets/images/ Site images, including uploaded images. yes
assets/files/ Public downloadable files linked from the site. yes
assets/js/ Custom JavaScript files. yes
.attachments/ Private chat attachments and reference docs. Never published. yes
_indexes/ Derived. Collection index JSON files built by the engine. Do not edit by hand. no (derived)

The current working directory is the site root. Tool paths should be root-relative like data/site.yaml, templates/layout.html, and pages/about.html.

Do not prepend site/ or /workspace/site/ to paths.

<workspace-root>/
├── data/
│   ├── site.yaml                # Site config (name, description, type, etc.)
│   ├── redirects.json           # URL redirects [{from, to, status}]
│   ├── forms/{id}.json          # Form configs (fields, validation, honeypot)
│   └── *.json                   # Custom data → accessible as {{ data.* }} in templates
├── templates/
│   ├── layout.html              # Base layout — used as fallback for everything
│   ├── single.html              # (optional) layout for all .md posts
│   ├── archive.html             # (optional) layout for all index/listing pages
│   ├── page.html                # (optional) layout for plain .html pages
│   ├── blog/
│   │   └── single.html          # (optional) layout for /blog/*.md only
│   └── components/*.html        # Reusable partials ({% render 'name' %} or {% include 'name' %})
├── pages/
│   ├── index.html               # Homepage (URL: /)
│   ├── about.html               # About page (URL: /about)
│   ├── blog/
│   │   ├── index.html           # Blog listing — {{ posts }} and {{ blog }} available
│   │   └── my-post.md           # Blog post — automatically uses single.html if it exists
│   ├── products/
│   │   ├── index.html           # Products listing — {{ products }} available
│   │   └── [slug].md            # Dynamic product page — {{ params.slug }} available
│   └── 404.html                 # Custom 404 page
├── theme.css                    # Tailwind v4 @theme tokens; avoid `@apply` component classes
├── assets/
│   ├── images/                  # Public images
│   ├── files/                   # Public downloadable files (PDFs, docs)
│   └── js/                      # JavaScript
├── .attachments/                # Private chat attachments/reference docs (not published)
└── favicon.ico

Prefer utility classes directly in pages/templates for styling and layout. Use theme.css for Tailwind v4 @theme tokens and at most tiny global CSS rules; avoid semantic component classes and avoid @apply there. Use assets/images/ for images, assets/files/ for public downloads, and assets/js/ for custom scripts. .attachments/ is private reference material and never published.

Page System

Pages use frontmatter at the top. The layout field is optional — the system picks the right layout automatically:

---
title: Page Title
description: A short description for SEO
image: https://example.com/og-image.jpg
---
<h1>Page content here</h1>
  • File-based routing: pages/about.html/about, pages/services/web-design.html/services/web-design
  • The rendered page body is injected into the layout as {{ content }} — every layout must include this placeholder
  • Use layout: name only when you need to force a specific layout — otherwise omit it
  • Use scripts: to inject third-party scripts before </body> on a specific page only:
    • Single: scripts: https://js.stripe.com/v3/
    • Multiple: scripts: ["https://js.stripe.com/v3/", "/assets/js/checkout.js"]

Frontmatter values use JSON types with a plain-string fallback. Valid JSON values are parsed automatically; anything else stays a string:

  • featured: true → boolean — use {% if page.featured %} directly
  • perPage: 6 → number — handled automatically by the engine
  • tags: ["ai", "web"] → array — use {% for tag in page.tags %}
  • tags: ai, web → string (not valid JSON) — use {{ page.tags | split: ", " }} to get an array
  • title: My Page → string (stays as-is since it's not valid JSON)

No YAML footguns: NO, yes, on, off are all plain strings, not booleans.

Canonical fields

Field Type Required Notes
title string no Page title. Used in , OG:title, breadcrumbs, feeds.</td> </tr> <tr> <td><code>description</code></td> <td><code>string</code></td> <td>no</td> <td>Page meta description. Used in <meta name="description">, OG:description, search snippets.</td> </tr> <tr> <td><code>layout</code></td> <td><code>string</code></td> <td>no</td> <td>Layout template under templates/ (without extension). Defaults to layout.html; collection pages fall back to {dir}/single.html or {dir}/archive.html.</td> </tr> <tr> <td><code>image</code></td> <td><code>string</code></td> <td>no</td> <td>Hero/social image URL. Used in OG:image, Twitter card, JSON-LD.</td> </tr> <tr> <td><code>date</code></td> <td><code>string</code></td> <td>no</td> <td>ISO date the content was originally authored. Drives collection sort order and RSS.</td> </tr> <tr> <td><code>publishDate</code></td> <td><code>string</code></td> <td>no</td> <td>ISO date the content should go live. Pages with a future publishDate are scheduled and hidden from listings until their date passes.</td> </tr> <tr> <td><code>perPage</code></td> <td><code>number</code></td> <td>no</td> <td>Items per page for paginated collection archives.</td> </tr> <tr> <td><code>scripts</code></td> <td><code>string | string[]</code></td> <td>no</td> <td>Named scripts to inject into the page (e.g. "alpine", "stripe"). Engine emits the corresponding <script> tags.</td> </tr> <tr> <td><code>route</code></td> <td><code>object</code></td> <td>no</td> <td>Dynamic-route config — generates one page per record in a named collection. See "dynamic pages" skill.</td> </tr> <tr> <td><code>collection</code></td> <td><code>string</code></td> <td>no</td> <td>Override: treat this page as an archive for a specific collection directory. Usually inferred from file location; set only to re-point.</td> </tr> <tr> <td><code>item_template</code></td> <td><code>string</code></td> <td>no</td> <td>Component under templates/components/ used to render each collection item on a declarative archive.</td> </tr> <tr> <td><code>sort_by</code></td> <td><code>string</code></td> <td>no</td> <td>Collection sort field. Defaults to date (desc).</td> </tr> <tr> <td><code>order</code></td> <td><code>"asc" | "desc"</code></td> <td>no</td> <td>Collection sort direction. Defaults to desc.</td> </tr> <tr> <td><code>limit</code></td> <td><code>number</code></td> <td>no</td> <td>Max collection items rendered on this page.</td> </tr> <tr> <td><code>offset</code></td> <td><code>number</code></td> <td>no</td> <td>Collection items to skip before rendering.</td> </tr> <tr> <td><code>where</code></td> <td><code>object | object[]</code></td> <td>no</td> <td>Filter collection items by field value(s). One object or an array of ANDed objects.</td> </tr> <tr> <td><code>tags</code></td> <td><code>string | string[]</code></td> <td>no</td> <td>Content tags. Drives tag listing pages and related-post suggestions.</td> </tr> <tr> <td><code>type</code></td> <td><code>string</code></td> <td>no</td> <td>Schema.org-ish type hint (Article, Product, LocalBusiness, etc.). Drives JSON-LD emission.</td> </tr> <tr> <td><code>robots</code></td> <td><code>string</code></td> <td>no</td> <td>Robots meta tag content (e.g. "noindex, nofollow"). Overrides site default.</td> </tr> <tr> <td><code>modified</code></td> <td><code>string</code></td> <td>no</td> <td>ISO date of last meaningful modification. Used in sitemap <lastmod>.</td> </tr> <tr> <td><code>dynamic</code></td> <td><code>boolean</code></td> <td>no</td> <td>Opt out of caching — render per-request. Required for gated, personalized, or request-dynamic content.</td> </tr> </tbody></table> <p>Custom fields are allowed (the schema uses <code>passthrough()</code>). The following user-defined names are common enough that typo detection warns on near-misses to them:</p> <p><code>author</code>, <code>excerpt</code>, <code>category</code>, <code>featured</code>, <code>draft</code>, <code>slug</code>, <code>cover</code>, <code>summary</code>, <code>subtitle</code>, <code>permalink</code></p> <h2 id="template-hierarchy">Template Hierarchy</h2> <p>The system automatically resolves the best layout without needing <code>layout:</code> in frontmatter:</p> <table> <thead> <tr> <th>Content type</th> <th>Cascade (first match wins)</th> </tr> </thead> <tbody><tr> <td>Homepage (<code>pages/index.html</code>)</td> <td><code>home.html</code> → <code>layout.html</code></td> </tr> <tr> <td>Blog post (<code>pages/blog/post.md</code>)</td> <td><code>blog/single.html</code> → <code>single.html</code> → <code>layout.html</code></td> </tr> <tr> <td>Blog listing (<code>pages/blog/index.html</code>)</td> <td><code>blog/archive.html</code> → <code>archive.html</code> → <code>layout.html</code></td> </tr> <tr> <td>Tag archive (<code>/blog/tag/design</code>)</td> <td><code>blog/tag.html</code> → <code>tag.html</code> → <code>blog/archive.html</code> → <code>archive.html</code> → <code>layout.html</code></td> </tr> <tr> <td>Plain page (<code>pages/about.html</code>)</td> <td><code>about/page.html</code> → <code>page.html</code> → <code>layout.html</code></td> </tr> </tbody></table> <p><strong>When to create specialized layouts:</strong></p> <ul> <li><code>templates/single.html</code> — all blog/article pages share a prose layout with back-link, date header, etc.</li> <li><code>templates/archive.html</code> — all listing pages share a grid/list layout</li> <li><code>templates/blog/single.html</code> — blog posts specifically need something different from other content types</li> </ul> <p><strong>When to use explicit <code>layout:</code>:</strong></p> <ul> <li>Only when you want one specific page to use a completely custom layout that breaks the cascade</li> </ul> <p><strong>IMPORTANT: Every layout is a complete HTML document.</strong> There is no template inheritance or <code>{% extends %}</code>. Each layout file must contain the full <code><html></code>, <code><head></code>, and <code><body></code> structure. Components are optional: some sites render shared pieces like headers and footers, while minimal starter sites may keep the shell inline in <code>templates/layout.html</code>. Always place <code>{{ content }}</code> where the page body should appear:</p> <pre><code class="language-html"><!-- templates/single.html — complete document, not a partial --> <!DOCTYPE html> <html> <head><title>{{ page.title }} — {{ site.name }}</title></head> <body> {% render 'header', site: site, currentPath: currentPath %} <main>{{ content }}</main> {% render 'footer', site: site, year: year %} </body> </html> </code></pre> <h2 id="template-variables-auto-injected">Template Variables (Auto-Injected)</h2> <p>Every page render receives these variables automatically:</p> <table> <thead> <tr> <th>Variable</th> <th>Source</th> <th>Example</th> </tr> </thead> <tbody><tr> <td><code>{{ content }}</code></td> <td>Rendered page body</td> <td>Required in every layout — this is where the page HTML/markdown is injected</td> </tr> <tr> <td><code>{{ site }}</code></td> <td><code>data/site.yaml</code></td> <td><code>{{ site.name }}</code>, <code>{{ site.phone }}</code>, <code>{{ site.menu }}</code></td> </tr> <tr> <td><code>{{ page }}</code></td> <td>Page frontmatter</td> <td><code>{{ page.title }}</code>, <code>{{ page.description }}</code></td> </tr> <tr> <td><code>{{ data.* }}</code></td> <td><code>data/*.json</code>, <code>*.yaml</code>, <code>*.csv</code></td> <td><code>{{ data.team }}</code>, <code>{{ data.faq }}</code></td> </tr> <tr> <td><code>{{ params }}</code></td> <td>Dynamic route captures</td> <td><code>{{ params.slug }}</code>, <code>{{ params.category }}</code></td> </tr> <tr> <td><code>{{ site.blog }}</code>, <code>{{ site.team }}</code></td> <td>Global collection access (opt-in)</td> <td><code>{% for post in site.blog limit:3 %}</code> — only for collections listed in <code>site.yaml</code> → <code>globalCollections</code></td> </tr> <tr> <td><code>{{ posts }}</code></td> <td>Collection files in current dir</td> <td><code>{% for post in posts %}</code> — only on collection index pages</td> </tr> <tr> <td><code>{{ {dirname} }}</code></td> <td>Same collection, named alias</td> <td><code>{% for p in products %}</code> on products index</td> </tr> <tr> <td><code>{{ pagination }}</code></td> <td>Paginated collections</td> <td><code>.page</code>, <code>.totalPages</code>, <code>.hasNext</code>, <code>.hasPrev</code>, <code>.nextUrl</code>, <code>.prevUrl</code></td> </tr> <tr> <td><code>{{ tag }}</code></td> <td>Current tag (on tag archives)</td> <td><code>/blog/tag/design</code> → <code>"design"</code></td> </tr> <tr> <td><code>{{ allTags }}</code></td> <td>All tags with counts</td> <td><code>{% for t in allTags %}{{ t.name }} ({{ t.count }}){% endfor %}</code></td> </tr> <tr> <td><code>{{ breadcrumbs }}</code></td> <td>Auto-built from URL path</td> <td><code>{% for crumb in breadcrumbs %}{% unless forloop.first %} › {% endunless %}<a href="{{ crumb.url }}">{{ crumb.name }}</a>{% endfor %}</code> — <code>.url</code>, <code>.name</code>, <code>.isLast</code></td> </tr> <tr> <td><code>{{ toc }}</code></td> <td>Markdown h2/h3 headings</td> <td><code>{% for item in toc %}{{ item.text }}{% endfor %}</code> — <code>.id</code>, <code>.level</code>, <code>.isH2</code>, <code>.isH3</code></td> </tr> <tr> <td><code>{{ shareLinks }}</code></td> <td>Social share URLs</td> <td><code><a href="{{ shareLinks.twitter }}">Twitter</a></code> — <code>.twitter</code>, <code>.facebook</code>, <code>.linkedin</code>, <code>.email</code></td> </tr> <tr> <td><code>{{ relatedPosts }}</code></td> <td>Posts with matching tags</td> <td><code>{% for p in relatedPosts %}</code></td> </tr> <tr> <td><code>{{ prevPost }}</code></td> <td>Previous post (newer by date)</td> <td><code>{% if prevPost %}<a href="{{ prevPost.url }}">← {{ prevPost.title }}</a>{% endif %}</code></td> </tr> <tr> <td><code>{{ nextPost }}</code></td> <td>Next post (older by date)</td> <td><code>{% if nextPost %}<a href="{{ nextPost.url }}">{{ nextPost.title }} →</a>{% endif %}</code></td> </tr> <tr> <td><code>{{ readingTime }}</code></td> <td>Estimated reading minutes</td> <td><code>{{ readingTime }} min read</code> (word count ÷ 200 wpm)</td> </tr> <tr> <td><code>{{ currentPath }}</code></td> <td>Current URL path</td> <td>Useful for active nav: <code>{% if currentPath == '/about' %}</code></td> </tr> <tr> <td><code>{{ canonicalUrl }}</code></td> <td>Full canonical URL</td> <td>For meta tags</td> </tr> <tr> <td><code>{{ year }}</code></td> <td>Current year</td> <td><code>&copy; {{ year }}</code></td> </tr> <tr> <td><code>{{ jsonLd }}</code></td> <td>Auto-generated Schema.org</td> <td>Place in <code><head></code></td> </tr> <tr> <td><code>{{ queryXxx }}</code></td> <td>URL query params</td> <td><code>?category=tech</code> → <code>{{ queryCategory }}</code>, useful for filtering</td> </tr> </tbody></table> <h2 id="auto-injected-by-engine-do-not-add-manually">Auto-Injected by Engine (DO NOT add manually)</h2> <p>The engine automatically injects these into every page. Adding them manually causes duplicates:</p> <ul> <li><strong><code><meta name="viewport"></code></strong> — auto-added if missing</li> <li><strong><code><title></code></strong> — auto-generated as <code>{{ page.title }} — {{ site.name }}</code> if missing</li> <li><strong><code><meta name="description"></code></strong> — from <code>page.description</code> frontmatter</li> <li><strong><code><link rel="canonical"></code></strong> — from <code>{{ canonicalUrl }}</code></li> <li><strong>Open Graph tags</strong> — <code>og:title</code>, <code>og:description</code>, <code>og:type</code> (auto-detects article vs website), <code>og:url</code>, <code>og:image</code></li> <li><strong>Twitter Card tags</strong> — <code>twitter:card</code>, <code>twitter:title</code>, <code>twitter:description</code>, <code>twitter:image</code></li> <li><strong>Favicon</strong> — default <code>/favicon.svg</code> + <code>/favicon.ico</code></li> <li><strong>Tailwind CSS CDN</strong> — with typography plugin</li> <li><strong>Alpine.js 3</strong> — loaded with <code>defer</code>, plus <code>[x-cloak]</code> styling</li> <li><strong><code>loading="lazy"</code></strong> — auto-added to all <code><img></code> tags except the first one</li> <li><strong><code><html lang="..."></code></strong> — from <code>site.language</code> or defaults to <code>en</code></li> <li><strong>JSON-LD schemas</strong> — auto-generated based on page type (see below)</li> <li><strong><code>/sitemap.xml</code></strong> — auto-generated from all pages (excludes <code>robots: noindex</code>); pages with a future <code>publishDate</code> are excluded until they go live</li> <li><strong><code>/feed.xml</code></strong> — RSS feed auto-generated from all dated markdown posts in <code>pages/blog/</code></li> <li><strong><code>/robots.txt</code></strong> — auto-generated. Allows all crawlers (<code>User-agent: *</code>, <code>Allow: /</code>) and links to <code>/sitemap.xml</code> and <code>/feed.xml</code>. Not configurable.</li> <li><strong><code>/<collection>/tag/<slug></code></strong> — auto-generated tag archive pages for any tagged collection (e.g. <code>/blog/tag/design</code>, <code>/news/tag/breaking</code>); supports pagination via <code>/page/N</code>. No files needed — just add <code>tags:</code> to your post frontmatter</li> </ul> <h3 id="json-ld-schemas-auto-generated">JSON-LD Schemas (auto-generated)</h3> <p>The engine generates structured data based on frontmatter <code>type</code> and data files:</p> <table> <thead> <tr> <th>Trigger</th> <th>Schema</th> <th>Data Source</th> </tr> </thead> <tbody><tr> <td>Every page</td> <td><code>Organization</code>, <code>WebPage</code></td> <td><code>data/site.yaml</code></td> </tr> <tr> <td>Non-homepage pages</td> <td><code>BreadcrumbList</code></td> <td>URL path</td> </tr> <tr> <td>Markdown posts with <code>date</code></td> <td><code>Article</code></td> <td>Frontmatter (<code>date</code>, <code>modified</code>, <code>image</code>)</td> </tr> <tr> <td>Homepage + <code>data/reviews</code></td> <td><code>LocalBusiness</code> with <code>AggregateRating</code></td> <td><code>data/site.yaml</code> (address) + <code>data/reviews.json</code></td> </tr> <tr> <td><code>type: faq</code> + <code>data/faq</code></td> <td><code>FAQPage</code></td> <td><code>data/faq.json</code></td> </tr> <tr> <td><code>type: products</code> + <code>data/products</code></td> <td><code>Product</code> with <code>Offer</code></td> <td><code>data/products.json</code> (price, currency)</td> </tr> <tr> <td><code>type: events</code> + <code>data/events</code></td> <td><code>Event</code></td> <td><code>data/events.json</code></td> </tr> <tr> <td><code>type: courses</code> + <code>data/courses</code></td> <td><code>Course</code></td> <td><code>data/courses.json</code></td> </tr> </tbody></table> <h3 id="url-redirects">URL Redirects</h3> <p><code>data/redirects.json</code> — array of redirect rules, checked before page rendering. Supports exact, wildcard, and param patterns:</p> <pre><code class="language-json">[ { "from": "/old-page", "to": "/new-page", "status": 301 }, { "from": "/blog/*", "to": "/articles/:splat" }, { "from": "/products/:slug", "to": "/shop/:slug" }, { "from": "/legacy", "to": "https://example.com", "status": 302 } ] </code></pre> <ul> <li><strong>Exact</strong>: <code>/old-page</code> → <code>/new-page</code></li> <li><strong>Wildcard</strong>: <code>/blog/*</code> captures everything after the prefix → <code>:splat</code> in the target (e.g. <code>/blog/my-post</code> → <code>/articles/my-post</code>)</li> <li><strong>Param</strong>: <code>/products/:slug</code> captures a single segment → reused in target (e.g. <code>/products/widget</code> → <code>/shop/widget</code>)</li> <li><strong>Status</strong>: defaults to 301, set <code>"status": 302</code> for temporary redirects</li> </ul> <h2 id="liquidjs-syntax">LiquidJS Syntax</h2> <pre><code class="language-liquid">{{ site.name }} — output variable {% render 'hero', title: 'Hi', sub: 'World' %} — include component with args {% for item in items %}...{% endfor %} — loop (supports {% else %} for empty) {% if condition %}...{% else %}...{% endif %} — conditional {% unless condition %}...{% endunless %} — inverse conditional {% assign featured = posts | where: "featured", "true" %} — assign filtered result </code></pre> <p><strong>IMPORTANT — component rules:</strong></p> <ol> <li><strong>Use the bare name only</strong> — <code>{% render 'header' %}</code> NOT <code>{% render 'templates/components/header.html' %}</code>. Full paths will fail.</li> <li><strong>Both <code>render</code> and <code>include</code> work</strong> — <code>{% render %}</code> has isolated scope (must pass variables), <code>{% include %}</code> inherits parent scope. Either is fine.</li> <li><strong>Always pass variables explicitly with <code>render</code></strong> — they won't be inherited:</li> </ol> <pre><code class="language-liquid">{% comment %} WRONG — site and currentPath will be undefined {% endcomment %} {% render 'header' %} {% comment %} CORRECT — pass every variable the component uses {% endcomment %} {% render 'header', site: site, currentPath: currentPath %} {% render 'footer', site: site, year: year %} {% render 'card', title: post.title, url: post.url, image: post.image %} {% comment %} ALSO CORRECT — include inherits scope, no need to pass vars {% endcomment %} {% include 'header' %} </code></pre> <h3 id="filters">Filters</h3> <pre><code class="language-liquid">{{ post.date | date: "%B %d, %Y" }} — format date {{ text | truncate: 150 }} — truncate string {{ array | size }} — array/string length {{ value | default: 'fallback' }} — fallback if blank {{ posts | where: "featured", "true" }} — filter by field value {{ posts | sort: "date" | reverse }} — sort + reverse {{ posts | group_by: "category" }} — group into { name, items } objects {{ posts | limit: 3 }} — take first N items {{ data.products | json }} — JSON string (useful for Alpine x-data) {{ item.bio | markdownify }} — render markdown to HTML inline </code></pre> <h2 id="page-body-convention">Page Body Convention</h2> <p><strong>Prefer plain HTML + <code>{{ }}</code> variables in page bodies.</strong> Reserve <code>{% %}</code> tags (for, if, render) for layouts and listing pages. This reduces template errors and keeps page content clean and readable.</p> <p>Good (plain HTML in page body):</p> <pre><code class="language-html"><section class="py-20"> <h1>Welcome to {{ site.name }}</h1> <p>{{ site.description }}</p> </section> </code></pre> <p>Avoid in page bodies when possible (use in layouts/listings only):</p> <pre><code class="language-html">{% for item in data.team %} ... {% endfor %} {% if page.image %} ... {% endif %} </code></pre> <h2 id="collections-any-content-type">Collections (Any Content Type)</h2> <p>Any <code>pages/{dir}/</code> with content files is a collection. Collections are <strong>opt-in</strong> for global access — list them under <code>globalCollections</code> in <code>data/site.yaml</code>:</p> <pre><code class="language-yaml"># data/site.yaml name: My Site globalCollections: - blog - products - services/web # nested paths are supported </code></pre> <p>Declared collections are available on every page as <code>site.{name}</code> (Jekyll-style):</p> <pre><code>{{ site.blog }} — all blog posts, available on ANY page {{ site.products }} — all products, available on ANY page {{ site.team }} — all team members, available on ANY page </code></pre> <p>Collections that aren't declared are still routable (e.g. <code>/blog</code>, <code>/blog/post-x</code> work without <code>blog</code> being in <code>globalCollections</code>) — they're just not preloaded into template scope.</p> <p>On collection index pages, <code>{{ posts }}</code> and <code>{{ {dirname} }}</code> are also available as aliases.</p> <p>Works for any content model — no config needed. Content files can be <code>.md</code> or <code>.html</code>.</p> <p><strong>Post fields available in loops:</strong> <code>{{ post.title }}</code>, <code>{{ post.description }}</code>, <code>{{ post.date }}</code>, <code>{{ post.url }}</code>, <code>{{ post.slug }}</code>, plus any custom frontmatter key.</p> <h3 id="simple-collection-frontmatter-driven">Simple collection (frontmatter-driven)</h3> <p>For standard listing pages, declare the collection in frontmatter and let the renderer handle iteration:</p> <pre><code class="language-html">--- title: Blog collection: blog item_template: card perPage: 6 --- <section class="max-w-4xl mx-auto px-6 py-16"> <h1 class="text-4xl font-bold mb-12">Blog</h1> <div class="grid gap-8 md:grid-cols-2"> {{ collection }} </div> {{ paginationHtml }} </section> </code></pre> <ul> <li><code>collection: blog</code> — collect <code>.md</code>/<code>.html</code> files from <code>pages/blog/</code></li> <li><code>item_template: card</code> — render each post through <code>templates/components/card.html</code></li> <li><code>{{ collection }}</code> — replaced with pre-rendered cards</li> <li><code>{{ paginationHtml }}</code> — replaced with prev/next navigation</li> </ul> <p><strong>Optional collection frontmatter options</strong> — declare filtering/sorting without Liquid:</p> <table> <thead> <tr> <th>Field</th> <th>Default</th> <th>Example</th> <th>Purpose</th> </tr> </thead> <tbody><tr> <td><code>sort_by</code></td> <td><code>date</code></td> <td><code>sort_by: title</code></td> <td>Sort by any post field</td> </tr> <tr> <td><code>order</code></td> <td><code>desc</code></td> <td><code>order: asc</code></td> <td>Sort direction</td> </tr> <tr> <td><code>limit</code></td> <td>(all)</td> <td><code>limit: 6</code></td> <td>Cap the number of items</td> </tr> <tr> <td><code>where</code></td> <td>(none)</td> <td><code>where: { field: category, value: tutorials }</code></td> <td>Filter by field value</td> </tr> </tbody></table> <h3 id="custom-collection-liquid-loops">Custom collection (Liquid loops)</h3> <p>For full control over the card HTML, use <code>{% for %}</code> directly in the page body:</p> <pre><code class="language-html">--- title: Blog perPage: 6 --- <div class="grid gap-8 md:grid-cols-2"> {% for post in posts %} <article> <h2><a href="{{ post.url }}">{{ post.title }}</a></h2> <p>{{ post.description }}</p> </article> {% endfor %} </div> </code></pre> <h3 id="pagination">Pagination</h3> <p>Add <code>perPage: 6</code> to an index page's frontmatter to paginate. Then use <code>{{ pagination }}</code>:</p> <pre><code class="language-html">{% if pagination %} {% if pagination.hasPrev %}<a href="{{ pagination.prevUrl }}">&larr; Prev</a>{% endif %} <span>Page {{ pagination.page }} of {{ pagination.totalPages }}</span> {% if pagination.hasNext %}<a href="{{ pagination.nextUrl }}">Next &rarr;</a>{% endif %} {% endif %} </code></pre> <h3 id="content-scheduling">Content Scheduling</h3> <p>Set <code>date</code> to a future <code>YYYY-MM-DD</code> to schedule a post — it stays hidden until that date. You can also use a separate <code>publishDate</code> field if the display date should differ from the go-live date.</p> <h2 id="dynamic-routes">Dynamic Routes</h2> <p>Use <code>[param]</code> in filenames for dynamic pages:</p> <pre><code>pages/blog/[slug].md → /blog/{any} → {{ params.slug }} pages/[category]/[slug].md → /{cat}/{slug} → {{ params.category }}, {{ params.slug }} pages/products/[id].html → /products/{any-id} → {{ params.id }} </code></pre> <p><strong>For static export, declare <code>route.generate</code> in frontmatter on every <code>[param]</code> page.</strong> This makes URL generation deterministic and avoids silent export misses when the template body changes.</p> <pre><code class="language-html">--- title: Room route: generate: from: rooms params: slug: slug where: status: published --- {% assign room = data.rooms | where: "slug", params.slug | first %} <h1>{{ room.name }}</h1> </code></pre> <ul> <li><code>from</code> — data source key (for <code>data/rooms.json</code>, use <code>rooms</code>; <code>data.</code> prefix is also accepted)</li> <li><code>params</code> — map URL param name → field name in each item</li> <li><code>where</code> — optional exact-match filter before URLs are generated</li> <li>The exporter does not guess dynamic routes from template code. <code>route.generate</code> is required on every <code>[param]</code> page that should export static URLs.</li> </ul> <h2 id="components">Components</h2> <p>Create reusable sections in <code>templates/components/</code> and include with <code>{% render %}</code>. In the first-party starter, the shared site shell already lives in <code>templates/components/head.html</code>, <code>header.html</code>, and <code>footer.html</code>, and collection cards live in <code>templates/components/card.html</code>. Reuse those components when they exist instead of copying the shell into every new layout.</p> <pre><code class="language-html"><!-- templates/components/card.html --> <div class="card"><h3>{{ title }}</h3><p>{{ body }}</p></div> </code></pre> <pre><code class="language-liquid">{% render 'head', site: site, page: page, canonicalUrl: canonicalUrl, jsonLd: jsonLd %} {% render 'header', site: site, currentPath: currentPath %} {% render 'card', title: 'Hello', body: 'World' %} {% render 'footer', site: site, year: year %} </code></pre> <h2 id="color-system-theme">Color System & Theme</h2> <p>Design tokens (colors, fonts, spacing) are defined in <code>theme.css</code> using Tailwind v4's <code>@theme</code> syntax:</p> <pre><code class="language-css">@theme { --color-primary: #2563eb; --color-primary-50: #eff6ff; --color-primary-600: #2563eb; --color-primary-900: #1e3a8a; --color-secondary: #475569; --color-secondary-50: #f8fafc; --color-secondary-900: #0f172a; --font-heading: "Syne", sans-serif; --font-body: "Manrope", sans-serif; } </code></pre> <table> <thead> <tr> <th>Color</th> <th>Purpose</th> </tr> </thead> <tbody><tr> <td><code>primary</code></td> <td>Brand color — buttons, links, accents (50-900 scale)</td> </tr> <tr> <td><code>secondary</code></td> <td>Neutral/gray — text, backgrounds, borders (50-900 scale)</td> </tr> </tbody></table> <p>Usage: <code>bg-primary-600</code>, <code>text-secondary-900</code>, <code>border-secondary-200</code>. Edit <code>theme.css</code> to change brand colors.</p> <p><strong>Important Tailwind v4 rules:</strong></p> <ul> <li><code>theme.css</code> is primarily for <code>@theme</code> tokens, not component abstractions.</li> <li>Prefer utility classes directly in HTML/templates for buttons, cards, sections, nav, and layout.</li> <li>Avoid semantic classes like <code>.btn-primary</code>, <code>.hero-card</code>, etc. in <code>theme.css</code>.</li> <li>Avoid <code>@apply</code> in <code>theme.css</code> — the preview browser runtime is stricter than the publish compiler and can fail hard on otherwise-valid-looking <code>@apply</code> rules.</li> <li><strong><code>@utility</code> names must be plain identifiers</strong> (letters, digits, hyphens) — no pseudo-selectors in the name. <code>@utility elegant-underline::after { … }</code> is <strong>wrong</strong>; use <code>@utility elegant-underline { &::after { … } }</code> instead. Preview accepts the invalid form but publish rejects it.</li> <li>Minimal plain CSS is okay for truly global rules (<code>html { scroll-behavior: smooth; }</code>, <code>[x-cloak]</code>, etc.).</li> <li><strong>Do NOT</strong> add <code><link rel="stylesheet" href="/theme.css"></code> or remove the <code><link rel="stylesheet" href="/tailwind.css"></code> tag in the layout — the engine compiles Tailwind locally (during <code>siteship preview</code> and <code>siteship push</code>) and serves the output at <code>/tailwind.css</code>.</li> </ul> <h2 id="build-artifacts">Build artifacts</h2> <p>Tailwind CSS is compiled automatically — <code>siteship preview</code> compiles on every file change and serves <code>/tailwind.css</code> from memory; <code>siteship push</code> does a one-shot compile, hashes the output, and uploads the stylesheet alongside the workspace. No manual Tailwind build step is needed.</p> <p>For JS bundling or any other build: produce the artifacts under <code>assets/</code>, link them from your layout, then push.</p> <h2 id="forms">Forms</h2> <p>Forms post to <code>/api/forms/{id}</code> — the Worker handles validation, honeypot, and storage. See the <strong>forms skill</strong> for full config format and HTML templates.</p> <h2 id="site-config-data-site-yaml">Site Config (<code>data/site.yaml</code>)</h2> <p>Any field added to <code>data/site.yaml</code> is available as <code>{{ site.* }}</code> everywhere.</p> <table> <thead> <tr> <th>Field</th> <th>Type</th> <th>Required</th> <th>Notes</th> </tr> </thead> <tbody><tr> <td><code>name</code></td> <td><code>string</code></td> <td>yes</td> <td>Site name. Used in layouts, SEO, feeds.</td> </tr> <tr> <td><code>description</code></td> <td><code>string</code></td> <td>no</td> <td>Site tagline. Used in meta description fallback and feeds.</td> </tr> <tr> <td><code>url</code></td> <td><code>string</code></td> <td>no</td> <td>Canonical site URL. Used for absolute links in sitemap.xml and RSS.</td> </tr> <tr> <td><code>type</code></td> <td><code>string</code></td> <td>no</td> <td>Site type hint (business, blog, portfolio, …). Should be one of SITE_TYPES.</td> </tr> <tr> <td><code>logo</code></td> <td><code>string</code></td> <td>no</td> <td>Logo URL. Used in layouts, Organization JSON-LD, OG fallback.</td> </tr> <tr> <td><code>defaultImage</code></td> <td><code>string</code></td> <td>no</td> <td>Default social share image URL. Fallback when a page has no image frontmatter.</td> </tr> <tr> <td><code>address</code></td> <td><code>object</code></td> <td>no</td> <td>Business address. Drives LocalBusiness JSON-LD.</td> </tr> <tr> <td><code>phone</code></td> <td><code>string</code></td> <td>no</td> <td>Business phone. Surfaces in LocalBusiness schema and layouts.</td> </tr> <tr> <td><code>email</code></td> <td><code>string</code></td> <td>no</td> <td>Business contact email.</td> </tr> <tr> <td><code>hours</code></td> <td><code>string</code></td> <td>no</td> <td>Human-readable opening hours (e.g. "Mo-Fr 09:00-17:00").</td> </tr> <tr> <td><code>priceRange</code></td> <td><code>string</code></td> <td>no</td> <td>Schema.org priceRange for LocalBusiness (e.g. "$$").</td> </tr> <tr> <td><code>analytics</code></td> <td><code>string</code></td> <td>no</td> <td>Analytics property ID (e.g. "G-XXXX"). Triggers cookie-consent + analytics injection when set.</td> </tr> <tr> <td><code>social</code></td> <td><code>record</code></td> <td>no</td> <td>Social profile URLs keyed by platform (e.g. instagram, facebook, twitter).</td> </tr> <tr> <td><code>menu</code></td> <td><code>object[]</code></td> <td>no</td> <td>Navigation menu items rendered at {{ site.menu }}.</td> </tr> <tr> <td><code>allowedDomains</code></td> <td><code>string[]</code></td> <td>no</td> <td>Extra domains to allowlist in the CSP for embeds (Shopify, Stripe, YouTube, etc.).</td> </tr> <tr> <td><code>globalCollections</code></td> <td><code>string[]</code></td> <td>no</td> <td>Collections to preload into template scope as site.{name}. Only declared collections are available globally — keeps per-page render cost proportional to content actually used.</td> </tr> </tbody></table> <p>Additional fields are accepted and available at <code>{{ site.* }}</code> in templates.</p> <pre><code class="language-yaml">name: My Site description: ... type: business analytics: G-XXXXXXXX defaultImage: https://... </code></pre> <ul> <li><code>analytics</code> — auto-injects Google Analytics with cookie consent banner (see <strong>cookie-consent skill</strong> for banner HTML)</li> <li><code>defaultImage</code> — set this during initial site creation whenever the site uses real imagery. Use the strongest homepage hero / featured social-share image so <code>/</code>, <code>/blog</code>, and other pages without page-level <code>image:</code> still get OG/Twitter tags.</li> <li><code>maintenance: true</code> (custom field) — serves <code>pages/maintenance.html</code> to all visitors (returns 503)</li> </ul> <h2 id="data-files">Data Files</h2> <p>Data files support JSON, YAML, and CSV. Available as <code>{{ data.filename }}</code>:</p> <ul> <li><code>data/testimonials.json</code> → <code>{{ data.testimonials }}</code></li> <li><code>data/team.yaml</code> → <code>{{ data.team }}</code></li> <li><code>data/pricing.csv</code> → <code>{{ data.pricing }}</code> (parsed as array of objects with header row as keys)</li> </ul> <p>Navigation menu is in <code>data/site.yaml</code> as <code>{{ site.menu }}</code>.</p> <p></p> </article> <!-- Edit / feedback footer --> <div class="mt-16 pt-8 border-t border-stone-300/60 flex items-center justify-between text-sm text-stone-500 font-body"> <span>Found something out of date? <a href="https://github.com/seedprod/siteship.ai/issues/new" class="text-link hover:text-link-hover hover:underline font-semibold">Open an issue</a>.</span> <a href="/docs" class="hover:text-ink transition-colors">← All docs</a> </div> </main> </div> </div> <footer class="bg-ink text-cream mt-32"> <div class="max-w-6xl mx-auto px-6 py-20 grid md:grid-cols-12 gap-12"> <div class="md:col-span-4"> <a href="/" class="inline-flex items-end relative leading-none mb-6" aria-label="SiteShip — home"> <span class="font-display font-bold text-[44px] tracking-tight text-cream" style="font-variation-settings: 'opsz' 144, 'SOFT' 30;">SiteShi</span> <span class="relative inline-block"> <span class="font-display font-bold text-[44px] tracking-tight text-cream" style="font-variation-settings: 'opsz' 144, 'SOFT' 30;">p</span> <svg width="48" height="43" viewBox="0 0 46 42" class="absolute -top-[32px] left-1/2 -translate-x-1/2" aria-hidden="true"> <path fill="#10b981" d="M 12 4 L 38 4 Q 44 4, 44 10 L 44 28 Q 44 34, 38 34 L 28 34 L 22 41 L 24 34 L 12 34 Q 6 34, 6 28 L 6 10 Q 6 4, 12 4 Z"/> <path fill="#047857" d="M 22 4 Q 25 -1, 30 2 Q 27 5, 25 5 Z"/> <path fill="#fbbf24" d="M 6 16 Q -2 17, -1 24 Q 2 27, 8 23 L 10 19 Z"/> <path fill="#d97706" d="M 6 20 Q 1 24, 6 26 L 9 23 Z"/> <circle cx="22" cy="17" r="3.5" fill="#ffffff"/> <circle cx="21" cy="17" r="1.8" fill="#1c1917"/> </svg> </span> </a> <p class="font-body text-stone-400 leading-relaxed max-w-sm mb-6"> The first website engine built for AI, not humans. Built for agents first, so sites stay easy to generate, edit, preview, and host. </p> <p class="font-mono text-[11px] uppercase tracking-[0.18em] text-stone-500"> A new product from SeedProd </p> </div> <div class="md:col-span-2"> <h4 class="font-mono text-[11px] uppercase tracking-[0.18em] text-stone-500 mb-4">Product</h4> <ul class="space-y-2 font-body text-sm text-stone-400"> <li><a href="/#how-it-works" class="hover:text-cream transition-colors">How it works</a></li> <li><a href="/#pricing" class="hover:text-cream transition-colors">Pricing</a></li> <li><a href="/agent" class="hover:text-cream transition-colors">The agent</a></li> <li><a href="/docs" class="hover:text-cream transition-colors">Docs</a></li> <li><a href="/#faq" class="hover:text-cream transition-colors">FAQ</a></li> </ul> </div> <div class="md:col-span-2"> <h4 class="font-mono text-[11px] uppercase tracking-[0.18em] text-stone-500 mb-4">Company</h4> <ul class="space-y-2 font-body text-sm text-stone-400"> <li><a href="/about" class="hover:text-cream transition-colors">About</a></li> <li><a href="/blog" class="hover:text-cream transition-colors">Blog</a></li> <li><a href="/press" class="hover:text-cream transition-colors">Press</a></li> <li><a href="https://www.seedprod.com" class="hover:text-cream transition-colors">SeedProd</a></li> <li><a href="/privacy" class="hover:text-cream transition-colors">Privacy</a></li> <li><a href="/terms" class="hover:text-cream transition-colors">Terms</a></li> </ul> </div> <div class="md:col-span-2"> <h4 class="font-mono text-[11px] uppercase tracking-[0.18em] text-stone-500 mb-4">Compare</h4> <ul class="space-y-2 font-body text-sm text-stone-400"> <li><a href="/alternatives" class="hover:text-cream transition-colors">All alternatives</a></li> <li><a href="/alternatives/wix" class="hover:text-cream transition-colors">vs Wix</a></li> <li><a href="/alternatives/squarespace" class="hover:text-cream transition-colors">vs Squarespace</a></li> <li><a href="/alternatives/elementor" class="hover:text-cream transition-colors">vs Elementor</a></li> <li><a href="/alternatives/lovable" class="hover:text-cream transition-colors">vs Lovable</a></li> </ul> </div> <div class="md:col-span-2"> <h4 class="font-mono text-[11px] uppercase tracking-[0.18em] text-stone-500 mb-4">Get started</h4> <a href="https://app.siteship.ai/" class="inline-flex items-center gap-1.5 bg-action hover:bg-action-hover text-white font-body font-medium text-sm px-4 py-2 rounded-md transition-colors mb-3"> Start free <span aria-hidden="true">→</span> </a> <p class="font-body text-xs text-stone-500 leading-relaxed"> Free to try. $20/month to publish. </p> </div> </div> <div class="border-t border-stone-800"> <div class="max-w-6xl mx-auto px-6 py-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> <div class="space-y-2"> <p class="font-mono text-[11px] text-stone-500">© 2026 SiteShip · A SeedProd product</p> <p class="font-mono text-[10px] text-stone-600 leading-relaxed max-w-xl"> Cloudflare<sup>®</sup> is a registered trademark of Cloudflare, Inc. All other product names, logos, and brands are property of their respective owners. SiteShip is not affiliated with, endorsed by, or sponsored by any third-party brand referenced on this site. Comparisons reflect publicly-available information at time of writing and may not reflect current practices. </p> </div> <div class="flex items-center gap-4 text-stone-500"> <a href="https://x.com/johnturner" aria-label="Twitter" class="hover:text-cream transition-colors"> <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> </a> </div> </div> </div> </footer> </body> </html>