@cparkerwebm/webmonterey 1.3.0 → 1.4.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/CHANGELOG.md +47 -0
- package/README.md +10 -0
- package/package.json +1 -1
- package/skills/launch/SKILL.md +5 -0
- package/skills/new-component/SKILL.md +6 -0
- package/skills/start/SKILL.md +2 -1
- package/skills/webmaster/SKILL.md +118 -0
- package/src/cli/checks.test.ts +21 -0
- package/src/cli/sync.test.ts +1 -0
- package/src/includes/webmonterey/copy-defaults.ts +9 -5
- package/src/includes/webmonterey/webmaster/webmaster.test.ts +61 -1
- package/src/includes/webmonterey/webmaster/webmaster.ts +57 -1
- package/src/integration/index.ts +9 -0
- package/src/integration/virtual.d.ts +11 -0
- package/src/pages/webmaster.astro +50 -28
- package/template/site/CLAUDE.md +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,53 @@ build. See `/webm:upgrade`.
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## 1.4.0 — 2026-09-04
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **A site can lay out the `/webmaster` page itself.** Export `webmasterPage` from
|
|
19
|
+
`src/components/registry.ts` - an Astro component receiving
|
|
20
|
+
`{ title, description, intro, body }` - and the page renders it in place of the built-in
|
|
21
|
+
`<h1>` and stack of paragraphs. `intro` and each `body` entry are HTML, rendered with
|
|
22
|
+
`set:html` - the agency link is already in `intro`; `title` and `description` are text. The route, the copy, the document title, the meta
|
|
23
|
+
description, the share image and the agency JSON-LD stay the package's, so the page says the
|
|
24
|
+
same thing on every site and looks like the site it is on. Until now a site whose legal pages
|
|
25
|
+
used a richer document layout got a `/webmaster` that looked like a different site, and adding
|
|
26
|
+
its own `src/pages/webmaster.astro` collided with the injected route. The component carries no
|
|
27
|
+
copy of its own: the words are still `copy.webmaster` in `webmonterey.json`.
|
|
28
|
+
|
|
29
|
+
**`/webm:webmaster` is the skill**, materialized on the next `webm sync` - it carries a
|
|
30
|
+
complete reference component, the registry line and the checks to run on the built page, so a
|
|
31
|
+
site session has something to copy rather than a description. `/webm:new-component` sends a
|
|
32
|
+
session there the moment a site gains its document block, and `/webm:start` and
|
|
33
|
+
`/webm:launch` both point at it, so on a new site it happens in sequence; on an existing site
|
|
34
|
+
the upgrade lists it as a new skill.
|
|
35
|
+
The props are exported as `WebmasterPageProps` from
|
|
36
|
+
`@cparkerwebm/webmonterey/webmonterey/webmaster`.
|
|
37
|
+
|
|
38
|
+
**Nothing changes on a site that does not export it.** The built-in layout, `pageHeader`
|
|
39
|
+
seam included, renders as before, and examples/minimal asserts the whole `<main>` as a literal
|
|
40
|
+
so it cannot drift.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **The `/webmaster` copy.** "This custom website was designed, built and managed by
|
|
45
|
+
WebMonterey, a webmaster service in Monterey, California. WebMonterey handles the hosting,
|
|
46
|
+
security, strategy and ongoing care of the site…" - _custom_ added, _maintenance_ dropped,
|
|
47
|
+
_updates_ is now _strategy_ - and the contact paragraph is bold. `copy.webmaster.intro` and
|
|
48
|
+
`copy.webmaster.body` now take the same inline subset as page prose (`**bold**`, `_italic_`,
|
|
49
|
+
`[text](/url)`), which is how the bold is expressed. A site overriding `copy.webmaster` in
|
|
50
|
+
`webmonterey.json` keeps its own words; one that does not gets these. The agency's
|
|
51
|
+
description in the page's JSON-LD drops _maintenance_ too.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **"managed byWebMonterey".** The built-in `/webmaster` intro had no space before the agency
|
|
56
|
+
link - Astro drops the whitespace between an expression and an element on separate lines. It
|
|
57
|
+
reads "managed by WebMonterey" now.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
14
61
|
## 1.3.0 — 2026-09-02
|
|
15
62
|
|
|
16
63
|
### Changed
|
package/README.md
CHANGED
|
@@ -20,6 +20,16 @@ the reset, the Cloudflare includes, the consent system, the form pipeline, the r
|
|
|
20
20
|
It ships **zero visible components**. Every block a visitor sees is built per client, in that
|
|
21
21
|
client's repo. Every default has a documented way to opt out of it.
|
|
22
22
|
|
|
23
|
+
A site hands the package its components through `src/components/registry.ts`: `blocks` for the
|
|
24
|
+
block router, `header`, `footer` and `panels` for the chrome, `pageHeader` in place of the
|
|
25
|
+
router's plain `<h1>`, `structuredData` for the site's JSON-LD, and `webmasterPage` for the body
|
|
26
|
+
of the `/webmaster` page. That last one is the seam for a site whose document pages have a richer
|
|
27
|
+
layout than a heading and a stack of paragraphs: the component receives the merged copy
|
|
28
|
+
(`{ title, description, intro, body }`; `intro` and `body` are HTML, the agency link already in
|
|
29
|
+
`intro`) and lays it out; the route, the words, the `<head>`, the share image and the agency graph
|
|
30
|
+
stay the package's. It carries no copy of its own - the words are overridden through
|
|
31
|
+
`copy.webmaster` in `webmonterey.json`, not in the component.
|
|
32
|
+
|
|
23
33
|
**The package is edited in this repo, and only here.** A session in a client site that finds a
|
|
24
34
|
package bug does not reach into `node_modules` or into this checkout; its deliverable is a
|
|
25
35
|
description of the fix, run later in a session opened here, and the site takes the result with
|
package/package.json
CHANGED
package/skills/launch/SKILL.md
CHANGED
|
@@ -99,6 +99,11 @@ to say, and this is where it gets decided - once, with the client's real details
|
|
|
99
99
|
4. **Validate** the built home page and one inner page with Google's Rich Results Test before
|
|
100
100
|
moving on.
|
|
101
101
|
|
|
102
|
+
5. **Look at `/webmaster`.** The package injects it on every site, with its own share image and
|
|
103
|
+
JSON-LD. If this site's document pages - privacy, terms - use a richer layout than an `<h1>`
|
|
104
|
+
and a stack of paragraphs, run `/webm:webmaster`: it exports a layout component from the
|
|
105
|
+
registry so the page looks like the site it is on, with the package's copy intact.
|
|
106
|
+
|
|
102
107
|
## 3. Turnstile
|
|
103
108
|
|
|
104
109
|
If the site has a form: create the widget in the Cloudflare dashboard (**Turnstile → Add
|
|
@@ -83,3 +83,9 @@ npm run preview # NOT dev - preview is a real build on real workerd
|
|
|
83
83
|
|
|
84
84
|
`dev` cannot detect CSS bundling, cascade order, or the asset router. Every trap worth catching is
|
|
85
85
|
invisible in `dev` and visible in `preview`. See `/webm:traps`.
|
|
86
|
+
|
|
87
|
+
## If this is the site's document block
|
|
88
|
+
|
|
89
|
+
A long-form block - the one the privacy and terms pages use - is also the layout the package's
|
|
90
|
+
`/webmaster` page should have. The moment that block exists, run `/webm:webmaster`: it exports a
|
|
91
|
+
layout component built on this block, so `/webmaster` stops looking like a different site.
|
package/skills/start/SKILL.md
CHANGED
|
@@ -173,5 +173,6 @@ client can click anything. **Every build of this site is a preview while `enviro
|
|
|
173
173
|
flips it, and that flip is what makes the site indexable. On a launched site the production
|
|
174
174
|
branch is `main`; anything else previews.
|
|
175
175
|
|
|
176
|
-
Next: `/webm:new-component` for each block,
|
|
176
|
+
Next: `/webm:new-component` for each block, `/webm:webmaster` once the site has its document
|
|
177
|
+
block so the package's `/webmaster` page shares its layout, then `/webm:launch` when the site is
|
|
177
178
|
content-complete and approved on a preview.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webmaster
|
|
3
|
+
description: Make the package's /webmaster page look like this site's other document pages by exporting a `webmasterPage` layout component from the registry. Use for "the webmaster page looks different", "style /webmaster like /privacy", "webmaster page layout", "webmasterPage".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The `/webmaster` page
|
|
7
|
+
|
|
8
|
+
**The package owns the page; this site may own its shape.** `/webmaster` is injected on every
|
|
9
|
+
site: who built it, who to call. The route, the words, the document title, the meta description,
|
|
10
|
+
the share image and the agency JSON-LD are the package's, so the page says the same thing on
|
|
11
|
+
every site. Without help it renders as an `<h1>` (or this site's `pageHeader`) and a stack of
|
|
12
|
+
paragraphs - which looks like a different site the moment the site's own document pages have a
|
|
13
|
+
richer layout.
|
|
14
|
+
|
|
15
|
+
**Do not add `src/pages/webmaster.astro`.** It collides with the injected route, and the injected
|
|
16
|
+
route wins. The seam is a registry export.
|
|
17
|
+
|
|
18
|
+
## What the component receives
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
interface WebmasterPageProps {
|
|
22
|
+
title: string; // text - also the document title
|
|
23
|
+
description: string; // text - also the meta description
|
|
24
|
+
intro: string; // HTML - the first paragraph, agency link already in it
|
|
25
|
+
body: string[]; // HTML - one entry per remaining paragraph
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`intro` and `body` are already HTML: the agency link with its UTM parameters, and the copy's own
|
|
30
|
+
inline formatting (`**bold**`, `_italic_`, `[text](/url)`) rendered and escaped. Render them with
|
|
31
|
+
`set:html`. `title` and `description` are text.
|
|
32
|
+
|
|
33
|
+
**The component carries no copy of its own.** Not a heading, not a caption, not a sentence. The
|
|
34
|
+
words are the same on every site; when a client wants them changed, that is `copy.webmaster` in
|
|
35
|
+
`webmonterey.json` (`title`, `description`, `intro.before`, `intro.after`, `body[]`), never the
|
|
36
|
+
component.
|
|
37
|
+
|
|
38
|
+
## 1. Find this site's document layout
|
|
39
|
+
|
|
40
|
+
Look at how the privacy or terms page is rendered - usually one long-form block under
|
|
41
|
+
`src/components/content/`. The webmaster page should reuse **that block's markup and classes**,
|
|
42
|
+
so a change to the document style reaches both.
|
|
43
|
+
|
|
44
|
+
## 2. Write the component
|
|
45
|
+
|
|
46
|
+
`src/components/general/webmaster-page.astro`. The reference below is complete and works on any
|
|
47
|
+
site; replace the element names and classes with the document block's own.
|
|
48
|
+
|
|
49
|
+
```astro
|
|
50
|
+
---
|
|
51
|
+
/*
|
|
52
|
+
* The /webmaster page body. The package hands in the copy; this lays it out like the site's
|
|
53
|
+
* other document pages. See /webm:webmaster.
|
|
54
|
+
*/
|
|
55
|
+
import type { WebmasterPageProps } from '@cparkerwebm/webmonterey/webmonterey/webmaster';
|
|
56
|
+
|
|
57
|
+
type Props = WebmasterPageProps;
|
|
58
|
+
|
|
59
|
+
const { title, intro, body } = Astro.props;
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<section class="webm-section" data-space="lg">
|
|
63
|
+
<div class="webm-container" data-width="text">
|
|
64
|
+
<article class="doc">
|
|
65
|
+
<header class="doc__head">
|
|
66
|
+
<h1 class="doc__title">{title}</h1>
|
|
67
|
+
</header>
|
|
68
|
+
<div class="doc__panel">
|
|
69
|
+
<p set:html={intro} />
|
|
70
|
+
{body.map((paragraph) => <p set:html={paragraph} />)}
|
|
71
|
+
</div>
|
|
72
|
+
</article>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the document block is itself a component that takes `title` and a slot or `html` prop, render
|
|
78
|
+
it directly rather than copying its markup:
|
|
79
|
+
|
|
80
|
+
```astro
|
|
81
|
+
---
|
|
82
|
+
import Doc from '../content/content-000001/content-000001.astro';
|
|
83
|
+
const { title, intro, body } = Astro.props;
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
<Doc title={title}>
|
|
87
|
+
<p set:html={intro} />
|
|
88
|
+
{body.map((paragraph) => <p set:html={paragraph} />)}
|
|
89
|
+
</Doc>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 3. Export it
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// src/components/registry.ts
|
|
96
|
+
export { default as webmasterPage } from './general/webmaster-page.astro';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Not an entry in `blocks` - it is not addressable from page JSON. A named export beside
|
|
100
|
+
`pageHeader` and `structuredData`.
|
|
101
|
+
|
|
102
|
+
## 4. Verify
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
npm run build
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then read `dist/client/webmaster/index.html`:
|
|
109
|
+
|
|
110
|
+
- the body is the new markup, and there is no `webm-stack` div left from the built-in layout
|
|
111
|
+
- the `<h1>` is the package's title (`Our Webmaster` unless `copy.webmaster.title` is set)
|
|
112
|
+
- the first paragraph has one `<a href="https://webmonterey.com/?utm_source=client…"
|
|
113
|
+
target="_blank" rel="noopener">` and nothing else links off-site
|
|
114
|
+
- `<title>`, `meta name="description"`, `og:image` (`/webmaster/og.png`) and the
|
|
115
|
+
`application/ld+json` block are present and unchanged from before the export
|
|
116
|
+
|
|
117
|
+
Then push and compare `/webmaster` against `/privacy` on the preview URL: same heading style,
|
|
118
|
+
same panel, the package's words.
|
package/src/cli/checks.test.ts
CHANGED
|
@@ -435,6 +435,27 @@ test('a registry keyed by name rather than by number is understood', () => {
|
|
|
435
435
|
assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
|
|
436
436
|
});
|
|
437
437
|
|
|
438
|
+
test('the chrome exports in a registry are not read as block types', () => {
|
|
439
|
+
/*
|
|
440
|
+
* `header`, `footer`, `pageHeader`, `structuredData` and `webmasterPage` sit in the same file
|
|
441
|
+
* as `blocks`, in either export form. None is a block type, and none may make the check trip
|
|
442
|
+
* over a registry that is otherwise complete.
|
|
443
|
+
*/
|
|
444
|
+
const ctx = base({
|
|
445
|
+
registry: [
|
|
446
|
+
`import WebmasterPage from './general/webmaster-page.astro';`,
|
|
447
|
+
`export const blocks = { 'content-000001': C };`,
|
|
448
|
+
`export const registeredTypes = () => Object.keys(blocks);`,
|
|
449
|
+
`export { default as pageHeader } from './general/page-header.astro';`,
|
|
450
|
+
`export const webmasterPage = WebmasterPage;`,
|
|
451
|
+
].join('\n'),
|
|
452
|
+
content: new Map([
|
|
453
|
+
['src/content/pages/home.json', JSON.stringify({ blocks: [{ type: 'content-000001' }] })],
|
|
454
|
+
]),
|
|
455
|
+
});
|
|
456
|
+
assert.equal(runCheck('block-types-registered', ctx).status, 'pass');
|
|
457
|
+
});
|
|
458
|
+
|
|
438
459
|
test('a site that has not launched only gets a warning about placeholders', () => {
|
|
439
460
|
/*
|
|
440
461
|
* `webm new` seeds every one of these, so a freshly scaffolded site has the full set. The
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -67,7 +67,10 @@ export interface Copy {
|
|
|
67
67
|
/** `{domain}` is replaced with the site's domain. */
|
|
68
68
|
footerNotice: string;
|
|
69
69
|
};
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The /webmaster page. `intro` wraps the agency link: `before` <a>WebMonterey</a> `after`.
|
|
72
|
+
* `intro` and `body` take the inline prose subset - `**bold**`, `_italic_`, `[text](/url)`.
|
|
73
|
+
*/
|
|
71
74
|
webmaster: {
|
|
72
75
|
title: string;
|
|
73
76
|
description: string;
|
|
@@ -135,14 +138,15 @@ export const DEFAULT_COPY: Copy = {
|
|
|
135
138
|
webmaster: {
|
|
136
139
|
title: 'Our Webmaster',
|
|
137
140
|
description:
|
|
138
|
-
'This website was designed, built and managed by WebMonterey, a webmaster
|
|
141
|
+
'This custom website was designed, built and managed by WebMonterey, a webmaster service in Monterey, California.',
|
|
139
142
|
intro: {
|
|
140
|
-
before: 'This website was designed, built and managed by',
|
|
143
|
+
before: 'This custom website was designed, built and managed by',
|
|
141
144
|
after:
|
|
142
|
-
', a webmaster
|
|
145
|
+
', a webmaster service in Monterey, California. WebMonterey handles the hosting, security, strategy and ongoing care of the site so that we can focus on what we do.',
|
|
143
146
|
},
|
|
147
|
+
/* Bold on purpose: the contact instruction is the paragraph a visitor with a problem needs. */
|
|
144
148
|
body: [
|
|
145
|
-
"If you have a question about this website, notice something that isn't working, or have trouble using a page, please let WebMonterey know and they will take care of it
|
|
149
|
+
"**If you have a question about this website, notice something that isn't working, or have trouble using a page, please let WebMonterey know and they will take care of it.**",
|
|
146
150
|
],
|
|
147
151
|
},
|
|
148
152
|
};
|
|
@@ -2,7 +2,15 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AGENCY,
|
|
7
|
+
contentTag,
|
|
8
|
+
CREDIT_TEXT,
|
|
9
|
+
creditUrl,
|
|
10
|
+
introHtml,
|
|
11
|
+
webmasterPageProps,
|
|
12
|
+
WEBMASTER_PATH,
|
|
13
|
+
} from './webmaster.ts';
|
|
6
14
|
|
|
7
15
|
/*
|
|
8
16
|
* Webmaster.astro read as SOURCE, because it cannot be imported here: an .astro file only
|
|
@@ -72,3 +80,55 @@ test('an internal link does not open a new tab and carries no rel', () => {
|
|
|
72
80
|
assert.doesNotMatch(anchor(), /rel=/);
|
|
73
81
|
assert.doesNotMatch(template, /opens in a new tab/);
|
|
74
82
|
});
|
|
83
|
+
|
|
84
|
+
/* ── the intro paragraph, as one HTML string for a site that owns the layout ─────────────── */
|
|
85
|
+
|
|
86
|
+
test('introHtml is before, a space, the agency link, then after', () => {
|
|
87
|
+
const html = introHtml({ before: 'Built by', after: ', a service.' }, 'https://x.test/?a=1&b=2');
|
|
88
|
+
assert.equal(
|
|
89
|
+
html,
|
|
90
|
+
'Built by <a href="https://x.test/?a=1&b=2" target="_blank" rel="noopener">WebMonterey</a>, a service.',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('introHtml escapes the copy and the href', () => {
|
|
95
|
+
const html = introHtml({ before: '<b>a & b</b>', after: '"q"' }, 'https://x.test/?"');
|
|
96
|
+
assert.ok(!html.includes('<b>'), 'copy markup must not pass through');
|
|
97
|
+
assert.ok(html.includes('<b>a & b</b>'));
|
|
98
|
+
assert.ok(html.includes('href="https://x.test/?""'));
|
|
99
|
+
assert.ok(html.endsWith('"q"'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('the copy carries the inline prose subset, in the intro and the body', () => {
|
|
103
|
+
const props = webmasterPageProps(
|
|
104
|
+
{
|
|
105
|
+
title: 'T',
|
|
106
|
+
description: 'D',
|
|
107
|
+
intro: { before: 'Built by', after: ', **really**.' },
|
|
108
|
+
body: ["**If it isn't working, say so.**", 'See [the policy](/privacy).'],
|
|
109
|
+
},
|
|
110
|
+
'https://x.test/',
|
|
111
|
+
);
|
|
112
|
+
assert.equal(props.title, 'T');
|
|
113
|
+
assert.equal(props.description, 'D');
|
|
114
|
+
assert.ok(props.intro.endsWith('</a>, <strong>really</strong>.'));
|
|
115
|
+
assert.deepEqual(props.body, [
|
|
116
|
+
'<strong>If it isn't working, say so.</strong>',
|
|
117
|
+
'See <a href="/privacy">the policy</a>.',
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the built-in page renders the same intro string a site layout receives', () => {
|
|
122
|
+
/*
|
|
123
|
+
* One source for the agency link. The page used to build its own <a> in the template, which
|
|
124
|
+
* is how a second copy of the attributes would drift; now both layouts render introHtml.
|
|
125
|
+
*/
|
|
126
|
+
const page = readFileSync(new URL('../../../pages/webmaster.astro', import.meta.url), 'utf8');
|
|
127
|
+
const template = page.slice(page.lastIndexOf('---'));
|
|
128
|
+
assert.match(template, /<p set:html=\{props\.intro\} \/>/);
|
|
129
|
+
assert.doesNotMatch(template, /<a\s/, 'the page must not assemble the agency link itself');
|
|
130
|
+
const html = introHtml({ before: '', after: '' }, 'https://x.test/');
|
|
131
|
+
assert.match(html, /target="_blank"/);
|
|
132
|
+
assert.match(html, /rel="noopener"/);
|
|
133
|
+
assert.doesNotMatch(html, /noreferrer/);
|
|
134
|
+
});
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* means a change to the name, the address or a profile is one edit.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { escapeHtml, renderInline } from '../prose/inline.ts';
|
|
29
|
+
|
|
28
30
|
/** The credit wording. Rendered verbatim on the site and in email. */
|
|
29
31
|
export const CREDIT_TEXT = 'Powered by WebMonterey';
|
|
30
32
|
|
|
@@ -44,7 +46,7 @@ export const AGENCY = {
|
|
|
44
46
|
name: 'WebMonterey',
|
|
45
47
|
url: 'https://webmonterey.com/',
|
|
46
48
|
description:
|
|
47
|
-
'A webmaster
|
|
49
|
+
'A webmaster service in Monterey, California: design, build, hosting, security and ongoing care for small-business websites.',
|
|
48
50
|
address: { addressLocality: 'Monterey', addressRegion: 'CA', addressCountry: 'US' },
|
|
49
51
|
sameAs: [
|
|
50
52
|
/* The Google Business Profile, by its Knowledge Graph id - the stable form of the share link. */
|
|
@@ -87,3 +89,57 @@ export function creditUrl(domain: string, medium: CreditMedium = 'website'): str
|
|
|
87
89
|
|
|
88
90
|
return `${AGENCY.url}?${params}`;
|
|
89
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What the site's `webmasterPage` component receives, when a site exports one from its
|
|
95
|
+
* registry: the merged copy, already resolved. The component lays it out and carries no copy of
|
|
96
|
+
* its own - the words are the package's on every site, the shape of the page is the client's.
|
|
97
|
+
*
|
|
98
|
+
* `intro` and `body` are HTML, rendered with `set:html`. They are the output of `renderInline`,
|
|
99
|
+
* so a paragraph in `copy.webmaster` may carry the same inline subset page prose does -
|
|
100
|
+
* `**bold**`, `_italic_`, `[text](/url)` - and everything else is escaped.
|
|
101
|
+
*/
|
|
102
|
+
export interface WebmasterPageProps {
|
|
103
|
+
/** `copy.webmaster.title`, also the document title. Text. */
|
|
104
|
+
title: string;
|
|
105
|
+
/** `copy.webmaster.description`, also the meta description. Text. */
|
|
106
|
+
description: string;
|
|
107
|
+
/** The first paragraph's inner HTML, with the agency link already resolved. */
|
|
108
|
+
intro: string;
|
|
109
|
+
/** The remaining paragraphs' inner HTML, one entry per `<p>`. */
|
|
110
|
+
body: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The intro paragraph's inner HTML: `before` <a>WebMonterey</a> `after`.
|
|
115
|
+
*
|
|
116
|
+
* ONE STRING, because it is the only part of the copy that is not plain text. A site taking
|
|
117
|
+
* over the page layout still gets the agency link exactly as the built-in page renders it - a
|
|
118
|
+
* followed link, opening in a new tab, `noopener` without `noreferrer` because the referrer is
|
|
119
|
+
* the attribution - rather than reassembling three fragments and forgetting one of the
|
|
120
|
+
* attributes. The built-in page renders this same string, so the two cannot drift. The space
|
|
121
|
+
* between `before` and the link is deliberate: Astro drops the whitespace between an expression
|
|
122
|
+
* and an element on separate lines, and "managed byWebMonterey" shipped.
|
|
123
|
+
*/
|
|
124
|
+
export function introHtml(intro: { before: string; after: string }, href: string): string {
|
|
125
|
+
const link = `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${escapeHtml(AGENCY.name)}</a>`;
|
|
126
|
+
return `${renderInline(intro.before)} ${link}${renderInline(intro.after)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The whole prop set, from the merged copy and the attributed agency link. */
|
|
130
|
+
export function webmasterPageProps(
|
|
131
|
+
text: {
|
|
132
|
+
title: string;
|
|
133
|
+
description: string;
|
|
134
|
+
intro: { before: string; after: string };
|
|
135
|
+
body: string[];
|
|
136
|
+
},
|
|
137
|
+
href: string,
|
|
138
|
+
): WebmasterPageProps {
|
|
139
|
+
return {
|
|
140
|
+
title: text.title,
|
|
141
|
+
description: text.description,
|
|
142
|
+
intro: introHtml(text.intro, href),
|
|
143
|
+
body: text.body.map(renderInline),
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/integration/index.ts
CHANGED
|
@@ -387,6 +387,15 @@ export default function webmonterey(options: WebmontereyOptions = {}): AstroInte
|
|
|
387
387
|
* router's plain <h1>; a site that does not keeps the <h1>.
|
|
388
388
|
*/
|
|
389
389
|
`export const pageHeader = mod.pageHeader ?? null;`,
|
|
390
|
+
/*
|
|
391
|
+
* THE /webmaster PAGE'S BODY. The route, the copy, the share image and
|
|
392
|
+
* the agency graph stay the package's; a site whose document pages use
|
|
393
|
+
* a richer layout than an <h1> and a stack of paragraphs hands over the
|
|
394
|
+
* layout only. Without this a site's own src/pages/webmaster.astro
|
|
395
|
+
* collides with the injected route, and on one site the injected route
|
|
396
|
+
* won.
|
|
397
|
+
*/
|
|
398
|
+
`export const webmasterPage = mod.webmasterPage ?? null;`,
|
|
390
399
|
/*
|
|
391
400
|
* THE SITE'S JSON-LD, rendered into <head> on every route. The package
|
|
392
401
|
* emits none of its own: what a business claims about itself is the
|
|
@@ -86,6 +86,17 @@ declare module 'virtual:webm/registry' {
|
|
|
86
86
|
* every later fix to block lookup, unknown-type reporting and FAQ extraction.
|
|
87
87
|
*/
|
|
88
88
|
export const pageHeader: AstroComponentFactory | null;
|
|
89
|
+
/**
|
|
90
|
+
* The body of the `/webmaster` page, when the site wants it laid out like its own document
|
|
91
|
+
* pages rather than as the package's `<h1>` and stack of paragraphs.
|
|
92
|
+
*
|
|
93
|
+
* Receives `WebmasterPageProps` from `includes/webmonterey/webmaster/webmaster.ts`:
|
|
94
|
+
* `{ title, description, intro, body }`, the merged `copy.webmaster` - `intro` and `body` as
|
|
95
|
+
* HTML through the inline prose renderer, the agency link already in `intro`. The component
|
|
96
|
+
* lays the words out and carries no copy of its own. The route, the `<head>`, the share image and the JSON-LD stay the package's.
|
|
97
|
+
* Null means the built-in layout, `pageHeader` seam included.
|
|
98
|
+
*/
|
|
99
|
+
export const webmasterPage: AstroComponentFactory | null;
|
|
89
100
|
/**
|
|
90
101
|
* The site's JSON-LD, rendered into <head> on every indexable route. The package emits none of
|
|
91
102
|
* its own; this component composes a graph from the builders in
|
|
@@ -27,17 +27,25 @@ import { copy } from '../includes/webmonterey/copy.ts';
|
|
|
27
27
|
import {
|
|
28
28
|
AGENCY,
|
|
29
29
|
creditUrl,
|
|
30
|
+
webmasterPageProps,
|
|
30
31
|
WEBMASTER_OG_PATH,
|
|
31
32
|
} from '../includes/webmonterey/webmaster/webmaster.ts';
|
|
32
33
|
import { domain, hasDomain } from '../includes/webmonterey/site.ts';
|
|
33
34
|
import { compact, renderJsonLd } from '../includes/webmonterey/structured-data/nodes.ts';
|
|
34
|
-
import { pageHeader } from 'virtual:webm/registry';
|
|
35
|
+
import { pageHeader, webmasterPage } from 'virtual:webm/registry';
|
|
35
36
|
import ogImage from 'virtual:webm/webmaster-og';
|
|
36
37
|
import build from 'virtual:webm/build';
|
|
37
38
|
|
|
38
39
|
const PageHeader = pageHeader;
|
|
40
|
+
const WebmasterPage = webmasterPage;
|
|
39
41
|
const text = copy.webmaster;
|
|
40
42
|
const agencyHref = hasDomain ? creditUrl(domain, 'website') : AGENCY.url;
|
|
43
|
+
/*
|
|
44
|
+
* The words, resolved once for both layouts: the intro with the agency link in it, every
|
|
45
|
+
* paragraph through the inline prose renderer, so `**bold**` in copy.webmaster works here
|
|
46
|
+
* exactly as it does in page JSON.
|
|
47
|
+
*/
|
|
48
|
+
const props = webmasterPageProps(text, agencyHref);
|
|
41
49
|
|
|
42
50
|
/*
|
|
43
51
|
* The graph needs a real origin for stable @id values, which is the same gate the canonical tag
|
|
@@ -85,37 +93,51 @@ if (Astro.site && !build.preview) {
|
|
|
85
93
|
|
|
86
94
|
{
|
|
87
95
|
/*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
96
|
+
* THE BODY IS THE SITE'S IF IT WANTS IT. A site whose document pages - privacy, terms - use a
|
|
97
|
+
* richer layout than an <h1> and a stack of paragraphs exports `webmasterPage` from its
|
|
98
|
+
* registry and gets the merged copy handed to it: the same title, description and paragraphs
|
|
99
|
+
* as below, with the agency link already resolved into `intro`. Everything above this
|
|
100
|
+
* expression - the route, the <head>, the share image, the agency graph - stays the
|
|
101
|
+
* package's, so the page reads the same on every site and looks like the one it is on.
|
|
90
102
|
*/
|
|
91
|
-
|
|
92
|
-
<
|
|
103
|
+
WebmasterPage ? (
|
|
104
|
+
<WebmasterPage {...props} />
|
|
93
105
|
) : (
|
|
94
|
-
<
|
|
95
|
-
<div class="webm-container" data-width="text">
|
|
96
|
-
<h1>{text.title}</h1>
|
|
97
|
-
</div>
|
|
98
|
-
</section>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
<section class="webm-section" data-space="lg">
|
|
103
|
-
<div class="webm-container" data-width="text">
|
|
104
|
-
<div class="webm-stack">
|
|
106
|
+
<Fragment>
|
|
105
107
|
{
|
|
106
108
|
/*
|
|
107
|
-
* The
|
|
108
|
-
*
|
|
109
|
-
* site; noopener without noreferrer, because the referrer is the attribution.
|
|
109
|
+
* The site's page header if it declares one, so this page looks like every other page
|
|
110
|
+
* on the site; the router's plain <h1> otherwise. Same fallback the block router uses.
|
|
110
111
|
*/
|
|
112
|
+
PageHeader ? (
|
|
113
|
+
<PageHeader title={text.title} description={text.description} />
|
|
114
|
+
) : (
|
|
115
|
+
<section class="webm-section" data-space="sm">
|
|
116
|
+
<div class="webm-container" data-width="text">
|
|
117
|
+
<h1>{text.title}</h1>
|
|
118
|
+
</div>
|
|
119
|
+
</section>
|
|
120
|
+
)
|
|
111
121
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
|
|
123
|
+
<section class="webm-section" data-space="lg">
|
|
124
|
+
<div class="webm-container" data-width="text">
|
|
125
|
+
<div class="webm-stack">
|
|
126
|
+
{/*
|
|
127
|
+
* The agency is named as a link in the first paragraph - the one outbound link
|
|
128
|
+
* on the site, followed, with the UTM parameters. Opens in a new tab because it
|
|
129
|
+
* leaves the site; noopener without noreferrer, because the referrer is the
|
|
130
|
+
* attribution. See introHtml in webmaster.ts - the same string a site's own
|
|
131
|
+
* layout receives, so the two cannot disagree about the link.
|
|
132
|
+
*/}
|
|
133
|
+
<p set:html={props.intro} />
|
|
134
|
+
{props.body.map((paragraph) => (
|
|
135
|
+
<p set:html={paragraph} />
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
</Fragment>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
121
143
|
</Base>
|
package/template/site/CLAUDE.md
CHANGED
|
@@ -231,7 +231,10 @@ webmonterey.json domain, client name, environment, features.
|
|
|
231
231
|
src/
|
|
232
232
|
components/ EVERY visible component. The package ships none.
|
|
233
233
|
registry.ts maps a block `type` to its component. A block whose type is
|
|
234
|
-
not here renders as nothing, silently.
|
|
234
|
+
not here renders as nothing, silently. Also exports the chrome:
|
|
235
|
+
`header`, `footer`, `panels`, `pageHeader`, `structuredData`, and
|
|
236
|
+
`webmasterPage` - the body of /webmaster, laid out like this
|
|
237
|
+
site's document pages, with the package's copy handed in.
|
|
235
238
|
content/pages/*.json the words. One file per route; home.json is `/`.
|
|
236
239
|
forms/*.json form definitions. Filename is the form id.
|
|
237
240
|
actions/index.ts re-exports the package pipeline. Wrap to customize.
|
|
@@ -264,6 +267,7 @@ replaced wholesale. This client's own skills go beside it at `.claude/skills/<na
|
|
|
264
267
|
| `/webm:start` | standing a new site up: repo, Cloudflare resources, first deploy |
|
|
265
268
|
| `/webm:launch` | launch checklist: structured data, sending domain, secrets, DNS, cutover |
|
|
266
269
|
| `/webm:upgrade` | taking a new package version |
|
|
270
|
+
| `/webm:webmaster` | laying out the package's `/webmaster` page like this site's own pages |
|
|
267
271
|
|
|
268
272
|
Content edits: branch, edit `src/content/pages/*.json`, `npm run check`, push — the branch
|
|
269
273
|
preview is the client's review link. Merge when approved. Never change a block's `type`.
|