mosaic-headless 1.31.2 → 1.32.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/SKILL.md +5 -1
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/package.json +1 -1
- package/references/design-system.md +27 -0
- package/references/dynamic-content.md +49 -1
- package/references/server-thumbnails.md +84 -0
- package/references/templates-and-conditions.md +25 -5
- package/tools/build_site.py +43 -15
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: |
|
|
|
5
5
|
license: "MIT"
|
|
6
6
|
metadata:
|
|
7
7
|
author: "moksa (https://moksaweb.com)"
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.32.0"
|
|
9
9
|
homepage: "https://github.com/Moksa1123/mosaic-headless"
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -529,6 +529,10 @@ so the pattern is in the data, not just in this paragraph.
|
|
|
529
529
|
15. `references/upgrading.md` — what a plugin update does to the data and to the
|
|
530
530
|
delivered page, measured on 1.0.7 -> 1.0.8; how to drive the migration and what
|
|
531
531
|
to re-verify afterwards.
|
|
532
|
+
16. `references/server-thumbnails.md` — Mosaic's headless-Chrome thumbnails on a host
|
|
533
|
+
without root: where Mosaic looks for the binary, a no-root install, and the two
|
|
534
|
+
things (PHP-FPM's disabled functions, a setting decided once) that make a working
|
|
535
|
+
install look broken.
|
|
532
536
|
|
|
533
537
|
## The data files
|
|
534
538
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.32.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Upload via Settings -> Skills -> Upload. Claude.ai parses SKILL.md frontmatter and surfaces the skill in your library. The extraction tool (extract-block-schema.php) needs a live WP-CLI connection and won't run in the sandbox; use it from a local terminal against your own site instead.",
|
|
26
26
|
"uploadSteps": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.32.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Auto-loads on session start when SKILL.md frontmatter parses successfully.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.32.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "Confirmed (2026-07-11): Codex CLI natively supports the SKILL.md spec. Place SKILL.md under .codex/skills/<name>/ (project) or ~/.codex/skills/<name>/ (personal) and Codex loads the name+description at session start, then the full body on demand. A parallel, broader convention .agents/skills/ (searched from cwd up to repo root, then ~/.agents/skills/) also exists across multiple tools - if your Codex CLI version prioritizes that path instead, mirror the same SKILL.md there.",
|
|
26
26
|
"verified": true,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.32.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: GitHub Copilot added a proper '.github/skills/' Agent Skills directory (December 2025), alongside the older single-file .github/copilot-instructions.md convention. This config targets the new skills-directory form. If your Copilot version predates this (pre Dec 2025), use the instructions-append fallback instead (see fallback below).",
|
|
26
26
|
"fallback": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"author": "moksa (https://moksaweb.com)",
|
|
23
|
-
"version": "1.
|
|
23
|
+
"version": "1.32.0"
|
|
24
24
|
},
|
|
25
25
|
"loaderBehaviour": "CHANGED as of 2026-07-11: Gemini CLI now natively supports the same SKILL.md standard as Claude Code and Codex CLI - the same directory-based skill works unmodified. Gemini CLI discovers skills in this precedence order: built-in, extension skills, ~/.gemini/skills/ (personal), .gemini/skills/ (project, shared via version control). At session start Gemini injects each discovered skill's name+description into the system prompt and calls activate_skill when a task matches.",
|
|
26
26
|
"verified": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mosaic-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"description": "AI-agent skill: build Mosaic Pro (Nextend) WordPress sites by writing the underlying data model directly - 23 custom tables, no visual editor, no DOM. Every node type, style property and node property swept against a live install and asserted on the delivered HTML and compiled CSS. Installs into Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot, Continue, Windsurf and Claude.ai.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wordpress",
|
|
@@ -218,6 +218,33 @@ only the slots that differ:
|
|
|
218
218
|
attrID is a SUFFIX in the delivered page and `getElementById` on it finds nothing.
|
|
219
219
|
Address instances with `[id$="-cs-card"]`.
|
|
220
220
|
|
|
221
|
+
Four more things a build that leans on components runs into:
|
|
222
|
+
|
|
223
|
+
- **The instance element itself carries no id.** Its `attrID` exists only as that
|
|
224
|
+
prefix, so a selector aimed at the instance's own attrID finds nothing - aim at
|
|
225
|
+
`<instance>-<definition id>`. Two instances given the SAME attrID (or none) get
|
|
226
|
+
the same prefix and so duplicate every id inside; give each a distinct attrID. A
|
|
227
|
+
page with two `case-card` instances, `cs-0` and `cs-1`, measured zero duplicates.
|
|
228
|
+
- **A component's id is its name.** `build_site.py` derives it with
|
|
229
|
+
`uuid5(COMPONENT_NAMESPACE, name)` so rebuilds update the same row; renaming the
|
|
230
|
+
key in `site["components"]` therefore creates a new component and leaves the old
|
|
231
|
+
one behind.
|
|
232
|
+
- **An expression inside a definition is evaluated where the instance is.** A link
|
|
233
|
+
`{"v": "@concat('/contact/?ref=', @VAR('post/title'), '#consult')"}` inside a
|
|
234
|
+
component, placed in a single-post template, carries THAT post's title (reported
|
|
235
|
+
from a client build on 1.0.10).
|
|
236
|
+
- **The header and footer can hold instances.** `build_site.py` commits the theme's
|
|
237
|
+
design tokens first, builds the components, and only then writes the shell's
|
|
238
|
+
header and footer - with components resolved and overrides applied in the master,
|
|
239
|
+
as on a page. Measured: a footer instance with one override rendered with its
|
|
240
|
+
prefixed ids and the overridden text. (Before this order, a component in the shell
|
|
241
|
+
was impossible: its styles name tokens the shell commit had not made yet.)
|
|
242
|
+
|
|
243
|
+
What this is good for on a client site: anything repeated in several places that
|
|
244
|
+
the client should be able to change themselves - an office address block, a contact
|
|
245
|
+
bar, opening hours. Make it a component and it is edited once, in Mosaic's
|
|
246
|
+
Components panel, and every page follows, with no "site settings" plugin page.
|
|
247
|
+
|
|
221
248
|
The claim worth verifying is not that three cards rendered. It is that they are the
|
|
222
249
|
same definition, and the evidence is in the class: all three carry the identical
|
|
223
250
|
generated token (`_fm` here) while their text differs. Three copy-pasted sections
|
|
@@ -116,7 +116,18 @@ fallback(…) defaulting
|
|
|
116
116
|
attachment(2) find_image(2) find_link(2) find_video(2) media
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
`sum`, `avg`, `concat` and `fallback` are variadic.
|
|
119
|
+
`sum`, `avg`, `concat` and `fallback` are variadic.
|
|
120
|
+
|
|
121
|
+
**`@concat` drops any argument that evaluates to `'0'`** (and `''`). Each argument is
|
|
122
|
+
`strval()`-ed and appended only `if ($item)` (`EvaluatorFunctionConcat.php`), and
|
|
123
|
+
`'0'` is the one non-empty string PHP calls false. So a number that can be zero must
|
|
124
|
+
not be built into a sentence with `@concat`:
|
|
125
|
+
`@concat('SEARCH · ', @VAR('wordpress_search/resultCount'), ' 件作品')` delivers
|
|
126
|
+
`SEARCH · 件作品` on an empty search - the count vanishes, the spaces stay. Put the
|
|
127
|
+
number in its own `wysiwyg-variable` between two text runs instead; a bare
|
|
128
|
+
`@VAR(…)` prints `0`. The same goes for a price, a stock count or a loop index that
|
|
129
|
+
can be 0. `@fallback` has the mirror-image rule: it treats `'0'` as empty, and returns
|
|
130
|
+
its fallback only if that is truthy, so `'0'` can never be the fallback. `date()`'s timestamp argument is a
|
|
120
131
|
true GMT Unix timestamp — pair it with `post/publish_timestamp_gmt`, not
|
|
121
132
|
`publish_timestamp`, unless you want the local-time value re-interpreted as UTC.
|
|
122
133
|
|
|
@@ -228,3 +239,40 @@ The less precise side sets the precision: a date against a date-time compares th
|
|
|
228
239
|
two days. Whatever cannot be read as a date matches **no** operator, so a typo is a
|
|
229
240
|
condition that is never true rather than an error. Loop filters compare in SQL
|
|
230
241
|
(`post_date_gmt` / `post_modified_gmt` as UTC, the others as the site's wall clock).
|
|
242
|
+
|
|
243
|
+
## The search results page
|
|
244
|
+
|
|
245
|
+
A `search.php` auto template (`references/templates-and-conditions.md`) has a
|
|
246
|
+
`wordpress_search` namespace. Measured on 1.0.10, searching a term with four matches
|
|
247
|
+
and one with none:
|
|
248
|
+
|
|
249
|
+
| expression | 4 results | none |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `@VAR('wordpress_search/resultCount')` | `4` | `0` |
|
|
252
|
+
| `@VAR('wordpress_search/searchQuery')` | the term | the term |
|
|
253
|
+
| `@fallback(@VAR('wordpress_search/resultCount'), 'none')` | `4` | `none` |
|
|
254
|
+
| `@fallback(@VAR('wordpress_search/resultCount'), '0')` | `4` | `0` |
|
|
255
|
+
|
|
256
|
+
`resultCount` is `number_format_i18n(found_posts)` - a formatted string, so `1,234`
|
|
257
|
+
past a thousand, and `0` (not empty) when nothing matched. Do not wrap it in
|
|
258
|
+
`@concat`: a `0` there is dropped (above), which is exactly how "the count is empty
|
|
259
|
+
on a search with no results" was first reported from a client build. `@fallback` treats `''`,
|
|
260
|
+
`'0'` and `'NaN'` as empty, and returns its second argument only if THAT is truthy in
|
|
261
|
+
PHP, so a fallback of `'0'` can never be chosen; any other text can.
|
|
262
|
+
|
|
263
|
+
The results are a loop: `loopType: "localContext"`, `loopSource:
|
|
264
|
+
{"v": "@VAR('wordpress_search/results')"}` (a `@VAR`, as every loop source must be).
|
|
265
|
+
**Inside it each result is `item`, unless you rename it**:
|
|
266
|
+
|
|
267
|
+
| per-result expression | result |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `@VAR('item/title')`, default namespace | the title |
|
|
270
|
+
| `@VAR('post/title')`, default namespace | empty - `post` is the page's own context, and a search page has none |
|
|
271
|
+
| `@VAR('post/title')` with `loopNamespace: "post"` on the loop | the title |
|
|
272
|
+
|
|
273
|
+
With `loopNamespace: "post"` every per-post expression works per result - meta,
|
|
274
|
+
`@LOOP('post/<taxonomy>', 1, 'term_name')`, the permalink. WordPress's own query
|
|
275
|
+
arguments narrow the search (`/?s=…&post_type=work&work_category=<slug>`), so a
|
|
276
|
+
filter bar is a set of `menu-link`s whose `url` is
|
|
277
|
+
`{"v": "@concat('/?post_type=work&work_category=<slug>&s=', @VAR('wordpress_search/searchQuery'))"}`
|
|
278
|
+
- the term survives the switch.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Server-side thumbnails without root
|
|
2
|
+
|
|
3
|
+
Mosaic can capture page and component thumbnails on the server with headless
|
|
4
|
+
Chrome. Its documentation installs Chrome with `apt` as root, which a managed host
|
|
5
|
+
(Cloudways and the like) does not allow. The procedure below needs no root. It was
|
|
6
|
+
worked through end to end on a live client site (Debian 12, Mosaic 1.0.10) - Mosaic's
|
|
7
|
+
"generate thumbnails" completed for every template - and each step is checked
|
|
8
|
+
against the plugin source.
|
|
9
|
+
|
|
10
|
+
## How Mosaic finds Chrome
|
|
11
|
+
|
|
12
|
+
`Mosaic/Common/ScreenshotChrome.php`:
|
|
13
|
+
|
|
14
|
+
- the binary is, in order: the constant **`MOSAIC_CHROME_BINARY`**, then whatever the
|
|
15
|
+
filter **`mosaic_chrome_binary_path`** returns, then chrome-php's `AutoDiscover`,
|
|
16
|
+
which also honours a `CHROME_PATH` server variable;
|
|
17
|
+
- `isChromiumAvailable()` checks only that `shell_exec`, `proc_open` and
|
|
18
|
+
`is_executable` exist and that the binary is executable;
|
|
19
|
+
- launch options pass through **`mosaic_screenshot_chrome_options`**, and Mosaic
|
|
20
|
+
already runs with `noSandbox`.
|
|
21
|
+
|
|
22
|
+
So anything executable at a path you name will do, including a wrapper script.
|
|
23
|
+
|
|
24
|
+
## The procedure
|
|
25
|
+
|
|
26
|
+
**1. Check from a web request, never from WP-CLI.** PHP-FPM and the CLI have
|
|
27
|
+
different `disable_functions`: on the client host FPM disabled `proc_open`,
|
|
28
|
+
`proc_close`, `proc_terminate` and `shell_exec` while the CLI did not, so `wp eval`
|
|
29
|
+
said Chrome was available and every web request said it was not. Put a temporary
|
|
30
|
+
PHP file in the web root that loads `wp-load.php` and prints
|
|
31
|
+
`function_exists('proc_open')`, `function_exists('shell_exec')` and
|
|
32
|
+
`\Mosaic\Common\ScreenshotChrome::isChromiumAvailable()`; fetch it once; delete it
|
|
33
|
+
straight away.
|
|
34
|
+
|
|
35
|
+
**2. Install Chrome as the site's own user**, in a directory the app owns outside
|
|
36
|
+
the web root (e.g. the app's `private_html/chrome`):
|
|
37
|
+
|
|
38
|
+
- download `chrome-headless-shell` from Chrome for Testing - the version is in
|
|
39
|
+
`https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE`, the
|
|
40
|
+
archive at
|
|
41
|
+
`https://storage.googleapis.com/chrome-for-testing-public/<version>/linux64/chrome-headless-shell-linux64.zip`;
|
|
42
|
+
- `ldd chrome-headless-shell | grep "not found"` - on the client host:
|
|
43
|
+
`libnspr4`, `libnss3`, `libnssutil3`, `libatk-1.0`, `libatk-bridge-2.0`,
|
|
44
|
+
`libXcomposite`, `libXdamage`, `libatspi`;
|
|
45
|
+
- `apt-get download libnspr4 libnss3 libatk1.0-0 libatk-bridge2.0-0 libxcomposite1
|
|
46
|
+
libxdamage1 libatspi2.0-0` (no root needed), unpack each with `dpkg-deb -x`, and
|
|
47
|
+
copy the `*.so*` files into `chrome/lib`; repeat `ldd` until nothing is missing;
|
|
48
|
+
- a wrapper `chrome/chrome`, mode 755, that sets `LD_LIBRARY_PATH` to `chrome/lib`
|
|
49
|
+
and `exec`s `chrome-headless-shell "$@"`.
|
|
50
|
+
|
|
51
|
+
CJK text needs a CJK font the system already has (`fc-list :lang=zh`); that host's
|
|
52
|
+
Droid Sans Fallback was enough.
|
|
53
|
+
|
|
54
|
+
**3. Point Mosaic at the wrapper**:
|
|
55
|
+
`wp config set MOSAIC_CHROME_BINARY /path/to/chrome/chrome --type=constant`.
|
|
56
|
+
|
|
57
|
+
**4. Let PHP-FPM run it.** Remove exactly `proc_open`, `proc_close`, `proc_terminate`
|
|
58
|
+
and `shell_exec` from the app's `php_admin_value[disable_functions]` and keep the rest.
|
|
59
|
+
On Cloudways that is Application Settings → PHP-FPM Settings, or the API's
|
|
60
|
+
`app_fpm_settings_update` (which takes the whole settings block, base64-encoded);
|
|
61
|
+
either way it changes only that app. chrome-php and symfony/process use
|
|
62
|
+
`proc_open` / `proc_close` / `proc_terminate` / `proc_get_status`; Mosaic's own
|
|
63
|
+
detection uses `shell_exec`.
|
|
64
|
+
|
|
65
|
+
**5. Repeat step 1.** `isChromiumAvailable()` from the web request must now be true.
|
|
66
|
+
|
|
67
|
+
**6. Turn the setting on by hand.** `serverSideThumbnailRender` is stored the first
|
|
68
|
+
time Mosaic reads its settings, as whatever `isChromiumAvailable()` said then, and is
|
|
69
|
+
never re-evaluated (`MosaicSettingsMResource.php`). Reload Mosaic → Settings: the
|
|
70
|
+
"Server-side thumbnail rendering" switch appears in the same group as "Load scripts
|
|
71
|
+
inline" and "WordPress emoji support" - only once Chrome is available - so switch it
|
|
72
|
+
on and save.
|
|
73
|
+
|
|
74
|
+
**7. Let the capture past a full-screen entrance.** A page with an entrance veil or
|
|
75
|
+
overlay is photographed as the veil: the client's homepage came out as a single flat
|
|
76
|
+
colour, a 760-byte JPEG. Have the front end drop the veil when
|
|
77
|
+
`/HeadlessChrome/.test(navigator.userAgent)` or `window.self !== window.top`. A
|
|
78
|
+
Playwright check of that entrance then needs a context whose user agent does not
|
|
79
|
+
contain `HeadlessChrome`, or it measures the veil-less page.
|
|
80
|
+
|
|
81
|
+
**8. Generate the thumbnails** from Mosaic. A 600x338 capture took about 2.6s.
|
|
82
|
+
|
|
83
|
+
Steps 2-4 belong to the app, not the site: a new server or a cloned app needs them
|
|
84
|
+
again; moving the same app to another domain does not.
|
|
@@ -31,11 +31,31 @@ post; `X-Mosaic-Paths` on a 406 names the paths Mosaic looked for, and `index.ph
|
|
|
31
31
|
in all of them. Measured A/B: a URL with no template returned 406, was handled once
|
|
32
32
|
the row existed, and returned to 406 when it was deleted.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
**On 1.0.10 an auto template is an ordinary, writable template.** Earlier releases
|
|
35
|
+
measured two limits here - `adminTemplateEditorInstance` did not list auto templates,
|
|
36
|
+
and their document had no `template-internal` root, so there was no route to putting
|
|
37
|
+
content in one. Re-measured on 1.0.10, both are gone:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
POST theme/<themeID>/adminTemplateEditorInstance/commit
|
|
41
|
+
revisionEnvelopes.template = [{"newRevisionRecord": {
|
|
42
|
+
"ID": "<uuid you mint>", "parentType": "", "parentID": "", "ordering": "a0",
|
|
43
|
+
"status": "publish", "revision": "", "version": "", "name": "Search",
|
|
44
|
+
"masterID": "<master>", "assign": "auto", "path": "search.php",
|
|
45
|
+
"conditions": []}, "originalRevisionRecord": null}]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
answers 200; the template's document already holds one node, the
|
|
49
|
+
`template-internal` root the server created; `build_site.build_page_document()`
|
|
50
|
+
writes a tree under it like any page's; and `/?s=…` renders it. The template is
|
|
51
|
+
listed by `adminTemplateEditorInstance` and is deleted the same way, with
|
|
52
|
+
`status: "delete"`. This is the commit the editor's own "add template" dialog sends -
|
|
53
|
+
`createAutoTemplate` is the post-type case only. Any path from the hierarchy works the
|
|
54
|
+
same way: `search.php`, `404.php`, `archive-<type>.php`, `taxonomy-<tax>.php`.
|
|
55
|
+
|
|
56
|
+
An auto template is not in `template_assigns` - it binds by path - so a clean-up that
|
|
57
|
+
deletes "templates nothing is assigned to" deletes it. Recreate it after any such
|
|
58
|
+
prune.
|
|
39
59
|
|
|
40
60
|
**`post/<id>` is the only resourceQuery `createManualTemplate` accepts.** The grammar in
|
|
41
61
|
`ResourceQuery::create()` is just `explode('/', $s, 2)`, so anything parses - but the
|
package/tools/build_site.py
CHANGED
|
@@ -109,19 +109,46 @@ def build_shell(client, cfg, site, surface):
|
|
|
109
109
|
|
|
110
110
|
VAR_IDS.clear()
|
|
111
111
|
payload = theme_records(site, doc, surface)
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
if payload:
|
|
113
|
+
commit(client, instance, doc, payload, "theme")
|
|
114
|
+
print("shell master=%s theme=%s" % (master_id[:8], ", ".join(sorted(payload)) or "-"))
|
|
115
|
+
return master_id, header_div["ID"], footer_div["ID"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fill_shell(client, cfg, site, surface, master_id, header_id, footer_id, comps):
|
|
119
|
+
"""Write the header and footer into the master - AFTER the components exist.
|
|
120
|
+
|
|
121
|
+
The shell used to be one commit with the theme, which made a component in the
|
|
122
|
+
header or footer impossible: a component's styles name design tokens, the tokens
|
|
123
|
+
are committed by the shell, so components had to be built after it, and by then
|
|
124
|
+
the shell was already written. The theme now goes first (build_shell), the
|
|
125
|
+
components next, and the regions last, with components resolved and their
|
|
126
|
+
overrides written into the master exactly as a page's are into its template.
|
|
127
|
+
The workaround this replaces - commit an empty shell, then a second real one -
|
|
128
|
+
leaves an extra master behind on every build."""
|
|
129
|
+
instance = "masterDocumentInstance/%s" % master_id
|
|
130
|
+
key = "node/master/%s" % master_id
|
|
114
131
|
shell = site.get("shell") or {}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
COMPONENT_USES.clear()
|
|
133
|
+
records = []
|
|
134
|
+
for region, parent in (("header", header_id), ("footer", footer_id)):
|
|
135
|
+
tree = shell.get(region)
|
|
136
|
+
if not tree:
|
|
137
|
+
continue
|
|
138
|
+
if comps:
|
|
139
|
+
tree = resolve_components(tree, comps)
|
|
140
|
+
records += flatten(tree, parent, master_id, surface, False, parent_type="div")
|
|
141
|
+
if not records:
|
|
142
|
+
return 0
|
|
143
|
+
uses = list(COMPONENT_USES)
|
|
144
|
+
doc = unwrap(client.get(instance), "masterDocumentInstance")
|
|
145
|
+
commit(client, instance, doc, {key: [{"newRevisionRecord": r, "originalRevisionRecord": None}
|
|
146
|
+
for r in records]}, "shell regions")
|
|
147
|
+
done = apply_overrides(client, instance, key, uses, comps,
|
|
148
|
+
envelope="masterDocumentInstance") if (comps and uses) else 0
|
|
149
|
+
print("shell header+footer nodes=%d component instances=%d overrides=%d"
|
|
150
|
+
% (len(records), len(uses), done))
|
|
151
|
+
return len(records)
|
|
125
152
|
|
|
126
153
|
|
|
127
154
|
# A component's row id is derived from its NAME, for the same reason a design
|
|
@@ -224,7 +251,7 @@ def ensure_components(client, cfg, site, surface):
|
|
|
224
251
|
return out
|
|
225
252
|
|
|
226
253
|
|
|
227
|
-
def apply_overrides(client, instance, key, uses, comps):
|
|
254
|
+
def apply_overrides(client, instance, key, uses, comps, envelope="templateDocumentInstance"):
|
|
228
255
|
"""Second pass: write each instance's own content into the override nodes.
|
|
229
256
|
|
|
230
257
|
Mosaic materialises one override node per component node per instance - node,
|
|
@@ -238,7 +265,7 @@ def apply_overrides(client, instance, key, uses, comps):
|
|
|
238
265
|
wanted = [u for u in uses if u.get("overrides")]
|
|
239
266
|
if not wanted:
|
|
240
267
|
return 0
|
|
241
|
-
doc = unwrap(client.get(instance),
|
|
268
|
+
doc = unwrap(client.get(instance), envelope)
|
|
242
269
|
by_attr = {(n.get("data") or {}).get("attrID"): n for n in doc[key]}
|
|
243
270
|
revisions = []
|
|
244
271
|
for use in wanted:
|
|
@@ -387,8 +414,9 @@ def main():
|
|
|
387
414
|
# AFTER the shell, not before: build_shell is what commits the theme's design
|
|
388
415
|
# tokens and fills VAR_IDS, and a component that uses one cannot be flattened
|
|
389
416
|
# until the token it names exists.
|
|
390
|
-
master_id = build_shell(client, cfg, site, surface)
|
|
417
|
+
master_id, header_id, footer_id = build_shell(client, cfg, site, surface)
|
|
391
418
|
comps = ensure_components(client, cfg, site, surface)
|
|
419
|
+
fill_shell(client, cfg, site, surface, master_id, header_id, footer_id, comps)
|
|
392
420
|
|
|
393
421
|
for page in site["pages"]:
|
|
394
422
|
template_id = bind_page(client, cfg, master_id, page["slug"], page["post_id"])
|