@typeroll/mcp-server 0.45.77 → 0.45.80
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/dist/bundled-content.js +29 -8
- package/dist/server.js +1 -1
- package/dist/tools/deploy.js +2 -1
- package/dist/tools/domain.js +3 -3
- package/dist/tools/media.js +1 -1
- package/dist/tools/migration.js +4 -2
- package/dist/tools/pages.js +1 -1
- package/dist/tools/preview.js +1 -1
- package/dist/tools/settings.js +9 -2
- package/dist/tools/working-copy.js +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.js +7 -6
- package/package.json +2 -2
- package/skills/tr-brand.md +43 -7
- package/skills/tr-header-footer.md +95 -41
- package/skills/tr-migrate-wp.md +77 -6
- package/skills/tr-migration-evidence.md +3 -1
- package/skills/tr-new-site.md +42 -8
- package/skills/tr-starter-design.md +36 -0
- package/workspace-templates/qa/checklist.md +52 -0
- package/workspace-templates/qa/typeroll-platform-feedback.md +11 -0
- package/workspace-templates/scripts/README.md +111 -0
- package/workspace-templates/scripts/typeroll/browser.cjs +50 -0
- package/workspace-templates/scripts/typeroll/call.py +12 -0
- package/workspace-templates/scripts/typeroll/capture_text.cjs +17 -0
- package/workspace-templates/scripts/typeroll/card-stacking-check.cjs +27 -0
- package/workspace-templates/scripts/typeroll/check_assets.py +72 -0
- package/workspace-templates/scripts/typeroll/check_unsaved.py +13 -0
- package/workspace-templates/scripts/typeroll/check_url_parity.py +74 -0
- package/workspace-templates/scripts/typeroll/color_tokens.py +81 -0
- package/workspace-templates/scripts/typeroll/compare_text.py +59 -0
- package/workspace-templates/scripts/typeroll/contact_sheet.py +20 -0
- package/workspace-templates/scripts/typeroll/layout_trace.cjs +33 -0
- package/workspace-templates/scripts/typeroll/prepare_image.py +64 -0
- package/workspace-templates/scripts/typeroll/shoot_hosted.cjs +26 -0
- package/workspace-templates/scripts/typeroll/shootframe.cjs +16 -0
- package/workspace-templates/scripts/typeroll/sweep_blocks.example.py +23 -0
- package/workspace-templates/scripts/typeroll/trlib.py +212 -0
- package/workspace-templates/workflows/design.md +65 -0
- package/workspace-templates/workflows/migration.md +94 -0
- package/workspace-templates/workflows/new-site.md +7 -0
- package/workspace-templates/workflows/source-media.md +50 -0
package/skills/tr-new-site.md
CHANGED
|
@@ -5,6 +5,42 @@ description: Use when the user wants to create a new Typeroll site from scratch,
|
|
|
5
5
|
|
|
6
6
|
# Bootstrap a new Typeroll site
|
|
7
7
|
|
|
8
|
+
## Site tokens before block overrides
|
|
9
|
+
|
|
10
|
+
Core 0.2.106 / template capabilities 0.54.0 adds palette names in all colour
|
|
11
|
+
fields and named typography/spacing steps. Check the connected capability
|
|
12
|
+
version; on older renderers use `var(--color-surface)` rather than a bare name.
|
|
13
|
+
|
|
14
|
+
Set colours and font families in site settings, then use named styles and
|
|
15
|
+
palette names in blocks. Do not write hex, rgb or font-family literals in block
|
|
16
|
+
content. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,
|
|
17
|
+
`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.
|
|
18
|
+
Use `surface_hover` for a darker interactive surface (derived automatically
|
|
19
|
+
unless explicitly configured). Existing semantic roles should be reused; a
|
|
20
|
+
new visual shade is not automatically a new semantic role. For a derived shade
|
|
21
|
+
use `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.
|
|
22
|
+
For an additional role not in the native palette, define a named site style
|
|
23
|
+
or a site CSS variable derived from existing palette variables, and document it.
|
|
24
|
+
|
|
25
|
+
Choose `heading`, `body` or `mono` for fonts. Font families belong only in the
|
|
26
|
+
site's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,
|
|
27
|
+
`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,
|
|
28
|
+
`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these
|
|
29
|
+
names, for example `font_size_px: {mobile: "md", desktop: "lg"}`. Read the field
|
|
30
|
+
schema: counts, widths and breakpoints are not spacing steps. Pixel values are
|
|
31
|
+
explicit exceptions, not the default authoring method. Configure scales in
|
|
32
|
+
`design_scales`, then reuse them in role styles and blocks.
|
|
33
|
+
|
|
34
|
+
Record deliberate exceptions in `decisions.md`, with source/block, reason and
|
|
35
|
+
review criteria (for example white text on a photograph). Run `lint_design_tokens`
|
|
36
|
+
before launch and after major design changes. It warns, never blocks launch.
|
|
37
|
+
Use the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;
|
|
38
|
+
review the plan before applying it in a dedicated Version. Equal hex values can
|
|
39
|
+
mean different roles: never automatically choose between background and text.
|
|
40
|
+
Verify a primary/surface change in a test Version across header, footer, cards,
|
|
41
|
+
menus and buttons, then restore the test settings. Do not republish automatically.
|
|
42
|
+
|
|
43
|
+
|
|
8
44
|
> **The buffer model (draft writes).** Every content write in this recipe
|
|
9
45
|
> (pages, blocks and partials) lands in an unsaved per-doc
|
|
10
46
|
> DRAFT — deploys and plain previews only see SAVED content. For recipe-style
|
|
@@ -165,7 +201,7 @@ it there.
|
|
|
165
201
|
### 3. Header + footer partials
|
|
166
202
|
|
|
167
203
|
**Start from the native preset — don't hand-roll navigation.** Read
|
|
168
|
-
`tr-header-footer` and use its `template/site_logo` + `core/
|
|
204
|
+
`tr-header-footer` and use its `template/site_logo` + `core/navigation_menu` with `core/navigation_links` slots
|
|
169
205
|
composition. The server-rendered landmark, current-page state, no-JS links,
|
|
170
206
|
mobile disclosure, focus treatment, and responsive behavior are Core
|
|
171
207
|
contracts rather than tenant CSS/JavaScript.
|
|
@@ -202,7 +238,7 @@ on a block *inside* the section instead):
|
|
|
202
238
|
|
|
203
239
|
```json
|
|
204
240
|
[
|
|
205
|
-
{ "type": "core/section", "data": { "background": "
|
|
241
|
+
{ "type": "core/section", "data": { "background": "surface", "padding_y": "lg" }, "children": [
|
|
206
242
|
{ "type": "core/media_card", "data": {
|
|
207
243
|
"image": "MEDIA_URL", "image_alt": "…", "image_side": "right",
|
|
208
244
|
"heading": "Huvudrubriken", "heading_level": "h2",
|
|
@@ -269,12 +305,10 @@ passing the whole tree inline (`block={ type: 'core/columns', slots:
|
|
|
269
305
|
slot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on
|
|
270
306
|
older sites use `core/grid` (children flow into columns) instead.
|
|
271
307
|
|
|
272
|
-
Partial last rows
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
don't hand-roll 6-column CSS tricks; both distort content to patch
|
|
277
|
-
layout. On older sites, pick a column count that divides N.
|
|
308
|
+
Partial last rows: keep `last_row: 'start'` on `core/grid`. Incomplete rows
|
|
309
|
+
stay left-aligned, preserving the reading edge. Center only after an explicit
|
|
310
|
+
owner design request, never as an automatic fix. Do not widen one peer card
|
|
311
|
+
or invent six-column CSS to fill a gap.
|
|
278
312
|
|
|
279
313
|
Icons (template_capabilities_version ≥ 0.16.0): `type: 'icon'` fields on
|
|
280
314
|
`core/icon`, `core/icon_box`, and `core/step_card` render inline SVG when
|
|
@@ -5,6 +5,42 @@ description: Reference for the default design new Typeroll sites start with —
|
|
|
5
5
|
|
|
6
6
|
# Starter design system
|
|
7
7
|
|
|
8
|
+
## Site tokens before block overrides
|
|
9
|
+
|
|
10
|
+
Core 0.2.106 / template capabilities 0.54.0 adds palette names in all colour
|
|
11
|
+
fields and named typography/spacing steps. Check the connected capability
|
|
12
|
+
version; on older renderers use `var(--color-surface)` rather than a bare name.
|
|
13
|
+
|
|
14
|
+
Set colours and font families in site settings, then use named styles and
|
|
15
|
+
palette names in blocks. Do not write hex, rgb or font-family literals in block
|
|
16
|
+
content. Prefer `primary`, `secondary`, `accent`, `background`, `surface`, `text`,
|
|
17
|
+
`text_light`, and `on_primary`/`on_secondary`/`on_accent` for text on brand colours.
|
|
18
|
+
Use `surface_hover` for a darker interactive surface (derived automatically
|
|
19
|
+
unless explicitly configured). Existing semantic roles should be reused; a
|
|
20
|
+
new visual shade is not automatically a new semantic role. For a derived shade
|
|
21
|
+
use `color-mix(in srgb, var(--color-surface) 92%, var(--color-primary))`.
|
|
22
|
+
For an additional role not in the native palette, define a named site style
|
|
23
|
+
or a site CSS variable derived from existing palette variables, and document it.
|
|
24
|
+
|
|
25
|
+
Choose `heading`, `body` or `mono` for fonts. Font families belong only in the
|
|
26
|
+
site's `fonts` settings. Use type-size steps `xs`, `sm`, `md`, `lg`, `xl`, `2xl`,
|
|
27
|
+
`3xl`; spacing `none`, `xs`, `sm`, `md`, `lg`, `xl`; line height `tight`, `snug`,
|
|
28
|
+
`normal`, `relaxed`, `loose`. Presentation fields accept responsive maps of these
|
|
29
|
+
names, for example `font_size_px: {mobile: "md", desktop: "lg"}`. Read the field
|
|
30
|
+
schema: counts, widths and breakpoints are not spacing steps. Pixel values are
|
|
31
|
+
explicit exceptions, not the default authoring method. Configure scales in
|
|
32
|
+
`design_scales`, then reuse them in role styles and blocks.
|
|
33
|
+
|
|
34
|
+
Record deliberate exceptions in `decisions.md`, with source/block, reason and
|
|
35
|
+
review criteria (for example white text on a photograph). Run `lint_design_tokens`
|
|
36
|
+
before launch and after major design changes. It warns, never blocks launch.
|
|
37
|
+
Use the starter's `scripts/typeroll/color_tokens.py` for an exact-match dry run;
|
|
38
|
+
review the plan before applying it in a dedicated Version. Equal hex values can
|
|
39
|
+
mean different roles: never automatically choose between background and text.
|
|
40
|
+
Verify a primary/surface change in a test Version across header, footer, cards,
|
|
41
|
+
menus and buttons, then restore the test settings. Do not republish automatically.
|
|
42
|
+
|
|
43
|
+
|
|
8
44
|
A reference for the *default* design that ships with new Typeroll sites.
|
|
9
45
|
The customer may have replaced it — always inspect the actual header partial
|
|
10
46
|
(`read_partial partial_id="header"`) and an existing page before assuming any
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
- Confirm Organization, Site, Version and deployed/source identity.
|
|
4
|
+
- Run `python3 scripts/typeroll/check_unsaved.py` before trigger_deploy. Review
|
|
5
|
+
and explicitly save authorized Pages and partials; templates save immediately.
|
|
6
|
+
- Compare preserved text with capture_text.cjs + compare_text.py and an explicit
|
|
7
|
+
selector. Check intentional additions, source comments and images separately.
|
|
8
|
+
- Inspect desktop/mobile previews and actual hosted output around breakpoints.
|
|
9
|
+
Use shootframe.cjs / shoot_hosted.cjs and inspect full screenshots, not only tiles.
|
|
10
|
+
- Exercise menus, keyboard focus, current links, forms and enabled app states.
|
|
11
|
+
Use card-stacking-check.cjs for whole-card excerpt/media/whitespace targets.
|
|
12
|
+
- Check actual contrast on light/dark/photo backgrounds, image subjects and
|
|
13
|
+
sharpness, footer balance, left-aligned last rows and heading hierarchy.
|
|
14
|
+
- Check home title/suffix, metadata, canonical/indexing policy and JSON-LD.
|
|
15
|
+
- Run check_url_parity.py and check_assets.py on the deployed candidate; compare
|
|
16
|
+
final targets with the approved URL map and flatten accidental redirect chains.
|
|
17
|
+
- Treat 403/network/DNS failures as unverified. Do not claim search-bot access
|
|
18
|
+
from a user-agent string or turn off customer protection without approval.
|
|
19
|
+
- Record tested pages, viewports, revision, results and unresolved issues per QA
|
|
20
|
+
round. Record product defects in qa/typeroll-platform-feedback.md.
|
|
21
|
+
- Keep screenshots and private runtime reports in ignored qa/reports/.
|
|
22
|
+
- Check publishing readiness and migration launch report. Passing technical
|
|
23
|
+
validation does not prove visual/editorial completeness or grant launch approval.
|
|
24
|
+
|
|
25
|
+
Commands/dependencies/limits: scripts/README.md. Design: workflows/design.md.
|
|
26
|
+
Follow existing owner authorization; test publication, traffic cutover and
|
|
27
|
+
indexing have distinct effects but do not require repeated approval if covered.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
- [ ] Check heading role spacing, slot gap and responsive vertical padding in the browser.
|
|
31
|
+
- [ ] Review global/current navigation colors against the real parent background.
|
|
32
|
+
- [ ] Check intrinsic-width image and full-image hero framing at breakpoint boundaries.
|
|
33
|
+
- [ ] Test click-to-load video with keyboard; no provider request before activation.
|
|
34
|
+
- [ ] Review matched drafts, unpublished targets and redirect chains before launch.
|
|
35
|
+
- [ ] Explicitly save working copies, or obtain approval to publish saved content only.
|
|
36
|
+
|
|
37
|
+
## Design tokens (Core 0.2.106+)
|
|
38
|
+
|
|
39
|
+
- [ ] Run `lint_design_tokens` before launch and after major design changes.
|
|
40
|
+
- [ ] Review hardcoded colours, fonts and sizes; record justified exceptions in decisions.md.
|
|
41
|
+
- [ ] Dry-run colour migration; do not infer semantic roles from equal hex values.
|
|
42
|
+
- [ ] Change primary and surface in a test Version: header, footer, cards, menus and buttons follow without block edits at mobile/tablet/desktop widths. Restore the test settings.
|
|
43
|
+
- [ ] Report actual versus simulated before/after counts separately; do not claim a migration was applied from a dry run.
|
|
44
|
+
|
|
45
|
+
- [ ] Before source retirement, review workflows/source-media.md: unused originals,
|
|
46
|
+
print files, private archive manifest, verified copies and a restore test.
|
|
47
|
+
- [ ] Review ICC-converted derivatives visually; preserve originals separately.
|
|
48
|
+
- [ ] Verify Analytics configuration and actual beacon/cookie behavior after build.
|
|
49
|
+
- [ ] Read domain publication intent before deployment; distinguish test-host
|
|
50
|
+
updates, future-host candidates and explicit traffic approval.
|
|
51
|
+
- [ ] Inspect the frozen candidate's noindex/robots output, not only current CMS settings.
|
|
52
|
+
- [ ] Review owner facts and dates against the chosen preserve/renew text policy.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Platform feedback
|
|
2
|
+
|
|
3
|
+
Record site-specific observations here, without credentials or private exports.
|
|
4
|
+
|
|
5
|
+
| ID | Priority | Surface / version | Observation | Evidence | Workaround | Status |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
7
|
+
|
|
8
|
+
For each finding record expected versus actual behavior, minimal reproduction,
|
|
9
|
+
Site/Version, page/block locator, relevant schema, tested widths, source revision
|
|
10
|
+
and whether it blocks launch. Distinguish bugs, capabilities, instructions and
|
|
11
|
+
project choices. Link upstream fixes; verify deployed output before closing them.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Repeatable site QA
|
|
2
|
+
|
|
3
|
+
These optional tools supplement MCP and the platform's publication validator.
|
|
4
|
+
They do not publish, change DNS, approve a launch or replace visual review.
|
|
5
|
+
Python tools use the standard library except contact_sheet.py (Pillow). Browser
|
|
6
|
+
tools require Node.js 22+, Playwright and its Chromium browser. From scripts/typeroll:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --no-save --package-lock=false playwright
|
|
10
|
+
npx playwright install chromium
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the commands below from the **workspace root**. Inject TYPEROLL_API_KEY through
|
|
14
|
+
your private credential manager. Never put it in a file or command argument.
|
|
15
|
+
trlib.py reads portal, organization_id, site_id and version from typeroll.json,
|
|
16
|
+
checks the connected Site/Organization, discovers the live tool schema, and
|
|
17
|
+
passes the Version where supported. It refuses unversioned operations on a
|
|
18
|
+
non-main binding; use the explicit interactive API workflow for organization or
|
|
19
|
+
shared media operations instead. It is a small hosted MCP helper for API keys,
|
|
20
|
+
not a replacement OAuth client or a new general CRUD CLI. REST is also supported
|
|
21
|
+
by Typeroll if your integration already uses it.
|
|
22
|
+
|
|
23
|
+
## Before publication
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
python3 scripts/typeroll/check_unsaved.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Exit 1 lists unsaved Pages **and partials**. Review those changes; save only the
|
|
30
|
+
work authorized for this publication, then run again. Templates save immediately.
|
|
31
|
+
A working-copy preview is not proof of saved output. Never auto-save other work.
|
|
32
|
+
|
|
33
|
+
## After an authorized publication
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python3 scripts/typeroll/check_url_parity.py https://YOUR-TEST-HOST
|
|
37
|
+
python3 scripts/typeroll/check_assets.py https://YOUR-TEST-HOST
|
|
38
|
+
node scripts/typeroll/shoot_hosted.cjs https://YOUR-TEST-HOST qa/reports/hosted /,/about/ 390 768 1023 1024 1280
|
|
39
|
+
node scripts/typeroll/card-stacking-check.cjs https://YOUR-TEST-HOST/listing/ 390
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
URL parity reads every migration inventory page and observed spelling, skips
|
|
43
|
+
explicit exclusions unless --show-excluded, keeps origins/query parameters,
|
|
44
|
+
detects loops and flags chains beyond a single redirect (one initial slash
|
|
45
|
+
normalization is allowed). This does **not** prove the final destination is the
|
|
46
|
+
editorially correct page: compare with the approved URL map too. No inventory
|
|
47
|
+
is a failure, not a passing zero-row check. Asset QA uses resolved routes,
|
|
48
|
+
checks published/unlisted Pages, relative URLs, srcset and downloads, uses four
|
|
49
|
+
concurrent HEAD requests and falls back to GET when HEAD is unsupported. CSS
|
|
50
|
+
backgrounds, authenticated assets and dynamically loaded resources need browser QA.
|
|
51
|
+
Neither tool spoofs a search crawler. A 403/DNS/network failure is unverified;
|
|
52
|
+
inspect access policy rather than treating it as a broken migration or a pass.
|
|
53
|
+
|
|
54
|
+
## Other helpers
|
|
55
|
+
|
|
56
|
+
- `call.py TOOL '{"argument":"value"}'`: raw MCP result; use `-` to read JSON
|
|
57
|
+
from stdin. Mutating tools require the same owner authorization as interactive
|
|
58
|
+
calls. Network failures are not automatically retried for mutations.
|
|
59
|
+
- `trlib.upload(path, content_type, alt)`: direct signed upload and mandatory
|
|
60
|
+
finalize SHA-256 verification. No image bytes pass through the model or Git.
|
|
61
|
+
- `sweep_blocks.example.py`: dry-run grid normalization across Pages, templates
|
|
62
|
+
and partials. Review before --apply; refuses to commit existing unsaved work.
|
|
63
|
+
It is not transactional: a failure can leave a partial draft; inspect before retry.
|
|
64
|
+
- `shootframe.cjs OUT-PREFIX SIGNED-PREVIEW-URL [widths...]`: selects only the
|
|
65
|
+
Typeroll preview iframe, reports fonts/hosts/overflow. Signed URLs are secrets;
|
|
66
|
+
pass privately, never paste into committed reports or shell history. Direct-frame
|
|
67
|
+
capture checks rendering only; test Extension storage/navigation/handoffs in
|
|
68
|
+
the original wrapper because those features depend on its parent bridge.
|
|
69
|
+
- `layout_trace.cjs URL [width] [selector]`: computed spacing of ancestors and
|
|
70
|
+
next section. Works with a direct page or Typeroll preview.
|
|
71
|
+
- `contact_sheet.py OUT-PREFIX COLS TILE-WIDTH PNG...`: Pillow contact sheets.
|
|
72
|
+
It crops tall screenshots; inspect full screenshots too.
|
|
73
|
+
- `capture_text.cjs URL qa/reports/text.json 'main .article-content'` followed by
|
|
74
|
+
`compare_text.py sources/private/page.html qa/reports/text.json --source-title 'Title'`:
|
|
75
|
+
word/punctuation diff. A saved single WP REST record (.json) is another source
|
|
76
|
+
adapter. Select exactly the preserved region, including its template-owned
|
|
77
|
+
title once. Deliberate shared additions need separate acceptance. Comments,
|
|
78
|
+
scripts and styles are excluded from source text. It never edits content.
|
|
79
|
+
|
|
80
|
+
Browser reports exit nonzero for missing images, overflow, unexpected H1 count,
|
|
81
|
+
failed requests or incomplete scrolling. They keep third-party failures visible.
|
|
82
|
+
Network checks retry only a bounded read. All reports, screenshots and source
|
|
83
|
+
exports stay under ignored qa/reports/ or sources/private/. Do not commit signed
|
|
84
|
+
URLs, personal data or customer assets. Adapt scripts deliberately and run tests
|
|
85
|
+
before applying broad changes. The workspace updater preserves your edits.
|
|
86
|
+
|
|
87
|
+
## Palette migration (Core 0.2.106 / MCP 0.45.80+)
|
|
88
|
+
|
|
89
|
+
`color_tokens.py --plan /tmp/design-token-plan.json` runs the read-only
|
|
90
|
+
`lint_design_tokens` audit and lists unambiguous direct block hex replacements.
|
|
91
|
+
Review the plan and remove deliberate exceptions. `--apply /tmp/design-token-plan.json`
|
|
92
|
+
requires the same site/palette and a dedicated non-main Version without existing
|
|
93
|
+
working copies. Pause other editing in that Version during the run: ordinary
|
|
94
|
+
field writes do not provide a site-wide transaction. The script refuses stale
|
|
95
|
+
values, never retries uncertain writes, and never publishes. On an interruption,
|
|
96
|
+
inspect working copies before saving or resuming. Templates save immediately.
|
|
97
|
+
|
|
98
|
+
Ambiguous colours, nested/responsive colour maps and custom CSS are reported for
|
|
99
|
+
manual review rather than rewritten. API updates through the normal block tools
|
|
100
|
+
can resolve these after choosing the semantic role. Run lint again and compare
|
|
101
|
+
header, footer, navigation, cards and buttons at all breakpoints. Keep generated
|
|
102
|
+
plans outside Git; they are version-specific review artifacts.
|
|
103
|
+
|
|
104
|
+
## Local image preparation
|
|
105
|
+
|
|
106
|
+
`prepare_image.py SOURCE OUTPUT.jpg` requires Pillow with ImageCms. It converts
|
|
107
|
+
through the original ICC profile to sRGB and applies orientation, without
|
|
108
|
+
uploading or changing CMS content. Missing CMYK profiles, transparency, multiple
|
|
109
|
+
frames and existing output files stop with an error. `--assume-srgb` is only for
|
|
110
|
+
reviewed unprofiled RGB/gray; `--source-profile FILE.icc` supplies the actual profile.
|
|
111
|
+
See workflows/source-media.md for archive verification and original preservation.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Isolated QA browser only. Never persist storage state, cookies or signed URLs.
|
|
2
|
+
const { chromium } = require('playwright');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
async function open(page, url) {
|
|
6
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
7
|
+
// Select only Typeroll's own preview, never an arbitrary ad/video iframe.
|
|
8
|
+
const frame = page.locator('iframe[title="Preview"], iframe[title="Site preview"]');
|
|
9
|
+
if (await frame.count() === 1) {
|
|
10
|
+
await page.waitForFunction(() => document.querySelector('iframe[title="Preview"], iframe[title="Site preview"]')?.getAttribute('src'), null, { timeout: 15000 });
|
|
11
|
+
const src = await frame.getAttribute('src');
|
|
12
|
+
if (!src) throw Error('Preview frame has no source');
|
|
13
|
+
const target = new URL(src, page.url());
|
|
14
|
+
if (target.origin !== new URL(page.url()).origin) throw Error('Unexpected preview frame origin');
|
|
15
|
+
await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
16
|
+
}
|
|
17
|
+
await page.evaluate(() => document.fonts.ready);
|
|
18
|
+
}
|
|
19
|
+
async function settle(page) {
|
|
20
|
+
// Bounded for infinite-scroll pages; report coverage rather than hanging.
|
|
21
|
+
for (let y = 0; y < 30000; y += 600) {
|
|
22
|
+
await page.evaluate(y => scrollTo(0, y), y);
|
|
23
|
+
await page.waitForTimeout(100);
|
|
24
|
+
if (y + 900 >= await page.evaluate(() => document.documentElement.scrollHeight)) break;
|
|
25
|
+
}
|
|
26
|
+
await page.evaluate(() => scrollTo(0, 0));
|
|
27
|
+
await page.waitForTimeout(300);
|
|
28
|
+
}
|
|
29
|
+
async function inspect(page) {
|
|
30
|
+
return page.evaluate(() => ({
|
|
31
|
+
overflow: document.documentElement.scrollWidth > innerWidth,
|
|
32
|
+
h1: document.querySelectorAll('h1').length,
|
|
33
|
+
title: document.title,
|
|
34
|
+
robots: document.querySelector('meta[name="robots"]')?.content ?? null,
|
|
35
|
+
fonts: { body: getComputedStyle(document.body).fontFamily, heading: getComputedStyle(document.querySelector('h1') || document.body).fontFamily },
|
|
36
|
+
incompleteImages: [...document.images].filter(i => !i.complete || !i.naturalWidth).map(i => new URL(i.currentSrc || i.src, location.href).pathname),
|
|
37
|
+
scrollCoverageComplete: document.documentElement.scrollHeight <= 30000,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async function screenshot(page, file) {
|
|
41
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
42
|
+
await page.screenshot({ path: file, fullPage: true, animations: 'disabled' });
|
|
43
|
+
}
|
|
44
|
+
function widths(args) {
|
|
45
|
+
const values = (args.length ? args : ['390', '768', '1023', '1024', '1280']).map(Number);
|
|
46
|
+
if (values.some(w => !Number.isInteger(w) || w < 240 || w > 4000)) throw Error('Invalid viewport width');
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
function fail() { console.error('QA failed; inspect the target privately. URLs and credentials are withheld.'); process.exitCode = 2; }
|
|
50
|
+
module.exports = { chromium, open, settle, inspect, screenshot, widths, fail };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""One MCP call; output can contain private data. Do not commit it."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import trlib
|
|
6
|
+
|
|
7
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
8
|
+
parser.add_argument('tool')
|
|
9
|
+
parser.add_argument('arguments', nargs='?', default='{}', help='JSON or - for stdin')
|
|
10
|
+
args = parser.parse_args()
|
|
11
|
+
value = json.loads(sys.stdin.read() if args.arguments == '-' else args.arguments)
|
|
12
|
+
print(json.dumps(trlib.call(args.tool, value), ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Explicit selector avoids silently excluding the wrong column/sidebar.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const [url, out, selector = 'main'] = process.argv.slice(2);
|
|
5
|
+
(async () => {
|
|
6
|
+
const browser = await qa.chromium.launch();
|
|
7
|
+
try {
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
await qa.open(page, url); await qa.settle(page);
|
|
10
|
+
const target = page.locator(selector);
|
|
11
|
+
if (await target.count() !== 1) throw Error('Select exactly one content region');
|
|
12
|
+
const value = await target.evaluate(el => ({ text: el.innerText,
|
|
13
|
+
images: [...el.querySelectorAll('img')].map(i => ({ alt: i.alt, path: new URL(i.currentSrc || i.src, location.href).pathname })) }));
|
|
14
|
+
await fs.mkdir(require('node:path').dirname(out), { recursive: true });
|
|
15
|
+
await fs.writeFile(out, JSON.stringify(value, null, 2));
|
|
16
|
+
} finally { await browser.close(); }
|
|
17
|
+
})().catch(qa.fail);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Check every whole-card target, including excerpt/media/meta, without navigating.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const [url, width = '390'] = process.argv.slice(2);
|
|
4
|
+
(async () => {
|
|
5
|
+
const browser = await qa.chromium.launch();
|
|
6
|
+
try {
|
|
7
|
+
const page = await browser.newPage({ viewport: { width: qa.widths([width])[0], height: 900 } });
|
|
8
|
+
await qa.open(page, url);
|
|
9
|
+
const cards = page.locator('[data-block="post_card"][data-whole="true"]');
|
|
10
|
+
const report = [];
|
|
11
|
+
for (let index = 0; index < await cards.count(); index++) {
|
|
12
|
+
const card = cards.nth(index);
|
|
13
|
+
await card.scrollIntoViewIfNeeded();
|
|
14
|
+
report.push(await card.evaluate(card => {
|
|
15
|
+
const link = card.querySelector('.block-postcard-title > a');
|
|
16
|
+
const targets = [card, ...card.querySelectorAll('.block-postcard-excerpt,.block-postcard-meta,.block-postcard-media')];
|
|
17
|
+
const hits = targets.filter(el => el.getBoundingClientRect().height > 0).map(el => {
|
|
18
|
+
const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
19
|
+
return { part: el.className || 'card', inViewport: y >= 0 && y < innerHeight, linked: document.elementFromPoint(x, y)?.closest('a') === link };
|
|
20
|
+
});
|
|
21
|
+
return { block: card.dataset.blockId || card.dataset.bid || null, title: link?.textContent, hits };
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
console.log(JSON.stringify(report, null, 2));
|
|
25
|
+
if (!report.length || report.some(r => r.hits.some(h => !h.inViewport || !h.linked))) process.exitCode = 1;
|
|
26
|
+
} finally { await browser.close(); }
|
|
27
|
+
})().catch(qa.fail);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Check published page assets, including relative URLs and srcset. CSS backgrounds need visual QA."""
|
|
2
|
+
import argparse
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
from html.parser import HTMLParser
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import trlib
|
|
8
|
+
from check_url_parity import origin, fetch, probe
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Assets(HTMLParser):
|
|
12
|
+
def __init__(self, base):
|
|
13
|
+
super().__init__(convert_charrefs=True)
|
|
14
|
+
self.base, self.urls = base, set()
|
|
15
|
+
|
|
16
|
+
def handle_starttag(self, tag, attrs):
|
|
17
|
+
data = dict(attrs)
|
|
18
|
+
values = []
|
|
19
|
+
if tag in ('img', 'source', 'video', 'audio', 'script'):
|
|
20
|
+
values.extend(data[k] for k in ('src', 'poster') if data.get(k))
|
|
21
|
+
if tag == 'link' and data.get('rel') == 'stylesheet':
|
|
22
|
+
values.append(data.get('href', ''))
|
|
23
|
+
href = data.get('href', '')
|
|
24
|
+
if tag == 'a' and ('download' in data or urllib.parse.urlsplit(href).path.lower().endswith(('.pdf', '.jpg', '.jpeg', '.png', '.webp', '.avif', '.svg', '.zip'))):
|
|
25
|
+
values.append(href)
|
|
26
|
+
if data.get('srcset') and not data['srcset'].strip().startswith('data:'):
|
|
27
|
+
values.extend(item.strip().split()[0] for item in data['srcset'].split(',') if item.strip())
|
|
28
|
+
for value in values:
|
|
29
|
+
url = urllib.parse.urljoin(self.base, value)
|
|
30
|
+
if urllib.parse.urlsplit(url).scheme in ('http', 'https'):
|
|
31
|
+
self.urls.add(url)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check(url):
|
|
35
|
+
status, location, _ = fetch(url, 'HEAD')
|
|
36
|
+
# HEAD is not supported everywhere; redirects must resolve too.
|
|
37
|
+
if status in (405, 501) or location:
|
|
38
|
+
status = probe(url).get('status', 0)
|
|
39
|
+
return {'url': url, 'status': status, 'ok': status == 200}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
44
|
+
parser.add_argument('origin', type=origin)
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
pages = [p for p in trlib.all_pages() if p.get('status') in ('published', 'unlisted')]
|
|
47
|
+
errors, assets = [], set()
|
|
48
|
+
for page in pages:
|
|
49
|
+
if not page.get('url'):
|
|
50
|
+
errors.append({'page_id': page['id'], 'error': 'missing_resolved_route'})
|
|
51
|
+
continue
|
|
52
|
+
route = urllib.parse.urlsplit(page['url'])
|
|
53
|
+
url = args.origin + (route.path or '/') + ('?' + route.query if route.query else '')
|
|
54
|
+
evidence = probe(url, expected_page=True)
|
|
55
|
+
if not evidence.get('ok'):
|
|
56
|
+
errors.append({'page_id': page['id'], **evidence})
|
|
57
|
+
continue
|
|
58
|
+
status, _, body = fetch(evidence['final_url'])
|
|
59
|
+
if status != 200:
|
|
60
|
+
errors.append({'page_id': page['id'], 'status': status})
|
|
61
|
+
continue
|
|
62
|
+
parser = Assets(evidence['final_url'])
|
|
63
|
+
parser.feed(body.decode('utf8', 'replace'))
|
|
64
|
+
assets.update(parser.urls)
|
|
65
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
|
66
|
+
results = list(pool.map(check, sorted(assets)))
|
|
67
|
+
print(json.dumps({'pages': len(pages), 'page_errors': errors, 'assets': results}, indent=2))
|
|
68
|
+
return 1 if not pages or errors or any(not item['ok'] for item in results) else 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == '__main__':
|
|
72
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Pre-deploy gate: list every Page and partial with an unsaved working copy (Page templates save immediately).
|
|
2
|
+
Deploys build saved content only, and previews with include_working_copy hide the difference.
|
|
3
|
+
usage: python3 scripts/typeroll/check_unsaved.py (exit 1 when something is unsaved)"""
|
|
4
|
+
import os, sys
|
|
5
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
6
|
+
import trlib
|
|
7
|
+
SITE = trlib.site_id()
|
|
8
|
+
bad = [t for t in trlib.containers() if t["kind"] != "template"
|
|
9
|
+
and trlib.call("read_working_copy", {"site_id": SITE, "target": t}).get("working_copy")]
|
|
10
|
+
for t in bad:
|
|
11
|
+
print("unsaved:", t["kind"], t["id"])
|
|
12
|
+
print("unsaved total:", len(bad))
|
|
13
|
+
sys.exit(1 if bad else 0)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Read-only URL continuity check on an explicitly supplied deployed origin."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
import trlib
|
|
8
|
+
|
|
9
|
+
UA = 'Typeroll-Starter-QA/1.0 (+https://typeroll.com/docs/)'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fetch(url, method='GET'):
|
|
13
|
+
request = urllib.request.Request(url, method=method, headers={'User-Agent': UA})
|
|
14
|
+
for attempt in range(2):
|
|
15
|
+
try:
|
|
16
|
+
with urllib.request.build_opener(trlib.NoRedirect).open(request, timeout=30) as response:
|
|
17
|
+
return response.status, response.headers.get('Location'), response.read() if method == 'GET' else b''
|
|
18
|
+
except urllib.error.HTTPError as error:
|
|
19
|
+
if error.code in (502, 503, 504) and attempt == 0:
|
|
20
|
+
continue
|
|
21
|
+
return error.code, error.headers.get('Location'), b''
|
|
22
|
+
except (OSError, TimeoutError):
|
|
23
|
+
if attempt:
|
|
24
|
+
return 0, None, b''
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def origin(value):
|
|
28
|
+
parsed = urllib.parse.urlsplit(value)
|
|
29
|
+
if parsed.scheme not in ('http', 'https') or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in ('', '/'):
|
|
30
|
+
raise ValueError('Supply a plain deployed HTTP(S) origin')
|
|
31
|
+
return value.rstrip('/')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def probe(url, expected_page=False):
|
|
35
|
+
initial, seen, hops = url, set(), []
|
|
36
|
+
for _ in range(6):
|
|
37
|
+
if url in seen:
|
|
38
|
+
return {'error': 'redirect_loop', 'hops': hops}
|
|
39
|
+
seen.add(url)
|
|
40
|
+
status, location, _ = fetch(url)
|
|
41
|
+
if status not in (301, 302, 303, 307, 308) or not location:
|
|
42
|
+
slash = bool(hops) and urllib.parse.urlsplit(initial)._replace(path=urllib.parse.urlsplit(initial).path.rstrip('/')) == urllib.parse.urlsplit(hops[0])._replace(path=urllib.parse.urlsplit(hops[0]).path.rstrip('/'))
|
|
43
|
+
maximum = 1 if expected_page else 2 if slash else 1
|
|
44
|
+
acceptable_page_hop = not expected_page or not hops or (len(hops) == 1 and slash)
|
|
45
|
+
ok = status == 200 and len(hops) <= maximum and acceptable_page_hop
|
|
46
|
+
return {'ok': ok, 'status': status, 'hops': hops, 'final_url': url}
|
|
47
|
+
url = urllib.parse.urljoin(url, location)
|
|
48
|
+
if urllib.parse.urlsplit(url).scheme not in ('http', 'https'):
|
|
49
|
+
return {'error': 'unsupported_redirect_scheme', 'hops': hops}
|
|
50
|
+
hops.append(url)
|
|
51
|
+
return {'error': 'too_many_redirects', 'hops': hops}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
56
|
+
parser.add_argument('origin', type=origin)
|
|
57
|
+
parser.add_argument('--show-excluded', action='store_true')
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
report = []
|
|
60
|
+
for row in trlib.all_migration_urls():
|
|
61
|
+
if row.get('status') == 'excluded' and not args.show_excluded:
|
|
62
|
+
continue
|
|
63
|
+
for path in sorted(set([row['path']] + (row.get('observed_paths') or []))):
|
|
64
|
+
# Inventory spellings must be paths, not a silent change of tested host.
|
|
65
|
+
if not path.startswith('/') or path.startswith('//'):
|
|
66
|
+
raise ValueError('Inventory contains a non-local path')
|
|
67
|
+
url = args.origin + urllib.parse.quote(path, safe='/%?=&:+')
|
|
68
|
+
report.append({'path': path, **probe(url, row.get('status') == 'migrated')})
|
|
69
|
+
print(json.dumps(report, indent=2))
|
|
70
|
+
return 1 if not report or any(not row.get('ok') for row in report) else 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == '__main__':
|
|
74
|
+
raise SystemExit(main())
|