Skip to content

Commit 4b4baf8

Browse files
committed
fix: remove <base> tag, restore url() helper for clean link handling
The <base> tag broke fragment links (#anchor resolved to root page instead of current page) and required workarounds. Revert to the url() helper which prefixes paths with basePath at render time. Fragment links (#tokens, #main) now work correctly on all pages. Skip link works on every page without JavaScript hacks. Add side-nav web component with IntersectionObserver to highlight the current section in the design system page as the user scrolls.
1 parent a7c40e7 commit 4b4baf8

18 files changed

Lines changed: 140 additions & 55 deletions

File tree

src/build/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ export function getBasePath(raw: string | undefined): string {
44
return `/${trimmed}/`
55
}
66

7+
export function url(path: string, basePath: string): string {
8+
const normalised = path.startsWith('/') ? path.slice(1) : path
9+
return basePath + normalised
10+
}
11+
712
export type SiteConfig = {
813
basePath: string
914
buildTime: string

src/design/common/layout.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Child } from 'hono/jsx'
22
import { Header } from '../components/header'
33
import { Footer } from '../components/footer'
4+
import { url } from '../../build/config'
45
import type { SiteConfig } from '../../build/config'
56

67
export function Layout({
@@ -18,20 +19,19 @@ export function Layout({
1819
<head>
1920
<meta charset="UTF-8" />
2021
<meta name="viewport" content="width=device-width, initial-scale=1" />
21-
<base href={config.basePath} />
2222
<title>{documentTitle}</title>
23-
<link rel="stylesheet" href="design/index.css" />
24-
<link rel="icon" href="assets/favicon-32x32.png" sizes="32x32" type="image/png" />
25-
<link rel="icon" href="assets/favicon-192x192.png" sizes="192x192" type="image/png" />
26-
<link rel="apple-touch-icon" href="assets/apple-touch-icon.png" />
27-
<meta name="msapplication-TileImage" content="assets/mstile-270x270.png" />
28-
<script type="module" src="enhancements/register.js" defer></script>
23+
<link rel="stylesheet" href={url('/design/index.css', config.basePath)} />
24+
<link rel="icon" href={url('/assets/favicon-32x32.png', config.basePath)} sizes="32x32" type="image/png" />
25+
<link rel="icon" href={url('/assets/favicon-192x192.png', config.basePath)} sizes="192x192" type="image/png" />
26+
<link rel="apple-touch-icon" href={url('/assets/apple-touch-icon.png', config.basePath)} />
27+
<meta name="msapplication-TileImage" content={url('/assets/mstile-270x270.png', config.basePath)} />
28+
<script type="module" src={url('/enhancements/register.js', config.basePath)} defer></script>
2929
</head>
3030
<body>
3131
<a href="#main" class="skip-link">Skip to main content</a>
32-
<Header />
32+
<Header config={config} />
3333
<main id="main">{children}</main>
34-
<Footer buildTime={config.buildTime} />
34+
<Footer config={config} />
3535
</body>
3636
</html>
3737
)

src/design/components/footer/examples.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function FooterExamples() {
66
<h2>Footer</h2>
77
<p>Site-wide footer with navigation links and build timestamp. Dark background (midnight) with sky-colored links for AAA contrast.</p>
88
<div style="border-radius: var(--radius-md); overflow: hidden;">
9-
<Footer buildTime={new Date().toISOString()} />
9+
<Footer config={{ basePath: '/', buildTime: new Date().toISOString() }} />
1010
</div>
1111
</section>
1212
)

src/design/components/footer/index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
export function Footer({ buildTime }: { buildTime: string }) {
1+
import { url } from '../../../build/config'
2+
import type { SiteConfig } from '../../../build/config'
3+
4+
export function Footer({ config }: { config: SiteConfig }) {
25
return (
36
<footer class="site-footer">
47
<nav aria-label="Footer">
58
<ul>
6-
<li><a href="work/">Work</a></li>
7-
<li><a href="commitment/">Commitment</a></li>
8-
<li><a href="about/">About</a></li>
9-
<li><a href="design-system/">Design system</a></li>
9+
<li><a href={url('/work/', config.basePath)}>Work</a></li>
10+
<li><a href={url('/commitment/', config.basePath)}>Commitment</a></li>
11+
<li><a href={url('/about/', config.basePath)}>About</a></li>
12+
<li><a href={url('/design-system/', config.basePath)}>Design system</a></li>
1013
</ul>
1114
</nav>
1215
<p class="site-footer__meta">
13-
Built {formatBuildTime(buildTime)}. Content licensed as noted per project.
16+
Built {formatBuildTime(config.buildTime)}. Content licensed as noted per project.
1417
</p>
1518
</footer>
1619
)

src/design/components/header/examples.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function HeaderExamples() {
66
<h2>Header</h2>
77
<p>Site-wide header with brand wordmark and primary navigation. Renders as a <code>&lt;header&gt;</code> landmark with <code>aria-label="Primary"</code> navigation.</p>
88
<div style="border: 1px solid var(--color-surface-alt); border-radius: var(--radius-md); overflow: hidden;">
9-
<Header />
9+
<Header config={{ basePath: '/', buildTime: '' }} />
1010
</div>
1111
</section>
1212
)

src/design/components/header/index.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { Link } from '../link'
2+
import { url } from '../../../build/config'
3+
import type { SiteConfig } from '../../../build/config'
24

3-
export function Header() {
5+
export function Header({ config }: { config: SiteConfig }) {
46
return (
57
<header class="site-header">
6-
<a href="./" class="site-brand">
8+
<a href={url('/', config.basePath)} class="site-brand">
79
Flexion Labs
810
</a>
911
<nav aria-label="Primary">
1012
<ul>
1113
<li>
12-
<a href="work/">Work</a>
14+
<a href={url('/work/', config.basePath)}>Work</a>
1315
</li>
1416
<li>
15-
<a href="commitment/">Commitment</a>
17+
<a href={url('/commitment/', config.basePath)}>Commitment</a>
1618
</li>
1719
<li>
18-
<a href="about/">About</a>
20+
<a href={url('/about/', config.basePath)}>About</a>
1921
</li>
2022
<li>
2123
<Link href="https://github.com/flexion" external>

src/design/components/repo-card/examples.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function RepoCardExamples() {
2828
<h2>Repo card</h2>
2929
<p>Displays a repository with its name, description, tier and category tags. Links to the repo's detail page.</p>
3030
<div style="max-inline-size: 24rem;">
31-
<RepoCard entry={example} />
31+
<RepoCard entry={example} basePath="/" />
3232
</div>
3333
</section>
3434
)

src/design/components/repo-card/index.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CatalogEntry } from '../../../catalog/types'
2-
import { Badge } from '../tag'
2+
import { Tag } from '../tag'
3+
import { url } from '../../../build/config'
34

45
const TIER_LABEL: Record<CatalogEntry['tier'], string> = {
56
active: 'Active',
@@ -17,20 +18,20 @@ const CATEGORY_LABEL: Record<CatalogEntry['category'], string> = {
1718
uncategorized: 'Uncategorized',
1819
}
1920

20-
export function RepoCard({ entry }: { entry: CatalogEntry }) {
21+
export function RepoCard({ entry, basePath }: { entry: CatalogEntry; basePath: string }) {
22+
const href = url(`/work/${entry.name}/`, basePath)
2123
const summary = entry.overlay?.summary ?? entry.description ?? ''
22-
const href = `work/${entry.name}/`
2324
return (
2425
<article class="repo-card">
2526
<h3 class="repo-card__name">
2627
<a href={href}>{entry.name}</a>
2728
</h3>
2829
{summary ? <p class="repo-card__summary">{summary}</p> : null}
2930
<p class="repo-card__meta">
30-
<Badge variant={`tier-${entry.tier}`}>{TIER_LABEL[entry.tier]}</Badge>{' '}
31-
<Badge variant={`category-${entry.category}`}>
31+
<Tag variant={`tier-${entry.tier}`}>{TIER_LABEL[entry.tier]}</Tag>{' '}
32+
<Tag variant={`category-${entry.category}`}>
3233
{CATEGORY_LABEL[entry.category]}
33-
</Badge>
34+
</Tag>
3435
</p>
3536
</article>
3637
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class SideNav extends HTMLElement {
2+
private observer: IntersectionObserver | null = null
3+
4+
connectedCallback() {
5+
// Collect the target IDs from the nav links
6+
const links = this.querySelectorAll<HTMLAnchorElement>('.side-nav__link')
7+
const ids: string[] = []
8+
for (const link of links) {
9+
const hash = new URL(link.href).hash
10+
if (hash) ids.push(hash.slice(1))
11+
}
12+
13+
if (ids.length === 0) return
14+
15+
// Mark the first link as current initially
16+
this.setCurrent(ids[0])
17+
18+
// Observe each target section
19+
this.observer = new IntersectionObserver(
20+
(entries) => {
21+
// Find the topmost visible section
22+
for (const entry of entries) {
23+
if (entry.isIntersecting) {
24+
this.setCurrent(entry.target.id)
25+
break
26+
}
27+
}
28+
},
29+
{ rootMargin: '-10% 0px -80% 0px' },
30+
)
31+
32+
for (const id of ids) {
33+
const target = document.getElementById(id)
34+
if (target) this.observer.observe(target)
35+
}
36+
}
37+
38+
disconnectedCallback() {
39+
this.observer?.disconnect()
40+
}
41+
42+
private setCurrent(id: string) {
43+
for (const link of this.querySelectorAll<HTMLAnchorElement>('.side-nav__link')) {
44+
const linkHash = new URL(link.href).hash
45+
if (linkHash === `#${id}`) {
46+
link.setAttribute('aria-current', 'true')
47+
} else {
48+
link.removeAttribute('aria-current')
49+
}
50+
}
51+
}
52+
}
53+
54+
if (!customElements.get('side-nav')) {
55+
customElements.define('side-nav', SideNav)
56+
}

src/design/register.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import './components/catalog-filter/client'
22
import './components/sortable-table/client'
33
import './components/copy-button/client'
4+
import './components/side-nav/client'

0 commit comments

Comments
 (0)