@minamocms/framework 0.0.0-stage → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +240 -0
- package/LICENSE +105 -0
- package/README.md +133 -2
- package/dist/boot.d.ts +19 -0
- package/dist/boot.js +22 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +44 -0
- package/dist/create-site/cli.d.ts +2 -0
- package/dist/create-site/cli.js +31 -0
- package/dist/create-site/generate-site.d.ts +12 -0
- package/dist/create-site/generate-site.js +168 -0
- package/dist/create-site/mint-token-cli.d.ts +2 -0
- package/dist/create-site/mint-token-cli.js +26 -0
- package/dist/create-site/mint-token.d.ts +7 -0
- package/dist/create-site/mint-token.js +49 -0
- package/dist/create-site/template/AGENTS.md +473 -0
- package/dist/create-site/template/content/menus/footerCompany.json +9 -0
- package/dist/create-site/template/content/menus/footerProduct.json +9 -0
- package/dist/create-site/template/content/menus/footerResources.json +9 -0
- package/dist/create-site/template/content/menus/main.json +9 -0
- package/dist/create-site/template/content/pages/404.json +23 -0
- package/dist/create-site/template/content/pages/about/careers.json +22 -0
- package/dist/create-site/template/content/pages/about/team.json +22 -0
- package/dist/create-site/template/content/pages/about.json +22 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +24 -0
- package/dist/create-site/template/content/pages/docs.json +22 -0
- package/dist/create-site/template/content/pages/index.json +182 -0
- package/dist/create-site/template/content/redirects.json +4 -0
- package/dist/create-site/template/dockerignore +6 -0
- package/dist/create-site/template/gitignore +11 -0
- package/dist/create-site/template/theme/assets/site.js +49 -0
- package/dist/create-site/template/theme/assets/style.css +1223 -0
- package/dist/create-site/template/theme/blocks/button.liquid +21 -0
- package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
- package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
- package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
- package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
- package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
- package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
- package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
- package/dist/create-site/template/theme/config/site_settings.json +11 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +102 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +36 -0
- package/dist/create-site/template/theme/sections/faq.liquid +27 -0
- package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
- package/dist/create-site/template/theme/sections/hero.liquid +48 -0
- package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
- package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
- package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
- package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
- package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
- package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
- package/dist/create-site/template/theme/snippets/responsive-media.liquid +107 -0
- package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
- package/dist/create-site/template/vhost/Dockerfile +33 -0
- package/dist/create-site/template/vhost/docker-entrypoint.sh +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/media/drivers/driver.d.ts +11 -0
- package/dist/media/drivers/driver.js +1 -0
- package/dist/media/drivers/local-fs-driver.d.ts +3 -0
- package/dist/media/drivers/local-fs-driver.js +94 -0
- package/dist/media/filename.d.ts +2 -0
- package/dist/media/filename.js +54 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/media/seed-media-cli.d.ts +2 -0
- package/dist/media/seed-media-cli.js +22 -0
- package/dist/media/seed-media.d.ts +11 -0
- package/dist/media/seed-media.js +76 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +83 -0
- package/dist/pull-site/cli.d.ts +2 -0
- package/dist/pull-site/cli.js +78 -0
- package/dist/pull-site/prompt-token.d.ts +16 -0
- package/dist/pull-site/prompt-token.js +69 -0
- package/dist/pull-site/prompts.d.ts +22 -0
- package/dist/pull-site/prompts.js +127 -0
- package/dist/pull-site/pull-site.d.ts +23 -0
- package/dist/pull-site/pull-site.js +211 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-cache.d.ts +11 -0
- package/dist/renderer/render-cache.js +11 -0
- package/dist/renderer/render-page.d.ts +34 -0
- package/dist/renderer/render-page.js +209 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/admin-redirect.d.ts +5 -0
- package/dist/routes/admin-redirect.js +26 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +30 -0
- package/dist/routes/batch.d.ts +10 -0
- package/dist/routes/batch.js +148 -0
- package/dist/routes/capabilities.d.ts +6 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +169 -0
- package/dist/routes/deleted-pages.d.ts +8 -0
- package/dist/routes/deleted-pages.js +9 -0
- package/dist/routes/drafts.d.ts +10 -0
- package/dist/routes/drafts.js +73 -0
- package/dist/routes/git.d.ts +8 -0
- package/dist/routes/git.js +158 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +86 -0
- package/dist/routes/links.d.ts +8 -0
- package/dist/routes/links.js +18 -0
- package/dist/routes/media-public.d.ts +11 -0
- package/dist/routes/media-public.js +125 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +80 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +148 -0
- package/dist/routes/preview-revision.d.ts +10 -0
- package/dist/routes/preview-revision.js +96 -0
- package/dist/routes/preview.d.ts +10 -0
- package/dist/routes/preview.js +56 -0
- package/dist/routes/public.d.ts +10 -0
- package/dist/routes/public.js +123 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +139 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search-public.d.ts +6 -0
- package/dist/routes/search-public.js +104 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +15 -0
- package/dist/routes/settings.d.ts +10 -0
- package/dist/routes/settings.js +68 -0
- package/dist/routes/sitemap.d.ts +7 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +116 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +24 -0
- package/dist/schemas/page.schema.json +26 -0
- package/dist/schemas/redirects.schema.json +24 -0
- package/dist/search/drivers/driver.d.ts +9 -0
- package/dist/search/drivers/driver.js +1 -0
- package/dist/search/drivers/node-sqlite-driver.d.ts +7 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-content.d.ts +32 -0
- package/dist/search/query-content.js +207 -0
- package/dist/search/query-index.d.ts +5 -0
- package/dist/search/query-index.js +21 -0
- package/dist/search/rebuild-index.d.ts +3 -0
- package/dist/search/rebuild-index.js +320 -0
- package/dist/server-config.d.ts +23 -0
- package/dist/server-config.js +237 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +235 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +145 -0
- package/dist/services/checkpoint.d.ts +4 -0
- package/dist/services/checkpoint.js +45 -0
- package/dist/services/content-read.d.ts +27 -0
- package/dist/services/content-read.js +151 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +166 -0
- package/dist/services/deleted-pages.d.ts +11 -0
- package/dist/services/deleted-pages.js +83 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +16 -0
- package/dist/services/drafts.js +164 -0
- package/dist/services/etag.d.ts +2 -0
- package/dist/services/etag.js +30 -0
- package/dist/services/fs-walk.d.ts +1 -0
- package/dist/services/fs-walk.js +26 -0
- package/dist/services/git-commit.d.ts +3 -0
- package/dist/services/git-commit.js +12 -0
- package/dist/services/git-history.d.ts +28 -0
- package/dist/services/git-history.js +131 -0
- package/dist/services/git-revert.d.ts +12 -0
- package/dist/services/git-revert.js +140 -0
- package/dist/services/git.d.ts +16 -0
- package/dist/services/git.js +127 -0
- package/dist/services/interval-job.d.ts +4 -0
- package/dist/services/interval-job.js +9 -0
- package/dist/services/ip-allowlist.d.ts +7 -0
- package/dist/services/ip-allowlist.js +31 -0
- package/dist/services/links.d.ts +22 -0
- package/dist/services/links.js +169 -0
- package/dist/services/manage-menus.d.ts +15 -0
- package/dist/services/manage-menus.js +138 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +156 -0
- package/dist/services/menu-references.d.ts +3 -0
- package/dist/services/menu-references.js +38 -0
- package/dist/services/menus.d.ts +9 -0
- package/dist/services/menus.js +43 -0
- package/dist/services/migration-runner.d.ts +16 -0
- package/dist/services/migration-runner.js +169 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +43 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +236 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/pid-file.d.ts +13 -0
- package/dist/services/pid-file.js +54 -0
- package/dist/services/post-urls.d.ts +3 -0
- package/dist/services/post-urls.js +27 -0
- package/dist/services/prepared-operation.d.ts +4 -0
- package/dist/services/prepared-operation.js +1 -0
- package/dist/services/publish.d.ts +15 -0
- package/dist/services/publish.js +313 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +22 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/reindex-on-write.d.ts +3 -0
- package/dist/services/reindex-on-write.js +30 -0
- package/dist/services/resolve-blog-url.d.ts +11 -0
- package/dist/services/resolve-blog-url.js +31 -0
- package/dist/services/resolve-url.d.ts +11 -0
- package/dist/services/resolve-url.js +20 -0
- package/dist/services/site-settings.d.ts +28 -0
- package/dist/services/site-settings.js +116 -0
- package/dist/services/startup-checks.d.ts +10 -0
- package/dist/services/startup-checks.js +81 -0
- package/dist/services/static-file.d.ts +7 -0
- package/dist/services/static-file.js +37 -0
- package/dist/services/theme-component-file.d.ts +6 -0
- package/dist/services/theme-component-file.js +39 -0
- package/dist/services/theme-files.d.ts +33 -0
- package/dist/services/theme-files.js +148 -0
- package/dist/services/theme-page-templates.d.ts +7 -0
- package/dist/services/theme-page-templates.js +53 -0
- package/dist/services/theme-schemas.d.ts +2 -0
- package/dist/services/theme-schemas.js +137 -0
- package/dist/services/token-auth.d.ts +10 -0
- package/dist/services/token-auth.js +64 -0
- package/dist/services/urls.d.ts +2 -0
- package/dist/services/urls.js +20 -0
- package/dist/services/validation.d.ts +26 -0
- package/dist/services/validation.js +195 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- package/dist/site-check/cli.d.ts +2 -0
- package/dist/site-check/cli.js +48 -0
- package/dist/site-check/run-check.d.ts +12 -0
- package/dist/site-check/run-check.js +188 -0
- package/dist/site-sync/cli-helpers.d.ts +23 -0
- package/dist/site-sync/cli-helpers.js +75 -0
- package/dist/site-sync/deploy.d.ts +36 -0
- package/dist/site-sync/deploy.js +124 -0
- package/dist/site-sync/live-content.d.ts +30 -0
- package/dist/site-sync/live-content.js +68 -0
- package/dist/site-sync/prompts.d.ts +30 -0
- package/dist/site-sync/prompts.js +237 -0
- package/dist/site-sync/pull-cli.d.ts +2 -0
- package/dist/site-sync/pull-cli.js +75 -0
- package/dist/site-sync/pull-site.d.ts +28 -0
- package/dist/site-sync/pull-site.js +171 -0
- package/dist/site-sync/push-cli.d.ts +2 -0
- package/dist/site-sync/push-cli.js +268 -0
- package/dist/site-sync/push-site.d.ts +96 -0
- package/dist/site-sync/push-site.js +376 -0
- package/dist/site-sync/remote-site.d.ts +38 -0
- package/dist/site-sync/remote-site.js +122 -0
- package/dist/site-sync/sync-record.d.ts +26 -0
- package/dist/site-sync/sync-record.js +76 -0
- package/dist/site-sync/theme-sync.d.ts +34 -0
- package/dist/site-sync/theme-sync.js +128 -0
- package/dist/stop-site/cli.d.ts +2 -0
- package/dist/stop-site/cli.js +28 -0
- package/dist/stop-site/stop-site.d.ts +26 -0
- package/dist/stop-site/stop-site.js +56 -0
- package/dist/theme-state.d.ts +20 -0
- package/dist/theme-state.js +33 -0
- package/dist/upgrade/changelog.d.ts +8 -0
- package/dist/upgrade/changelog.js +46 -0
- package/dist/upgrade/finish.d.ts +9 -0
- package/dist/upgrade/finish.js +30 -0
- package/dist/upgrade/owned-files.d.ts +11 -0
- package/dist/upgrade/owned-files.js +42 -0
- package/dist/upgrade/package-names.d.ts +4 -0
- package/dist/upgrade/package-names.js +26 -0
- package/dist/upgrade/upgrade-cli.d.ts +2 -0
- package/dist/upgrade/upgrade-cli.js +170 -0
- package/package.json +64 -4
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file orients an AI coding agent working in this repository. It is written for the task of turning a visual design (a screenshot, a Figma export, a written brief) into working site code - not for general software engineering advice.
|
|
4
|
+
|
|
5
|
+
This repo is a single site built on Minamo: a self-hosted, git-backed CMS. There is no framework source code here to read - the CMS itself is an installed dependency (`vhost/node_modules/@minamocms/framework`). Everything that makes this site what it is lives in two folders:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
theme/ Liquid templates - the design/markup layer
|
|
9
|
+
content/ JSON files - the actual page content, git-tracked, the source of truth
|
|
10
|
+
media/ uploaded images - not git-tracked, see "Images" below
|
|
11
|
+
vhost/ deploy config (package.json, server.js, site.config.json) - rarely needs editing
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Every rule below is exact, not a rough guide - the CMS validates content against real JSON Schemas and will reject anything that deviates. Where this file gives a worked example, prefer copying its shape over improvising a new one.
|
|
15
|
+
|
|
16
|
+
## Folder structure inside `theme/`
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
theme/
|
|
20
|
+
layouts/ *.liquid, flat, no schema - page wrappers (<html>, <head>, nav, footer)
|
|
21
|
+
sections/ *.liquid, flat, one per section type - markup + embedded settings schema
|
|
22
|
+
blocks/ *.liquid, flat, one per block type - markup + embedded settings schema
|
|
23
|
+
snippets/ *.liquid, flat, no schema - small reusable partials, invoked with {% render %}
|
|
24
|
+
assets/ design assets only (CSS, JS, icons, sprites) - served as-is at /assets/<path>. NOT content images: photographs and any image an editor would ever replace belong in media/, see "Images" below
|
|
25
|
+
root/ optional - only files that must sit at an exact path at the site root (robots.txt, .well-known/, verification files). Favicons go in assets/, see below
|
|
26
|
+
templates/ *.json, flat, optional - prebuilt starting pages an editor can pick from
|
|
27
|
+
config/ site_settings.json, optional - the site-wide settings editors can change, see "Site settings"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Favicons and other icons go in `theme/assets/`,** linked from the layout's `<head>` - `<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">`, `<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">` - not in `theme/root/`. Browsers use whatever the `<link>` names, so the site root doesn't need them. `theme/root/` is optional: create it only for a file that genuinely must sit at an exact path at the site's root, such as `robots.txt`, something under `.well-known/`, or a verification file a service asks for by name.
|
|
31
|
+
|
|
32
|
+
No subfolders inside `layouts/`, `sections/`, `blocks/`, or `snippets/` - one file, one component, named directly. The filename (without `.liquid`) is that component's type identifier and must match `^[a-z0-9][a-z0-9-]*$` (lowercase, digits, hyphens only). This exact string is what page content JSON uses in its own `"type"` field - they must match exactly.
|
|
33
|
+
|
|
34
|
+
This scaffold already ships real, working examples worth reading before writing anything new: `theme/sections/hero.liquid`, `theme/blocks/button.liquid`, and `theme/layouts/theme.liquid`. Match their conventions rather than inventing a different style.
|
|
35
|
+
|
|
36
|
+
## Turning a design into code - the actual workflow
|
|
37
|
+
|
|
38
|
+
1. Break the design into distinct repeating/reusable visual components. Each one becomes a `theme/sections/<name>.liquid` (a self-contained region of a page) or `theme/blocks/<name>.liquid` (a smaller item nested inside a section, e.g. one card in a grid, one FAQ row).
|
|
39
|
+
2. Each file has two parts: ordinary Liquid/HTML markup, and a `{% schema %} ... {% endschema %}` block containing a single JSON object - a settings description using *only* the JSON Schema draft-07 keywords listed below, never the full spec. The schema block is stripped out before rendering, so a real Liquid tag never sees it - place it anywhere in the file (convention: at the end).
|
|
40
|
+
|
|
41
|
+
**Only these keywords are supported**: `type`, `properties`, `required`, `additionalProperties`, `default`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `enum`, `items`, `minItems`, `maxItems`, plus the custom `format`/`title`/`description`/`group`/`allowedBlocks`/`api`/`swatches`/`step`/`unit` keywords documented below. **Never `$ref`, `$defs`, `definitions`, `allOf`, `anyOf`, `oneOf`, `not`, or `if`/`then`/`else`** - every property's schema must be fully self-contained, written out in full where it's used. If the same shape (e.g. an image object) repeats across several properties or several component files, write it out each time rather than trying to share/reference a definition - there is no cross-referencing mechanism here, in a single schema block or across files, regardless of what standard JSON Schema itself supports elsewhere.
|
|
42
|
+
3. Once the theme components exist, compose an actual page by writing a file under `content/pages/` whose `sections` array references those types by filename, with a `settings` object matching each one's schema (see "Content JSON model" below).
|
|
43
|
+
4. **For any page type the site will have more than one of** - a project, an article, a case study, a team member - also write a starting point for it under `theme/templates/<name>.json`. This is easy to skip and worth not skipping: without a template, every new page an editor creates starts completely blank, and they have to rebuild the same section stack by hand every time. A template is just a real page file kept in a different folder - the same shape as anything under `content/pages/` (`schemaVersion`, `name`, `title`, `type`, `layout`, `published`, `sections`), validated identically, using the section types this theme already defines. Its `"title"` is the label an editor picks from, so name it for the page type (`"Project"`, `"Article"`), never `"Untitled"`. **Set its `"type"` to that kind too** (`"project"`, `"article"`) - not `"page"`. Every page an editor creates from a template inherits the template's own `type`, and that value is what listings filter on, so an Article template left at `"type": "page"` silently produces articles no blog index can find. This is easy to get wrong because the template still validates and previews perfectly either way; nothing fails, the listing is simply always empty. Fill each section's settings with short placeholder copy rather than leaving them empty - a template is a starting point to edit, not a blank form. A template that fails validation is skipped silently at boot, so preview a page built from it.
|
|
44
|
+
5. **Put every image through `seed-media` before referencing it - never copy image files into the repo by hand.** Photographs and other content images do not belong in the site root, in `theme/root/`, or in `theme/assets/`; they belong in `media/`, under a content-addressed filename the CMS generates. Run `npm run seed-media -- .. <file>...` from `vhost/` (the same place `npm run check` runs from) and use the `/media/...` URL it prints, as the `url` of a `format: "image"` setting rendered through the `responsive-media` snippet - never a hand-written `<img>`. See "Images" below for the detail and for how this changes once a server is running. Getting this wrong is quiet rather than loud: the page still renders, the image is simply missing.
|
|
45
|
+
6. Preview the result, then run `npm run check` before considering the task done - it renders every page for real and fails on any image or link pointing at a file that does not exist, which is the fastest way to catch a misplaced image. See "Previewing your work".
|
|
46
|
+
|
|
47
|
+
### Worked example - a section
|
|
48
|
+
|
|
49
|
+
```liquid
|
|
50
|
+
<section class="hero" data-section-id="{{ section.id }}">
|
|
51
|
+
<h1>{{ section.settings.heading }}</h1>
|
|
52
|
+
{% if section.settings.subheading %}<p>{{ section.settings.subheading }}</p>{% endif %}
|
|
53
|
+
{% if section.settings.image.url %}
|
|
54
|
+
{% render 'responsive-media',
|
|
55
|
+
media: section.settings.image,
|
|
56
|
+
kind: 'image',
|
|
57
|
+
field: 'image',
|
|
58
|
+
alt: section.settings.heading,
|
|
59
|
+
ratio: '16 / 9' %}
|
|
60
|
+
{% endif %}
|
|
61
|
+
<div class="hero__blocks">{% for html in blocksHtml %}{{ html | raw }}{% endfor %}</div>
|
|
62
|
+
</section>
|
|
63
|
+
{% schema %}
|
|
64
|
+
{
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["heading"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"heading": { "type": "string", "minLength": 1, "default": "New section" },
|
|
70
|
+
"subheading": { "type": "string" },
|
|
71
|
+
"image": { "type": "object", "format": "image" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
{% endschema %}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Available variables in a section: `section.id`, `section.settings.<key>`, `blocksHtml` (an array of already-rendered child block HTML strings - a section never sees raw block data, only finished HTML, output with `{{ html | raw }}`), and `page` - the same built-in envelope a layout gets (`page.title`, `page.author`, `page.publishDate`, `page.tags`; see "Content JSON model" below). A block template gets the same shape: `block.id`, `block.settings.<key>`, `page`, and (rarely) its own `blocksHtml` if it nests further blocks. `page.author`/`publishDate`/`tags` render as empty/absent, not an error, on a page that doesn't set them - useful for printing a byline/date inside the page body without duplicating the value into a settings field.
|
|
78
|
+
|
|
79
|
+
**Every property listed in a schema's `"required"` array must also declare a `"default"`** that itself satisfies the property's own constraints (e.g. not `"default": ""` against `"minLength": 1`). A schema that violates this - or one with no `{% schema %}` block at all, or invalid JSON inside it - is excluded from the theme entirely (it simply won't be selectable), but never silently: boot prints a warning naming the type and the specific reason. If a new section/block isn't showing up, check the server's console output first.
|
|
80
|
+
|
|
81
|
+
To restrict which block types are allowed under a given section/block, add `"allowedBlocks": ["button", "logo-mark"]` alongside `"properties"` in its schema - omit it entirely for no restriction (the default).
|
|
82
|
+
|
|
83
|
+
### Naming a section or block for the admin
|
|
84
|
+
|
|
85
|
+
Give every section and block a `"title"` and a `"description"`, as plain annotations on the schema object alongside `"properties"`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"title": "Image band",
|
|
90
|
+
"description": "A full width photograph with an optional caption.",
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": { }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Neither affects validation. The admin's "Add a Section" dialog lists one row per type showing the title with the description beside it, so a type with no description shows a bare name and an editor has to guess what it is for. Omit the `"title"` and the type's filename is shown instead (`image-band`), which reads as code, not as a choice.
|
|
97
|
+
|
|
98
|
+
Write the description as one short sentence about what the section **is**, not which fields it has - the fields are already visible the moment the section is added. "A full width photograph with an optional caption" is useful; "Heading, image and caption fields" is not.
|
|
99
|
+
|
|
100
|
+
### Layouts
|
|
101
|
+
|
|
102
|
+
`theme/layouts/theme.liquid` is required - every theme must define a layout named exactly `theme` as the default (a page can opt into a different one via its own `"layout"` field). A layout only ever sees:
|
|
103
|
+
|
|
104
|
+
```liquid
|
|
105
|
+
{{ content_for_layout | raw }} the page's fully-rendered sections, concatenated
|
|
106
|
+
{{ page.title }} the page's title
|
|
107
|
+
{{ page.author }} the page's author, if set
|
|
108
|
+
{{ page.publishDate }} the page's publish date, if set
|
|
109
|
+
{% for tag in page.tags %}...{% endfor %} the page's tags, if any
|
|
110
|
+
{{ menus.<handle>.items }} every menu in content/menus/, keyed by handle (its filename)
|
|
111
|
+
{{ menus.<handle>.name }} that menu's optional display name (blank when unset)
|
|
112
|
+
{{ settings.<name> }} a site setting, in any layout, section, block or snippet (see below)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Snippets
|
|
116
|
+
|
|
117
|
+
Flat `.liquid` files in `snippets/`, invoked with `{% render 'name', param1: value %}` - never `{% include %}`. A snippet only sees parameters explicitly passed to it; the calling scope never leaks in.
|
|
118
|
+
|
|
119
|
+
This scaffold ships one you must use: **`snippets/responsive-media.liquid`, through which every content image and video is rendered.** It emits the attribute that lets an editor drag a file from the media library straight onto the picture in the live preview. See "Making an image or video droppable" below, and `theme/sections/hero.liquid` / `theme/sections/cta-banner.liquid` for real call sites.
|
|
120
|
+
|
|
121
|
+
## Site settings
|
|
122
|
+
|
|
123
|
+
Anything used across the whole site that an editor should be able to change - social links, a contact email, a choice of font - is a **site setting**, not a hard-coded value. Define them in `theme/config/site_settings.json`, in the same JSON Schema form as a section's `{% schema %}` (field formats, `title`, `default`, and `group` to put related settings under one collapsible heading; a `required` property needs a valid `default`), and read them anywhere as `{{ settings.<name> }}`, snippets included. For a font, list the choices with `enum` and map each to a font stack in the layout; the theme loads the font files. Check a link isn't blank before rendering it. Editors set the values in the admin's Settings screen; they are saved in `content/settings.json` and go live on save. See `guide-theme-authoring.md`'s "Site settings".
|
|
124
|
+
|
|
125
|
+
## Field format hints
|
|
126
|
+
|
|
127
|
+
Every setting is plain JSON Schema (`string`, `integer`, `number`, `boolean`, `array`, with `minLength`/`minimum`/`enum`/etc. for real validation). One extra keyword, `"format"`, is a UI hint only (never validated server-side) that the admin reads to choose a richer input widget:
|
|
128
|
+
|
|
129
|
+
| `format` | On type | Effect |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `richtext` | `string` | Rich-text editor; render with `{{ ... | raw }}`, not plain `{{ }}` |
|
|
132
|
+
| `image` | `object` | Image picker with focal point; object shape is exactly `{ "url": "...", "focalX": 0.5, "focalY": 0.5 }`. **Render it with `{% render 'responsive-media' %}`, never a hand-written `<img>`** - see "Making an image or video droppable" below |
|
|
133
|
+
| `video` | `object` | Video picker for a short, silent background loop; object shape is exactly `{ "url": "...", "poster": "..." }`. **Render it with `{% render 'responsive-media' %}`, never a hand-written `<video>`** - see "Making an image or video droppable" below. The snippet sets the poster for you, which matters because the poster is what shows before the clip loads and whenever it cannot play. Only `.mp4`/`.webm` can be uploaded, under the same 10MB media cap - long-form video belongs on YouTube/Vimeo as an embed, not here. No focal point: a loop is played as a background rather than cropped around a subject |
|
|
134
|
+
| `textarea` | `string` | Multi-line `<textarea>` |
|
|
135
|
+
| `url` | `string` | A link. The admin suggests the site's own pages as the editor types (by title or path) and stores the chosen page's path, e.g. `/about`; any external address, `mailto:`, `tel:` or `#anchor` can be typed instead. Store only the address - for an "open in a new window" option, add a separate `boolean` field. (`uri`, the older name, still works; `npm run check` suggests renaming it.) |
|
|
136
|
+
| `date` | `string` | `<input type="date">`, value as `YYYY-MM-DD` |
|
|
137
|
+
| `color` | `string` | Hex value (e.g. `"#ff6600"`); optional sibling `"swatches": ["#c2410c", ...]` for a preset palette (not an `enum` - a custom colour is still always allowed) |
|
|
138
|
+
| `range` | `integer`/`number` | Slider + number box; requires `minimum`/`maximum`; optional `"step"` (default `1`) and `"unit"` (e.g. `"px"`) |
|
|
139
|
+
| `toggle` | `boolean` | Switch instead of a checkbox (same underlying data) |
|
|
140
|
+
| (none) | `boolean` | Plain checkbox |
|
|
141
|
+
| (none) | `string` + `"enum"` | Segmented tabs (few short options) or a `<select>` (more/longer) - decided automatically, not choosable |
|
|
142
|
+
| (none) | `array` + `items.type: "string"` | Repeatable list of text lines, with add/remove/drag-to-reorder - `minItems`/`maxItems` bound how many lines the admin UI allows; `items.minLength`/`items.maxLength` apply per line |
|
|
143
|
+
| (none) | `array` + `items.type: "object", items.format: "image"` | A gallery: a grid of image thumbnails, add via the media picker, remove/drag-to-reorder - `minItems`/`maxItems` bound how many images the admin UI allows. Each item is exactly `{ "url": "...", "focalX": 0.5, "focalY": 0.5 }`, the same shape a lone `format: "image"` field stores - **no other properties are supported on a gallery item** (see "This is a closed set" below) |
|
|
144
|
+
|
|
145
|
+
**Grouping fields:** give related fields the same `"group": "Background"` and the admin shows them together under one collapsible heading - ungrouped fields first, then each group in the order its first field appears, the first group open. It works the same in sections, blocks and site settings, and changes only the admin's layout: the setting's name and how a template reads it stay the same. How to group well:
|
|
146
|
+
|
|
147
|
+
- **Leave the fields an editor changes most ungrouped, at the top** - a section's heading, text, main image or link - so they're visible without opening anything.
|
|
148
|
+
- **Only group once there are more than about five or six fields,** and give each group at least two fields; a group of one is just an extra click.
|
|
149
|
+
- **Group by what the fields affect:** `"Layout"`, `"Background"`, `"Button"`, `"Advanced"` - short nouns, sentence case.
|
|
150
|
+
- **Use the same name for the same idea in every component** - always `"Background"`, never `"Background settings"` in one and `"Backdrop"` in another - so editors learn where things are once.
|
|
151
|
+
- **Put the most-used group first,** since the first group is the one that starts open.
|
|
152
|
+
- **Site settings group by part of the site:** `"Header"`, `"Announcement bar"`, `"Footer"`, `"Social links"`, `"Contact"`, `"Typography"`.
|
|
153
|
+
|
|
154
|
+
A `format` on the wrong `type` (e.g. `image` on a `string`) is a mistake, not something the admin guesses around - it silently falls back to a plain widget for that type.
|
|
155
|
+
|
|
156
|
+
### Minimal form - prefer this
|
|
157
|
+
|
|
158
|
+
Every type/format combination above needs nothing beyond what triggers it - no `additionalProperties`, no `required`, no `default`, no nested `properties` describing an object's own shape. None of that is what makes the admin recognise a field; it only matters if you actually need the stricter validation or a guaranteed starting value (see below). Default to the minimal form:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
"heading": {
|
|
162
|
+
"type": "string"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
"columns": {
|
|
166
|
+
"type": "integer"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
"enabled": {
|
|
170
|
+
"type": "boolean"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
"enabled": {
|
|
174
|
+
"type": "boolean",
|
|
175
|
+
"format": "toggle"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
"bio": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"format": "textarea"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
"body": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"format": "richtext"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
"link": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"format": "url"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
"publishDate": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"format": "date"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
"accent": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"format": "color"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
"align": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"enum": ["left", "center", "right"]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
"fontSize": {
|
|
209
|
+
"type": "integer",
|
|
210
|
+
"format": "range",
|
|
211
|
+
"minimum": 12,
|
|
212
|
+
"maximum": 24
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
"poster": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"format": "image"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
"backgroundLoop": {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"format": "video"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
"tags": {
|
|
226
|
+
"type": "array",
|
|
227
|
+
"items": { "type": "string" }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
"gallery": {
|
|
231
|
+
"type": "array",
|
|
232
|
+
"items": { "type": "object", "format": "image" }
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`fontSize`'s `minimum`/`maximum` are the one exception - they're not optional boilerplate, the `range` widget genuinely doesn't trigger without both.
|
|
237
|
+
|
|
238
|
+
A custom display label uses the standard JSON Schema `"title"` keyword (`"title": "Section Heading"`) - skip it and the property key auto-humanizes instead (`backgroundImage` -> "Background Image"), which is why none of the examples above bother with one.
|
|
239
|
+
|
|
240
|
+
Only reach for `"required"` + `"default"` (and, for stricter content validation, `"additionalProperties": false` on an object/`minLength`/`pattern`/etc.) when a field genuinely must always have a value from the moment a component is added - skip both and it just starts empty/unset, which is a valid state for every type above. See "Worked example - a section" above for what that fuller form looks like once it's actually needed, and remember the L1 rule if you do use it: a required field's `default` is validated against that field's *own* full schema, including `minItems`/`minLength`/etc. - `"default": []` against `"minItems": 1` fails just as surely as an empty string against `"minLength": 1` does.
|
|
241
|
+
|
|
242
|
+
### This is a closed set - do not invent a new field shape
|
|
243
|
+
|
|
244
|
+
**The table above is exhaustive.** These are the only setting shapes the admin has a real editor for. A setting whose shape doesn't match one of these rows exactly still technically works - Ajv validates it, the content saves - but the admin can only offer a raw JSON textarea for it, which is a bad editing experience for a human, not a fallback to design around. Never invent a new combination of `type`/`format`/`items` hoping the admin will render something sensible for it; if a design need doesn't map onto one of these rows, use the pattern below instead of a wider array shape.
|
|
245
|
+
|
|
246
|
+
**A repeating item with more than one independent field is a block, never an array-shaped setting.** For example, a "before/after" or "lightbox" style section needing several frames, each with its own image *and* a caption *and* a timestamp, is not `"type": "array", "items": { "type": "object", "properties": { "image": ..., "caption": ..., "time": ... } } }` - that shape has no admin widget and never will (it's an open-ended amount of nested field types, not a closed set like the table above). Model it as a block type instead:
|
|
247
|
+
|
|
248
|
+
```liquid
|
|
249
|
+
{# theme/blocks/frame.liquid #}
|
|
250
|
+
<figure class="lightstudy__frame">
|
|
251
|
+
<img src="{{ block.settings.image.url }}" alt="{{ block.settings.caption }}">
|
|
252
|
+
<figcaption><span class="numeral">{{ block.settings.time }}</span> {{ block.settings.caption }}</figcaption>
|
|
253
|
+
</figure>
|
|
254
|
+
{% schema %}
|
|
255
|
+
{
|
|
256
|
+
"type": "object",
|
|
257
|
+
"additionalProperties": false,
|
|
258
|
+
"required": ["image", "caption", "time"],
|
|
259
|
+
"properties": {
|
|
260
|
+
"image": { "type": "object", "format": "image", "default": { "url": "", "focalX": 0.5, "focalY": 0.5 } },
|
|
261
|
+
"caption": { "type": "string", "minLength": 1, "default": "The room in a particular light" },
|
|
262
|
+
"time": { "type": "string", "minLength": 1, "default": "12:00" }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
{% endschema %}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Then the parent section just loops `blocksHtml`, exactly as it already does for any other block type - see "Section markup" above. This gets real add/remove/drag-to-reorder and a proper per-field settings form (image picker, text inputs) for every one of `image`/`caption`/`time` independently, for free - a single array setting never gets that, no matter how its `items` schema is shaped.
|
|
269
|
+
|
|
270
|
+
A multi-line field (e.g. an animated headline, one line per array entry) uses the array shape above rather than a single `format: "textarea"` string - each line is edited and reordered independently:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"type": "array",
|
|
275
|
+
"minItems": 1,
|
|
276
|
+
"maxItems": 4,
|
|
277
|
+
"items": { "type": "string", "minLength": 1 },
|
|
278
|
+
"default": ["New section"]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```liquid
|
|
283
|
+
{% for line in section.settings.heading %}<span>{{ line }}</span>{% endfor %}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
A separate keyword, `"api": true`, can be added to any scalar property to expose its value through `GET /search.json` for structured filtering, independent of full-text search - see that section below.
|
|
287
|
+
|
|
288
|
+
## Content JSON model
|
|
289
|
+
|
|
290
|
+
`content/pages/*.json` - nested folders allowed via a **sibling** pattern: a page with children is a `.json` file sitting beside a same-named folder (`about.json` next to `about/team.json`, never `about/about.json`). A URL maps directly to this path; `/` maps to `index.json`.
|
|
291
|
+
|
|
292
|
+
Required fields, `additionalProperties: false`:
|
|
293
|
+
|
|
294
|
+
| Field | Type | Notes |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `schemaVersion` | integer | Always `7` for new content |
|
|
297
|
+
| `name` | string | Internal label (shown in the admin's page tree) |
|
|
298
|
+
| `title` | string | Rendered as `{{ page.title }}` |
|
|
299
|
+
| `type` | string | **The field that decides which listings a page appears in.** Free-form (e.g. `"page"`, `"project"`, `"article"`), lowercase by convention. It is indexed as `pageType` and is what `GET /search.json?pageType=...` filters on, so a project listing, a blog index and a team directory each depend on their pages carrying the right value here. Give every kind of page its own type; `"page"` is for ordinary one-off pages only |
|
|
300
|
+
| `layout` | string | A filename in `theme/layouts/` (no extension) - `"theme"` unless a different layout exists |
|
|
301
|
+
| `published` | boolean | `false` behaves as if the page doesn't exist on the live site at all |
|
|
302
|
+
| `sections` | array | Section instances - see below |
|
|
303
|
+
|
|
304
|
+
Optional fields, any page may carry them: `author` (string), `publishDate` (string, `YYYY-MM-DD` recommended - it's indexed numerically for sorting/range filters), `tags` (array of non-empty strings). There is no separate "post" content type - a blog article is just a page, conventionally nested under a `content/pages/blog/` folder.
|
|
305
|
+
|
|
306
|
+
Each entry in `sections` requires `id` (any non-empty string, unique within the page), `type` (must exactly match a filename in `theme/sections/`), and `settings` (matching that type's schema). Optional `blocks` array, same shape, referencing `theme/blocks/`.
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"schemaVersion": 7,
|
|
311
|
+
"name": "Home",
|
|
312
|
+
"title": "Welcome",
|
|
313
|
+
"type": "page",
|
|
314
|
+
"layout": "theme",
|
|
315
|
+
"published": true,
|
|
316
|
+
"sections": [
|
|
317
|
+
{
|
|
318
|
+
"id": "sec-hero",
|
|
319
|
+
"type": "hero",
|
|
320
|
+
"settings": { "heading": "Welcome" },
|
|
321
|
+
"blocks": [
|
|
322
|
+
{ "id": "blk-cta", "type": "button", "settings": { "label": "Get started", "url": "/" } }
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`content/menus/<handle>.json` - the filename is the menu's handle, referenced in layouts as `{{ menus.<handle>.items }}`. An optional `"name"` is its display name (editable in the admin, available as `{{ menus.<handle>.name }}`). Renaming the file changes the handle and empties every layout still using the old one, so change `"name"` to rename a menu:
|
|
330
|
+
|
|
331
|
+
```json
|
|
332
|
+
{ "schemaVersion": 7, "items": [{ "label": "Home", "url": "/" }, { "label": "About", "url": "/about" }] }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`content/redirects.json` - a single file, not a folder:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{ "schemaVersion": 1, "entries": [{ "from": "/old-path", "to": "/new-path" }] }
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`to` must be a bare internal path (no `https://`, no leading `//`). A redirect never overrides a real page at the same URL.
|
|
342
|
+
|
|
343
|
+
**Links to the site's own pages are always paths** - `/about`, `/about#team` - in link fields, menu items, site settings and rich text alike, never `https://the-site.example/about`. When an editor moves a page, every link written as a path is rewritten to the new address automatically (and deleting a page shows what links to it); a full address to the site's own domain is left alone.
|
|
344
|
+
|
|
345
|
+
`content/pages/404.json`, if present and `published`, renders through the normal page pipeline with the HTTP status forced to 404 - the standard way to give a broken URL a real branded page instead of a bare JSON error.
|
|
346
|
+
|
|
347
|
+
## Images
|
|
348
|
+
|
|
349
|
+
**Every content image lives in `media/`, named by the CMS itself - never placed there, or anywhere else in the repo, by hand.** If you are generating a site and have image files to use, `seed-media` (below) is the way to get them in; it is step 5 of the workflow above. Images do not go in the site root, in `theme/root/`, or in `theme/assets/` (that folder is for design assets - CSS, JS, icons).
|
|
350
|
+
|
|
351
|
+
**One image is one file and one URL. The CMS does not generate resized variants, and you must not hand-build them.** Do not produce fixed-width copies of a photo, do not write a `srcset` listing several generated files, and do not add a build step that emits them. Server-side resizing is a deliberately deferred feature - there is no image processing in the CMS at all - so hand-made variants are files nothing manages: the media library cannot show them as one image, and an editor replacing the picture gets the original swapped while every variant silently keeps the old photo. Reference the single `/media/` URL and let the browser scale it, with CSS (`max-width: 100%`, `object-fit`) doing the responsive work. `width`/`height` attributes and `loading="lazy"` are worth setting; multiple sources are not available.
|
|
352
|
+
|
|
353
|
+
Once a server is running, uploads go through `POST /v1/media` (multipart, requires a token with `media` scope) or the admin's own media library UI - never write directly into `media/` from an agent, since the CMS names files by content hash. A successful upload returns `{ "url": "/media/<name>" }`. In theme content, **an image is a `format: "image"` object setting, never a plain string** - and a short silent loop is a `format: "video"` one:
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
"image": { "type": "object", "format": "image" }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This is not a stylistic preference. The admin's picker, the focal point, and drag-and-drop replacement all work on that object shape (`{ "url", "focalX", "focalY" }`; a video stores `{ "url", "poster" }`). A plain string setting gets a bare text box and nothing else - and since dropping an image writes the object shape, dragging onto a field declared as a string fails validation outright. Use a plain string only for a path that is never editable content, such as a theme asset bundled under `theme/assets/`.
|
|
360
|
+
|
|
361
|
+
### Making an image or video droppable
|
|
362
|
+
|
|
363
|
+
Render every content image and video through the `responsive-media` snippet the scaffold ships in `theme/snippets/` - see `hero.liquid` and `cta-banner.liquid` for working call sites:
|
|
364
|
+
|
|
365
|
+
```liquid
|
|
366
|
+
{% if section.settings.image.url %}
|
|
367
|
+
{% render 'responsive-media',
|
|
368
|
+
media: section.settings.image,
|
|
369
|
+
kind: 'image',
|
|
370
|
+
field: 'image',
|
|
371
|
+
alt: 'What is actually in the picture',
|
|
372
|
+
ratio: '16 / 9' %}
|
|
373
|
+
{% endif %}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
A video is the same call with `kind: 'video'` and the video setting - the snippet handles the `<video>`, the poster and the autoplay/muted/loop attributes itself:
|
|
377
|
+
|
|
378
|
+
```liquid
|
|
379
|
+
{% if section.settings.backgroundLoop.url %}
|
|
380
|
+
{% render 'responsive-media',
|
|
381
|
+
media: section.settings.backgroundLoop,
|
|
382
|
+
kind: 'video',
|
|
383
|
+
field: 'backgroundLoop',
|
|
384
|
+
ratio: '21 / 9' %}
|
|
385
|
+
{% endif %}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Parameters:
|
|
389
|
+
|
|
390
|
+
| Parameter | Effect |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `media` | **Required.** The whole setting object, not its `.url` - the snippet reads `url`, and `focalX`/`focalY` or `poster` from it |
|
|
393
|
+
| `field` | **Required.** The schema property name this renders. A drop writes the new URL back to exactly this key, so a wrong value silently writes to the wrong setting |
|
|
394
|
+
| `kind` | `'image'` (the default) or `'video'` |
|
|
395
|
+
| `alt` | Real alt text describing what is in the picture. On a video it becomes an `aria-label`; omit it for a purely decorative loop and the clip is marked `aria-hidden` instead |
|
|
396
|
+
| `ratio` | Any CSS `aspect-ratio` value, e.g. `'16 / 9'`. Defaults to `'3 / 2'` |
|
|
397
|
+
| `loading` | `'eager'` for anything above the fold, otherwise omit - it defaults to `lazy`. Lazy-loading a hero image delays the largest thing on the page |
|
|
398
|
+
| `priority` | `true` adds `fetchpriority="high"`. The hero image only, never more than one per page |
|
|
399
|
+
| `class` | Extra classes on the wrapper element |
|
|
400
|
+
|
|
401
|
+
The snippet puts `data-cms-media="<field>"` on the element it renders (plus `data-cms-image` for images). **That attribute is the whole contract.** The admin finds a drop target by searching the previewed page for it, and uses its value to know which setting to write the new URL into, so `field` must match the schema property name exactly. A hand-written `<img>` without it renders perfectly and is simply never droppable - a silent gap, because nothing about the page looks wrong, which is exactly how a real generated site shipped with every image un-editable.
|
|
402
|
+
|
|
403
|
+
Guard the call on the url, as above. An unset image renders nothing at all rather than a placeholder: a theme cannot tell whether it is rendering for the admin preview or the public site, so an editor-only affordance would show to real visitors. The first image is set through the Fields panel's own picker; drag-and-drop replaces it from then on.
|
|
404
|
+
|
|
405
|
+
If you're writing starter content before a server is even running - so `POST /v1/media` isn't reachable yet - use the `seed-media` CLI instead of placing images under `theme/root/`. It computes the exact same content-addressed filename a real upload would, so the result is indistinguishable from one:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
cd vhost
|
|
409
|
+
npm run seed-media -- .. photo.jpg another.png
|
|
410
|
+
# photo.jpg -> /media/photo-3f9a2b7c1e04.jpg
|
|
411
|
+
# another.png -> /media/another-91cd4a08f2b1.png
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Run it from `vhost/`, not the site root. There is no `package.json` or `node_modules` at the site root - the CMS is installed under `vhost/` - so a bare `npx seed-media` there resolves nothing locally, goes to the public npm registry looking for a package called "seed-media", and fails with a 404 while still exiting 0.
|
|
415
|
+
|
|
416
|
+
Then use the printed URL as the `url` of a `format: "image"` setting, exactly like a real upload's - not as a plain string:
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
"image": {
|
|
420
|
+
"type": "object",
|
|
421
|
+
"format": "image",
|
|
422
|
+
"default": { "url": "/media/photo-3f9a2b7c1e04.jpg", "focalX": 0.5, "focalY": 0.5 }
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
This is only for seeding starter content offline - once a server is running, a later image change from an editor still goes through `POST /v1/media` or the admin's media library as normal.
|
|
427
|
+
|
|
428
|
+
## `GET /search.json`
|
|
429
|
+
|
|
430
|
+
A public, unauthenticated, read-only endpoint - safe to call directly from a section's own client-side JavaScript with a plain `fetch()`, no token needed. Only ever returns already-published content. Query params: `q` (full-text), `filter=field:op:value` (repeatable, ANDed; `op` is `eq`/`gt`/`gte`/`lt`/`lte`), `pageType`, `sort` (`-publishDate` for newest-first), `limit`, `offset`. Useful for a blog listing, a filterable directory, or a live search box.
|
|
431
|
+
|
|
432
|
+
The index behind this endpoint keeps itself current automatically - it rebuilds in the background after every publish/unpublish/delete/move, and once at boot if no index exists yet (a fresh clone, since the index itself is never git-tracked). No manual step is needed for a blog listing built on this endpoint to work on a freshly deployed site.
|
|
433
|
+
|
|
434
|
+
## Hard constraints - do not deviate from these
|
|
435
|
+
|
|
436
|
+
- **No dynamically registered Liquid tags or filters, ever.** Only standard LiquidJS built-ins (`if`, `for`, `assign`, `render`, filters like `upcase`, `times`) plus the CMS-provided context objects described above. Never invent a custom tag.
|
|
437
|
+
- **`{{ }}` auto-escapes HTML by default.** Only use `| raw` for values the CMS itself already produced as safe HTML (`blocksHtml` entries, `content_for_layout`, a `format: "richtext"` field). Never apply `| raw` to an ordinary setting value.
|
|
438
|
+
- **Every template render is bounded to roughly 50ms.** Keep Liquid logic simple - loops and conditionals, no heavy computation.
|
|
439
|
+
- **One file, one type, no subfolders** inside `layouts/`, `sections/`, `blocks/`, `snippets/` - and the filename must match `^[a-z0-9][a-z0-9-]*$` exactly.
|
|
440
|
+
- **The `{% schema %}` block must be valid, parseable JSON.** A malformed or missing schema fails the whole component, not just the settings half.
|
|
441
|
+
- **`additionalProperties: false` applies everywhere in content JSON** - don't add a field "just in case"; anything not in the tables above fails validation.
|
|
442
|
+
- **Never invent a new setting field shape.** The "Field format hints" table is the complete, closed list of what the admin can actually edit - a plain type, a type plus one of the listed `format` values, `array` of plain strings, or `array` of plain images. A repeating item with more than one field of its own (an image plus a caption, a date, anything else) is a block type, not a wider array setting - see "This is a closed set" above.
|
|
443
|
+
|
|
444
|
+
## Previewing your work
|
|
445
|
+
|
|
446
|
+
From `vhost/`:
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
npm start # boots the site on the port set in vhost/site.config.json
|
|
450
|
+
npm run tunnel # same, plus a public tunnel URL for sharing a preview
|
|
451
|
+
npm run dev # same, plus auto-restart whenever a theme/ file changes - use this one while iterating
|
|
452
|
+
npm run stop # stops a site started by any of the above, from any terminal
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
A site is deployed once; after that, `npm run pull` and `npm run push` move changes between this copy and the live site. `npm run pull` makes this copy match the live site for whatever is chosen - content (pages, menus, redirects, site settings, images), theme, or both - asking for the address and access token (or `npm run pull -- <its URL>` with `CMS_TOKEN` set). It refuses while the parts it would overwrite have uncommitted changes, and commits nothing. `npm run push` sends back only what changed here since the last pull: anything an editor changed on the live site since then stops the push and is listed instead of overwritten, it shows everything it will change, and it asks for the site's address to be typed first. After `npm run upgrade` it can also upgrade the live site's CMS. Never run push on a user's behalf without them confirming.
|
|
456
|
+
|
|
457
|
+
`npm run dev` only watches `theme/` - content changes (via the API) already show up on the next request with no restart needed, so there's nothing to gain watching `content/` too.
|
|
458
|
+
|
|
459
|
+
Then request the page you changed (`curl http://localhost:<port>/<path>`, or open it in a browser) and confirm it actually renders as expected before considering a change finished - a page that fails schema validation or references a non-existent section type won't crash the server, but the specific page/component involved will misbehave silently.
|
|
460
|
+
|
|
461
|
+
## Checking your work: `npm run check`
|
|
462
|
+
|
|
463
|
+
From `vhost/`, with the site's dependencies already installed (no need for the server to be running):
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
npm run check
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Renders every published page for real and reports, in one pass: any theme component excluded at boot (same warnings the server itself prints, see "Minimal form" above); any `src`/`srcset`/`poster`/`href` in the rendered HTML pointing at a `/media/`, `/assets/`, or root-static file that doesn't actually exist on disk; any internal link that doesn't point at a real, published page; and **any content image or video being served from `theme/root/` or `theme/assets/` instead of `media/`**. Exits non-zero if it finds anything - safe to run after generating content, not just as a manual spot-check. It may also list **Suggestions** (for example a link field still declared `"format": "uri"` rather than `"url"`): worth fixing, but they never fail the check.
|
|
470
|
+
|
|
471
|
+
That last one has no other way of being noticed: the file exists, so the page renders perfectly. It is simply invisible to the media library and an editor can never replace it.
|
|
472
|
+
|
|
473
|
+
This catches the specific failure mode a snippet like `responsive-image` can introduce silently: a `widths` list that includes a size nothing was actually uploaded/generated for renders a perfectly normal-looking page with one broken image at that breakpoint - nothing about the page itself is wrong, so nothing else would ever flag it.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 7,
|
|
3
|
+
"name": "Page not found",
|
|
4
|
+
"title": "Page not found - Minamo",
|
|
5
|
+
"type": "page",
|
|
6
|
+
"layout": "theme",
|
|
7
|
+
"published": true,
|
|
8
|
+
"sections": [
|
|
9
|
+
{
|
|
10
|
+
"id": "sec-hero",
|
|
11
|
+
"type": "hero",
|
|
12
|
+
"settings": {
|
|
13
|
+
"eyebrow": "404",
|
|
14
|
+
"heading": "This page didn't make it into the build.",
|
|
15
|
+
"subheading": "The page you're looking for doesn't exist, or has moved. Check the URL, or head back to solid ground."
|
|
16
|
+
},
|
|
17
|
+
"blocks": [
|
|
18
|
+
{ "id": "blk-home", "type": "button", "settings": { "label": "Back to home", "url": "/", "style": "primary" } },
|
|
19
|
+
{ "id": "blk-docs", "type": "button", "settings": { "label": "View the docs", "url": "/docs", "style": "secondary" } }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 7,
|
|
3
|
+
"name": "Careers",
|
|
4
|
+
"title": "Careers - Minamo",
|
|
5
|
+
"type": "page",
|
|
6
|
+
"layout": "theme",
|
|
7
|
+
"published": true,
|
|
8
|
+
"sections": [
|
|
9
|
+
{
|
|
10
|
+
"id": "sec-hero",
|
|
11
|
+
"type": "hero",
|
|
12
|
+
"settings": {
|
|
13
|
+
"eyebrow": "About / Careers",
|
|
14
|
+
"heading": "We're not hiring right now.",
|
|
15
|
+
"subheading": "Minamo is a small, deliberately lean team. When that changes, openings will show up here first."
|
|
16
|
+
},
|
|
17
|
+
"blocks": [
|
|
18
|
+
{ "id": "blk-careers-cta", "type": "button", "settings": { "label": "Back to About", "url": "/about", "style": "secondary" } }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 7,
|
|
3
|
+
"name": "Team",
|
|
4
|
+
"title": "Team - Minamo",
|
|
5
|
+
"type": "page",
|
|
6
|
+
"layout": "theme",
|
|
7
|
+
"published": true,
|
|
8
|
+
"sections": [
|
|
9
|
+
{
|
|
10
|
+
"id": "sec-hero",
|
|
11
|
+
"type": "hero",
|
|
12
|
+
"settings": {
|
|
13
|
+
"eyebrow": "About / Team",
|
|
14
|
+
"heading": "A small team, on purpose.",
|
|
15
|
+
"subheading": "Minamo is maintained by a handful of people who'd rather ship a focused tool than manage a plugin marketplace."
|
|
16
|
+
},
|
|
17
|
+
"blocks": [
|
|
18
|
+
{ "id": "blk-team-cta", "type": "button", "settings": { "label": "Back to About", "url": "/about", "style": "secondary" } }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 7,
|
|
3
|
+
"name": "About",
|
|
4
|
+
"title": "About - Minamo",
|
|
5
|
+
"type": "page",
|
|
6
|
+
"layout": "theme",
|
|
7
|
+
"published": true,
|
|
8
|
+
"sections": [
|
|
9
|
+
{
|
|
10
|
+
"id": "sec-hero",
|
|
11
|
+
"type": "hero",
|
|
12
|
+
"settings": {
|
|
13
|
+
"eyebrow": "About",
|
|
14
|
+
"heading": "Built by people tired of fighting their CMS.",
|
|
15
|
+
"subheading": "Minamo started as an internal tool for an agency shipping a dozen client sites a year, and grew into something worth sharing."
|
|
16
|
+
},
|
|
17
|
+
"blocks": [
|
|
18
|
+
{ "id": "blk-about-cta", "type": "button", "settings": { "label": "Meet the team", "url": "/about/team", "style": "primary" } }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|