@port60/template-kit 0.20.4 → 1.0.0

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.
Files changed (58) hide show
  1. package/README.md +42 -3
  2. package/bin/cli.mjs +10 -0
  3. package/package.json +4 -2
  4. package/src/commands/content.mjs +4 -3
  5. package/src/commands/create.mjs +11 -3
  6. package/src/commands/dev.mjs +9 -3
  7. package/src/commands/model.mjs +3 -3
  8. package/src/commands/packageCmd.mjs +1 -1
  9. package/src/commands/publish.mjs +1 -1
  10. package/src/commands/refresh.mjs +8 -3
  11. package/src/commands/release.mjs +22 -0
  12. package/src/commands/setupPreviews.mjs +19 -0
  13. package/src/commands/validate.mjs +1 -1
  14. package/src/lib/agentsMd.mjs +108 -155
  15. package/src/lib/designer-bridge.js +26 -0
  16. package/src/lib/designerPalette.mjs +62 -0
  17. package/src/lib/galleryPosters.mjs +105 -0
  18. package/src/lib/previewOptions.mjs +1 -1
  19. package/src/lib/releaseBundle.mjs +141 -0
  20. package/src/vendor/contract/v1/context.json +1 -1
  21. package/src/vendor/contract/v1/manifest.schema.json +6 -0
  22. package/src/vendor/contract/v2/behaviours.json +346 -0
  23. package/src/vendor/contract/v2/content-bounds.json +117 -0
  24. package/src/vendor/contract/v2/content-model.json +766 -0
  25. package/src/vendor/contract/v2/context.json +1569 -0
  26. package/src/vendor/contract/v2/dialect.json +105 -0
  27. package/src/vendor/contract/v2/fonts.json +910 -0
  28. package/src/vendor/contract/v2/imagery.json +32 -0
  29. package/src/vendor/contract/v2/islands.json +263 -0
  30. package/src/vendor/contract/v2/layout.json +28 -0
  31. package/src/vendor/contract/v2/manifest.schema.json +388 -0
  32. package/src/vendor/contract/v2/sections.json +947 -0
  33. package/src/vendor/contract/v2/site.schema.json +1353 -0
  34. package/src/vendor/contract/v2/tokens.json +101 -0
  35. package/src/vendor/contract/v2.lock.json +6931 -0
  36. package/src/vendor/engine/content-footprint.mjs +76 -1
  37. package/src/vendor/engine/majors.mjs +42 -0
  38. package/src/vendor/validator/gift-aid-logo.svg +6 -0
  39. package/src/vendor/validator/model-reference-v2.mjs +95 -0
  40. package/src/vendor/validator/platform-base.css +359 -2
  41. package/src/vendor/validator/preview-v1.mjs +691 -0
  42. package/src/vendor/validator/preview-v2.mjs +654 -0
  43. package/src/vendor/validator/preview.mjs +8 -688
  44. package/src/vendor/validator/site-context-v2.mjs +113 -0
  45. package/src/vendor/validator/validate-v1.mjs +666 -0
  46. package/src/vendor/validator/validate-v2.mjs +642 -0
  47. package/src/vendor/validator/validate.mjs +21 -621
  48. package/starter/assets/theme.css +3 -0
  49. package/starter/layout.liquid +15 -18
  50. package/starter/manifest.json +6 -4
  51. package/starter/preview/config.json +4 -0
  52. package/starter/preview/media/README.md +9 -0
  53. package/starter/sections/campaigns.liquid +5 -4
  54. package/starter/sections/cta.liquid +2 -2
  55. package/starter/sections/hero.liquid +9 -2
  56. package/starter/sections/homeHero.liquid +28 -24
  57. package/starter/sections/people.liquid +7 -6
  58. package/starter/sections/values.liquid +3 -3
package/README.md CHANGED
@@ -33,6 +33,8 @@ newer contract asks of you.
33
33
  | `dev [dir]` | Live preview over the contract's sample fixtures; validation re-runs on save. |
34
34
  | `validate [dir] [--json]` | The exact checks the platform runs at upload. `--json` emits `{ok, errors, warnings, provenSupports}`. |
35
35
  | `package [dir]` | Validate, then build the contract-shaped zip the studio accepts as-is. |
36
+ | `release [dir]` | Build separate runtime and independent designer-preview folders for a trusted store release. Does not upload. |
37
+ | `setup-previews [--with-deps]` | Install the pinned headless browser for automatic gallery images. Add `--with-deps` in Linux CI. |
36
38
  | `model [--json]` | The content model, in hand; `--json` for agents. |
37
39
  | `content [dir]` | Eject the model's data as your editable copy; render it with `dev --content`. |
38
40
  | `upgrade [dir]` | Latest kit + contract for an existing template; re-briefs and re-validates. |
@@ -48,9 +50,46 @@ See the [AI quickstart](https://developers.port60.com/guides/ai-quickstart/).
48
50
 
49
51
  ## The contract
50
52
 
51
- The vendored contract in `src/vendor/contract` is the **Charity Platform contract v1**, the
52
- platform's first product surface. The dialect, validation rules and this toolchain are
53
- platform-wide; future Port60 products ship their own contract packs for the same kit.
53
+ Kit 1.0.0 authors **port60-liquid@2**, content model **2.0**, using the contract under
54
+ `src/vendor/contract/v2`. Collection envelopes, independent header/footer navigation,
55
+ resolved actions and page-scoped sections are explicit. V1 sources need a deliberate migration,
56
+ not a manifest-only relabel. Historical v1 contracts remain frozen for existing platform pins.
57
+
58
+ ## Designer previews
59
+
60
+ The scaffold creates `preview/config.json` and `preview/media/`. Optional `content` points to
61
+ fictional v2 sample JSON relative to `preview/`; omission uses contract fixtures. `focus` is
62
+ `none`, `donate` or `volunteer`. Use approved local JPEG, PNG or WebP images via
63
+ `p60preview:filename.jpg` and retain provenance with the source. Never use selecting-tenant data.
64
+
65
+ `npm run release` writes `dist/release/NAME/VERSION/`: lightweight `template/` files, separate
66
+ `preview/` HTML/media/metadata, and a `release.json` completion record. Every Look is generated
67
+ from actual Liquid/CSS with matching palettes. Existing output is refused. Limits are 24 Looks,
68
+ 2 MiB per image/page and 24 MiB per release. Demo forms, navigation and transactions are inactive.
69
+
70
+ The first-party publisher uploads completion last. The catalogue supplies matching preview
71
+ metadata to tenant-admin; charity-site reads only runtime files. Previews are not bundled into
72
+ admin and do not become tenant content. Preview-only changes also need a new template version.
73
+ The Studio ZIP `package`/`publish` path remains separate and runtime-only.
74
+
75
+ Before your first release build, run `npm run preview:setup` in a new scaffold, or
76
+ `p60-template-kit setup-previews` in an existing project. Repeat after upgrading the kit.
77
+ The release builder automatically captures each Look from the actual template at 1440x900
78
+ and writes a 960x600 WebP to `preview/gallery/`, capped at 160 KiB. No separate screenshot
79
+ authoring or upload is needed. The gallery uses these images; opening a design loads its HTML.
80
+ The pinned browser is a build dependency only, never part of the live website.
81
+
82
+ Linux capture requires a working Chromium sandbox. Ubuntu 24.04 hosted CI installs the
83
+ repository's browser-path-scoped AppArmor profile to allow its user namespaces.
84
+ No `--no-sandbox` or global AppArmor disablement is used. On another Linux
85
+ host, configure an administrator-approved sandbox according to
86
+ [Chromium's guidance](https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md).
87
+ Browser installation alone does not change the host's security policy.
88
+
89
+ Only packaged preview assets and fonts.bunny.net are accessible during capture. Failed required
90
+ imagery or Latin fonts stop generation. Arabic-specific poster font fidelity is deferred;
91
+ full previews and live-site typography are unchanged. Preserve the same kit/browser/OS for
92
+ immutable upload retries; output changes need a new version, not an overwrite.
54
93
 
55
94
  Full documentation: [developers.port60.com](https://developers.port60.com)
56
95
 
package/bin/cli.mjs CHANGED
@@ -12,6 +12,8 @@ import { upgrade } from '../src/commands/upgrade.mjs';
12
12
  import { refresh } from '../src/commands/refresh.mjs';
13
13
  import { login, logout, whoami } from '../src/commands/auth.mjs';
14
14
  import { publish } from '../src/commands/publish.mjs';
15
+ import { releaseCmd } from '../src/commands/release.mjs';
16
+ import { setupPreviews } from '../src/commands/setupPreviews.mjs';
15
17
 
16
18
  function parseArgs(argv) {
17
19
  const args = { _: [] };
@@ -37,6 +39,12 @@ const [command, ...rest] = process.argv.slice(2);
37
39
  const args = parseArgs(rest);
38
40
 
39
41
  switch (command) {
42
+ case 'setup-previews':
43
+ setupPreviews(args);
44
+ break;
45
+ case 'release':
46
+ await releaseCmd(args);
47
+ break;
40
48
  case 'create':
41
49
  create(args);
42
50
  break;
@@ -85,6 +93,8 @@ usage:
85
93
  p60-template-kit content [dir] [--out file] eject the model's data as your editable copy
86
94
  p60-template-kit validate [dir] [--json] the platform's exact conformance checks
87
95
  p60-template-kit package [dir] validate + build the uploadable zip
96
+ p60-template-kit release [dir] build separate template/ and preview/ store bundles
97
+ p60-template-kit setup-previews [--with-deps] install the release builder's headless browser
88
98
  p60-template-kit upgrade [dir] move an existing template to the latest kit
89
99
  and contract, re-brief, re-validate
90
100
  p60-template-kit login sign in from the terminal (the browser does the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@port60/template-kit",
3
- "version": "0.20.4",
3
+ "version": "1.0.0",
4
4
  "description": "Build Port60 site templates locally: scaffold, live-preview, validate against the platform contract, and package for studio upload. AI-agent ready: every scaffold ships AGENTS.md and validate emits machine-readable JSON.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,8 +16,10 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "ajv": "^8.20.0",
19
+ "jsdom": "29.1.1",
19
20
  "liquidjs": "^10.27.2",
20
- "lucide-static": "^1.41.0"
21
+ "lucide-static": "^1.41.0",
22
+ "playwright": "1.63.0"
21
23
  },
22
24
  "license": "MIT",
23
25
  "repository": {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, writeFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { buildSiteFixture, composePage, validatePreviewContent } from '../vendor/validator/site-context.mjs';
3
+ import { buildSiteFixture, composePage, validatePreviewContent } from '../vendor/validator/site-context-v2.mjs';
4
4
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
5
5
 
6
6
  /**
@@ -34,9 +34,10 @@ export function content(args) {
34
34
  }
35
35
  const data = {
36
36
  brand: site.brand,
37
- nav: { items: site.nav?.items ?? [] },
37
+ nav: site.nav,
38
+ actions: site.actions,
38
39
  ...(manifest ? { pages: { home: composePage(manifest, 'home'), about: composePage(manifest, 'about') } } : {}),
39
- ...Object.fromEntries(Object.entries(site.content).filter(([name]) => name !== 'about'))
40
+ ...site.content
40
41
  };
41
42
  const problems = validatePreviewContent(data);
42
43
  if (problems.length > 0) {
@@ -19,7 +19,7 @@ export function create(args) {
19
19
  const name = (args.name ?? basename(target)).toLowerCase();
20
20
  if (!NAME_PATTERN.test(name)) {
21
21
  console.error(`✗ '${name}' is not a valid template name (lowercase letters, digits, hyphens, `
22
- + '3–50 chars, starting with a letter). Pass --name.');
22
+ + '3 to 50 chars, starting with a letter). Pass --name.');
23
23
  process.exit(1);
24
24
  }
25
25
  if (existsSync(join(target, 'manifest.json'))) {
@@ -50,7 +50,9 @@ export function create(args) {
50
50
  dev: 'p60-template-kit dev .',
51
51
  validate: 'p60-template-kit validate .',
52
52
  'validate:json': 'p60-template-kit validate . --json',
53
- package: 'p60-template-kit package .'
53
+ package: 'p60-template-kit package .',
54
+ release: 'p60-template-kit release .',
55
+ 'preview:setup': 'p60-template-kit setup-previews'
54
56
  },
55
57
  devDependencies: {
56
58
  '@port60/template-kit': `^${KIT_PACKAGE.version}`
@@ -58,11 +60,17 @@ export function create(args) {
58
60
  }, null, 2) + '\n');
59
61
  writeFileSync(join(target, 'README.md'), `# ${label}
60
62
 
61
- A Port60 site template. Start with \`npm install\`, then:
63
+ A Port60 site template using \`port60-liquid@2\`, content model \`2.0\` and kit \`1.0.0\`.
64
+ Existing v1 platform pins keep historical support; this kit accepts v2 authoring only.
65
+ Start with \`npm install\`, then:
62
66
 
63
67
  - \`npm run dev\`, live preview at http://localhost:4400
64
68
  - \`npm run validate\`, conformance against the platform contract
65
69
  - \`npm run package\`, the uploadable zip in \`dist/\` (recreated on every run)
70
+ - \`npm run release\`, separate runtime and designer-preview folders in \`dist/release/\`
71
+ - \`npm run preview:setup\`, install the pinned browser before your first release build
72
+ - \`preview/config.json\` selects author sample content and widget state; put optional
73
+ author images in \`preview/media/\` and reference them as \`p60preview:filename.jpg\`.
66
74
 
67
75
  **Working with an AI agent?** Point it at this directory, \`AGENTS.md\` (and \`CLAUDE.md\`)
68
76
  brief it on the contract, the rules and the validate loop.
@@ -3,9 +3,9 @@ import { watch, readFileSync, existsSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { surfaceFor, knobOverridesFromQuery, previewAssetPath, previewAssetType } from '../lib/previewOptions.mjs';
5
5
  import { renderStudioPreview } from '../vendor/validator/preview.mjs';
6
- import { validateArtifact } from '../vendor/validator/validate.mjs';
7
- import { applyPreviewContent, buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context.mjs';
8
- import { renderModelReferenceHtml } from '../vendor/validator/model-reference.mjs';
6
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
7
+ import { applyPreviewContent, buildSiteFixture, validatePreviewContent } from '../vendor/validator/site-context-v2.mjs';
8
+ import { renderModelReferenceHtml } from '../vendor/validator/model-reference-v2.mjs';
9
9
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
10
10
  import { join } from 'node:path';
11
11
 
@@ -80,6 +80,12 @@ function imageOriginsOf(json) {
80
80
  */
81
81
  export async function dev(args) {
82
82
  const dir = resolve(args._[0] ?? '.');
83
+ const initialManifest = JSON.parse(loadArtifactDir(dir)['manifest.json'] ?? '{}');
84
+ if (initialManifest.format !== 'port60-liquid@2') {
85
+ console.error('✗ Kit 1.0.0 requires port60-liquid@2. Existing v1 pins remain supported by the platform, not by the new authoring kit.');
86
+ process.exitCode = 1;
87
+ return;
88
+ }
83
89
  const port = Number(args.port ?? 4400);
84
90
  // The dev-richer half of the imagery split: point P60_FIXTURE_IMAGES at the platform's fixture
85
91
  // imagery base and the preview loads photographic fixtures from that ONE origin instead of the
@@ -1,4 +1,4 @@
1
- import { contentModel } from '../vendor/engine/content-footprint.mjs';
1
+ import { contentModelV2 as contentModel } from '../vendor/engine/content-footprint.mjs';
2
2
 
3
3
  /**
4
4
  * `model [--json]`, the content model, in hand. Prints every `site.content.*` collection with
@@ -15,7 +15,7 @@ export function model(args) {
15
15
  console.log('site also carries: ' + contentModel.siblings.keys.map((k) => `site.${k}`).join(', '));
16
16
  console.log('');
17
17
  for (const [name, collection] of Object.entries(contentModel.collections)) {
18
- const more = collection.moreHref ? ` · more at ${collection.moreHref}` : '';
18
+ const more = collection.shape === 'envelope' ? ' · {label, href, items, pagination}' : ' · array';
19
19
  console.log(`site.content.${name} (bounded at ${collection.cap}${more}, since ${collection.since})`);
20
20
  for (const [field, spec] of Object.entries(collection.item)) {
21
21
  const notes = [
@@ -27,7 +27,7 @@ export function model(args) {
27
27
  }
28
28
  console.log('');
29
29
  }
30
- console.log('Rules: collections are bounded (link onward via moreHref); enums are open (branch and');
30
+ console.log('Rules: collections are bounded (link onward via href and pagination); enums are open (branch and');
31
31
  console.log('fall back); nullable fields need a branch; the model only grows. Your footprint and');
32
32
  console.log('minimum model version are computed at publish, you never declare them. Live examples:');
33
33
  console.log('the /model page on your dev preview.');
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { resolve, join } from 'node:path';
3
- import { validateArtifact } from '../vendor/validator/validate.mjs';
3
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
4
4
  import { loadArtifactDir, skippedNotice } from '../lib/artifactFiles.mjs';
5
5
  import { buildZip } from '../lib/zip.mjs';
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { validateArtifact } from '../vendor/validator/validate.mjs';
2
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
3
3
  import { loadArtifactDir, skippedNotice } from '../lib/artifactFiles.mjs';
4
4
  import { buildZip } from '../lib/zip.mjs';
5
5
  import { accessToken, api, apiBase, loadCredentials, requireCredentials } from '../lib/auth.mjs';
@@ -2,9 +2,9 @@ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { resolve, join } from 'node:path';
3
3
  import { agentsMd } from '../lib/agentsMd.mjs';
4
4
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
5
- import { validateArtifact } from '../vendor/validator/validate.mjs';
6
- import { contentModel } from '../vendor/engine/content-footprint.mjs';
7
- import dialect from '../vendor/contract/v1/dialect.json' with { type: 'json' };
5
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
6
+ import { contentModelV2 as contentModel } from '../vendor/engine/content-footprint.mjs';
7
+ import dialect from '../vendor/contract/v2/dialect.json' with { type: 'json' };
8
8
 
9
9
  const KIT_PACKAGE = JSON.parse(
10
10
  readFileSync(resolve(import.meta.dirname, '../../package.json'), 'utf8')
@@ -25,6 +25,11 @@ export async function refresh(args) {
25
25
  process.exit(1);
26
26
  }
27
27
  const manifest = JSON.parse(files['manifest.json']);
28
+ if (manifest.format !== dialect.format) {
29
+ console.error(`✗ Kit ${KIT_PACKAGE.version} requires ${dialect.format}. Migrate the template before refreshing its instructions. No files changed.`);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
28
33
 
29
34
  const briefing = agentsMd(manifest.name);
30
35
  writeFileSync(join(dir, 'AGENTS.md'), briefing);
@@ -0,0 +1,22 @@
1
+ import { mkdirSync, writeFileSync, existsSync, lstatSync } from 'node:fs';
2
+ import { resolve, join, dirname } from 'node:path';
3
+ import { buildReleaseBundle } from '../lib/releaseBundle.mjs';
4
+
5
+ export async function releaseCmd(args) {
6
+ const root = resolve(args._[0] ?? '.');
7
+ const bundle = await buildReleaseBundle(root);
8
+ const output = join(root, 'dist', 'release', bundle.manifest.name, bundle.manifest.version);
9
+ // Never follow a linked build destination or silently mix two builds of an immutable release.
10
+ for (const dir of [join(root, 'dist'), join(root, 'dist', 'release'), join(root, 'dist', 'release', bundle.manifest.name), output]) {
11
+ if (existsSync(dir) && lstatSync(dir).isSymbolicLink()) throw new Error('Linked release destination refused');
12
+ }
13
+ if (existsSync(output)) throw new Error(`Release output already exists: ${output}. Review/remove that generated directory or bump the version before rebuilding.`);
14
+ for (const [path, file] of Object.entries(bundle.files)) {
15
+ const destination = join(output, path);
16
+ mkdirSync(dirname(destination), { recursive: true });
17
+ writeFileSync(destination, file.bytes, { flag: 'wx' });
18
+ }
19
+ console.log(`Validated release: ${output}`);
20
+ console.log('template/ is runtime only; preview/ is the independent designer gallery. Publish release.json last.');
21
+ console.log('Studio package/publish still accepts the runtime ZIP; store release publication uses the first-party pipeline.');
22
+ }
@@ -0,0 +1,19 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+
5
+ export function previewSetupCommand(args = {}) {
6
+ const require = createRequire(import.meta.url);
7
+ // Playwright exports its manifest, not a ./cli module. Use its declared binary so
8
+ // this resolves the kit's pinned installation even inside another npm project.
9
+ const manifestPath = require.resolve('playwright/package.json');
10
+ const command = [resolve(dirname(manifestPath), require(manifestPath).bin.playwright), 'install', 'chromium', '--only-shell'];
11
+ if (args['with-deps']) command.push('--with-deps');
12
+ return command;
13
+ }
14
+
15
+ export function setupPreviews(args) {
16
+ const result = spawnSync(process.execPath, previewSetupCommand(args), { stdio: 'inherit' });
17
+ if (result.error) throw result.error;
18
+ process.exitCode = result.status ?? 1;
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { validateArtifact } from '../vendor/validator/validate.mjs';
2
+ import { validateNewArtifact as validateArtifact } from '../vendor/validator/validate.mjs';
3
3
  import { loadArtifactDir } from '../lib/artifactFiles.mjs';
4
4
 
5
5
  /**
@@ -1,160 +1,113 @@
1
- // The scaffold's AI-agent briefing (T3, user requirement: the kit must let people put an AI
2
- // engine on template work and be productive immediately). Written for ANY coding agent, // AGENTS.md is the cross-tool convention and CLAUDE.md carries the identical content for tools
3
- // that read that name. The briefing is the contract's rules + the iteration loop, so an agent's
4
- // first move is always the same: read this, edit, validate --json, repeat until clean.
5
-
1
+ // A contract briefing shared by every generated coding-agent instruction file.
6
2
  export function agentsMd(name) {
7
3
  return `# Working on the "${name}" Port60 template
8
4
 
9
- You are working on a **Port60 site template**, a small, versioned artifact of Liquid renderers
10
- and CSS that a charity's site is rendered through. It contains **no application code**: no
11
- JavaScript, no API calls, no payment logic. Templates decide how a site *looks*; the platform
12
- owns what it *does*.
13
-
14
- ## The iteration loop (use this constantly)
15
-
16
- \`\`\`bash
17
- npm run validate # human-readable conformance check
18
- npm run validate:json # machine-readable: {ok, errors[], warnings[], provenSupports}
19
- npm run dev # local preview at http://localhost:4400 (re-renders on refresh)
20
- npm run package # validate + produce the uploadable <name>-<version>.zip
21
- \`\`\`
22
-
23
- **After every meaningful edit, run \`npm run validate:json\` and fix every error before moving
24
- on.** The validator is the exact code the platform runs at upload, if it passes here, the
25
- platform accepts it; if it fails here, the upload will fail identically.
26
-
27
- ## The file layout (nothing else is accepted)
28
-
29
- - \`manifest.json\`, identity + what you support. \`name\` and \`version\` are immutable
30
- identity; bump \`version\` (semver) for every published change.
31
- - \`layout.liquid\`, the page chrome (header/nav/footer). Must contain **exactly one**
32
- \`{% content %}\` slot. Only needed when \`supports.layout\` is true.
33
- - \`sections/<type>.liquid\`, one renderer per section type you declare in
34
- \`supports.sections\`. Charities compose pages from a **closed catalogue** of section types
35
- (see \`npm run validate\` output or the docs), you cannot invent new types.
36
- - \`pages/<page>.liquid\`, optional full-page templates for \`supports.pageTemplates\`.
37
- - \`assets/theme.css\`, required, your entire look, and the ONLY stylesheet the platform loads
38
- (other \`.css\` files under \`assets/\` are packaged but never loaded, so keep everything in
39
- it). **No images, no fonts, no JS**: \`package\` and \`publish\` leave them out and list what
40
- they left out; the upload refuses them. Photographs belong in the charity's media library;
41
- decorative textures go inline in the CSS as data URIs.
42
-
43
- ## The rules the validator enforces (do not fight them)
44
-
45
- 1. **Escape-by-default.** Every output is HTML-escaped unless you use \`| raw\`, and the only
46
- values you may pass through raw are the contract's sanitised richtext fields.
47
- 2. **The dialect is a whitelist.** \`{% include %}\`, \`{% render %}\`, \`{% layout %}\` and
48
- several other tags are excluded and fail at parse. Unknown filters throw.
49
- 3. **Islands are placed, never implemented.** Live functionality (donations, sign-in, events) is
50
- \`{% island 'donation_widget' %}\` etc. Every island you place must be declared in
51
- \`supports.islands\` and exist in the platform registry. Style them via their stable class
52
- API; never reimplement them.
53
- 4. **Declared ⇒ rendered, rendered ⇒ declared.** Supports flags are PROVEN behaviourally: if you
54
- declare \`supports.worship\` the layout must actually render the worship fixture's times, must
55
- hide the rail when \`worship\` is null, and rendering it undeclared is equally an error. The
56
- same honesty applies across the contract.
57
- 5. **Render budgets are real.** Runaway loops are killed (~1s per render). Keep renderers simple.
58
- 6. **Context is a whitelist.** Sections see \`{section, brand}\` plus only the collection named by
59
- that section in the contract; layouts see \`{brand, nav, socials, worship, locale}\`; page
60
- templates see their documented fixture + \`brand\`. Nothing else exists, do not invent
61
- variables.
62
- 7. **Capabilities are matching metadata, never entitlements.** Every value in
63
- \`requiresCapabilities\` must have a declared section, page template or island that presents it.
64
- \`suitsProfiles\` describes design intent and changes catalogue ordering only.
65
-
66
- ## What each declaration owns
67
-
68
- - \`supports.layout\` owns the visible header, navigation and footer around platform pages. The
69
- platform still owns the document head, consent and identity.
70
- - \`supports.pages\` owns section based bodies for \`home\` and \`about\` through the declared
71
- renderers in \`sections/\`.
72
- - \`compositions\` owns each page's preferred order: the section types the design is built around,
73
- each \`core\`, \`recommended\` or \`optional\`. An organisation may reorder, add or remove;
74
- removing a core section warns them, it never stops them.
75
- - \`supports.pageTemplates: ["events"]\` owns the events listing only. Event details, RSVP and
76
- ticket purchase remain platform owned.
77
- - \`supports.pageTemplates: ["course"]\` owns a course detail presentation only. The course
78
- listing stays platform owned and enrolment remains the \`course_enrol\` island.
79
- - \`supports.pageTemplates: ["articles"]\` owns the article front page and archive listings.
80
- - \`supports.pageTemplates: ["article"]\` owns article detail presentation; engagement and
81
- comments stay the \`article_engagement\` and \`article_comments\` islands.
82
- - Other platform routes keep their platform body and render inside your layout. Capability flags
83
- expose documented optional context; they do not transfer transaction or route ownership.
84
-
85
- ## The content model
86
-
87
- Everything you read comes from ONE tree: \`site\`, \`site.brand\`, \`site.nav\`,
88
- \`site.socials\`, \`site.locale\` and the typed collections under \`site.content.*\`
89
- (services, events, articles, campaigns, causes, courses, resources, locations,
90
- schedules, about). Four rules it never breaks, so neither should you:
91
-
92
- - Every collection is BOUNDED (a documented cap plus a \`moreHref\`), link onward, never
93
- assume you have everything.
94
- - Enum fields are OPEN, branch on the values you style and fall back for the rest; the
95
- validator proves your template survives values it has never seen.
96
- - Optional fields are explicitly nullable, always branch.
97
- - The model only grows. The validator computes your content footprint from the paths you read
98
- and stamps the minimum model version at publish; you never declare versions, and dynamic
99
- indexing into \`site.content\` is refused so that stays decidable.
100
-
101
- SEE the model: \`npm run dev\` serves the full reference with live example data at \`/model\`;
102
- \`npx p60-template-kit model --json\` prints the machine-readable registry; the same reference
103
- lives at https://developers.port60.com/reference/content-model/.
104
-
105
- Bring your own content and imagery: \`npx p60-template-kit content .\` ejects every collection,
106
- fully populated, as an editable JSON file; replace the copy and the imageUrl values, then
107
- \`npx p60-template-kit dev . --content my-org.json\` renders YOUR data (the preview admits
108
- exactly the image hosts your file names, nothing else).
109
-
110
- You may replace the preview DATA with your own via \`preview-content.json\` beside the
111
- manifest ({ collection: [items] }, schema-checked, hot-reloaded). The shape is fixed, packaging
112
- excludes it, and conformance proofs always run on the canonical fixtures.
113
-
114
- ## What to build with
115
-
116
- - Theme via CSS custom properties and the settings knobs you declare in
117
- \`manifest.settings.schema\`, they surface in the charity's Appearance editor as
118
- \`--p60s-<key>\` variables and \`data-p60s-<key>\` body attributes.
119
- - Hero photographs (\`homeHero.images\`, when you declare \`supports.heroImagery\`): render one
120
- photo directly as a TREATED backdrop (a scrim/tint built from your own palette variables via
121
- color-mix, never raw), place the \`hero_carousel\` island for two or more (style its
122
- \`.hero-slide-scrim\`), and design the no-photo state as a gradient/colour, never a
123
- placeholder. The starter's \`.lq-homehero\` is the reference; the validator checks all of this
124
- behaviourally.
125
- - "Looks" = named one-click bundles of knob values in \`manifest.looks\`.
126
- - Fonts: only families from the platform font catalogue, declared with the weights you use.
127
- - Navigation can contain two levels below a top item. Render every supplied child and branch on
128
- optional \`group\`, \`description\`, \`imageUrl\` and \`megaMenu\` promo metadata. Never hardcode
129
- menu groups that are not in \`nav\`.
130
- - Navigation highlights are optional design support, not implied by the \`nav\` behaviour.
131
- Declare \`supports.navigationHighlights: true\` only when your layout renders one supplied
132
- \`site.nav.items[].megaMenu.promo\` card per expanded top-level menu. Otherwise declare false.
133
- The platform resolves linked content into \`title\`, \`text\`, \`href\`, \`label\` and optional
134
- \`imageUrl\`; no entity lookup belongs in a template. Preserve a text-only card when its image
135
- is absent, omit an absent card and keep normal navigation links. Validation proves the explicit
136
- declaration; missing declarations never enable the editor feature automatically.
137
- - Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources and
138
- locations. Derive or omit when a collection is empty and use the supplied URLs rather than
139
- constructing routes.
140
- - Submission and behaviour surfaces such as \`newsletter_signup\`, \`form\`, \`search\`,
141
- \`language_switch\` and \`next_prayer\` are platform islands. Place and style them;
142
- never reproduce their API calls or consent behaviour.
143
- - \`manifest.imagery.hero\` (optional but recommended): declare the photo shape YOUR hero
144
- composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`), the charity's editor
145
- measures their actual upload against it and advises. Advice, never enforcement.
146
-
147
- ## Which contract this is
148
-
149
- The section catalogue, islands and fixtures here are the **Charity Platform contract v1**, the
150
- platform's first product surface. The dialect, the rules above and this toolchain are
151
- platform-wide; other Port60 products will ship their own contract packs. Do not assume the
152
- current section list is universal.
153
-
154
- ## Reference
155
-
156
- The full generated reference (sections, islands, context variables, tokens, dialect) lives at
157
- https://developers.port60.com, also available in one file for agents at
158
- https://developers.port60.com/llms-full.txt
5
+ This is a Liquid and CSS artifact, not an application. The platform owns public eligibility,
6
+ routes, consent, authentication, payments and interactive islands. Preserve the design's visual
7
+ identity, authored content and inline editing markers while changing presentation.
8
+
9
+ ## Versions and iteration
10
+
11
+ Use format port60-liquid@2, content model2.0 and kit1.0.0. Existing v1 platform pins retain
12
+ their historical contract; this kit explicitly rejects v1 for new authoring and uploads.
13
+ Never relabel v1 without migrating its reads. Published name/version identities are immutable.
14
+
15
+ - npm run validate:json is the machine-readable feedback loop. Fix all errors after each edit.
16
+ - npm run validate checks the same contract as upload.
17
+ - npm run dev previews all supported pages locally.
18
+ - npm run package validates and writes the uploadable zip.
19
+ - npm run release builds a store release with separate template/ and preview/ bundles.
20
+ - p60-template-kit setup-previews installs the pinned build browser once (CI: --with-deps).
21
+ - Check all Looks, empty states, long text and mobile layouts. Validation is not visual QA.
22
+
23
+ ## Artifact shape
24
+
25
+ manifest.json declares support. layout.liquid has exactly one {% content %} slot.
26
+ sections/<type>.liquid implements catalogued types; pages/<page>.liquid implements declared
27
+ page templates. assets/theme.css is the only loaded stylesheet. No JavaScript, fonts, API calls,
28
+ remote CSS imports or image files belong in the runtime artifact. preview/ contains independent
29
+ author-demo inputs. Its config.json names a content JSON file and optional widget focus. Put
30
+ author JPEG/PNG/WebP imagery in preview/media/ and use p60preview:filename references in that
31
+ content. The release builder seals every Look and publishes that imagery only in the separate
32
+ preview/ bundle. Runtime package/publish ZIPs never contain it. Studio preview-bundle intake is
33
+ separate from the first-party store release lane. Use platform media URLs in runtime content.
34
+
35
+ Release automatically captures each Look as a 960x600 WebP under preview/gallery/, from a
36
+ 1440x900 desktop render. Do not author separate screenshots. Posters have a 160 KiB cap;
37
+ HTML, images and metadata remain beside them. The gallery loads posters; details load HTML.
38
+ The build needs Chromium plus access to fonts.bunny.net, and refuses failed required assets.
39
+ Use the same kit/browser/OS for immutable-upload retries; bump the version for changed output.
40
+ Arabic-specific poster font fidelity is deferred, not proof of Arabic-locale conformance.
41
+
42
+ ## The only public site tree
43
+
44
+ Read site.brand, site.nav, site.socials, site.locale, site.actions, site.page and site.content.
45
+ No flat brand/nav/collection aliases or site.focus exist. A section also receives section, its
46
+ current instance's raw authored content. Article/course details retain their documented record
47
+ context. impactMap receives the selected map with its contained points, never root locations.
48
+
49
+ services/events/articles/campaigns/causes/courses/documents are envelopes:
50
+ {label, href, items, pagination}. Iterate site.content.events.items, not the envelope.
51
+ Documents href can be null. Pagination is null outside listings, otherwise it carries page,
52
+ size, totalElements, totalPages, nextHref and previousHref. Use supplied URLs, not guessed routes.
53
+ Only site.content.schedules stays an array. Lists are bounded; enum values are open, so always
54
+ include fallbacks. Nullable values need guards. Metadata-only label/href/pagination reads do
55
+ not fetch items, but whole-envelope aliases do. Dynamic indexing of the site tree is refused.
56
+
57
+ The current render is site.page = {key, path, sections:[{key,type,content}]}.
58
+ Repeated section types have independent stable keys. Canonical section types include services,
59
+ courses, events and documents. No programmes, whatsOn, infoEvents, resources, locations or
60
+ content.about public aliases exist. Documents remain selected existing public records, never
61
+ an automatically exposed media library. Section support does not confer source entitlements.
62
+
63
+ ## Authored ownership and clearing
64
+
65
+ For collection introductions, absent section.title inherits site.content.<collection>.label;
66
+ an explicit empty string hides it; other text overrides it. subtitle and eyebrow are authored.
67
+ Use nil checks, not Liquid default, wherever clearing has meaning. Keep generated labels out
68
+ of raw section content. Mark an authored title only in the nonempty override branch. An inherited
69
+ heading has no data-p60-field marker.
70
+
71
+ Declare supports.fieldMarkers:true when showing authored field markers. A marker such as
72
+ data-p60-field="title" or data-p60-field="items.{{ forloop.index0 }}.label" addresses only that
73
+ section's content. Its node must contain exactly the authored value. Use a span when punctuation
74
+ or generated text surrounds it. Never mark source records, generated labels or resolved actions.
75
+
76
+ ## Navigation and actions
77
+
78
+ site.nav.header and site.nav.footer are independent arrays. kind link has href; kind group has
79
+ null href and children. Use disclosure controls for groups, not fake links. Render two child
80
+ levels and preserve description, imageUrl and optional megaMenu.promo. No derived menus, CTA
81
+ flags or generated columns exist. The template owns responsive menu layout.
82
+ supports.navigationHighlights:true must render supplied promos, including text-only cards,
83
+ without losing normal links. The nav behaviour alone never enables this feature.
84
+ site.actions.header and site.actions.hero are resolved actions or null. site.actions.widget is
85
+ donate, volunteer or none. Do not infer actions from navigation. Authored hero override text
86
+ retains its field marker; resolved fallback actions do not.
87
+
88
+ ## Safety and design
89
+
90
+ - Output is escaped. Use raw only for contract-sanitised richtext.
91
+ - The dialect is whitelisted. include/render/layout and unknown filters are rejected.
92
+ - Place declared islands with {% island 'donation_widget' %}. Style their stable API and never
93
+ recreate transactions, API calls, forms, identity or consent logic.
94
+ - Render collection envelopes with template markup and declared behaviours. The historical
95
+ events_carousel, whats_on_strip and latest_articles islands are v1-only and rejected by v2.
96
+ Collection route continuation belongs to the host, not a second template data fetch or pager.
97
+ - Declare only what you render, and render what you declare. Capability matching is metadata,
98
+ not a transfer of route ownership or tenant entitlements.
99
+ - Hero photos need a palette scrim, a carousel for multiple photos and a designed no-photo state.
100
+ - Preserve settings and Looks. Use platform fonts and declared CSS tokens.
101
+ - Scope behaviour-dependent hidden content under .p60-js so no-JavaScript stays readable.
102
+ - Keep loops bounded. Test empty collections, cleared text and unknown enum values.
103
+
104
+ ## Data and reference
105
+
106
+ p60-template-kit content . writes editable envelope fixtures, independent header/footer menus
107
+ and keyed page compositions. p60-template-kit dev . --content my.json previews those fixtures.
108
+ Overrides are schema-checked, never packaged and never replace canonical conformance fixtures.
109
+ The dev server /model shows live values beside the registry. p60-template-kit model --json
110
+ prints the current model. Generated reference: https://developers.port60.com/reference/content-model/
111
+ Full agent reference: https://developers.port60.com/llms-full.txt
159
112
  `;
160
113
  }