MainLayout.astro: <script is:inline defer src="/ibo-stats.js"> in head (served by the portal; no-op until Umami is switched on).
portfolio.astro: photo-open event right after the existing cookie-free counter (unchanged).
data-umami-event="instagram-click" on the two Instagram links (footer + start page). No Facebook link is rendered.
No visible change: main vs branch builds differ only by the script tag, the data attributes and the one JS call. Cookie box and Meta Pixel untouched. /privacy: the "Anonym statistik" / "Anonymous statistics" section now covers Umami (text accepted by Linus 2026-11-14, commit b2aab0e); date 14 november 2026. Nothing else on the page changed.
**Merged after Linus's go-ahead 2026-11-14 03:19 (was: go-live needs Linus's "kör" (runbook: `P:\imagesbyolofsson-growth\analytics\2026-11-14-umami-go-live-runbook.md`).**
- `MainLayout.astro`: `<script is:inline defer src="/ibo-stats.js">` in head (served by the portal; no-op until Umami is switched on).
- `portfolio.astro`: `photo-open` event right after the existing cookie-free counter (unchanged).
- `data-umami-event="instagram-click"` on the two Instagram links (footer + start page). No Facebook link is rendered.
No visible change: main vs branch builds differ only by the script tag, the data attributes and the one JS call. Cookie box and Meta Pixel untouched. `/privacy`: the "Anonym statistik" / "Anonymous statistics" section now covers Umami (text accepted by Linus 2026-11-14, commit b2aab0e); date 14 november 2026. Nothing else on the page changed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The line (window as any).iboStats?.track('photo-open', { photo: statsItem(item) }); uses TypeScript‑only syntax (as any). Inline <script> blocks are emitted to the browser as plain JavaScript, so the cast will cause a syntax error and abort the whole script (the photo‑open handler will never run).
Breaks the photo‑open interaction and may also stop the preceding stat('open', …) call, defeating the analytics goal.
Remove the TypeScript cast. Use plain JavaScript, e.g.: if (window.iboStats && typeof window.iboStats.track === 'function') { window.iboStats.track('photo-open', { photo: statsItem(item) }); } or, if you want to keep optional‑chaining, simply: window.iboStats?.track('photo-open', { photo: statsItem(item) });
is:inline tells Astro to inline the script content, but you also provide a src attribute. This combination is contradictory: Astro will try to inline the tag without fetching the external file, resulting in a script tag that has no content and a src that may be ignored, or it may produce a warning/error depending on the Astro version.
The analytics loader (ibo-stats.js) might never be loaded, so all subsequent stat/iboStats calls will be no‑ops.
Remove is:inline when using an external source: <script defer src="/ibo-stats.js"></script> If you need the script inlined (e.g., for CSP), fetch the file at build time and inline its contents instead of using src.
src/layouts/MainLayout.astro – data-umami-event on Instagram link
Adding data-umami-event="instagram-click" is fine, but the link also has target="_blank"withoutrel="noopener noreferrer" in the updated markup? (The original line already had those attributes, and the diff shows they are still present, so this is OK.) No blocking issue here.
Potential global namespace clash
The new script injects a global iboStats object. If another script on the site also defines a global with that name, it could be overwritten.
Could cause unexpected behavior in other parts of the site.
Verify that iboStats is uniquely named (e.g., window.__iboStats) or namespace it inside your own module. This is not a breaking change but worth a quick sanity check.
2. Non‑blocking suggestions
Consistency of optional‑chaining – In portfolio.astro you already call stat('open', …). If iboStats is not loaded yet, the optional‑chaining prevents errors, but you might want to guard the call with a check that the analytics script has finished loading (e.g., listen for load event on the script tag).
Lazy‑load the stats script – Since the stats are only needed after a user interaction (photo open), you could defer loading ibo-stats.js until the first interaction to reduce initial payload.
Add a small comment near the new script tag explaining why it’s placed in the <head> and that it must stay after the SEO/SchemaOrg components (helps future maintainers).
Accessibility – The data-umami-event attribute does not affect accessibility, but ensure that any additional clickable elements (e.g., the Instagram icon) have appropriate aria-labels, which they already do.
3. Tests
Unit / component tests – There are no automated tests covering the new analytics hooks. Consider adding a test that mounts the portfolio page, simulates a click on a gallery item, and asserts that window.iboStats.track is called with the expected payload (mock the global).
End‑to‑end test – In a CI pipeline, run a Cypress/Playwright test that opens a photo and checks that the network request to your analytics endpoint (or a mock) is triggered. This will catch regressions if the script fails to load or the call is syntactically broken.
4. Overall assessment
The PR introduces valuable analytics events (Umami click tracking and photo‑open tracking) but contains two critical implementation errors that will prevent the code from executing:
A TypeScript cast inside a client‑side <script> block (window as any) causing a syntax error.
Misuse of is:inline together with a src attribute on the stats loader script, likely preventing the external script from loading.
Fix those two items (remove the TS cast, correct the script tag) and the changes are safe to merge. Afterwards, consider adding small test coverage for the new analytics hooks.
Automated review by AI Git Bot
## 🤖 AI Code Review
**1. Blocking issues**
| File / Area | Problem | Why it matters | Suggested fix |
|-------------|---------|----------------|---------------|
| **`src/pages/portfolio.astro` – inline `<script>`** | The line ` (window as any).iboStats?.track('photo-open', { photo: statsItem(item) }); ` uses TypeScript‑only syntax (`as any`). Inline `<script>` blocks are emitted to the browser as plain JavaScript, so the cast will cause a **syntax error** and abort the whole script (the photo‑open handler will never run). | Breaks the photo‑open interaction and may also stop the preceding `stat('open', …)` call, defeating the analytics goal. | Remove the TypeScript cast. Use plain JavaScript, e.g.: <br>`if (window.iboStats && typeof window.iboStats.track === 'function') { window.iboStats.track('photo-open', { photo: statsItem(item) }); }` <br>or, if you want to keep optional‑chaining, simply: <br>`window.iboStats?.track('photo-open', { photo: statsItem(item) });` |
| **`src/layouts/MainLayout.astro` – `<script is:inline defer src="/ibo-stats.js">`** | `is:inline` tells Astro to inline the **script content**, but you also provide a `src` attribute. This combination is contradictory: Astro will try to inline the tag *without* fetching the external file, resulting in a script tag that has no content and a `src` that may be ignored, or it may produce a warning/error depending on the Astro version. | The analytics loader (`ibo-stats.js`) might never be loaded, so all subsequent `stat`/`iboStats` calls will be no‑ops. | Remove `is:inline` when using an external source: <br>`<script defer src="/ibo-stats.js"></script>` <br>If you need the script inlined (e.g., for CSP), fetch the file at build time and inline its contents instead of using `src`. |
| **`src/layouts/MainLayout.astro` – `data-umami-event` on Instagram link** | Adding `data-umami-event="instagram-click"` is fine, but the link also has `target="_blank"` **without** `rel="noopener noreferrer"` in the updated markup? (The original line already had those attributes, and the diff shows they are still present, so this is OK.) No blocking issue here. |
| **Potential global namespace clash** | The new script injects a global `iboStats` object. If another script on the site also defines a global with that name, it could be overwritten. | Could cause unexpected behavior in other parts of the site. | Verify that `iboStats` is uniquely named (e.g., `window.__iboStats`) or namespace it inside your own module. This is not a breaking change but worth a quick sanity check. |
**2. Non‑blocking suggestions**
* **Consistency of optional‑chaining** – In `portfolio.astro` you already call `stat('open', …)`. If `iboStats` is not loaded yet, the optional‑chaining prevents errors, but you might want to guard the call with a check that the analytics script has finished loading (e.g., listen for `load` event on the script tag).
* **Lazy‑load the stats script** – Since the stats are only needed after a user interaction (photo open), you could defer loading `ibo-stats.js` until the first interaction to reduce initial payload.
* **Add a small comment** near the new script tag explaining why it’s placed in the `<head>` and that it must stay after the SEO/SchemaOrg components (helps future maintainers).
* **Accessibility** – The `data-umami-event` attribute does not affect accessibility, but ensure that any additional clickable elements (e.g., the Instagram icon) have appropriate `aria-label`s, which they already do.
**3. Tests**
* **Unit / component tests** – There are no automated tests covering the new analytics hooks. Consider adding a test that mounts the `portfolio` page, simulates a click on a gallery item, and asserts that `window.iboStats.track` is called with the expected payload (mock the global).
* **End‑to‑end test** – In a CI pipeline, run a Cypress/Playwright test that opens a photo and checks that the network request to your analytics endpoint (or a mock) is triggered. This will catch regressions if the script fails to load or the call is syntactically broken.
**4. Overall assessment**
The PR introduces valuable analytics events (Umami click tracking and photo‑open tracking) but contains two critical implementation errors that will prevent the code from executing:
1. A TypeScript cast inside a client‑side `<script>` block (`window as any`) causing a syntax error.
2. Misuse of `is:inline` together with a `src` attribute on the stats loader script, likely preventing the external script from loading.
Fix those two items (remove the TS cast, correct the script tag) and the changes are safe to merge. Afterwards, consider adding small test coverage for the new analytics hooks.
---
*Automated review by AI Git Bot*
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Merged after Linus's go-ahead 2026-11-14 03:19 (was: go-live needs Linus's "kör" (runbook:
P:\imagesbyolofsson-growth\analytics\2026-11-14-umami-go-live-runbook.md).MainLayout.astro:<script is:inline defer src="/ibo-stats.js">in head (served by the portal; no-op until Umami is switched on).portfolio.astro:photo-openevent right after the existing cookie-free counter (unchanged).data-umami-event="instagram-click"on the two Instagram links (footer + start page). No Facebook link is rendered.No visible change: main vs branch builds differ only by the script tag, the data attributes and the one JS call. Cookie box and Meta Pixel untouched.
/privacy: the "Anonym statistik" / "Anonymous statistics" section now covers Umami (text accepted by Linus 2026-11-14, commitb2aab0e); date 14 november 2026. Nothing else on the page changed.🤖 Generated with Claude Code
🤖 AI Code Review
1. Blocking issues
src/pages/portfolio.astro– inline<script>(window as any).iboStats?.track('photo-open', { photo: statsItem(item) });uses TypeScript‑only syntax (as any). Inline<script>blocks are emitted to the browser as plain JavaScript, so the cast will cause a syntax error and abort the whole script (the photo‑open handler will never run).stat('open', …)call, defeating the analytics goal.if (window.iboStats && typeof window.iboStats.track === 'function') { window.iboStats.track('photo-open', { photo: statsItem(item) }); }or, if you want to keep optional‑chaining, simply:
window.iboStats?.track('photo-open', { photo: statsItem(item) });src/layouts/MainLayout.astro–<script is:inline defer src="/ibo-stats.js">is:inlinetells Astro to inline the script content, but you also provide asrcattribute. This combination is contradictory: Astro will try to inline the tag without fetching the external file, resulting in a script tag that has no content and asrcthat may be ignored, or it may produce a warning/error depending on the Astro version.ibo-stats.js) might never be loaded, so all subsequentstat/iboStatscalls will be no‑ops.is:inlinewhen using an external source:<script defer src="/ibo-stats.js"></script>If you need the script inlined (e.g., for CSP), fetch the file at build time and inline its contents instead of using
src.src/layouts/MainLayout.astro–data-umami-eventon Instagram linkdata-umami-event="instagram-click"is fine, but the link also hastarget="_blank"withoutrel="noopener noreferrer"in the updated markup? (The original line already had those attributes, and the diff shows they are still present, so this is OK.) No blocking issue here.iboStatsobject. If another script on the site also defines a global with that name, it could be overwritten.iboStatsis uniquely named (e.g.,window.__iboStats) or namespace it inside your own module. This is not a breaking change but worth a quick sanity check.2. Non‑blocking suggestions
portfolio.astroyou already callstat('open', …). IfiboStatsis not loaded yet, the optional‑chaining prevents errors, but you might want to guard the call with a check that the analytics script has finished loading (e.g., listen forloadevent on the script tag).ibo-stats.jsuntil the first interaction to reduce initial payload.<head>and that it must stay after the SEO/SchemaOrg components (helps future maintainers).data-umami-eventattribute does not affect accessibility, but ensure that any additional clickable elements (e.g., the Instagram icon) have appropriatearia-labels, which they already do.3. Tests
portfoliopage, simulates a click on a gallery item, and asserts thatwindow.iboStats.trackis called with the expected payload (mock the global).4. Overall assessment
The PR introduces valuable analytics events (Umami click tracking and photo‑open tracking) but contains two critical implementation errors that will prevent the code from executing:
<script>block (window as any) causing a syntax error.is:inlinetogether with asrcattribute on the stats loader script, likely preventing the external script from loading.Fix those two items (remove the TS cast, correct the script tag) and the changes are safe to merge. Afterwards, consider adding small test coverage for the new analytics hooks.
Automated review by AI Git Bot
WIP: Umami-statistik – skripttagg, photo-open, instagram-click (integritetstext saknas)to WIP: Umami-statistik – skripttagg, photo-open, instagram-click, integritetstext