Install
openclaw skills install fastmodeBuild, deploy, and host websites for free with full CMS. Create a live website from scratch, deploy it to the cloud with free hosting, free SSL, and custom d...
openclaw skills install fastmodeFastMode lets you create a live website, deploy it to the cloud, and manage all its content — entirely from the command line. One-time browser login for OAuth authentication, then every operation runs in the terminal. No local servers, no manual dashboards.
yoursite.fastmode.aiwww.example.com)This is the complete sequence to go from nothing to a live website.
# 1. Authenticate (one-time — credentials persist at ~/.fastmode/credentials.json)
fastmode login
# 2. Create a project (gets a free hosted URL instantly)
fastmode projects create "Acme Corp"
fastmode use "Acme Corp"
# 3. Define content structure (write schema.json, then sync it)
fastmode schema sync -f schema.json
# 4. Add content
fastmode items create posts -n "Welcome" -d '{"title": "Welcome to Acme", "body": "<p>We build great things.</p>"}'
fastmode items create team -n "Jane Doe" -d '{"role": "Founder", "bio": "<p>Visionary leader.</p>"}'
# 5. Build HTML templates with {{tokens}}, package into a zip, validate, deploy
fastmode validate package site.zip
fastmode deploy site.zip
# Deploy waits for build to finish and reports success or failure with error details
# 6. If the build failed, check what went wrong
fastmode status
# Fix the issue, then re-deploy
STOP. Before writing ANY HTML, templates, or manifest.json, complete these steps.
fastmode projects
This lists all the user's existing FastMode projects.
If projects exist: Ask the user: "Is this website for one of your existing projects, or should I create a new one?" Let the user choose.
If NO projects exist: This is a new user — ask: "What would you like to name your new project?"
fastmode use "Project Name" to set it as defaultfastmode schema show to get the current collections and fieldsfastmode projects create "Project Name"fastmode use "Project Name"fastmode schema syncfastmode generate-samples| Requirement | How to Get It |
|---|---|
| Project selected/created | fastmode projects / fastmode projects create |
| Default set | fastmode use "Project Name" |
| Schema known (existing) | fastmode schema show |
If you don't have a project set, DO NOT PROCEED. Go back to Step 1.
WHY THIS MATTERS:
Before writing any HTML or templates, analyze the site:
/, /about, /blog, /blog/post-slug, etc.)/resources for articles, keep /resources. Do NOT change it to /blog. Use the manifest's path configuration.fastmode login # Open browser for OAuth device flow
fastmode logout # Delete ~/.fastmode/credentials.json
fastmode whoami # Show current user email and name
login uses OAuth 2.0 device authorization flow: opens a browser window where the user approves access on fastmode.ai, then credentials are saved automatically. The browser is only needed for this one-time login step.~/.fastmode/credentials.json with restricted file permissions (0o600 — owner read/write only). Treat this file as a sensitive secret.logout deletes ~/.fastmode/credentials.json and revokes the stored tokens.fastmode projects # List all projects (default action)
fastmode projects list # Same as above
fastmode projects create "Name" # Create a new project
fastmode projects create "Name" --subdomain custom-sub # Custom subdomain
fastmode projects create "Name" --force # Skip similar-name check
fastmode use <project> # Set default project for all commands
projects create checks for existing projects with similar names. Use --force to skip.use stores the default in ~/.fastmode/config.json. Does NOT validate the project exists.fastmode schema show # Show all collections and fields
fastmode schema show -p "Project Name" # Specify project
fastmode schema sync -f schema.json # Create collections and fields from JSON file
fastmode schema field-types # List all available field types (no auth needed)
schema show requires authentication and a project.schema sync reads a local JSON file and creates/updates the schema. Skips duplicates. Two-phase: creates collections first, then fields (handles relation dependencies).schema field-types works without authentication.fastmode items list <collection> # List all items
fastmode items list posts --limit 10 --sort publishedAt --order desc
fastmode items get <collection> <slug> # Get single item
fastmode items create <collection> -n "Name" -d '{"field": "value"}'
fastmode items create posts -n "Title" -f data.json # Data from file
fastmode items create posts -n "Draft Post" -d '{}' --draft
fastmode items update <collection> <slug> -d '{"field": "new value"}'
fastmode items update posts my-post -n "New Title"
fastmode items update posts my-post --publish # Publish a draft
fastmode items update posts my-post --unpublish # Revert to draft
fastmode items delete <collection> <slug> --confirm # REQUIRES --confirm
fastmode items relations <collection> # Show linkable items for relation fields
fastmode items relations posts --field author # Options for specific field
See the Content Items section below for detailed rules on data formats, relation fields, and drafts.
fastmode clients list # List portal clients with access
fastmode clients invite client@example.com # Invite with default permissions
fastmode clients invite client@example.com -n "Jane" --permissions cms.read,cms.write
fastmode clients invitations # List pending invitations
fastmode clients update-permissions <accessId> --permissions cms.read,editor
fastmode clients revoke <accessId> --confirm # REQUIRES --confirm
fastmode clients cancel-invite <invitationId> --confirm # REQUIRES --confirm
See the Client Portal Management section below for details on permissions, invite flow, and examples.
fastmode deploy site.zip # Deploy and wait for build to finish
fastmode deploy site.zip --force # Skip GitHub sync check
fastmode deploy site.zip --no-wait # Upload only, don't wait for build
fastmode deploy site.zip --timeout 300000 # Custom timeout in ms (default: 120000)
fastmode status # Check current build/deploy status
fastmode deploys # List deployment history
fastmode deploys --limit 5 # Limit number of results
See Deployment & Build Status below for the full deploy lifecycle.
fastmode validate manifest manifest.json
fastmode validate template index.html -t custom_index
fastmode validate template post.html -t custom_detail -c posts
fastmode validate template post.html -t custom_detail -c posts -p "My Project"
fastmode validate template about.html -t static_page
fastmode validate package site.zip
custom_index (collection listing), custom_detail (single item), static_page (fixed page).-c specifies the collection slug (required for custom_index and custom_detail).-p validates tokens against the actual project schema (reports missing fields).fastmode examples <type> # Code examples for a specific pattern
fastmode guide # Full website conversion guide
fastmode guide templates # Template syntax guide
fastmode guide common_mistakes # Common pitfalls to avoid
fastmode generate-samples # Generate placeholder content for empty collections
fastmode generate-samples -c posts team # Specific collections only
Available example types: manifest_basic, manifest_custom_paths, blog_index_template, blog_post_template, team_template, downloads_template, form_handling, asset_paths, data_edit_keys, each_loop, conditional_if, nested_fields, featured_posts, parent_context, equality_comparison, comparison_helpers, youtube_embed, nested_collection_loop, loop_variables, common_mistakes.
Available guide sections: full, first_steps, analysis, structure, seo, manifest, templates, tokens, forms, assets, checklist, common_mistakes.
Every project-scoped command (schema, items, deploy, status, etc.) needs a project. Resolution order:
-p / --project flag — explicit on the command: -p "My Project" or -p abc123-uuidFASTMODE_PROJECT environment variable — set in shell: export FASTMODE_PROJECT="My Project"fastmode use "My Project" in ~/.fastmode/config.jsonIf none is set, the command prints an error and exits with code 1:
Error: No project specified.
Use -p <id-or-name>, set FASTMODE_PROJECT env var, or run: fastmode use <project>
Project identifiers can be:
550e8400-e29b-41d4-a716-446655440000)Write a schema.json file and sync it:
fastmode schema sync -f schema.json
{
"collections": [
{
"slug": "posts",
"name": "Blog Posts",
"nameSingular": "Blog Post",
"fields": [
{ "slug": "title", "name": "Title", "type": "text", "isRequired": true },
{ "slug": "excerpt", "name": "Excerpt", "type": "textarea" },
{ "slug": "body", "name": "Body", "type": "richText" },
{ "slug": "featured-image", "name": "Featured Image", "type": "image" },
{ "slug": "category", "name": "Category", "type": "select", "options": "News, Tutorial, Update" },
{ "slug": "tags", "name": "Tags", "type": "multiSelect", "options": "JavaScript, Python, DevOps, AI" },
{ "slug": "featured", "name": "Featured", "type": "boolean" },
{ "slug": "author", "name": "Author", "type": "relation", "referenceCollection": "team" }
]
},
{
"slug": "team",
"name": "Team Members",
"nameSingular": "Team Member",
"fields": [
{ "slug": "role", "name": "Role", "type": "text" },
{ "slug": "bio", "name": "Bio", "type": "richText" },
{ "slug": "photo", "name": "Photo", "type": "image" },
{ "slug": "email", "name": "Email", "type": "email" }
]
}
]
}
To add fields to existing collections, use fieldsToAdd:
{
"fieldsToAdd": [
{
"collectionSlug": "posts",
"fields": [
{ "slug": "reading-time", "name": "Reading Time", "type": "number" }
]
}
]
}
You can combine collections and fieldsToAdd in the same file. Duplicate collections and fields are automatically skipped.
| Type | Description | Template Usage | Notes |
|---|---|---|---|
text | Single-line text | {{field}} | Titles, names, short strings |
textarea | Multi-line plain text | {{field}} | Descriptions, excerpts |
richText | Formatted HTML content | {{{field}}} | MUST use triple braces |
number | Numeric value | {{field}} | Prices, counts, order |
boolean | True/false toggle | {{#if field}} | Toggles, flags |
date | Date only | {{field}} | Birth dates, event dates |
datetime | Date and time | {{field}} | Timestamps |
image | Image file/URL | {{field}} | Renders as URL |
file | Downloadable file (max 10MB) | {{field}} | Link as <a href="{{field}}" download> |
url | Web link | {{field}} | External URLs |
videoEmbed | YouTube/Vimeo/Wistia/Loom | {{field}} | Embed URL |
email | Email with validation | {{field}} | Validated email addresses |
select | Single dropdown | {{field}} | Requires "options": "A, B, C" |
multiSelect | Multiple selections | {{field}} | Requires "options": "A, B, C" |
relation | Link to another collection | {{field.name}} | Requires "referenceCollection": "slug" |
Relation fields link items between collections (e.g. a post has an author from the team collection). When creating or updating items with relation fields:
fastmode items relations <collection> to get the available IDsfastmode items relations posts --field author shows team member IDs# First, find the author's item ID
fastmode items relations posts --field author
# Output shows: ID: 550e8400-..., Name: Jane Doe, Slug: jane-doe
# Then use that ID when creating a post
fastmode items create posts -n "My Post" -d '{"title": "My Post", "author": "550e8400-e29b-41d4-a716-446655440000"}'
WRONG: "author": "Jane Doe" — this will NOT work.
CORRECT: "author": "550e8400-e29b-41d4-a716-446655440000" — use the UUID.
fastmode items create <collection> -n "Item Name" -d '{"field": "value"}'
| Flag | Description |
|---|---|
-n, --name <name> | Required. Item name/title. |
-s, --slug <slug> | URL slug. Auto-generated from name if omitted. |
-d, --data <json> | Field data as JSON string. |
-f, --file <path> | Read field data from a JSON file (takes precedence over -d). |
-p, --project <id> | Project ID or name. |
--draft | Create as unpublished draft. |
Data rules:
-d value must be valid JSON. Keys are field slugs.-f reads from a JSON file. If both -f and -d are given, -f wins.-d nor -f is given, the item is created with just the name (no field data)."body": "<h2>Title</h2><p>Content here.</p>"--draft, items are published immediately (publishedAt set to now).fastmode items update <collection> <slug> -d '{"field": "new value"}'
| Flag | Description |
|---|---|
-n, --name <name> | New name/title. |
-d, --data <json> | Updated fields as JSON. Only provided fields change — others are preserved. |
-f, --file <path> | Read updated data from a JSON file. |
-p, --project <id> | Project ID or name. |
--publish | Set publishedAt to now (make item live). |
--unpublish | Set publishedAt to null (revert to draft). |
Update is a partial merge. Only the fields you provide in -d are changed. All other fields remain as they are.
fastmode items delete <collection> <slug> --confirm
The --confirm flag is required. Without it, the command refuses to run and exits with code 1. This is a safety measure — deletion is permanent and cannot be undone.
Always ask the user for confirmation before deleting.
| Action | Command |
|---|---|
| Create as published (default) | fastmode items create posts -n "Title" -d '{...}' |
| Create as draft | fastmode items create posts -n "Title" -d '{...}' --draft |
| Publish a draft | fastmode items update posts my-slug --publish |
| Unpublish (revert to draft) | fastmode items update posts my-slug --unpublish |
publishedAt: null and are not visible on the live site.publishedAt timestamp and appear on the live site.--draft, new items are published immediately.The client portal lets you give external clients (your customers, collaborators) limited access to manage content on your FastMode site. Clients get their own login, separate from your admin account, with configurable permissions.
The portal is auto-enabled on the project when you send the first invitation. No manual setup needed.
| Permission | Description |
|---|---|
cms.read | View collection items |
cms.write | Create, edit, archive, and delete items |
editor | Access the visual editor |
forms.read | View form submissions |
dns | Manage DNS settings |
api | Access API and integrations |
notifications | Manage notification rules |
billing | View plans and manage billing |
Default permissions (used when none specified): cms.read, cms.write, editor, forms.read
# Invite with default permissions
fastmode clients invite client@example.com
# Invite with a name
fastmode clients invite client@example.com -n "Jane Smith"
# Invite with specific permissions
fastmode clients invite client@example.com -n "Jane Smith" --permissions cms.read,forms.read
# Invite with all permissions
fastmode clients invite client@example.com --permissions cms.read,cms.write,editor,forms.read,dns,api,notifications,billing
The command returns an invite URL — share this with the client. The link expires in 7 days.
Important:
# See who has portal access
fastmode clients list
# See pending (unaccepted) invitations
fastmode clients invitations
clients list shows the access ID for each client — you need this ID to update permissions or revoke access.
# First, get the access ID from the list
fastmode clients list
# Update permissions (replaces ALL existing permissions)
fastmode clients update-permissions <accessId> --permissions cms.read,cms.write,editor
Permissions are replaced entirely — if a client had cms.read,cms.write,editor,forms.read and you set --permissions cms.read, they will ONLY have cms.read.
# Revoke a client's portal access (requires --confirm)
fastmode clients revoke <accessId> --confirm
The --confirm flag is required. Without it, the command refuses to run.
Always ask the user for confirmation before revoking access.
Revoking access is a soft delete — the client's account still exists but they cannot access this project's portal. Their active sessions are terminated immediately.
# Cancel a pending invitation (requires --confirm)
fastmode clients cancel-invite <invitationId> --confirm
The invitation link will no longer work. Use fastmode clients invitations to get the invitation ID.
# 1. Invite your client
fastmode clients invite designer@agency.com -n "Design Agency" --permissions cms.read,cms.write,editor
# 2. Share the invite URL from the output with the client
# 3. Later, check who has access
fastmode clients list
# 4. Restrict a client to read-only
fastmode clients update-permissions abc12345 --permissions cms.read
# 5. Remove a client who no longer needs access
fastmode clients revoke abc12345 --confirm
The deployment package is a .zip file with this exact structure:
site.zip
├── manifest.json # REQUIRED — defines pages and CMS templates
├── pages/ # Static HTML pages
│ ├── index.html # Homepage (REQUIRED — must have path "/")
│ ├── about.html
│ └── contact.html
├── templates/ # CMS-powered templates (if using collections)
│ ├── posts_index.html # Blog listing page
│ ├── posts_detail.html # Single blog post page
│ └── team_index.html # Team listing page
└── public/ # ALL static assets (CSS, JS, images, fonts)
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
├── logo.png
└── favicon.ico
manifest.json MUST be at the root of the zip.pages/. One HTML file per page.templates/. Convention: {collection}_index.html and {collection}_detail.html.public/. CSS, JavaScript, images, fonts — everything./public/ prefix in HTML. Example: <link href="/public/css/style.css" rel="stylesheet">."path": "/" in the manifest.WRONG — will 404:
<link href="/assets/css/style.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
<script src="js/main.js"></script>
CORRECT:
<link href="/public/css/style.css" rel="stylesheet">
<script src="/public/js/main.js"></script>
<img src="/public/images/logo.png" alt="Logo">
This also applies inside CSS files — background images AND fonts:
/* WRONG */
background-image: url('../images/bg.jpg');
background-image: url('images/bg.jpg');
src: url('../fonts/custom.woff2');
/* CORRECT */
background-image: url('/public/images/bg.jpg');
src: url('/public/fonts/custom.woff2');
Asset path conversion table:
| Original Path | Converted Path |
|---|---|
css/style.css | /public/css/style.css |
../css/style.css | /public/css/style.css |
./images/logo.png | /public/images/logo.png |
/images/logo.png | /public/images/logo.png |
../fonts/custom.woff | /public/fonts/custom.woff |
External URLs (Google Fonts, CDNs, etc.) stay unchanged.
The manifest.json file defines the site structure. It uses a FLAT format for CMS templates (not nested).
{
"pages": [
{ "path": "/", "file": "pages/index.html", "title": "Home" },
{ "path": "/about", "file": "pages/about.html", "title": "About" },
{ "path": "/contact", "file": "pages/contact.html", "title": "Contact" }
]
}
{
"pages": [
{ "path": "/", "file": "pages/index.html", "title": "Home" },
{ "path": "/about", "file": "pages/about.html", "title": "About" }
],
"cmsTemplates": {
"postsIndex": "templates/posts_index.html",
"postsIndexPath": "/blog",
"postsDetail": "templates/posts_detail.html",
"postsDetailPath": "/blog",
"teamIndex": "templates/team_index.html",
"teamIndexPath": "/team"
}
}
Each collection needs 2-4 keys in cmsTemplates. The format is {collectionSlug}Index, {collectionSlug}IndexPath, {collectionSlug}Detail, {collectionSlug}DetailPath.
| Key | Required | Description |
|---|---|---|
{slug}Index | Yes | Path to the collection listing template file |
{slug}IndexPath | Yes | URL path for the listing page (e.g. /blog) |
{slug}Detail | No | Path to the single item template file |
{slug}DetailPath | No | URL path prefix for item pages (e.g. /blog → /blog/item-slug) |
Example for a "services" collection:
"cmsTemplates": {
"servicesIndex": "templates/services_index.html",
"servicesIndexPath": "/services",
"servicesDetail": "templates/services_detail.html",
"servicesDetailPath": "/services"
}
AI agents frequently use a nested object format or "collections" key that FastMode does NOT support. Read carefully.
WRONG — using "collections" key (MOST COMMON AI MISTAKE):
{
"collections": {
"posts": {
"indexPath": "/blog",
"indexFile": "collections/posts/index.html",
"detailPath": "/blog/:slug",
"detailFile": "collections/posts/detail.html"
}
}
}
WRONG — nested objects inside cmsTemplates:
"cmsTemplates": {
"posts": {
"indexPath": "/blog",
"detailPath": "/blog"
}
}
WRONG — singular slug names:
"postIndex": "..." // Should be "postsIndex"
"postDetail": "..." // Should be "postsDetail"
CORRECT — flat keys using cmsTemplates, matching the collection slug exactly:
"cmsTemplates": {
"postsIndex": "templates/posts_index.html",
"postsIndexPath": "/blog",
"postsDetail": "templates/posts_detail.html",
"postsDetailPath": "/blog"
}
Key rules:
cmsTemplates, NOT collections{slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPathfastmode validate manifest manifest.json to catch these errors before deploying{
"pages": [...],
"cmsTemplates": {...},
"defaultHeadHtml": "<link rel=\"stylesheet\" href=\"/public/css/global.css\">",
"defaultBodyEndHtml": "<script src=\"/public/js/analytics.js\"></script>"
}
FastMode templates use Handlebars-style tokens. There are three types of templates:
pages/): Fixed HTML with optional data-edit-key attributes for inline CMS editing and optional {{#each}} loops for dynamic content.templates/): Collection listing pages. MUST contain at least one {{#each collectionSlug}} loop.templates/): Single item pages. MUST contain CMS tokens like {{name}}, {{{body}}}, etc.FastMode automatically manages all SEO meta tags. Including them in your HTML will cause duplicate tags (bad for SEO ranking). Remove ALL of these from your templates:
| Tag | Why to Remove |
|---|---|
<title>...</title> | Managed via CMS Settings |
<meta name="description"> | Managed via CMS Settings |
<meta name="keywords"> | Managed via CMS Settings |
<meta property="og:*"> | Open Graph auto-generated |
<meta name="twitter:*"> | Twitter cards auto-generated |
<link rel="icon"> | Favicon managed in settings |
<link rel="shortcut icon"> | Favicon managed in settings |
<link rel="apple-touch-icon"> | Managed by FastMode |
<meta name="google-site-verification"> | Managed in settings |
Correct <head> structure:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO managed by Fast Mode — do not add title, description, or OG tags -->
<link rel="stylesheet" href="/public/css/style.css">
<!-- External fonts, scripts, etc. are fine -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
| Token | Description | Example |
|---|---|---|
{{name}} | Item name/title | <h1>{{name}}</h1> |
{{slug}} | URL slug | <a href="/posts/{{slug}}"> |
{{url}} | Full URL to detail page | <a href="{{url}}">Read more</a> |
{{publishedAt}} | Publish date | <time>{{publishedAt}}</time> |
{{createdAt}} | Creation date | |
{{updatedAt}} | Last modified date |
{{field}}Used for text, number, date, image, url, email, select, boolean fields:
<h1>{{name}}</h1>
<p>{{excerpt}}</p>
<img src="{{featured-image}}" alt="{{name}}">
<span>Category: {{category}}</span>
<a href="{{website-url}}">Visit</a>
{{{field}}}CRITICAL: Rich text fields contain HTML. You MUST use triple braces {{{ }}} so the HTML renders correctly. Double braces will escape the HTML and display raw tags as text.
<!-- CORRECT — HTML renders properly -->
<div class="content">{{{body}}}</div>
<div class="bio">{{{bio}}}</div>
<!-- WRONG — HTML appears as escaped text like <p>Hello</p> -->
<div class="content">{{body}}</div>
{{#each collection}}Used in index templates and static pages to iterate over collection items.
Basic loop:
{{#each posts}}
<article>
<h2><a href="{{url}}">{{name}}</a></h2>
<p>{{excerpt}}</p>
</article>
{{/each}}
Loop modifiers:
| Modifier | Description | Example |
|---|---|---|
limit=N | Maximum items | {{#each posts limit=6}} |
sort="field" | Sort by field | {{#each posts sort="publishedAt"}} |
order="asc|desc" | Sort direction | {{#each posts sort="name" order="asc"}} |
featured=true | Only featured items | {{#each posts featured=true limit=3}} |
where="field.slug:{{slug}}" | Filter by relation | {{#each posts where="author.slug:{{slug}}"}} |
Combined modifiers:
<!-- Latest 3 featured posts, newest first -->
{{#each posts featured=true limit=3 sort="publishedAt" order="desc"}}
<article>{{name}}</article>
{{/each}}
Available only inside {{#each}} blocks:
| Variable | Description | Example |
|---|---|---|
{{@index}} | Zero-based index (0, 1, 2...) | Item {{@index}} |
{{@first}} | True for the first item | {{#if @first}}hero{{/if}} |
{{@last}} | True for the last item | {{#unless @last}},{{/unless}} |
{{@length}} | Total number of items | Showing {{@length}} items |
Do NOT use loop variables outside {{#each}} blocks — they will produce warnings and undefined values.
{{#each posts}}
{{#if @first}}
<div class="hero">
<h1>{{name}}</h1>
</div>
{{else}}
<div class="card">
<h3>{{name}}</h3>
</div>
{{/if}}
{{/each}}
{{#if}}, {{#unless}}<!-- Show if field has a value -->
{{#if image}}
<img src="{{image}}" alt="{{name}}">
{{/if}}
<!-- Show if field has a value, with fallback -->
{{#if thumbnail}}
<img src="{{thumbnail}}" alt="">
{{else}}
<div class="placeholder">No image</div>
{{/if}}
<!-- Show if field is empty/missing -->
{{#unless posts}}
<p>No posts yet.</p>
{{/unless}}
<!-- Equal -->
{{#if (eq status "published")}}
<span class="badge">Published</span>
{{/if}}
<!-- Not equal — useful for "Related Items" excluding current item -->
{{#unless (eq slug ../slug)}}
<a href="{{url}}">{{name}}</a>
{{/unless}}
<!-- Numeric comparisons -->
{{#if (lt @index 1)}} <!-- Less than -->
{{#if (gt @index 0)}} <!-- Greater than -->
{{#if (lte price 100)}} <!-- Less than or equal -->
{{#if (gte stock 5)}} <!-- Greater than or equal -->
{{#if (ne status "draft")}} <!-- Not equal -->
Hero + grid layout pattern:
{{#each posts}}
{{#if (lt @index 1)}}
<div class="hero"><h1>{{name}}</h1></div>
{{else}}
{{#if (lt @index 4)}}
<div class="featured"><h3>{{name}}</h3></div>
{{else}}
<div class="list-item">{{name}}</div>
{{/if}}
{{/if}}
{{/each}}
Access fields on related items using dot notation:
{{#each posts}}
<article>
<h2>{{name}}</h2>
{{#if author}}
<span class="author">By {{author.name}}</span>
{{#if author.photo}}
<img src="{{author.photo}}" alt="{{author.name}}">
{{/if}}
{{/if}}
</article>
{{/each}}
Available: {{relation.name}}, {{relation.slug}}, {{relation.url}}, {{relation.anyField}}.
../Inside a loop, access the parent scope (the current page's item) with ../:
<!-- On an author detail page, show only THIS author's posts -->
<h1>{{name}}</h1>
<h2>Posts by {{name}}</h2>
{{#each posts}}
{{#if (eq author.name ../name)}}
<article>
<h2><a href="{{url}}">{{name}}</a></h2>
</article>
{{/if}}
{{/each}}
@root.When nesting loops, use @root. to reference root-level collections:
{{#each categories}}
<h3>{{name}}</h3>
{{#each @root.posts where="category.slug:{{slug}}"}}
<a href="{{url}}">{{name}}</a>
{{/each}}
{{/each}}
data-edit-key (CRITICAL for Static Pages)Without data-edit-key attributes, static pages have NO editable content in the CMS dashboard. Every text element that should be editable MUST have one.
<!-- Static pages — REQUIRED for editable content -->
<h1 data-edit-key="home-hero-title">Welcome to Our Site</h1>
<p data-edit-key="home-hero-subtitle">We build amazing things.</p>
<p data-edit-key="home-about-text">Our story began in 2020...</p>
<!-- Hierarchical naming for sections -->
<section class="about">
<h2 data-edit-key="about-section-title">About Us</h2>
<p data-edit-key="about-section-paragraph-1">First paragraph...</p>
<p data-edit-key="about-section-paragraph-2">Second paragraph...</p>
</section>
<!-- CMS templates — optional, for hardcoded headers -->
<h1 data-edit-key="blog-page-title">Our Blog</h1>
Naming convention: {page}-{section}-{element}
Examples: home-hero-title, about-team-heading, contact-form-intro, services-cta-button
Rules:
{{#if video}}
<iframe
src="{{video}}"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"
title="Video"
></iframe>
{{/if}}
The referrerpolicy="strict-origin-when-cross-origin" attribute is required for YouTube embeds — without it, videos may show Error 150/153.
There are two types of images and they are handled differently:
1. Static/UI images — logos, icons, decorative backgrounds bundled with the site:
<!-- KEEP these as static /public/ paths -->
<img src="/public/images/logo.png" alt="Company Logo">
<img src="/public/images/icons/arrow.svg" alt="">
2. CMS content images — post images, team photos, product images managed through the CMS:
<!-- USE CMS tokens — NEVER hardcode content image URLs -->
{{#if image}}
<img src="{{image}}" alt="{{name}}">
{{/if}}
Rule of thumb: If it's site branding/design → keep static. If it's content that changes per item → use CMS tokens.
Always wrap CMS images in {{#if}} — not every item may have an image:
{{#if image}}
<img src="{{image}}" alt="{{name}}">
{{else}}
<div class="placeholder-image"></div>
{{/if}}
Common mistake — mixing static and CMS images:
<!-- WRONG: hardcoded image inside a CMS loop -->
{{#each products}}
<img src="/images/product-placeholder.jpg" alt="Product"> <!-- BAD -->
<h2>{{name}}</h2>
{{/each}}
<!-- CORRECT: all content comes from CMS -->
{{#each products}}
{{#if image}}
<img src="{{image}}" alt="{{name}}">
{{/if}}
<h2>{{name}}</h2>
{{/each}}
<form data-form-name="contact" action="/_forms/contact" method="POST">
<input type="text" name="name" placeholder="Your name" required>
<input type="email" name="email" placeholder="Your email" required>
<textarea name="message" placeholder="Your message" required></textarea>
<button type="submit">Send Message</button>
</form>
Rules:
data-form-name attribute is required.action must point to /_forms/{formName}.name attributes.CRITICAL: Remove Original Form Handlers
If the source site has JavaScript that handles form submissions, you MUST remove or replace it. Original site JS often does e.preventDefault() and shows a "fake" success toast — the data goes nowhere.
// PROBLEM: This blocks real submissions!
form.addEventListener('submit', (e) => {
e.preventDefault();
showToast('Message sent!'); // FAKE! Data not saved!
});
Option A (simplest): Remove the original JavaScript form handler entirely. The native <form action="/_forms/contact" method="POST"> will submit correctly.
Option B (keep JS UX): Replace the handler with one that actually POSTs to FastMode:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formName = form.dataset.formName;
const response = await fetch('/_forms/' + formName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(new FormData(form)))
});
if (response.ok) {
form.reset();
alert('Message sent!'); // NOW it's real!
}
});
fastmode deploy site.zip reads the zip, validates it, and uploads it to the server.deploy polls for build status every 3 seconds until the build completes or times out (default 2 minutes).| Flag | Description |
|---|---|
--force | Skip the GitHub connection check. Use if the project has GitHub connected but you want to deploy via CLI anyway. |
--no-wait | Upload only — don't wait for the build to finish. Useful for fire-and-forget. |
--timeout <ms> | Custom build timeout in milliseconds. Default: 120000 (2 minutes). |
After every deploy or content change, check the build status:
fastmode status
If the build failed, status shows:
Always run fastmode status after deploying to verify the build succeeded.
If a build fails:
fastmode status to see the errorfastmode deploy site.zipfastmode deploys # Show last 10 deployments
fastmode deploys --limit 5 # Show last 5
Shows status, version, duration, source, and errors for each deployment.
deploy: Exits 1 if the build fails (when waiting).status: Exits 1 if the latest deploy shows "Failed".Always validate before deploying. Validation catches errors that would cause build failures.
# 1. Validate the manifest
fastmode validate manifest manifest.json
# 2. Validate each template
fastmode validate template pages/index.html -t static_page
fastmode validate template templates/posts_index.html -t custom_index -c posts
fastmode validate template templates/posts_detail.html -t custom_detail -c posts
# 3. Validate the complete package
fastmode validate package site.zip
Manifest validation:
pages array exists and is not empty/) existscmsTemplates format is correct (flat keys, not nested)Template validation:
{{#each}} has matching {{/each}}, {{#if}} has {{/if}}{{#each}} loop{{{field}}}/public/ prefixreferrerpolicyPackage validation:
public/ (not assets/ or root)templates/ (not collections/)Add -p to validate tokens against the actual project schema:
fastmode validate template templates/posts_detail.html -t custom_detail -c posts -p "My Project"
This reports which tokens reference fields that don't exist in the schema yet, with instructions to create them via fastmode schema sync.
Problem: CSS, JS, or images don't load.
Cause: Files are in /assets/ instead of /public/, or paths don't include /public/.
Fix: Move all static files to the public/ folder. Reference them as /public/css/style.css.
Problem: Content displays <p>Hello</p> as text instead of rendering it.
Cause: Using double braces {{body}} on a rich text field.
Fix: Use triple braces {{{body}}} for all richText fields.
Problem: Index template shows no items.
Cause: Missing {{#each collectionSlug}} loop.
Fix: Add a loop: {{#each posts}}...{{/each}}.
Problem: Every item page shows identical content.
Cause: Detail template has no CMS tokens — just static HTML.
Fix: Use tokens like {{name}}, {{{body}}}, {{image}} in the detail template.
Problem: Build fails with manifest errors.
Cause: Using nested objects or "collections" instead of flat "cmsTemplates" keys.
Fix: Use flat format: "postsIndex", "postsIndexPath", "postsDetail", "postsDetailPath".
Problem: Created an item with a relation field but it's null.
Cause: Used the item's name instead of its UUID.
Fix: Run fastmode items relations <collection> --field <field> to get the UUID, then use that.
Problem: Form appears to submit (shows toast/alert) but no data is received.
Cause: Original JavaScript calls preventDefault() and shows a fake success message.
Fix: Remove any form JavaScript that blocks submission. Use data-form-name and action="/_forms/formName".
Problem: Pages appear in the CMS but have no editable content.
Cause: Missing data-edit-key attributes on text elements.
Fix: Add data-edit-key="unique-key" to every text element that should be editable.
Problem: deploy refuses to upload, says GitHub is connected.
Cause: Project has GitHub auto-deploy enabled.
Fix: Use --force flag: fastmode deploy site.zip --force.
Problem: Upload succeeds but build fails.
Fix: Run fastmode status to see the error. Common causes: invalid tokens, missing template files, malformed manifest. Fix the issue and re-deploy.
Problem: Links in templates point to wrong paths.
Cause: Template hardcodes /posts/ but manifest sets "postsIndexPath": "/blog".
Fix: Make sure hardcoded links in templates match the paths defined in manifest.json.
Problem: {{@index}} or {{@first}} shows nothing.
Cause: Used outside of a {{#each}} block.
Fix: Only use loop variables inside {{#each}}...{{/each}}.
Problem: SEO tags show up twice in the rendered HTML.
Cause: HTML templates include <title>, <meta name="description">, or Open Graph tags.
Fix: Remove all SEO tags from templates. FastMode manages them automatically via CMS Settings. See the SEO Tags section above.
Problem: Background images or custom fonts don't load.
Cause: CSS files use relative paths like url('../images/bg.jpg') or url('../fonts/custom.woff').
Fix: Update all paths inside CSS files to use /public/ prefix: url('/public/images/bg.jpg'), url('/public/fonts/custom.woff2').
Problem: Index page shows static placeholder cards instead of real CMS data.
Cause: Template has hardcoded HTML cards instead of {{#each}} loops with CMS tokens.
Fix: Replace hardcoded content blocks with {{#each collection}}...{{/each}} loops using CMS field tokens.
Run through this checklist before every deploy:
Structure:
manifest.json at package rootpages/ foldertemplates/ folderpublic/ folder (not assets/)SEO (CRITICAL):
<title> tags in HTML<meta name="description"> tags<meta property="og:*"> tags<link rel="icon"> tagsManifest:
"path": "/" existscmsTemplates keys (NOT nested, NOT collections)Templates:
{{#each}} loops{{name}}, {{{body}}}, etc.){{{field}}}{{#each}} have matching {{/each}}{{#if}} have matching {{/if}}/public/ paths{{#if}} wrappersStatic Pages:
data-edit-key on every editable text elementdata-form-name and action="/_forms/{name}"Assets:
/public/ prefixbackground-image and font url() paths use /public/ prefixValidation:
fastmode validate manifest manifest.json
fastmode validate template <each-template> -t <type> [-c <collection>]
fastmode validate package site.zip
All commands exit with code 0 on success and code 1 on failure.
| Scenario | Commands |
|---|---|
| No project specified | All project-scoped commands |
| File not found | schema sync, items create -f, validate * |
| Invalid JSON | schema sync, items create -d, items update -d |
| Validation errors | validate manifest, validate template, validate package |
| Build failed | deploy (when waiting), status |
Delete without --confirm | items delete |
Error: No project specified.
Use -p <id-or-name>, set FASTMODE_PROJECT env var, or run: fastmode use <project>
Error: File not found: schema.json
Error: Invalid JSON in --data argument
Error: Deletion requires the --confirm flag. This action cannot be undone.
Most commands auto-trigger fastmode login if credentials are missing or expired. If authentication fails:
fastmode login manually| Path | Purpose |
|---|---|
~/.fastmode/credentials.json | OAuth tokens (auto-created by login) |
~/.fastmode/config.json | Default project setting (created by use) |
Both files have restricted permissions (0o600 — owner read/write only).
fastmode use). Override with -p <name-or-id>.-d) must be valid JSON. For complex data, write a file and use -f data.json.<p>, <h2>, <ul>, <a>). Always use triple braces in templates.fastmode items relations to find available IDs.--draft flag creates unpublished items. Use --publish/--unpublish to change status..fastmode.ai subdomain. Custom domains can be configured.fastmode status to verify the build succeeded.fastmode examples <type> and fastmode guide [section] for built-in documentation and code snippets.