@sarimarcus/content-sites-ui 0.29.1 → 0.29.3

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All packages in the platform release in lockstep; entries are per release version.
4
4
 
5
+ ## 0.29.3 (2026-10-07)
6
+
7
+ - `./layout`: `ConsentAnalytics` builds the absolute `gtm.js` URL at render time and passes it to the inline script, which no longer holds a relative `'/gtm.js?id='` literal that Googlebot crawled as a 404 on each site (SLA-3001).
8
+
9
+ ## 0.29.2 (2026-10-06)
10
+
11
+ - `./navigation`: `ArticlesNavMenu`'s feature image is a CSS background applied once the menu has opened, so a page load no longer fetches it; it stays painted through the close fade (the controller marks a dropdown `data-opened` on its first open), falls back to the 1x file where `image-set()` is unsupported, and is decorative when there is no alt text (SLA-2993).
12
+
5
13
  ## 0.29.1 (2026-10-06)
6
14
 
7
15
  - `./editorial`: `ArticleTopicPage` takes `proseClass`, the overview's prose theme modifier added to `prose` (default `prose-lg`, as before), so a dark site can render the overview in its own prose colours (SLA-2994).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarimarcus/content-sites-ui",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,6 +49,6 @@
49
49
  "@lucide/astro": "^1.23.0",
50
50
  "astro": "^7.0.6",
51
51
  "tailwindcss": "^4.1.18",
52
- "@sarimarcus/content-sites-core": "0.29.1"
52
+ "@sarimarcus/content-sites-core": "0.29.3"
53
53
  }
54
54
  }
@@ -29,16 +29,17 @@ interface Props {
29
29
 
30
30
  const { gtmId, part, prompt = '', consentTypes = [] } = Astro.props;
31
31
  const isProd = import.meta.env.PROD;
32
- const gtmOrigin = GTM_ORIGIN;
32
+ // Absolute and built here: Googlebot crawls a relative '/gtm.js?id=' literal in the inline script as a page (SLA-3001).
33
+ const gtmSrc = `${GTM_ORIGIN}/gtm.js?id=${gtmId}`;
33
34
  const silktideScript = `${SILKTIDE_BASE}/silktide-consent-manager.js`;
34
35
  const silktideConfig = { text: { prompt: { description: prompt } }, consentTypes };
35
36
  ---
36
37
 
37
38
  {isProd && part === 'noscript' && (
38
- <noscript set:html={`<iframe src="${gtmOrigin}/ns.html?id=${gtmId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`} />
39
+ <noscript set:html={`<iframe src="${GTM_ORIGIN}/ns.html?id=${gtmId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`} />
39
40
  )}
40
41
  {isProd && part === 'script' && (
41
- <script is:inline define:vars={{ gtmId, gtmOrigin, silktideScript, silktideConfig }}>
42
+ <script is:inline define:vars={{ gtmSrc, silktideScript, silktideConfig }}>
42
43
  (function () {
43
44
  let thirdPartyLoaded = false;
44
45
 
@@ -66,7 +67,7 @@ const silktideConfig = { text: { prompt: { description: prompt } }, consentTypes
66
67
  // On the main thread: Partytown race-dropped the initial page_view (SLA-2661).
67
68
  var gtmScript = document.createElement('script');
68
69
  gtmScript.async = true;
69
- gtmScript.src = gtmOrigin + '/gtm.js?id=' + gtmId;
70
+ gtmScript.src = gtmSrc;
70
71
  document.head.appendChild(gtmScript);
71
72
 
72
73
  var consentScript = document.createElement('script');
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { normalizePagePath } from '@sarimarcus/content-sites-core/url';
9
9
  import { formatUtcDate } from '@sarimarcus/content-sites-core/dates';
10
- import ResponsiveImage from '../primitives/ResponsiveImage.astro';
10
+ import { getImage } from 'astro:assets';
11
11
  import NavDropdown from './NavDropdown.astro';
12
12
  import NavMenuLink from './NavMenuLink.astro';
13
13
  import MobileNavSection from './MobileNavSection.astro';
@@ -51,6 +51,14 @@ const day = (iso?: string) => formatUtcDate(iso, { day: 'numeric', month: 'short
51
51
  const [first, ...rest] = latest;
52
52
  const feature = first?.image ? first : undefined;
53
53
  const rows = feature ? rest : latest;
54
+ // The feature image is a CSS background that only an open menu applies, so a page load never fetches it
55
+ // (an <img> in the hidden panel would load on every page). A background has no srcset, so this is the one place
56
+ // that picks widths instead of ResponsiveImage; the variant service snaps them to existing files.
57
+ const featureBg = feature?.image
58
+ ? await Promise.all([480, 960].map((width) => getImage({ src: feature.image!, width })))
59
+ : undefined;
60
+ const featureStyle = featureBg
61
+ && `--articles-nav-feature-1x: url("${featureBg[0].src}"); --articles-nav-feature: image-set(url("${featureBg[0].src}") 1x, url("${featureBg[1].src}") 2x)`;
54
62
  ---
55
63
 
56
64
  {variant === 'desktop' ? (
@@ -67,7 +75,7 @@ const rows = feature ? rest : latest;
67
75
  {feature && (
68
76
  <a href={feature.href} aria-current={here(feature.href) ? 'page' : undefined} class:list={[`group block p-2 rounded-lg hover:bg-muted transition-colors ${ring}`, here(feature.href) && 'bg-muted']}>
69
77
  <div class="aspect-video rounded-md overflow-hidden bg-muted">
70
- <ResponsiveImage src={feature.image!} alt={feature.imageAlt ?? ''} sizes="16rem" loading="lazy" decoding="async" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" />
78
+ <div role={feature.imageAlt ? 'img' : undefined} aria-label={feature.imageAlt || undefined} aria-hidden={feature.imageAlt ? undefined : 'true'} class="articles-nav-feature w-full h-full bg-cover bg-center transition-transform duration-300 group-hover:scale-[1.03]" style={featureStyle} />
71
79
  </div>
72
80
  {(feature.eyebrow || feature.date) && (
73
81
  <span class="mt-2 block text-xs">
@@ -115,3 +123,19 @@ const rows = feature ? rest : latest;
115
123
  {topics.map((link) => <MobileNavLink link={link} tone={tone} active={on(link.href)} />)}
116
124
  </MobileNavSection>
117
125
  )}
126
+
127
+ <style>
128
+ /* data-opened is set on the first open and kept; :focus-within covers a keyboard visit before any script runs. */
129
+ :global([data-nav-dropdown][data-opened]) .articles-nav-feature,
130
+ :global(.nav-dropdown:focus-within) .articles-nav-feature {
131
+ background-image: var(--articles-nav-feature, none);
132
+ }
133
+
134
+ /* A var() holding an unsupported image-set() computes to none, so older engines get the 1x file instead. */
135
+ @supports not (background-image: image-set(url('x.webp') 1x)) {
136
+ :global([data-nav-dropdown][data-opened]) .articles-nav-feature,
137
+ :global(.nav-dropdown:focus-within) .articles-nav-feature {
138
+ background-image: var(--articles-nav-feature-1x, none);
139
+ }
140
+ }
141
+ </style>
@@ -17,6 +17,8 @@ const controlled = (el: Element) => {
17
17
  function setDropdown(button: HTMLButtonElement, open: boolean): void {
18
18
  if (open) dropdownButtons().forEach((other) => other !== button && other.setAttribute('aria-expanded', 'false'));
19
19
  button.setAttribute('aria-expanded', String(open));
20
+ // Stays set after the first open, so content deferred until then (ArticlesNavMenu's image) never unpaints mid-fade.
21
+ if (open) button.closest<HTMLElement>('[data-nav-dropdown]')?.setAttribute('data-opened', '');
20
22
  }
21
23
 
22
24
  function setMobileMenu(root: HTMLElement, toggle: HTMLButtonElement, open: boolean, focus: boolean): void {