Skip to content

docs: add cross-site announcement banner#857

Merged
jdx merged 10 commits intomainfrom
feat/site-banner
Apr 23, 2026
Merged

docs: add cross-site announcement banner#857
jdx merged 10 commits intomainfrom
feat/site-banner

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 23, 2026

Summary

  • Adds docs/.vitepress/theme/banner.ts + banner.css \u2014 fetches banner config from https://jdx.dev/banner.json and renders a dismissible announcement bar at the top of the docs
  • Wires it into the existing enhanceApp hook
  • Dismissals persist per banner id in localStorage; bumping the id re-shows it

Used to announce en.dev, and any future cross-site announcements.

Test plan

  • Run docs dev server, confirm banner appears at top of page
  • Click the \u00d7 \u2014 banner disappears and stays dismissed across reloads
  • Clear localStorage jdx-banner-dismissed, reload \u2014 banner returns

\U0001F916 Generated with Claude Code


Note

Medium Risk
Adds client-side DOM injection that fetches remote banner content and persists dismissal in localStorage, which can affect layout and introduces a new external dependency for docs pages.

Overview
Introduces a cross-site, dismissible announcement banner for the docs theme: initBanner() fetches config from https://jdx.dev/banner.json, renders a fixed top bar with optional outbound link, adjusts --vp-layout-top-height, and stores per-banner dismissals in localStorage.

Adds a new EndevFooter component and wires it into Layout.vue via the layout-bottom slot to display license/copyright and an en.dev link.

Reviewed by Cursor Bugbot for commit 622e8db. Bugbot is set up for automated code reviews on this repo. Configure here.

Fetches banner config from https://jdx.dev/banner.json and renders a
dismissible top-of-page announcement. Dismissals persist per banner id
in localStorage, so bumping the id re-shows it.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds a dismissible cross-site announcement banner that fetches config from https://jdx.dev/banner.json and a new EndevFooter component wired into the VitePress layout-bottom slot. Two of the four previously-flagged concerns have been addressed (protocol validation via isHttpUrl and responsive height tracking via ResizeObserver), but two earlier P1 findings — rel="noopener noreferrer" on the _blank link and the unguarded localStorage.setItem in the dismiss click handler — remain open in the current diff.

Confidence Score: 4/5

Safe to merge after addressing two open findings from the prior review round still visible in the diff.

Good progress on the prior review — protocol injection and ResizeObserver concerns are resolved. Two P1 findings from earlier rounds (missing noreferrer, unguarded localStorage.setItem in the click handler) are still present in the code. No new P1/P0 issues identified in this pass.

docs/.vitepress/theme/banner.ts — the dismiss click handler and the anchor rel attribute.

Important Files Changed

Filename Overview
docs/.vitepress/theme/banner.ts Core banner logic: fetches remote config, validates links with isHttpUrl, renders dismissible DOM element, syncs --vp-layout-top-height via ResizeObserver. Two previously-flagged issues (noreferrer, localStorage.setItem not wrapped in try/catch) remain unaddressed.
docs/.vitepress/theme/EndevFooter.vue New footer component with copyright year and en.dev branding. Year computed at render time; same-tab link with no target="_blank" so no rel attribute needed. Straightforward addition.
docs/.vitepress/theme/Layout.vue Removes stale comments, adds EndevFooter via layout-bottom slot. Clean change.
docs/.vitepress/theme/banner.css Styles for fixed-position banner with responsive breakpoint at 640px. z-index 1001 safely sits above VitePress nav. No issues.
docs/.vitepress/theme/index.ts Wires initBanner() into enhanceApp; SSR guard in initBanner handles the non-browser context correctly.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant VitePress as VitePress (enhanceApp)
    participant BannerTS as banner.ts
    participant Endpoint as jdx.dev/banner.json
    participant LocalStorage

    Browser->>VitePress: Page load / hydration
    VitePress->>BannerTS: initBanner()
    BannerTS->>Endpoint: fetch(ENDPOINT)
    Endpoint-->>BannerTS: BannerData { id, enabled, message, link }
    BannerTS->>LocalStorage: getItem("jdx-banner-dismissed")
    LocalStorage-->>BannerTS: stored id or null
    alt not dismissed & enabled
        BannerTS->>Browser: render(b) → prepend .jdx-banner to body
        BannerTS->>Browser: requestAnimationFrame(syncHeight)
        BannerTS->>Browser: ResizeObserver.observe(el)
        Browser-->>BannerTS: User clicks ×
        BannerTS->>LocalStorage: setItem("jdx-banner-dismissed", b.id)
        BannerTS->>Browser: el.remove() + removeProperty(--vp-layout-top-height)
    else dismissed or disabled
        BannerTS-->>Browser: no-op
    end
Loading

Reviews (9): Last reviewed commit: "docs: keep --vp-layout-top-height in syn..." | Re-trigger Greptile

Comment thread docs/.vitepress/theme/banner.ts Outdated
Comment thread docs/.vitepress/theme/banner.ts Outdated
Comment thread docs/.vitepress/theme/banner.ts Outdated
Only allow http(s): links from the upstream JSON. Defense-in-depth
against a compromised jdx.dev injecting a javascript: URL that would
execute arbitrary code in the docs origin on click.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a dynamic announcement banner system for the documentation site. It includes a new CSS file for styling, a TypeScript module to fetch banner data from a remote endpoint and manage its display, and integration into the VitePress theme initialization. The banner supports a dismissal feature that persists via localStorage. The review feedback suggests adding error handling around localStorage access to prevent potential runtime errors in environments where storage is blocked or full.

.then((r) => (r.ok ? (r.json() as Promise<BannerData>) : null))
.then((b) => {
if (!b || !b.enabled) return;
if (localStorage.getItem(STORAGE_KEY) === b.id) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing localStorage can throw an error in some environments (e.g., if cookies/storage are blocked or in certain private browsing modes). It is safer to wrap this check in a try-catch block to ensure the banner logic doesn't crash the application initialization.

      try {
        if (localStorage.getItem(STORAGE_KEY) === b.id) return
      } catch (e) {
        // ignore storage errors
      }

a.href = b.link;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.textContent = b.linkText || "Learn more";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the retrieval, setting an item in localStorage can throw an error if storage is disabled or full. Wrapping this in a try-catch ensures the dismissal functionality fails gracefully without impacting the rest of the UI.

    try {
      localStorage.setItem(STORAGE_KEY, b.id)
    } catch (e) {
      // ignore storage errors
    }

Comment thread docs/.vitepress/theme/banner.css Outdated
Banner was using position: relative which put it in document flow
*and* VitePress applies --vp-layout-top-height offset, causing content
to be pushed down twice. Switch to position: fixed so the banner is
out of flow and --vp-layout-top-height alone handles the content
offset (which is what VitePress's layout-top slot assumes).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Comment thread docs/.vitepress/theme/banner.css Outdated
jdx and others added 3 commits April 23, 2026 13:46
- Bump z-index to 1001 so the banner sits above custom nav overrides
  (e.g. hk's .VPNav at z-index: 1000 !important).
- Use dark text on light brand backgrounds (coral/pink/gold/cyan) for
  readable contrast. Sites with dark brand backgrounds keep white text.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds a small footer with license, copyright, and link back to en.dev,
matching the footer used on the mise docs. Rendered via VitePress's
layout-bottom slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Center message+link in the banner, position dismiss button absolutely
  at the right edge so it doesn't skew the centering.
- Drop rel=noreferrer on the link so en.dev gets analytics attribution
  for traffic from the docs. Keep rel=noopener for security.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Comment thread docs/.vitepress/theme/banner.ts Outdated
jdx and others added 3 commits April 23, 2026 14:00
Left/right padding was asymmetric (1rem vs 2.75rem desktop; 0.75rem vs
2.5rem mobile), which shifted the "centered" text off from true viewport
center. Match the sides so justify-content: center lines up with the
viewport midpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
no-cache forces a conditional GET on every page load. The server sends
Cache-Control: public, max-age=300, must-revalidate, so default browser
caching already gives 5-min freshness, which is plenty for an
announcement banner. Returning users with a dismissed banner also
already short-circuit via localStorage before the fetch runs anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The previous fallback (#3451b2, VitePress default dark blue) made black
banner text unreadable if --vp-c-brand-1 ever fails to resolve. Use a
fallback that matches this site's actual brand color so black text
stays readable in the fallback scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Comment thread docs/.vitepress/theme/banner.ts Outdated
The height CSS variable was set once in a single rAF and never updated
afterward. On window resize, orientation change, or text reflow the
banner height could change, leaving the VitePress nav offset stale and
causing overlap or a visible gap. Observe the banner element and
resync the variable whenever its size changes; disconnect the observer
when the banner is dismissed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 622e8db. Configure here.

observer?.disconnect();
el.remove();
document.documentElement.style.removeProperty("--vp-layout-top-height");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled localStorage.setItem exception blocks banner dismissal

Low Severity

The dismiss button's click handler calls localStorage.setItem as its first statement. If this throws (e.g., QuotaExceededError on a full storage, or storage disabled in certain browsing contexts), the remaining cleanup — observer?.disconnect(), el.remove(), and removeProperty("--vp-layout-top-height") — never executes. This leaves the banner permanently visible and undismissable for the session, since every click re-throws. Wrapping the setItem in a try-catch (or moving it after el.remove()) would let dismissal still work even when persistence fails.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 622e8db. Configure here.

@jdx jdx merged commit b834ca7 into main Apr 23, 2026
21 checks passed
@jdx jdx deleted the feat/site-banner branch April 23, 2026 19:49
@jdx jdx mentioned this pull request Apr 23, 2026
jdx added a commit that referenced this pull request Apr 24, 2026
### 🐛 Bug Fixes

- **(git)** skip untracked scan when HK_STASH_UNTRACKED=false by
[@jdx](https://github.com/jdx) in
[#861](#861)
- **(run)** add post-commit and pre-rebase subcommands by
[@jdx](https://github.com/jdx) in
[#858](#858)

### 📚 Documentation

- **(install)** recommend global hooks as primary setup path by
[@jdx](https://github.com/jdx) in
[#855](#855)
- add cross-site announcement banner by [@jdx](https://github.com/jdx)
in [#857](#857)
- respect banner expires field by [@jdx](https://github.com/jdx) in
[#862](#862)

### 🔍 Other Changes

- vendor bats test helpers instead of git submodules by
[@jdx](https://github.com/jdx) in
[#859](#859)

### 📦️ Dependency Updates

- bump communique to 1.0.3 by [@jdx](https://github.com/jdx) in
[#863](#863)
- update anthropics/claude-code-action digest to e58dfa5 by
[@renovate[bot]](https://github.com/renovate[bot]) in
[#864](#864)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk release bookkeeping only: version numbers, generated
CLI/docs, and lockfile dependency bumps with no runtime logic changes.
> 
> **Overview**
> **Release prep for `v1.44.1`.** Updates `Cargo.toml`/`Cargo.lock`
(including `libc`) and all version references in generated CLI docs
(`docs/cli/*`, `hk.usage.kdl`).
> 
> Refreshes documentation examples to reference the new `v1.44.1` Pkl
package URLs and adds the `1.44.1` entry to `CHANGELOG.md`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d7637d4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: mise-en-dev <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant