@getvitops/astro 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,34 +1,48 @@
1
1
  ---
2
2
  /**
3
- * Cards — a `Subgrid` whose items are `card`s. The child's own class and style
4
- * ride along, so `<article class="bg-surface-muted">` becomes `<li class="card bg-surface-muted">`.
3
+ * Cards — a `Subgrid` whose cards are clickable across their whole area, with their
4
+ * text still selectable.
5
5
  *
6
- * The class has to be put on the `<li>` we hand to Subgrid rather than written
7
- * into the slotted element: `serialize()` returns a node's *inner* HTML, so the
8
- * element carrying the attribute is discarded on the way through.
6
+ * **This component emits its own `<wc-cards>`** — do NOT wrap it in one. That is the
7
+ * tier-3 rule ("the wrapper emits its `<wc-*>` tag with the accessible fallback
8
+ * inside"), and `Tree.astro` and `CookieConsent.astro` do the same.
9
+ * `<wc-cards><Cards /></wc-cards>` nests two elements governing the same cards; the
10
+ * inner one defers to the enclosing one so it degrades to a no-op rather than two
11
+ * forwarded clicks, but the single call is the intended shape.
12
+ *
13
+ * <Cards>
14
+ * <li class="card subgrid-card">
15
+ * <p class="font-eyebrow">Service</p>
16
+ * <p class="font-heading"><a class="link" href="/a">Title</a></p>
17
+ * <p>Blurb…</p>
18
+ * </li>
19
+ * </Cards>
20
+ *
21
+ * The fallback is the card's own link, so with no JS every card is still reachable —
22
+ * only the click-anywhere convenience is missing, and the pointer cursor comes from
23
+ * the element too, so the affordance never appears without the behaviour.
24
+ *
25
+ * **`<Subgrid>` is the unenhanced form**, which is why there is no `enhance={false}`
26
+ * prop: a flag would be a second way to say the same thing. Reach for
27
+ * `.stretched-link` instead when you want whole-card clicking with no JS at all and
28
+ * can accept that the card's text stops being selectable.
29
+ *
30
+ * It no longer parses the slot to add `class="card"`. That was its only job, and it is
31
+ * what forced `Subgrid` to rebuild every child as an `<li>` — see that file. Write the
32
+ * class on the item.
9
33
  */
10
34
  import type { HTMLAttributes } from 'astro/types';
11
35
  import Subgrid from './Subgrid.astro';
12
- import { parseRenderedSlots, getAttribute, serialize } from '@getvitops/utils';
13
-
14
- export interface Props extends HTMLAttributes<'ul'> {}
15
36
 
16
- const { class: className, ...rest } = Astro.props;
37
+ export interface Props extends HTMLAttributes<'ul'> {
38
+ as?: 'ul' | 'ol' | 'div';
39
+ /** Selector for the cards the element governs. Defaults to `.card`. */
40
+ item?: string;
41
+ }
17
42
 
18
- const nodes = parseRenderedSlots(await Astro.slots.render('default'));
19
- const slots = nodes.filter(node => 'tagName' in node).map(node => ({
20
- html: serialize(node),
21
- class: getAttribute(node, 'class'),
22
- style: getAttribute(node, 'style'),
23
- }));
43
+ const { item, ...rest } = Astro.props;
24
44
  ---
25
45
 
26
- <Subgrid
27
- role="list"
28
- class={className}
29
- {...rest}
30
- >
31
- {slots.map((slot) => (
32
- <li class:list={["card", slot.class]} style={slot.style} set:html={slot.html}></li>
33
- ))}
34
- </Subgrid>
46
+ <wc-cards item={item}>
47
+ <Subgrid {...rest}><slot /></Subgrid>
48
+ </wc-cards>
@@ -0,0 +1,46 @@
1
+ ---
2
+ /**
3
+ * <GatedTags /> — the one place a third-party tag becomes markup.
4
+ *
5
+ * Shared by <Analytics /> and <Ads />, and shared deliberately: the gate is
6
+ * `type="text/plain"` with the URL on `data-src`, so the browser never parses the
7
+ * body as script and never fetches the library. An undecided or declining
8
+ * visitor's page issues no third-party request at all. Consent implemented by
9
+ * *asking a tracker not to track* is a promise; this is a fact about the document
10
+ * — and a fact with two implementations is one rewrite away from being neither.
11
+ *
12
+ * Decides nothing. Every tag arrives fully resolved from `analytics.ts` or
13
+ * `ads.ts`, both of which are pure and unit-tested.
14
+ *
15
+ * Emits no `preconnect` on purpose: warming a connection at parse time is exactly
16
+ * the critical-path cost the `idle` strategy exists to avoid.
17
+ */
18
+ import type { GatedTag } from '../analytics.ts';
19
+
20
+ interface Props {
21
+ tags: GatedTag[];
22
+ }
23
+ const { tags } = Astro.props;
24
+ ---
25
+
26
+ {tags.map((tag) => tag.gated
27
+ ? (
28
+ <script
29
+ is:inline
30
+ type="text/plain"
31
+ data-vitops-tag
32
+ data-consent={tag.category}
33
+ data-strategy={tag.strategy}
34
+ data-src={tag.src}
35
+ data-consent-cookies={tag.cookies.length ? tag.cookies.join(',') : undefined}
36
+ {...tag.attrs}
37
+ set:html={tag.inline}
38
+ />
39
+ )
40
+ : (
41
+ <>
42
+ {tag.inline && <script is:inline set:html={tag.inline} />}
43
+ {tag.src && <script is:inline async src={tag.src} {...tag.attrs}></script>}
44
+ </>
45
+ )
46
+ )}
@@ -1,32 +1,50 @@
1
1
  ---
2
2
  /**
3
- * Subgrid — re-emits slotted children as `<li>` items of a `.subgrid` grid, so
4
- * the tranches inside each one align on shared row lines. Every declaration
5
- * lives in the framework's `subgrid` pattern (patterns/subgrid.css), which is
6
- * emitted in all three output formats; this file is markup only.
3
+ * Subgrid — a grid whose items re-declare the parent's row tracks, so the tranches
4
+ * inside every item (head, body, footer) land on the same row lines no matter how
5
+ * much content each one holds. Every declaration lives in the framework's `subgrid`
6
+ * pattern (patterns/subgrid.css); this file is markup only.
7
7
  *
8
- * Nothing here may reach for a Tailwind utility: Tailwind doesn't scan
9
- * node_modules, so a class only it can emit is silently never generated — and
10
- * classes in the generator's TW_CLASH list are stripped from the framework's
11
- * own tailwind bundle by design.
8
+ * **The slot is rendered verbatim — author the items yourself.** This used to parse
9
+ * the slotted HTML and replace each child with an `<li>`, carrying over its class
10
+ * and style. That existed only because a `<ul>` may contain nothing but `<li>`, and
11
+ * it cost more than it bought: the child's tag was discarded, and every attribute
12
+ * had to be copied by hand, so `id`, `data-*` and `aria-*` were silently dropped and
13
+ * `href` could not survive at all. Passing the slot through means there is nothing to
14
+ * copy and nothing to lose.
12
15
  *
13
- * Size it from the consumer's own CSS, which keeps media-query overrides
14
- * working: `--subgrid-cols`, `--subgrid-row-span`, `--subgrid-gap`,
15
- * `--subgrid-row-gap`. `--num-items` is set here for nth-child-style maths.
16
+ * <Subgrid>
17
+ * <li class="card subgrid-card">…</li>
18
+ * </Subgrid>
19
+ *
20
+ * `as` picks the container. It defaults to `ul`, because a set of cards IS a list and
21
+ * that is what tells assistive tech how many there are and where you are among them.
22
+ * Use `as="div"` when the items are genuinely not a list.
23
+ *
24
+ * **`role="list"` is restored automatically for `ul`/`ol`.** `.subgrid` sets
25
+ * `list-style: none`, and that stops Safari + VoiceOver announcing a list at all — so
26
+ * the marker reset silently costs the semantics the `<ul>` was chosen for. Passing
27
+ * your own `role` wins.
28
+ *
29
+ * Size it from the consumer's own CSS, which keeps media-query overrides working:
30
+ * `--subgrid-cols`, `--subgrid-row-span`, `--subgrid-gap`, `--subgrid-row-gap`.
31
+ *
32
+ * For a card whose whole area should be clickable, see `<Cards />` (selectable text,
33
+ * needs JS) or `.stretched-link` (no JS, text not selectable).
16
34
  */
17
35
  import type { HTMLAttributes } from 'astro/types';
18
- import { parseRenderedSlots, getAttribute, serialize, styleList } from '@getvitops/utils';
19
36
 
20
- export interface Props extends HTMLAttributes<'ul'> {};
37
+ export interface Props extends HTMLAttributes<'ul'> {
38
+ /** Container element. `ul`/`ol` also get `role="list"`. */
39
+ as?: 'ul' | 'ol' | 'div';
40
+ }
21
41
 
22
- const { class: className, style } = Astro.props;
42
+ const { as: Tag = 'ul', class: className, role, ...rest } = Astro.props;
23
43
 
24
- const nodes = parseRenderedSlots(await Astro.slots.render('default'));
25
- const slots = nodes.filter(node => 'tagName' in node).map(node => ({ html: serialize(node), attrs: node.attrs }));
44
+ // Only lists lose semantics to `list-style: none`, so only lists get the role back.
45
+ const listRole = Tag === 'ul' || Tag === 'ol' ? 'list' : undefined;
26
46
  ---
27
47
 
28
- <ul class:list={["subgrid", className]} style={styleList({'--num-items': slots.length}, style)}>
29
- {slots.map((slot) => (
30
- <li class:list={[getAttribute(slot, 'class')]} style={styleList(getAttribute(slot, 'style'))} set:html={slot.html}></li>
31
- ))}
32
- </ul>
48
+ <Tag class:list={['subgrid', className]} role={role ?? listRole} {...rest}>
49
+ <slot />
50
+ </Tag>
@@ -65,14 +65,14 @@ const {
65
65
  {
66
66
  enhance ? (
67
67
  <wc-tree label={label} {...rest}>
68
- <ul class="tree tree--lines" aria-label={ariaLabel}>
68
+ <ul class="tree tree--lines" role="list" aria-label={ariaLabel}>
69
69
  {items.map((item) => (
70
70
  <TreeNode item={item} depth={0} openDepth={openDepth} />
71
71
  ))}
72
72
  </ul>
73
73
  </wc-tree>
74
74
  ) : (
75
- <ul class="tree tree--lines" aria-label={ariaLabel}>
75
+ <ul class="tree tree--lines" role="list" aria-label={ariaLabel}>
76
76
  {items.map((item) => (
77
77
  <TreeNode item={item} depth={0} openDepth={openDepth} />
78
78
  ))}
@@ -41,7 +41,7 @@ const isBranch = kids.length > 0;
41
41
  </span>
42
42
  </summary>
43
43
  {item.descriptionHtml && <p class="tree__desc" set:html={item.descriptionHtml} />}
44
- <ul class="tree">
44
+ <ul class="tree" role="list">
45
45
  {kids.map((kid) => (
46
46
  <Astro.self item={kid} depth={depth + 1} openDepth={openDepth} />
47
47
  ))}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@getvitops/astro/tracking` — the attribution vocabulary, re-exported.
3
+ *
4
+ * The documented conversion flow spans two packages: `<Tracking />` captures the
5
+ * click ID from `@getvitops/astro`, and the route reads it back with
6
+ * `parseTrackingCookie` from `@getvitops/utils/tracking`. Under strict pnpm a
7
+ * consumer cannot resolve the second from app code without adding
8
+ * `@getvitops/utils` as a direct dependency — so the flow as written in the
9
+ * changelog needed two installs, and the obvious workaround (importing the same
10
+ * symbols from `@getvitops/astro`'s index) is the one that drags the whole
11
+ * integration, and its Node builtins, toward a Worker bundle.
12
+ *
13
+ * This entry is the third option and the correct one: one install, and a module
14
+ * graph that reaches only `@getvitops/utils/tracking` — no `sharp`, no
15
+ * `node:child_process`, nothing that cannot run in a Worker. It is a separate
16
+ * build entry for exactly the reason `./routes` is.
17
+ */
18
+ export * from '@getvitops/utils/tracking';
19
+
20
+ /**
21
+ * Where the capture script beacons `tel:` conversions.
22
+ *
23
+ * Re-exported here so a consumer writing the route can assert against the same
24
+ * constant the script uses, rather than retyping the path.
25
+ */
26
+ export { TRACKING_ENDPOINT } from '../tracking.ts';
@@ -26,6 +26,16 @@ const COOKIE_NAME = '_ac';
26
26
  const COOKIE_DAYS = 90;
27
27
  const AB_COOKIE = '_ab';
28
28
 
29
+ // Where the `tel:` conversion beacon goes. Mirrors TRACKING_ENDPOINT in
30
+ // ../tracking.ts (pinned by `tracking.test.ts`), copied for the same reason as
31
+ // the parameter lists below — this file is inlined into the document.
32
+ //
33
+ // It was previously a bare literal at the call site, which made the contract
34
+ // between this script and the route the consumer writes invisible: name the
35
+ // file anything else and every beacon 404s, conversions vanish, and nothing
36
+ // errors anywhere. The integration now warns at build when the route is absent.
37
+ const TRACK_ENDPOINT = '/api/track';
38
+
29
39
  // Mirrors CLICK_ID_PARAMS/UTM_PARAMS in @getvitops/utils/tracking, deliberately
30
40
  // copied rather than imported: this file is inlined into the document by
31
41
  // <Tracking />, so a bare import would either fail or drag a module graph into
@@ -39,6 +49,8 @@ const CLICK_ID_PARAMS = [
39
49
  'rdt_cid', // Reddit
40
50
  'ScCid', // Snapchat
41
51
  'msclkid', // Microsoft/Bing
52
+ 'li_fat_id', // LinkedIn
53
+ 'epik', // Pinterest
42
54
  ] as const;
43
55
 
44
56
  const UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const;
@@ -142,6 +154,6 @@ document.addEventListener('click', (e) => {
142
154
 
143
155
  // Fire-and-forget beacon; don't block the tel: navigation
144
156
  if (navigator.sendBeacon) {
145
- navigator.sendBeacon('/api/track', JSON.stringify({ event: 'call', phone: link.href }));
157
+ navigator.sendBeacon(TRACK_ENDPOINT, JSON.stringify({ event: 'call', phone: link.href }));
146
158
  }
147
159
  });
package/src/tracking.ts CHANGED
@@ -27,6 +27,19 @@ export interface GetvitopsTrackingOptions {
27
27
  /** The cookie the capture script writes. Mirrors `TRACKING_COOKIE` in utils. */
28
28
  export const TRACKING_COOKIE = '_ac';
29
29
 
30
+ /**
31
+ * Where the capture script sends its `tel:` conversion beacon.
32
+ *
33
+ * The route itself is the consumer's to write (`createConversionRoute()` in
34
+ * `@getvitops/astro/routes`), so this is the one place the two halves agree on
35
+ * a path. It was a bare literal inside the inlined script: a consumer who named
36
+ * the file anything else got a silent 404 on every beacon and lost conversions
37
+ * with no error anywhere to explain it. Exported so a consumer can assert
38
+ * against it, mirrored in `scripts/tracking.ts` because that file is inlined
39
+ * into the document and cannot import, and pinned by `tracking.test.ts`.
40
+ */
41
+ export const TRACKING_ENDPOINT = '/api/track';
42
+
30
43
  export interface ResolvedTracking {
31
44
  enabled: boolean;
32
45
  category: TrackingCategory;
package/virtual.d.ts CHANGED
@@ -52,6 +52,12 @@ declare module 'virtual:getvitops/head' {
52
52
  // the others, and the offered consent categories it checks against are known
53
53
  // only there.
54
54
  tracking: { enabled: boolean; category: string; cookies: string[] } | null;
55
+ // The site config's `site.ads` block plus this environment's switch —
56
+ // structurally `{ enabled, properties }` (src/ads.ts), re-typed by <Ads /> on
57
+ // the way into `resolveAds`. Read in the integration because that is where the
58
+ // site config is already parsed, and because a pixel is a `marketing` demand
59
+ // the banner must have a row for.
60
+ ads: { enabled: boolean; properties: Record<string, unknown> } | null;
55
61
  }
56
62
  const data: HeadData;
57
63
  export default data;