blog umami fix
This commit is contained in:
2
openspec/changes/blog-umami-fix/.openspec.yaml
Normal file
2
openspec/changes/blog-umami-fix/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-10
|
||||||
44
openspec/changes/blog-umami-fix/design.md
Normal file
44
openspec/changes/blog-umami-fix/design.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The site uses Umami for analytics. Most site clickables are instrumented using Umami’s data-attribute method (`data-umami-event` and optional `data-umami-event-*` properties) so events are recorded automatically on click.
|
||||||
|
|
||||||
|
The Blog section was added recently and its clickables (post cards, category nav, page links) are not consistently emitting Umami events. This creates a measurement blind spot for the `/blog` surface.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Ensure all blog clickables emit Umami events using the documented data-attribute method.
|
||||||
|
- Ensure every tracked clickable has a deterministic, unique `target_id` and includes at minimum `placement` and `target_url` per taxonomy.
|
||||||
|
- Keep event names within Umami limits (<= 50 chars) and avoid sending event data without an event name.
|
||||||
|
- Add tests to prevent regressions (blog pages/components should contain required Umami attributes).
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Introducing custom JavaScript tracking (`window.umami.track`) for v1; we will use Umami’s data-attribute method.
|
||||||
|
- Adding new analytics providers or changing Umami initialization.
|
||||||
|
- Tracking PII or user-generated content in event properties.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- **Decision: Use Umami-native data attributes on every blog clickable.**
|
||||||
|
- Rationale: Aligns with Umami’s “Track events” docs and the rest of the site’s tracking approach; avoids adding JS listeners that can interfere with other handlers.
|
||||||
|
|
||||||
|
- **Decision: Use consistent event names by clickable type.**
|
||||||
|
- Rationale: Keeps reporting clean while still allowing segmentation via event properties.
|
||||||
|
- Proposed:
|
||||||
|
- `click` for internal navigation links (including blog category navigation)
|
||||||
|
- `outbound_click` for external links (if any in blog chrome)
|
||||||
|
|
||||||
|
- **Decision: Add a deterministic `target_id` namespace for blog elements.**
|
||||||
|
- Rationale: Blog has many repeated elements; we need unique IDs that remain stable across builds.
|
||||||
|
- Proposed conventions:
|
||||||
|
- Blog header link: `nav.blog`
|
||||||
|
- Blog secondary nav: `blog.subnav.all`, `blog.subnav.pages`, `blog.subnav.category.<slug>`
|
||||||
|
- Blog post card: `blog.card.post.<slug>` (placement `blog.index` or `blog.category.<slug>`)
|
||||||
|
- Blog post detail back link: `blog.post.back`
|
||||||
|
- Blog page list links: `blog.pages.link.<slug>`
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Risk] Some blog content areas render raw HTML from WordPress; links inside content are not instrumented. -> Mitigation: Track the blog chrome (cards/nav/back links) first; consider JS-based delegated tracking for content-body links in a future change if needed.
|
||||||
|
- [Risk] Over-instrumentation adds noisy events. -> Mitigation: Keep event names simple, rely on `target_id` + `placement` for segmentation, and avoid tracking non-clickable elements.
|
||||||
|
|
||||||
26
openspec/changes/blog-umami-fix/proposal.md
Normal file
26
openspec/changes/blog-umami-fix/proposal.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The Blog section’s click tracking is not firing reliably in Umami, which prevents measuring what users do in `/blog` and where they go next.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Update the Blog section UI so every clickable element uses Umami’s data-attribute event tracking format:
|
||||||
|
- `data-umami-event="<event-name>"`
|
||||||
|
- `data-umami-event-*` attributes for event data
|
||||||
|
- Ensure every tracked clickable item has a unique, deterministic set of event data elements (especially `target_id`, `placement`, `target_url`) so clicks can be measured independently.
|
||||||
|
- Add verification/tests to ensure Blog clickables are instrumented and follow the same taxonomy as the rest of the site.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- (none)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `blog-section-surface`: instrument blog clickables (post cards, post/page links, category secondary nav, blog header link) using Umami `data-umami-event` attributes.
|
||||||
|
- `interaction-tracking-taxonomy`: extend/clarify tracking rules to cover blog-specific UI elements and namespaces for `target_id`.
|
||||||
|
- `analytics-umami`: ensure the implementation adheres to Umami’s Track Events specification for data attributes.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected UI/components: blog pages and components under `site/src/pages/blog/` and `site/src/components/` (cards and secondary nav), plus any shared navigation link to `/blog`.
|
||||||
|
- Testing: add/update tests to assert required Umami data attributes exist and are unique per clickable element in blog surfaces.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Custom event tracking
|
||||||
|
When Umami is enabled, the site MUST support custom event emission for:
|
||||||
|
- `cta_click`
|
||||||
|
- `outbound_click`
|
||||||
|
- a general click interaction event for all instrumented clickable items (per the site tracking taxonomy)
|
||||||
|
|
||||||
|
Each emitted event MUST include enough properties to segment reports by platform and placement when applicable.
|
||||||
|
|
||||||
|
All tracked clickable items MUST emit events with a unique, consistent set of data elements as defined by the site tracking taxonomy, including at minimum `target_id` and `placement`.
|
||||||
|
|
||||||
|
The site MUST instrument tracked clickables using Umami’s supported Track Events data-attribute method:
|
||||||
|
- `data-umami-event="<event-name>"`
|
||||||
|
- optional event data using `data-umami-event-*`
|
||||||
|
|
||||||
|
#### Scenario: Emit outbound click event
|
||||||
|
- **WHEN** a user clicks a non-CTA outbound link from the homepage
|
||||||
|
- **THEN** the system emits an `outbound_click` event with a property identifying the destination domain
|
||||||
|
|
||||||
|
#### Scenario: Emit general click event for any clickable
|
||||||
|
- **WHEN** a user clicks an instrumented navigation link
|
||||||
|
- **THEN** the system emits a click interaction event with `target_id` and `placement`
|
||||||
|
|
||||||
|
#### Scenario: Uninstrumented clicks do not break the page
|
||||||
|
- **WHEN** a user clicks an element with no tracking metadata
|
||||||
|
- **THEN** the system does not throw and navigation/interaction proceeds normally
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Blog index listing (posts)
|
||||||
|
The site MUST provide a blog index page at `/blog` that lists WordPress posts as cards containing:
|
||||||
|
- featured image (when available)
|
||||||
|
- title
|
||||||
|
- excerpt/summary
|
||||||
|
|
||||||
|
The listing MUST be ordered by publish date descending (newest first).
|
||||||
|
|
||||||
|
Each post card MUST be instrumented with Umami Track Events data attributes and MUST include at minimum:
|
||||||
|
- `data-umami-event`
|
||||||
|
- `data-umami-event-target_id`
|
||||||
|
- `data-umami-event-placement`
|
||||||
|
- `data-umami-event-target_url`
|
||||||
|
|
||||||
|
#### Scenario: Blog index lists posts
|
||||||
|
- **WHEN** the cached WordPress dataset contains posts
|
||||||
|
- **THEN** `/blog` renders a list of post cards ordered by publish date descending
|
||||||
|
|
||||||
|
#### Scenario: Blog post card click is tracked
|
||||||
|
- **WHEN** a user clicks a blog post card on `/blog`
|
||||||
|
- **THEN** the click emits an Umami event with `target_id`, `placement`, and `target_url`
|
||||||
|
|
||||||
|
### Requirement: Category-based secondary navigation
|
||||||
|
The blog section MUST render a secondary navigation under the header derived from the cached WordPress categories.
|
||||||
|
|
||||||
|
Selecting a category MUST navigate to a category listing page showing only posts in that category.
|
||||||
|
|
||||||
|
Each secondary navigation link MUST be instrumented with Umami Track Events data attributes and MUST include at minimum:
|
||||||
|
- `data-umami-event`
|
||||||
|
- `data-umami-event-target_id`
|
||||||
|
- `data-umami-event-placement`
|
||||||
|
- `data-umami-event-target_url`
|
||||||
|
|
||||||
|
#### Scenario: Category nav present
|
||||||
|
- **WHEN** the cached WordPress dataset contains categories
|
||||||
|
- **THEN** the blog section shows a secondary navigation with those categories
|
||||||
|
|
||||||
|
#### Scenario: Category listing filters posts
|
||||||
|
- **WHEN** a user navigates to a category listing page
|
||||||
|
- **THEN** only posts assigned to that category are listed
|
||||||
|
|
||||||
|
#### Scenario: Category nav click is tracked
|
||||||
|
- **WHEN** a user clicks a category link in the blog secondary navigation
|
||||||
|
- **THEN** the click emits an Umami event with `target_id`, `placement`, and `target_url`
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Unique identifier for every clickable item
|
||||||
|
Every clickable item that is tracked MUST have a stable identifier (`target_id`) that is unique across the site (or unique within a documented namespace).
|
||||||
|
|
||||||
|
The identifier MUST be deterministic across builds for the same element and placement.
|
||||||
|
|
||||||
|
The taxonomy MUST define namespaces for repeated UI surfaces. For the blog surface, the following namespaces MUST be used:
|
||||||
|
- `blog.subnav.*` for secondary navigation links
|
||||||
|
- `blog.card.post.<slug>` for blog post cards
|
||||||
|
- `blog.pages.link.<slug>` for blog page listing links
|
||||||
|
- `blog.post.*` / `blog.page.*` for detail page chrome links (e.g., back links)
|
||||||
|
|
||||||
|
#### Scenario: Two links in different placements
|
||||||
|
- **WHEN** two links point to the same destination but appear in different placements
|
||||||
|
- **THEN** their `target_id` values are different so their clicks can be measured independently
|
||||||
|
|
||||||
15
openspec/changes/blog-umami-fix/tasks.md
Normal file
15
openspec/changes/blog-umami-fix/tasks.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## 1. Audit Blog Clickables
|
||||||
|
|
||||||
|
- [x] 1.1 Inventory blog clickables (`site/src/pages/blog/**`, `site/src/components/Blog*`) that should emit Umami events (post cards, category subnav, pages list links, detail chrome links)
|
||||||
|
- [x] 1.2 Confirm each clickable has the required Umami attributes and a deterministic unique `target_id` per taxonomy
|
||||||
|
|
||||||
|
## 2. Implement Umami Attributes
|
||||||
|
|
||||||
|
- [x] 2.1 Instrument blog secondary navigation links with `data-umami-event` and required event data (`target_id`, `placement`, `target_url`)
|
||||||
|
- [x] 2.2 Instrument blog post cards and any inline links in listing UIs with `data-umami-event` and required event data
|
||||||
|
- [x] 2.3 Instrument blog detail page chrome links (e.g., Back) and pages listing links with required Umami attributes
|
||||||
|
|
||||||
|
## 3. Verify
|
||||||
|
|
||||||
|
- [x] 3.1 Add/update tests to assert blog components/pages contain Umami `data-umami-event` attributes (and key properties like `target_id`, `placement`, `target_url`)
|
||||||
|
- [x] 3.2 Build the site and confirm `/blog` and a blog detail page render with instrumented clickables
|
||||||
@@ -3,9 +3,11 @@ import type { WordpressPost } from "../lib/content/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: WordpressPost;
|
post: WordpressPost;
|
||||||
|
placement: string;
|
||||||
|
targetId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { post } = Astro.props;
|
const { post, placement, targetId } = Astro.props;
|
||||||
|
|
||||||
function truncate(s: string, n: number) {
|
function truncate(s: string, n: number) {
|
||||||
if (!s) return "";
|
if (!s) return "";
|
||||||
@@ -15,11 +17,17 @@ function truncate(s: string, n: number) {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<a class="blog-card" href={`/blog/post/${post.slug}`}>
|
<a
|
||||||
|
class="blog-card"
|
||||||
|
href={`/blog/post/${post.slug}`}
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id={targetId}
|
||||||
|
data-umami-event-placement={placement}
|
||||||
|
data-umami-event-target_url={`/blog/post/${post.slug}`}
|
||||||
|
>
|
||||||
{post.featuredImageUrl ? <img src={post.featuredImageUrl} alt="" loading="lazy" /> : null}
|
{post.featuredImageUrl ? <img src={post.featuredImageUrl} alt="" loading="lazy" /> : null}
|
||||||
<div class="blog-card-body">
|
<div class="blog-card-body">
|
||||||
<h3 class="blog-card-title">{post.title}</h3>
|
<h3 class="blog-card-title">{post.title}</h3>
|
||||||
<p class="blog-card-excerpt">{truncate(post.excerpt || "", 180)}</p>
|
<p class="blog-card-excerpt">{truncate(post.excerpt || "", 180)}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,36 @@ const { categories, activeCategorySlug } = Astro.props;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<nav class="subnav" aria-label="Blog categories">
|
<nav class="subnav" aria-label="Blog categories">
|
||||||
<a class={!activeCategorySlug ? "active" : ""} href="/blog">
|
<a
|
||||||
|
class={!activeCategorySlug ? "active" : ""}
|
||||||
|
href="/blog"
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id="blog.subnav.all"
|
||||||
|
data-umami-event-placement="blog.subnav"
|
||||||
|
data-umami-event-target_url="/blog"
|
||||||
|
>
|
||||||
All
|
All
|
||||||
</a>
|
</a>
|
||||||
<a class={activeCategorySlug === "__pages" ? "active" : ""} href="/blog/pages">
|
<a
|
||||||
|
class={activeCategorySlug === "__pages" ? "active" : ""}
|
||||||
|
href="/blog/pages"
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id="blog.subnav.pages"
|
||||||
|
data-umami-event-placement="blog.subnav"
|
||||||
|
data-umami-event-target_url="/blog/pages"
|
||||||
|
>
|
||||||
Pages
|
Pages
|
||||||
</a>
|
</a>
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<a class={activeCategorySlug === c.slug ? "active" : ""} href={`/blog/category/${c.slug}`}>
|
<a
|
||||||
|
class={activeCategorySlug === c.slug ? "active" : ""}
|
||||||
|
href={`/blog/category/${c.slug}`}
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id={`blog.subnav.category.${c.slug}`}
|
||||||
|
data-umami-event-placement="blog.subnav"
|
||||||
|
data-umami-event-target_url={`/blog/category/${c.slug}`}
|
||||||
|
>
|
||||||
{c.name}
|
{c.name}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ if (!activeCategory) {
|
|||||||
{posts.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
<div class="blog-grid">
|
<div class="blog-grid">
|
||||||
{posts.map((p) => (
|
{posts.map((p) => (
|
||||||
<BlogPostCard post={p} />
|
<BlogPostCard
|
||||||
|
post={p}
|
||||||
|
placement={`blog.category.${activeCategory.slug}`}
|
||||||
|
targetId={`blog.category.${activeCategory.slug}.card.post.${p.slug}`}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const pages = wordpressPages(cache);
|
|||||||
{posts.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
<div class="blog-grid">
|
<div class="blog-grid">
|
||||||
{posts.map((p) => (
|
{posts.map((p) => (
|
||||||
<BlogPostCard post={p} />
|
<BlogPostCard post={p} placement="blog.index" targetId={`blog.index.card.post.${p.slug}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -37,18 +37,32 @@ const pages = wordpressPages(cache);
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>Pages</h2>
|
<h2>Pages</h2>
|
||||||
<a class="muted" href="/blog/pages">
|
<a
|
||||||
|
class="muted"
|
||||||
|
href="/blog/pages"
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id="blog.index.pages.browse"
|
||||||
|
data-umami-event-placement="blog.index.pages_preview"
|
||||||
|
data-umami-event-target_url="/blog/pages"
|
||||||
|
>
|
||||||
Browse pages →
|
Browse pages →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
{pages.slice(0, 6).map((p) => (
|
{pages.slice(0, 6).map((p) => (
|
||||||
<div>
|
<div>
|
||||||
<a href={`/blog/page/${p.slug}`}>{p.title}</a>
|
<a
|
||||||
|
href={`/blog/page/${p.slug}`}
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id={`blog.index.pages.link.${p.slug}`}
|
||||||
|
data-umami-event-placement="blog.index.pages_preview"
|
||||||
|
data-umami-event-target_url={`/blog/page/${p.slug}`}
|
||||||
|
>
|
||||||
|
{p.title}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,16 @@ const metaDescription = (page.excerpt || "").slice(0, 160);
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2 style="margin: 0;">{page.title}</h2>
|
<h2 style="margin: 0;">{page.title}</h2>
|
||||||
<a class="muted" href="/blog">Back →</a>
|
<a
|
||||||
|
class="muted"
|
||||||
|
href="/blog"
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id="blog.page.back"
|
||||||
|
data-umami-event-placement="blog.page"
|
||||||
|
data-umami-event-target_url="/blog"
|
||||||
|
>
|
||||||
|
Back →
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{page.featuredImageUrl ? (
|
{page.featuredImageUrl ? (
|
||||||
<img
|
<img
|
||||||
@@ -46,4 +55,3 @@ const metaDescription = (page.excerpt || "").slice(0, 160);
|
|||||||
<div class="prose" set:html={page.contentHtml} />
|
<div class="prose" set:html={page.contentHtml} />
|
||||||
</section>
|
</section>
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,15 @@ const pages = wordpressPages(cache);
|
|||||||
<div class="empty">
|
<div class="empty">
|
||||||
{pages.map((p) => (
|
{pages.map((p) => (
|
||||||
<div style="padding: 6px 0;">
|
<div style="padding: 6px 0;">
|
||||||
<a href={`/blog/page/${p.slug}`}>{p.title}</a>
|
<a
|
||||||
|
href={`/blog/page/${p.slug}`}
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id={`blog.pages.link.${p.slug}`}
|
||||||
|
data-umami-event-placement="blog.pages.list"
|
||||||
|
data-umami-event-target_url={`/blog/page/${p.slug}`}
|
||||||
|
>
|
||||||
|
{p.title}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -34,4 +42,3 @@ const pages = wordpressPages(cache);
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,16 @@ const metaDescription = (post.excerpt || "").slice(0, 160);
|
|||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2 style="margin: 0;">{post.title}</h2>
|
<h2 style="margin: 0;">{post.title}</h2>
|
||||||
<a class="muted" href="/blog">Back →</a>
|
<a
|
||||||
|
class="muted"
|
||||||
|
href="/blog"
|
||||||
|
data-umami-event="click"
|
||||||
|
data-umami-event-target_id="blog.post.back"
|
||||||
|
data-umami-event-placement="blog.post"
|
||||||
|
data-umami-event-target_url="/blog"
|
||||||
|
>
|
||||||
|
Back →
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted" style="margin-top: 0;">
|
<p class="muted" style="margin-top: 0;">
|
||||||
{new Date(post.publishedAt).toLocaleDateString()}
|
{new Date(post.publishedAt).toLocaleDateString()}
|
||||||
@@ -49,4 +58,3 @@ const metaDescription = (post.excerpt || "").slice(0, 160);
|
|||||||
<div class="prose" set:html={post.contentHtml} />
|
<div class="prose" set:html={post.contentHtml} />
|
||||||
</section>
|
</section>
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
64
site/tests/blog-umami-attributes.test.ts
Normal file
64
site/tests/blog-umami-attributes.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
async function read(rel: string) {
|
||||||
|
return await readFile(path.join(process.cwd(), rel), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("blog umami event attributes", () => {
|
||||||
|
it("instruments blog secondary nav links", async () => {
|
||||||
|
const src = await read("src/components/BlogSecondaryNav.astro");
|
||||||
|
|
||||||
|
expect(src).toContain('data-umami-event="click"');
|
||||||
|
expect(src).toContain('data-umami-event-target_id="blog.subnav.all"');
|
||||||
|
expect(src).toContain('data-umami-event-target_id="blog.subnav.pages"');
|
||||||
|
expect(src).toContain("data-umami-event-target_id={`blog.subnav.category.${c.slug}`}");
|
||||||
|
expect(src).toContain('data-umami-event-placement="blog.subnav"');
|
||||||
|
expect(src).toContain("data-umami-event-target_url");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instruments blog post cards with deterministic target_id and placement", async () => {
|
||||||
|
const src = await read("src/components/BlogPostCard.astro");
|
||||||
|
|
||||||
|
expect(src).toContain('data-umami-event="click"');
|
||||||
|
expect(src).toContain("data-umami-event-target_id={targetId}");
|
||||||
|
expect(src).toContain("data-umami-event-placement={placement}");
|
||||||
|
expect(src).toContain("data-umami-event-target_url");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instruments blog pages list links (and keeps distinct IDs per placement)", async () => {
|
||||||
|
const indexSrc = await read("src/pages/blog/index.astro");
|
||||||
|
expect(indexSrc).toContain('data-umami-event-target_id="blog.index.pages.browse"');
|
||||||
|
expect(indexSrc).toContain("data-umami-event-target_id={`blog.index.pages.link.${p.slug}`}");
|
||||||
|
expect(indexSrc).toContain('data-umami-event-placement="blog.index.pages_preview"');
|
||||||
|
|
||||||
|
const pagesSrc = await read("src/pages/blog/pages.astro");
|
||||||
|
expect(pagesSrc).toContain("data-umami-event-target_id={`blog.pages.link.${p.slug}`}");
|
||||||
|
expect(pagesSrc).toContain('data-umami-event-placement="blog.pages.list"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instruments blog detail back links", async () => {
|
||||||
|
const postSrc = await read("src/pages/blog/post/[slug].astro");
|
||||||
|
expect(postSrc).toContain('data-umami-event-target_id="blog.post.back"');
|
||||||
|
expect(postSrc).toContain('data-umami-event-placement="blog.post"');
|
||||||
|
expect(postSrc).toContain('data-umami-event-target_url="/blog"');
|
||||||
|
|
||||||
|
const pageSrc = await read("src/pages/blog/page/[slug].astro");
|
||||||
|
expect(pageSrc).toContain('data-umami-event-target_id="blog.page.back"');
|
||||||
|
expect(pageSrc).toContain('data-umami-event-placement="blog.page"');
|
||||||
|
expect(pageSrc).toContain('data-umami-event-target_url="/blog"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses placement-specific target_id for post cards", async () => {
|
||||||
|
const indexSrc = await read("src/pages/blog/index.astro");
|
||||||
|
expect(indexSrc).toContain("targetId={`blog.index.card.post.${p.slug}`}");
|
||||||
|
expect(indexSrc).toContain('placement="blog.index"');
|
||||||
|
|
||||||
|
const categorySrc = await read("src/pages/blog/category/[slug].astro");
|
||||||
|
expect(categorySrc).toContain("placement={`blog.category.${activeCategory.slug}`}");
|
||||||
|
expect(categorySrc).toContain("targetId={`blog.category.${activeCategory.slug}.card.post.${p.slug}`}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user