These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

Static sites

A folder with sails.site.json at the mount root compiles as a content site when shipped. There is no build command. Pages compile on first HTTP request (JIT). Derivatives are cached by the page plus the glob collection files it reads. Query strings like search are not cached.

Use this for blogs, docs, landing pages, and portfolios. Forms and JSON APIs live in *.api.sh on the same mount. App rows use sql --db, not page globs.

Done looks like

cp -r /templates/static-blog ./site
ship create site --policy 644
ship list

Open public_url. / is the homepage. /blog/hello is HTML from pages/blog/hello.page.md. /blog/hello.md is the markdown twin for agents. Edit a page under site/pages/ and stop — autosave updates the same URL. You did not run ship create again.

Worked example

cp -r /templates/static-site-demo ./site
# or: personal-site, product-landing, saas-marketing, static-blog, static-site-kit
ship create site --policy 644

Expected create JSON shape (not exact ids):

{
  "name": "site",
  "path": "site",
  "policy": "644",
  "public_url": "https://{id}.sails.app/"
}
RequestSource
/ or /index.htmlpages/index.page.html
/blog/hellopages/blog/hello.page.md (compiled HTML)
/blog/hello.mdMarkdown twin of that page
/styles.csspublic/styles.css

Markdown for agents: prefer GET /blog/hello.md, or Accept: text/markdown on the HTML URL. HTML responses also advertise an alternate markdown Link header. Source paths (pages/, components/, sails.site.json) are not served publicly.

Layout

my-site/
  sails.site.json     # required — enables static site mode
  pages/              # .page.html, .page.md, .html, *.api.sh
  components/         # optional — *.component.html layouts
  public/             # static assets at site root (optional)

Components may live anywhere under the site root except public/, not only in components/.

sails.site.json defaults:

{
  "pages": "pages",
  "output": "dist",
  "public": "public"
}
FieldDefaultPurpose
cacheControlno-cacheCache-Control for pages and assets. Default requires ETag revalidation. Example: public, max-age=3600
notFoundPage(none)Page stem (e.g. 404) returned with HTTP 404 for missing page-like URLs. Browser URL stays the request path. Missing assets still plain 404
spaFallbackPage(none)Same resolution, returned with HTTP 200 for client routers (e.g. index). Takes precedence over notFoundPage. Use for Remix/SPA shells

sails.site.json must sit at the directory passed to ship create, not a nested folder inside the mount.

File extensions

RoleExtensionPublished?
HTML page.page.html→ .html
Markdown page.page.md→ .html
Server handler.api.shhidden
Component.component.htmlhidden
Plain HTML.html→ .html (no markdown step)

Legacy aliases still work: .shtml → .page.html, .smd → .page.md, _components/*.html → *.component.html.

Markdown page

---
title: Hello
date: 2026-07-02
---
<site-shell title="{{ title }}">
# Heading

Some **markdown** in the slot.
</site-shell>

Content inside a component tag is markdown-rendered. Front matter and MiniJinja work the same as HTML pages.

Components

components/site-shell.component.html becomes the site-shell tag. Attributes become template variables; slot content goes in the component slot variable.

Do not put a literal component tag in markdown prose — the expander treats it as a component. Use backticks around the tag name.

Equality checks in MiniJinja if blocks work in attribute values. Use ordinary quotes. Do not HTML-escape quotes inside the expression.

URL mapping

Source under pages/Published URL
index.page.html/ or /index.html
about.page.md/about or /about.html
blog/post.page.html/blog/post
threads/$threadId.page.html/threads/:threadId (splat; threadId is a template variable)
sketch/$uuid.api.sh/sketch/:uuid

Routing order: exact path, then {path}/index.html, then {path}.html, then $param splat files. Exact files win over splat. Links may omit .html.

Files under public/ are served at the site root. Tailwind entry CSS compiles at publish time (@import "tailwindcss"; @plugin "@tailwindcss/typography"). Wrap prose in prose prose-slate dark:prose-invert. Sails emits @layer base and @layer utilities only — @layer components is dropped.

Collections vs SQL

Glob other pages in front matter (glob: pages/blog/**) — see Collections. JSON / front-matter file sets use page filter: (same index as the filter command). App tables use SQL / sql --db.

Failure modes

What you seeWhat it meansWhat to do
Folder served as raw filesNo sails.site.json at mount rootAdd the JSON at the path you passed to ship create
publish: command not foundBinary is shipship create …
Template syntax error on first visitMiniJinja failedcheck --path site before you ship; fix quotes inside if expressions
New blog post missing from indexCollection cache stale or where dropped itAutosave the new file. draft: true is excluded by draft != true; missing draft is null and still matches
Literal component tag in proseExpander ate the tagBacktick the tag name
SPA deep link 404sNo client fallbackSet spaFallbackPage (HTTP 200). Content sites that want a real 404 use notFoundPage

How to: Publish a site, Check a site before you publish, Put a form on a site, Add sign-in to a site.