@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.
- package/README.md +42 -3
- package/bin/cli.mjs +10 -0
- package/package.json +4 -2
- package/src/commands/content.mjs +4 -3
- package/src/commands/create.mjs +11 -3
- package/src/commands/dev.mjs +9 -3
- package/src/commands/model.mjs +3 -3
- package/src/commands/packageCmd.mjs +1 -1
- package/src/commands/publish.mjs +1 -1
- package/src/commands/refresh.mjs +8 -3
- package/src/commands/release.mjs +22 -0
- package/src/commands/setupPreviews.mjs +19 -0
- package/src/commands/validate.mjs +1 -1
- package/src/lib/agentsMd.mjs +108 -155
- package/src/lib/designer-bridge.js +26 -0
- package/src/lib/designerPalette.mjs +62 -0
- package/src/lib/galleryPosters.mjs +105 -0
- package/src/lib/previewOptions.mjs +1 -1
- package/src/lib/releaseBundle.mjs +141 -0
- package/src/vendor/contract/v1/context.json +1 -1
- package/src/vendor/contract/v1/manifest.schema.json +6 -0
- package/src/vendor/contract/v2/behaviours.json +346 -0
- package/src/vendor/contract/v2/content-bounds.json +117 -0
- package/src/vendor/contract/v2/content-model.json +766 -0
- package/src/vendor/contract/v2/context.json +1569 -0
- package/src/vendor/contract/v2/dialect.json +105 -0
- package/src/vendor/contract/v2/fonts.json +910 -0
- package/src/vendor/contract/v2/imagery.json +32 -0
- package/src/vendor/contract/v2/islands.json +263 -0
- package/src/vendor/contract/v2/layout.json +28 -0
- package/src/vendor/contract/v2/manifest.schema.json +388 -0
- package/src/vendor/contract/v2/sections.json +947 -0
- package/src/vendor/contract/v2/site.schema.json +1353 -0
- package/src/vendor/contract/v2/tokens.json +101 -0
- package/src/vendor/contract/v2.lock.json +6931 -0
- package/src/vendor/engine/content-footprint.mjs +76 -1
- package/src/vendor/engine/majors.mjs +42 -0
- package/src/vendor/validator/gift-aid-logo.svg +6 -0
- package/src/vendor/validator/model-reference-v2.mjs +95 -0
- package/src/vendor/validator/platform-base.css +359 -2
- package/src/vendor/validator/preview-v1.mjs +691 -0
- package/src/vendor/validator/preview-v2.mjs +654 -0
- package/src/vendor/validator/preview.mjs +8 -688
- package/src/vendor/validator/site-context-v2.mjs +113 -0
- package/src/vendor/validator/validate-v1.mjs +666 -0
- package/src/vendor/validator/validate-v2.mjs +642 -0
- package/src/vendor/validator/validate.mjs +21 -621
- package/starter/assets/theme.css +3 -0
- package/starter/layout.liquid +15 -18
- package/starter/manifest.json +6 -4
- package/starter/preview/config.json +4 -0
- package/starter/preview/media/README.md +9 -0
- package/starter/sections/campaigns.liquid +5 -4
- package/starter/sections/cta.liquid +2 -2
- package/starter/sections/hero.liquid +9 -2
- package/starter/sections/homeHero.liquid +28 -24
- package/starter/sections/people.liquid +7 -6
- 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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": {
|
package/src/commands/content.mjs
CHANGED
|
@@ -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:
|
|
37
|
+
nav: site.nav,
|
|
38
|
+
actions: site.actions,
|
|
38
39
|
...(manifest ? { pages: { home: composePage(manifest, 'home'), about: composePage(manifest, 'about') } } : {}),
|
|
39
|
-
...
|
|
40
|
+
...site.content
|
|
40
41
|
};
|
|
41
42
|
const problems = validatePreviewContent(data);
|
|
42
43
|
if (problems.length > 0) {
|
package/src/commands/create.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
package/src/commands/dev.mjs
CHANGED
|
@@ -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
|
package/src/commands/model.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
package/src/commands/publish.mjs
CHANGED
|
@@ -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';
|
package/src/commands/refresh.mjs
CHANGED
|
@@ -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/
|
|
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
|
/**
|
package/src/lib/agentsMd.mjs
CHANGED
|
@@ -1,160 +1,113 @@
|
|
|
1
|
-
//
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
npm run
|
|
20
|
-
npm run
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
}
|