@getvitops/skills 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +35 -0
- package/README.md +12 -7
- package/package.json +1 -1
- package/skills/emdash-building-pages/SKILL.md +82 -0
- package/skills/{contact-form → emdash-contact-form}/SKILL.md +1 -1
- package/skills/{content-and-deploys → emdash-content-and-deploys}/SKILL.md +28 -1
- package/skills/{custom-blocks → emdash-custom-blocks}/SKILL.md +1 -1
- package/skills/vitops-ads/SKILL.md +65 -0
- package/skills/vitops-ads/call-tracking.md +106 -0
- package/skills/vitops-consent/SKILL.md +35 -0
- package/skills/{csp → vitops-csp}/SKILL.md +1 -1
- package/skills/vitops-see-the-page/SKILL.md +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @getvitops/skills
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6b5ea17: **Leaner scaffolded `AGENTS.md`, and skills named by scope.** The EmDash template's `AGENTS.md` went from 212 to about 70 lines and the static one from 141 to about 50: only always-true rules stay (source of truth, generated CSS, branches, `vp`/`vpr`, where to look), and task-specific guidance moved into skills loaded when the task matches.
|
|
8
|
+
|
|
9
|
+
- **Breaking, `@getvitops/skills`: skills are renamed by scope.** `custom-blocks` → `emdash-custom-blocks`, `content-and-deploys` → `emdash-content-and-deploys`, `contact-form` → `emdash-contact-form`, `csp` → `vitops-csp`, `see-the-page` → `vitops-see-the-page`, `ads` → `vitops-ads`. `emdash-*` skills only apply to an EmDash site; `vitops-*` apply anywhere, and both prefixes are reserved for toolkit skills. New: `emdash-building-pages` (blocks, nesting, page opener, widget areas, escape hatches, layout primitives, `site.chrome`) and `vitops-consent` (split out of `vitops-design-system`, whose description and body now cover styling and tokens only). Existing sites: run `vp install` so `skills experimental_sync` relinks, and replace the per-skill `.agents/skills/<name>` / `.claude/skills/<name>` lines in `.gitignore` with `.agents/skills/vitops-*`, `.agents/skills/emdash-*` and the same two under `.claude/skills/`. A site's own skill must not use those prefixes.
|
|
10
|
+
- **`vitops agents sync`:** the block lists only skills that apply (a project without `emdash` in its dependencies gets no `emdash-*` ones, and they are not linked), summaries are cut at a clause rather than with an ellipsis, the pointer says `vp exec vitops docs`, and the version stamp is gone so the file no longer changes on every CLI bump.
|
|
11
|
+
- **`@getvitops/create`:** both templates ship the trimmed `AGENTS.md`; the project-naming steps moved into the `setup` skill and EmDash 1.x behaviours, legal-page replacement and the audit prerequisites into `emdash-content-and-deploys`. Existing sites: nothing to change beyond the skill rename; delete the sections you no longer need.
|
|
12
|
+
|
|
13
|
+
- bb61a35: **Call tracking numbers: show an ad visitor a number that attributes the phone call.** Two additive config blocks, no CSS, no breaking change — all formats unaffected.
|
|
14
|
+
|
|
15
|
+
- **`site.ads.google.callTracking`** — Google Ads _website call conversions_. `{ label, number, cache? }` makes the consent-gated Google tag fetch a Google forwarding number (Canada supported) and swap it in for visitors who arrived from a Google ad. Google replaces text only, so the capture script also rewrites the matching `tel:` href. Needs `pixelId` and `site.tracking.enabled`; rejected on any other platform (only Google can put a number on the website — Microsoft's exist only inside the ad, in the US/UK/AU).
|
|
16
|
+
- **`site.tracking.phoneNumbers`** — per-platform numbers for every other platform: the real `number`, `sources` keyed by `site.ads` platform (plus `default-paid`), and an informational `provider`. You provision the numbers with a call-tracking service (Twilio, CallRail, …). The landing URL decides first, then the `_ac` cookie, so a visitor who declines `marketing` sees a tracking number on the landing page only. An organic visitor is never swapped, and the server-rendered HTML always carries the real number.
|
|
17
|
+
- The `tel:` tap beacon now carries which number was shown (`phoneSource`), and the conversion notification says so.
|
|
18
|
+
- `vitops ads lint` flags a call-tracking provider that is not in `site.legal.privacyPolicy.processors`, and notes that `callTracking` plus `conversionLabel.call` can count one call twice (make one secondary in Google Ads).
|
|
19
|
+
- `@getvitops/utils/tracking` exports the pure `selectPhoneNumber()` and helpers; the capture script mirrors it and `tracking.test.ts` pins the two.
|
|
20
|
+
- `@getvitops/skills` gains a **`vitops-ads`** skill (overview plus a `call-tracking.md` topic). The templates gitignore synced skills by prefix (`vitops-*`, `emdash-*`), so it needs no `.gitignore` line of its own.
|
|
21
|
+
|
|
22
|
+
`@getvitops/core`, `@getvitops/vite` and `@getvitops/emdash` — no consumer-facing change in these packages.
|
|
23
|
+
|
|
24
|
+
- 7069f93: **Agents now look at rendered pages with `playwright-cli`.** Both templates add `playwright` as a devDependency, run `playwright cli install --skills` from `prepare` (writing the upstream command reference to `.claude/skills/playwright-cli/`), ship `.playwright/cli.config.json`, allow `pnpm exec playwright cli`, and deny `mcp__claude-in-chrome__*` and `mcp__playwright__*` in `.claude/settings.json`. `@getvitops/skills` adds a short `see-the-page` skill (serve first, snapshot before screenshot, dark mode, mobile, no-JS) that defers to the upstream one. Existing sites: add `playwright` as a devDependency, extend `prepare`, and copy the settings. `vitops audit` now needs only `@axe-core/playwright lighthouse` added.
|
|
25
|
+
|
|
26
|
+
## 0.1.0-alpha.1
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 322dd6a: Tooling fixes from the vitops.ca build (findings 14, 23, 24, 25).
|
|
31
|
+
|
|
32
|
+
- **`vitops emdash form-template --from <seed.json | export.json> [--name contact] [--site site.json] [--dry]`** copies a `vitops_form` block into `site.templates` with the renderer's own field resolver, so the generated privacy policy and `check-prerender` know what the form collects. A re-run is a no-op, and the result is validated before it is written.
|
|
33
|
+
- `site.templates` form fields accept `checkboxes` (the block's spelling), and the template union now discriminates on `type`, so a bad entry is reported at its path instead of as a bare "Invalid input".
|
|
34
|
+
- **`prepare-db` fails fast and says why.** It validates `site.json` before starting the dev server, writes the server's output to `.vitops/prepare-db.log`, stops waiting as soon as the log shows an error, and prints the log's tail on failure, instead of waiting out a five-minute timeout with nothing to read.
|
|
35
|
+
- **`check-prerender`** skips the toolkit's own assets under `vitops/` (no more "0 h1" for the theme-editor page) and warns when the sitemap lists URLs the build produced no page for (a collection with no `urlPattern` is listed as `/<collection>/<slug>` while the site serves `/<slug>`).
|
|
36
|
+
- The emdash template ignores the duplicate `pnpm-@getvitops+skills-*` links `skills experimental_sync` leaves beside each skill; the `content-and-deploys` skill explains the sitemap warning.
|
|
37
|
+
|
|
3
38
|
## 0.1.0-alpha.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -28,15 +28,20 @@ Claude Code is selected), records them in `skills-lock.json`, and relinks on eve
|
|
|
28
28
|
|
|
29
29
|
`experimental_sync` is a preview command and may change; pin the `skills` version.
|
|
30
30
|
|
|
31
|
-
A skill the site installed or wrote itself is never replaced, so a site can override one by name.
|
|
31
|
+
A skill the site installed or wrote itself is never replaced, so a site can override one by name. The `vitops-` and `emdash-` prefixes are reserved for toolkit skills (the templates gitignore them by prefix), so name a site's own skill without them.
|
|
32
32
|
|
|
33
33
|
## Skills
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
35
|
+
Names carry scope: `emdash-*` only apply to an EmDash site, `vitops-*` to any site built with the toolchain. `vitops agents sync` lists only the ones that apply.
|
|
36
|
+
|
|
37
|
+
| Skill | Use it for |
|
|
38
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `emdash-custom-blocks` | Deciding whether a page section needs a new EmDash block type, and building it |
|
|
40
|
+
| `emdash-content-and-deploys` | Where content vs code is the truth, prerendering, publish-triggered rebuilds |
|
|
41
|
+
| `emdash-contact-form` | The `/api/track` route, no-JS behaviour, spam protection, privacy inventory |
|
|
42
|
+
| `vitops-csp` | The Report-Only CSP and what to check before enforcing it |
|
|
43
|
+
| `vitops-ads` | Ad pixels, conversions, attribution and call-tracking numbers (`site.ads`) |
|
|
44
|
+
| `vitops-see-the-page` | Looking at a rendered page with `playwright-cli` |
|
|
45
|
+
| `emdash-building-pages` | Authoring pages from `vitops_*` blocks: nesting, page opener, escape hatches, `site.chrome` |
|
|
41
46
|
|
|
42
47
|
The `vitops-design-system` skill still ships with `@getvitops/cli` and is linked by `vitops agents`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvitops/skills",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Agent skills for sites built with the Vitops toolchain. Installed as a dependency and linked into a project with `skills experimental_sync`, so they update with the package.",
|
|
5
5
|
"homepage": "https://docs.vitops.ca",
|
|
6
6
|
"bugs": "https://github.com/getvitops/design-system/issues",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: emdash-building-pages
|
|
3
|
+
description: Use when authoring or changing a page's layout or content on this EmDash site — composing `vitops_*` blocks, nesting sections, page openers and heroes, widget areas, block `theme`/`anchor`/`className`/`style`, heading levels, card grids, or the header (`site.chrome`, `<Masthead>`, mega menu panels).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building pages
|
|
7
|
+
|
|
8
|
+
Reference, rendered from this project: `vp exec vitops docs blocks` (every block and field),
|
|
9
|
+
`vp exec vitops docs components` (every pattern, which tiers provide it and which block renders it),
|
|
10
|
+
`vp exec vitops docs classes` (class vocabulary), `vp exec vitops docs config` (`site.chrome`).
|
|
11
|
+
|
|
12
|
+
## Blocks
|
|
13
|
+
|
|
14
|
+
- The editor's insert menu offers the `vitops_*` blocks.
|
|
15
|
+
- **Nesting.** A block type with a `portableText` field, such as `vitops_section`, holds other
|
|
16
|
+
blocks. A `vitops_*` block holds only scalar fields and repeaters (cards, gallery images,
|
|
17
|
+
accordion items, slides), so it can't. EmDash's built-in `columns` doesn't render `vitops_*`
|
|
18
|
+
children. For layout, nest in a section, use a `blocks` field, or hand-write the template with
|
|
19
|
+
`@getvitops/astro` components (`Subgrid`, `Cards`, `Carousel`, …).
|
|
20
|
+
- **Page opener.** Use `vitops_section` at heading level 1 with its button fields (`cta_label`,
|
|
21
|
+
`secondary_label`) and `stack: rhythm`; don't nest a `vitops_hero` or another section wrapper in a
|
|
22
|
+
section body, which renders two layers of `section > .centered`. `vitops_hero` is for a hero
|
|
23
|
+
that stands alone (it carries option cards).
|
|
24
|
+
- **Section, block or widget area?** Copy-and-edit content is an EmDash Section. The same content
|
|
25
|
+
everywhere (announcement bar, pre-footer call to action, sidebar, footer columns) is a **widget
|
|
26
|
+
area**: define it under Widgets in the admin and render it with `<VitopsWidgetArea name="…" />`
|
|
27
|
+
from `@getvitops/emdash/astro` in the layout, or in page content with the `vitops_widgetArea`
|
|
28
|
+
block. Page-specific content is blocks. An area can't hold another area, and prerendered pages
|
|
29
|
+
show an edit after the next deploy (like menus).
|
|
30
|
+
- **Escape hatches on every block:** `theme` (a backdrop from the site's colour roles, `surface` or
|
|
31
|
+
`brand-primary:solid`, with the readable text for it picked by the design system, so contrast is
|
|
32
|
+
not the editor's judgement), `anchor` (the `id`, for `#links`), `className` (extra classes; under
|
|
33
|
+
the tailwind format only ones the site already uses) and `style` (CSS custom properties only,
|
|
34
|
+
e.g. `--p-card: 2rem`; anything else is dropped). The same backdrop is available on a widget area
|
|
35
|
+
(`<VitopsWidgetArea theme="…">`) and on your own markup (`themeAttrs('surface')` from
|
|
36
|
+
`@getvitops/emdash/astro`, spread on the layout's `<body>`).
|
|
37
|
+
- Override a block per site with `<PortableText components={{ type: { 'vitops_cards': MyCards } }} />`;
|
|
38
|
+
hide one with `vitopsEmdash({ blocks: { exclude: [...] } })`. Before adding a block **type**, use
|
|
39
|
+
the `emdash-custom-blocks` skill: types can't be renamed or deleted once pages use them.
|
|
40
|
+
- **One `<h1>` per page.** The template's page routes render the title as an `<h1>`, and a hero's
|
|
41
|
+
heading level defaults to `h1`, so set a hero's level to `2` on those pages (or let the hero own
|
|
42
|
+
the `h1`). Use `p` for a display heading that is not part of the outline. `vitops emdash build`
|
|
43
|
+
warns on any page without exactly one `<h1>`.
|
|
44
|
+
- Labels: `badge` is static (status, count, category); `tag` may be editable or dismissable. Don't
|
|
45
|
+
invent `chip` or `pill`. The button class is `btn`.
|
|
46
|
+
|
|
47
|
+
## Layout foundations — the answer, not a starting point
|
|
48
|
+
|
|
49
|
+
| About to write… | Write instead |
|
|
50
|
+
| ------------------------------------------------------- | --------------- |
|
|
51
|
+
| a `wrap`/`wrapper`/`container`/`inner` class | **`centered`** |
|
|
52
|
+
| `max-width` + `margin-inline: auto` | **`centered`** |
|
|
53
|
+
| `padding-block` on a section | **`region`** |
|
|
54
|
+
| margins between headings/paragraphs/lists | **`rhythm`** |
|
|
55
|
+
| `display: grid` + `grid-template-columns: repeat(n, …)` | **`subgrid`** |
|
|
56
|
+
| `repeat(auto-fit, minmax(…))` | **`grid-auto`** |
|
|
57
|
+
| `display: flex` + `gap` for a row of controls | **`cluster`** |
|
|
58
|
+
|
|
59
|
+
`centered` is a grid of _named tracks_: a child widens itself with `breakout`/`spotlight`/
|
|
60
|
+
`fullbleed` without the parent knowing, which a bespoke `.wrap` can't do.
|
|
61
|
+
|
|
62
|
+
- **More than one card means `subgrid`**, so headings and CTAs align across the set:
|
|
63
|
+
`<ul class="subgrid subgrid-cols-3" role="list">` with `<li class="card subgrid-card">`, or
|
|
64
|
+
`<Subgrid />` (renders the slot verbatim; author the `<li>`s).
|
|
65
|
+
- **A card that is a link:** `stretched-link` inside it is zero-JS but kills text selection;
|
|
66
|
+
`<Cards>` keeps selection and falls back to the same link with no JS. Never both. Never
|
|
67
|
+
`<li><a class="card">`: the `li` is the grid item, so the tranches don't align.
|
|
68
|
+
- `vpr lint:design` reports all of these and the pre-commit hook fails on them. Fix the class, not
|
|
69
|
+
the CSS.
|
|
70
|
+
|
|
71
|
+
## The header: `site.chrome`
|
|
72
|
+
|
|
73
|
+
The header is configuration, not markup. `site.chrome.masthead` sets position
|
|
74
|
+
(`static|sticky|fixed`), `behavior` (`always|hide-on-scroll`), `surface`
|
|
75
|
+
(`solid|glass|transparent|transparent-until-scrolled`), `layout` (`full|inset|floating`), `height`,
|
|
76
|
+
`inset` and `elevation`; `site.chrome.nav` sets `compact` (`drawer|dropdown`), `drawerSide`,
|
|
77
|
+
breakpoints and depths. A `<Masthead>` prop overrides one field for a layout (a landing page can be
|
|
78
|
+
`position="fixed" surface="transparent-until-scrolled"`). A sticky masthead sets `--header-offset`
|
|
79
|
+
itself, so anchors clear it. Nav tiers are menus: `primary`, optional `primary-mobile` and
|
|
80
|
+
`primary-mega`. For the mega panel's featured column, create a widget area named
|
|
81
|
+
`mega-<item url slug>` (e.g. `mega-services`), or put `mega-area:<name>` in the menu item's CSS
|
|
82
|
+
classes.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: contact-form
|
|
2
|
+
name: emdash-contact-form
|
|
3
3
|
description: How the contact form works on this site — the on-demand `/api/track` route, no-JS behaviour, where submissions go, spam protection and Turnstile, and the privacy-policy inventory. Use when adding or changing a form, fixing submissions that don't arrive, setting up notifications, CRM or Turnstile, or touching `src/pages/api/track.ts`.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: content-and-deploys
|
|
2
|
+
name: emdash-content-and-deploys
|
|
3
3
|
description: How content, builds and deploys fit together on this EmDash site — where each kind of data is the source of truth, why pages are prerendered, how a publish reaches the live site, and refreshing the dev database. Use for any deploy or build problem, stale or missing content, "my publish isn't live", scheduled posts, previews, 404s, server islands, or refreshing dev from prod.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -101,3 +101,30 @@ reviewers' approval** like any prod deploy (relax that rule if editors' publishe
|
|
|
101
101
|
unattended), and bursts of publishes queue behind the `deploy-prod` concurrency group rather than
|
|
102
102
|
cancelling each other. Without the hook, a manual run of **Deploy prod** (or a push to `main`) does
|
|
103
103
|
the same job.
|
|
104
|
+
|
|
105
|
+
## Sitemap lists `/pages/<slug>`
|
|
106
|
+
|
|
107
|
+
A collection with no `urlPattern` is listed as `/<collection>/<slug>`, so the sitemap would send
|
|
108
|
+
search engines to 404s while the site serves `/<slug>`. `vitops emdash build` (check-prerender) warns
|
|
109
|
+
when the sitemap lists URLs the build has no page for. Set the pattern on the collection (the seed's
|
|
110
|
+
`urlPattern`, or Content types in the admin); a database made before the seed set it needs the admin fix.
|
|
111
|
+
|
|
112
|
+
## EmDash 1.x behaviours
|
|
113
|
+
|
|
114
|
+
- Passkeys are bound to the hostname they were created on. After a domain cutover use **Settings →
|
|
115
|
+
General → Change domain**, then **Continue on**.
|
|
116
|
+
- Link entries by `entry.data.slug`, never `entry.id` (multilingual sites prefix `id` with a locale).
|
|
117
|
+
- Submit `/sitemap.xml` to search engines, not a per-collection sitemap.
|
|
118
|
+
- Clear a stuck D1 migration lock with `vp exec emdash migrate --take-over-lock <id>` (needs
|
|
119
|
+
`--expected-target-fingerprint` non-interactively).
|
|
120
|
+
|
|
121
|
+
## Legal pages and the audit
|
|
122
|
+
|
|
123
|
+
- The sample `/terms` and `/privacy` pages are short placeholders. To replace them, fill in
|
|
124
|
+
`site.json`'s `organization`/`site.legal`, run
|
|
125
|
+
`vp exec vitops legal --format portable-text --out ./legal`, and paste each document into the page
|
|
126
|
+
in the admin. The output still needs review before launch.
|
|
127
|
+
- `vitops audit` needs optional packages and a browser, checked before it starts a server:
|
|
128
|
+
`vp add -D @axe-core/playwright lighthouse && vp exec playwright install chromium`
|
|
129
|
+
(`playwright` itself is already a devDependency). Its page list is the built sitemap, then the
|
|
130
|
+
seed's menus.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: custom-blocks
|
|
2
|
+
name: emdash-custom-blocks
|
|
3
3
|
description: Decide whether a page section needs a new EmDash block type, and if so how to name, build, version and retire it. Use before creating, renaming or deleting any block type, any `src/components/blocks/*` renderer, or when asked to "add a block", "make an editable section" or "add a component the editors can place".
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vitops-ads
|
|
3
|
+
description: How ad platforms, pixels, conversions and attribution work on this site — `site.ads`, the consent-gated tags, click-ID capture into `_ac`, `vitops ads setup|tags|lint`, and the `tel:`/form conversions they report. Also call tracking and forwarding numbers (Google website call conversions, per-platform CallRail/Twilio numbers). Use when adding or changing a Google/Meta/LinkedIn/TikTok/Microsoft/Reddit/Pinterest/Snapchat pixel, verifying a domain with an ad platform, debugging conversions that arrive unattributed, or showing different phone numbers to ad visitors.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ads
|
|
7
|
+
|
|
8
|
+
An ad platform is recorded as facts in `site.json` under `site.ads.<platform>`; the toolchain turns
|
|
9
|
+
those facts into the tag, the cookie-notice disclosure, the consent behaviour and the lint checks. A
|
|
10
|
+
pixel pasted into a template is invisible to all four, so don't.
|
|
11
|
+
|
|
12
|
+
- **Platforms:** `google` `meta` `linkedin` `reddit` `tiktok` `microsoft` `pinterest` `snapchat`. Each
|
|
13
|
+
entry names the account (`accountId`), the tag (`pixelId`; for Google the `AW-…` conversion ID, not
|
|
14
|
+
the customer ID) and, where the platform verifies a domain, the token (`domainVerification`).
|
|
15
|
+
- **Tags are inert until consent.** `<Ads />` renders each as `type="text/plain"` with its URL on
|
|
16
|
+
`data-src`, so an undecided visitor's page makes no third-party request. Never give an ad tag a live
|
|
17
|
+
`src`. Category is `marketing` unless `category: "analytics"`. `environments.<env>.ads: false`
|
|
18
|
+
switches every tag off for that environment (a preview must not fire conversion pixels).
|
|
19
|
+
- **Click IDs are captured into `_ac`** by `<Tracking />` once `site.tracking.enabled` is true. It
|
|
20
|
+
reads the landing URL synchronously, but only _writes_ the 90-day cookie after `marketing` is
|
|
21
|
+
granted, and only an arrival that carried a click ID or UTM raises the banner. With tracking off,
|
|
22
|
+
every conversion from an ad arrives unattributed — it looks exactly like organic.
|
|
23
|
+
- **Conversions** are `tel:` taps, `mailto:` taps and form posts. For Google, `conversionLabel`
|
|
24
|
+
takes one label or `{ call, email, form }`, and each is its own action in Google Ads (the category —
|
|
25
|
+
"Phone call lead", "Contact", "Submit lead form" — is set there, not in code).
|
|
26
|
+
`reporting: "server"` + `conversionActionId` uploads from the conversion route instead of the browser.
|
|
27
|
+
- **Domain verification:** only Meta, TikTok, Pinterest and Snapchat verify, by an apex DNS TXT record.
|
|
28
|
+
Google Ads, LinkedIn, Reddit and Microsoft have none — linking is the tag and the account ID.
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
- `vitops ads setup` — creates the verification TXT (needs `CLOUDFLARE_API_TOKEN`), asks for a missing
|
|
33
|
+
token and writes it back to `site.json`. It never edits or deletes a DNS record, and calls no
|
|
34
|
+
platform API; the final "Verify" click stays manual (it prints a reminder).
|
|
35
|
+
- `vitops ads tags` — prints the inert, gated `<script>` markup for stacks that aren't Astro.
|
|
36
|
+
- `vitops ads lint` — reports gaps nothing shows at runtime: a click ID that isn't captured, tracking
|
|
37
|
+
turned off, an undeclared call-tracking processor. Run it before launch and when adding a platform.
|
|
38
|
+
|
|
39
|
+
## Topics
|
|
40
|
+
|
|
41
|
+
- [call-tracking.md](call-tracking.md) — showing ad visitors a forwarding number: Google's website
|
|
42
|
+
call conversions, per-platform numbers from a call-tracking provider, what each platform can and
|
|
43
|
+
can't do, consent timing, double counting, privacy policy, and how to test. Read when the task
|
|
44
|
+
involves phone numbers, `tel:` links or "calls from ads".
|
|
45
|
+
|
|
46
|
+
See also the `emdash-contact-form` skill (where form conversions land and what the notification says) and the
|
|
47
|
+
`vitops-csp` skill (origins a new tag needs before the CSP is enforced).
|
|
48
|
+
|
|
49
|
+
## Conversion tracking details
|
|
50
|
+
|
|
51
|
+
`vitops docs tracking`. `site.tracking.enabled` captures ad click IDs and UTMs from the
|
|
52
|
+
landing URL into the first-party `_ac` cookie; a conversion route reads it back and notifies
|
|
53
|
+
via `site.notifications`. Points worth holding:
|
|
54
|
+
|
|
55
|
+
- `_ac` is a 90-day identifier, so it waits on `marketing` (or `site.tracking.category`) and
|
|
56
|
+
the capture **`require()`s** it — see above.
|
|
57
|
+
- Only an arrival actually carrying a click ID or UTM asks; organic visitors are never
|
|
58
|
+
interrupted.
|
|
59
|
+
- The read is synchronous and the _write_ is what waits — the click ID exists only in the
|
|
60
|
+
landing URL, so it cannot be deferred.
|
|
61
|
+
- Only the `email` channel (Cloudflare Email Sending) is implemented. Its sending domain must
|
|
62
|
+
be onboarded with `wrangler email sending enable <domain>` or every send fails; the tool
|
|
63
|
+
surfaces that error verbatim rather than as "send failed".
|
|
64
|
+
- `--dry`-style planning is pure and reports **why** anything was skipped. A silently unsent
|
|
65
|
+
notification is indistinguishable from no conversion, so never leave one unexplained.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Call tracking numbers
|
|
2
|
+
|
|
3
|
+
Goal: attribute a real phone call to the ad that caused it, not just the `tel:` tap. That means
|
|
4
|
+
showing an ad visitor a different number than everyone else, then reading which number was dialled.
|
|
5
|
+
|
|
6
|
+
## What each platform can do
|
|
7
|
+
|
|
8
|
+
| Platform | Number on the website? |
|
|
9
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
10
|
+
| **Google Ads** | Yes — _website call conversions_. Google supplies the forwarding number. Canada is supported (local, toll-free). |
|
|
11
|
+
| **Microsoft Ads** | No. Forwarding numbers exist only inside the ad's call extension, and only in the US, UK and Australia. |
|
|
12
|
+
| Meta, LinkedIn, TikTok, Reddit, Pinterest, Snapchat | No platform-provided numbers at all. |
|
|
13
|
+
|
|
14
|
+
For everything except Google the only route is a number the client provisions with a call-tracking
|
|
15
|
+
provider (Twilio, CallRail, CallScaler, …), forwarding to the real line, shown to that platform's
|
|
16
|
+
visitors. That is `site.tracking.phoneNumbers`. Re-check Google's and Microsoft's availability pages
|
|
17
|
+
before promising a client either: country lists change.
|
|
18
|
+
|
|
19
|
+
## Rules that hold for both routes
|
|
20
|
+
|
|
21
|
+
- **The served HTML always carries the real number.** Swapping happens in the browser only, so
|
|
22
|
+
crawlers, structured data, no-JS visitors and local-SEO listings all see one consistent number, and
|
|
23
|
+
`organization.phone` stays the truth. Never render a tracking number server-side.
|
|
24
|
+
- **Only matching links and marked text are swapped.** A `tel:` link is swapped when its digits equal
|
|
25
|
+
the configured number (href and any text that is that number; a link labelled "Call us" only has its
|
|
26
|
+
href changed). Other text is swapped only inside an element with `data-vitops-phone`. Write the
|
|
27
|
+
number on the page in the same digits as the config — `613-555-0100`, `(613) 555-0100` and
|
|
28
|
+
`+1 613 555 0100` all match; a number written with a typo never does and is silently left alone.
|
|
29
|
+
- **An organic visitor is never swapped.** Nobody sees a tracking number they weren't sent to.
|
|
30
|
+
- **Needs `site.tracking.enabled: true`.** The capture script does the swapping; validation rejects
|
|
31
|
+
either feature without it.
|
|
32
|
+
|
|
33
|
+
## Google: `site.ads.google.callTracking`
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"ads": { "google": {
|
|
37
|
+
"pixelId": "AW-123456789",
|
|
38
|
+
"callTracking": { "label": "AbC_dEf-1", "number": "613-555-0100" }
|
|
39
|
+
} }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `label` belongs to a **separate** Google Ads conversion action of type _Calls from a phone number on
|
|
43
|
+
your website_, not the `conversionLabel.call` action (that one counts taps).
|
|
44
|
+
- Output is `gtag('config', 'AW-…/<label>', { phone_conversion_number, phone_conversion_callback })`,
|
|
45
|
+
inside the same consent-gated `marketing` tag, so nothing loads or swaps before consent.
|
|
46
|
+
- Google replaces displayed **text** only. The callback hands the result to the capture script, which
|
|
47
|
+
rewrites the `tel:` href. Without that, the visible number would be Google's and the tap would dial
|
|
48
|
+
the real one.
|
|
49
|
+
- Google decides who is shown a number (ad click, remembered for a while in a cookie; `cache: false`
|
|
50
|
+
turns that cache off). Only one number per page can be tracked this way.
|
|
51
|
+
- **Double counting:** one call can count as the tap action and the call action. In Google Ads make
|
|
52
|
+
one secondary. `vitops ads lint` reminds you when both are set.
|
|
53
|
+
- **Verification:** with a swapped number Google can't verify the domain by scanning the page. Link
|
|
54
|
+
Search Console. A tag installed through Google Tag Manager can't verify it either, so don't move this
|
|
55
|
+
tag into GTM.
|
|
56
|
+
- Test: after granting marketing consent, load a landing URL with `#google-wcc-debug` appended and
|
|
57
|
+
confirm the number and the `tel:` href both changed. Set the action's minimum call length in
|
|
58
|
+
Google Ads.
|
|
59
|
+
- Not available on the Display Network.
|
|
60
|
+
|
|
61
|
+
## Everyone else: `site.tracking.phoneNumbers`
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
"tracking": { "enabled": true, "phoneNumbers": {
|
|
65
|
+
"number": "613-555-0100",
|
|
66
|
+
"provider": "callrail",
|
|
67
|
+
"sources": { "meta": "613-555-0101", "microsoft": "613-555-0102", "default-paid": "613-555-0199" }
|
|
68
|
+
} }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `sources` keys are the `site.ads` platform names plus `default-paid`, the fallback for a paid visit
|
|
72
|
+
(any click ID, or `utm_medium` of `cpc`/`ppc`/`paid…`) from a platform with no number of its own.
|
|
73
|
+
Unknown keys fail validation. A `google` key is rejected while `callTracking` is set — two numbers
|
|
74
|
+
would compete for one visitor.
|
|
75
|
+
- Which source wins: the **landing URL** first (click ID, then a known `utm_source` such as
|
|
76
|
+
`facebook`→meta, `bing`→microsoft), then the stored `_ac` cookie. A fresh Meta click beats an old
|
|
77
|
+
stored Google ID.
|
|
78
|
+
- **Consent timing:** on the landing page the number is chosen from the URL, which is not storage and
|
|
79
|
+
needs no permission. On later pages it comes from `_ac`, which exists only if `marketing` was
|
|
80
|
+
granted. A visitor who declines therefore sees a tracking number on the landing page only — say so
|
|
81
|
+
to the client; it is a deliberate consequence of not storing without consent.
|
|
82
|
+
- The tap beacon carries which number was dialled, so the conversion notification says e.g. "Number
|
|
83
|
+
shown: meta tracking number". The call itself is attributed by the provider's own reporting.
|
|
84
|
+
- The decision lives in `@getvitops/utils/tracking`'s `selectPhoneNumber()` (pure, tested). The
|
|
85
|
+
capture script `packages/astro/src/scripts/tracking.ts` mirrors it because it is inlined into the
|
|
86
|
+
document; `tracking.test.ts` pins the two, so change them together.
|
|
87
|
+
|
|
88
|
+
## Privacy policy
|
|
89
|
+
|
|
90
|
+
A call-tracking provider records every caller's number and often a recording, so it is a processor the
|
|
91
|
+
generated policy must name. State it in `site.legal.privacyPolicy.processors` (the toolchain has no
|
|
92
|
+
known-provider entry for these yet). `phoneNumbers.provider` is informational and exists so `vitops ads
|
|
93
|
+
lint` can flag a provider missing from that list. Call recording needs its own caller notice; that
|
|
94
|
+
belongs in the provider's settings, not here.
|
|
95
|
+
|
|
96
|
+
## CSP
|
|
97
|
+
|
|
98
|
+
If the CSP is enforced, Google's number fetch needs Google origins in `script-src`/`connect-src`;
|
|
99
|
+
watch the Report-Only console on a landing page with `#google-wcc-debug` for the exact ones. See the
|
|
100
|
+
`vitops-csp` skill.
|
|
101
|
+
|
|
102
|
+
## Verifying a change
|
|
103
|
+
|
|
104
|
+
- `vitops validate` / `vitops ads lint` after editing `site.json`.
|
|
105
|
+
- In a browser: load `/?fbclid=test` and confirm the Meta number appears and the `tel:` href matches;
|
|
106
|
+
load `/` and confirm the real number; load with JavaScript disabled and confirm the real number.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vitops-consent
|
|
3
|
+
description: Use before gating, adding or changing any non-essential third-party tag, cookie, embed or `localStorage` write on a site with the consent gate (`<wc-consent>`, `data-consent`, `window.vitopsConsent`), or when a consent banner does not appear or a tag loads before consent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Consent and third-party tags
|
|
7
|
+
|
|
8
|
+
If the site has a consent gate, **every** non-essential third party goes through it. Run
|
|
9
|
+
`vitops docs consent` before wiring one up — the failure modes here are silent and legal.
|
|
10
|
+
The four that bite:
|
|
11
|
+
|
|
12
|
+
- **A gated tag must render inert.** `type="text/plain"` + `data-consent="<category>"` +
|
|
13
|
+
the URL on `data-src` (never `src`), plus `data-consent-cookies` naming what it sets so
|
|
14
|
+
a revoke can clear them. Given a live `src` the browser fetches the library immediately
|
|
15
|
+
and the gate is decorative — an undecided visitor's page must issue **no** third-party
|
|
16
|
+
request.
|
|
17
|
+
- **Use `require()`, not `granted()`, when you want permission.** `granted()` is passive:
|
|
18
|
+
it never raises the banner, so on a site where nothing else demands that category it is a
|
|
19
|
+
**permanent silent no-op** and your write never happens. `require()` registers the demand,
|
|
20
|
+
which is what makes the banner appear. `request()` is the promise form.
|
|
21
|
+
- **No `window.vitopsConsent` means the site has no gate — store freely.** It does not mean
|
|
22
|
+
denied. (A synchronous stub in `<head>` covers the window before the runtime loads, so
|
|
23
|
+
absence is reliable. Listen for the `vitops:consent` event rather than calling
|
|
24
|
+
`subscribe()`, which the stub lacks.)
|
|
25
|
+
- **Patch exactly the categories you put on screen.** `acceptAll()` / `rejectAll()` mean
|
|
26
|
+
literally every optional category — using one to answer a single-category prompt records
|
|
27
|
+
consent nobody gave. "Not asked" is a third value (`null`), distinct from declined, and
|
|
28
|
+
only it can be re-prompted.
|
|
29
|
+
|
|
30
|
+
The banner is **demand-driven**: it appears when something actually asks, not on first visit.
|
|
31
|
+
A site whose only analytics provider is cookieless never interrupts anyone. Don't "fix" a
|
|
32
|
+
missing banner by forcing it to show — find out what should have demanded a category.
|
|
33
|
+
|
|
34
|
+
Whatever loads must match what the **generated cookie notice** discloses; the Astro
|
|
35
|
+
integration warns when they disagree, and that is a compliance defect, not a doc nit.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: csp
|
|
2
|
+
name: vitops-csp
|
|
3
3
|
description: The Content Security Policy this site ships (Report-Only) and what to check before enforcing it. Use when changing `public/_headers`, adding an embed, tracking snippet or third-party script, turning the CSP on, or seeing CSP reports in the console.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vitops-see-the-page
|
|
3
|
+
description: How to look at a rendered page of this site (layout, colour, dark mode, no-JS fallback, mobile width) with playwright-cli. Use whenever you need to see, screenshot or click through a page, verify a visual change, or check how a page behaves without JavaScript.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Seeing the rendered page
|
|
7
|
+
|
|
8
|
+
`playwright-cli` is the only way to look at a page here. Don't use the Claude-in-Chrome extension or a
|
|
9
|
+
browser MCP server; `.claude/settings.json` denies them. The full command reference is the upstream
|
|
10
|
+
`playwright-cli` skill (written by `prepare` into `.claude/skills/playwright-cli/`); this file only
|
|
11
|
+
covers what is specific to this site.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. **Serve the page first.** `vpr dev` for source changes, or `vpr build` then the preview command
|
|
16
|
+
for exactly what ships. On EmDash, run `vpr prepare-local-db` before the first build.
|
|
17
|
+
2. **Open a named session:** `pnpm exec playwright cli -s=<task> open http://localhost:<port>/<path>`.
|
|
18
|
+
One session per task; `close` it when done.
|
|
19
|
+
3. **`snapshot` first.** It's text, cheap, and gives element refs. `screenshot` only when the
|
|
20
|
+
question is visual (spacing, colour, overflow). Output files land in `.playwright-cli/`
|
|
21
|
+
(gitignored).
|
|
22
|
+
4. **Dark mode:** set `data-theme="dark"` on `<html>` (the attribute `<wc-color-scheme-toggle>` writes)
|
|
23
|
+
rather than clicking through the toggle.
|
|
24
|
+
5. **Mobile width:** `open --device="iPhone 15"` or `--mobile`.
|
|
25
|
+
6. **No-JS fallback:** the web components must be usable without JavaScript. Open with a config
|
|
26
|
+
file whose `browser.contextOptions` sets `javaScriptEnabled: false` and check the slotted markup
|
|
27
|
+
is readable and operable.
|
|
28
|
+
|
|
29
|
+
## Don't
|
|
30
|
+
|
|
31
|
+
- Don't sign in to a production admin or paste real credentials into a session.
|
|
32
|
+
- Don't leave sessions running (`close-all` if unsure).
|
|
33
|
+
- If the browser is missing: `pnpm exec playwright cli install-browser chromium`.
|