diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 92ca412..07a7185 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -35,8 +35,13 @@ jobs: exit 1 fi - - name: Build - run: npm run build + - name: Typecheck + run: npm run typecheck + + - name: Build & QA + run: | + npx playwright install chromium + npm run qa - name: Copy build to host pages mount if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/astro.config.mjs b/astro.config.mjs index b0ca5dd..dd203d3 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -83,10 +83,11 @@ export default defineConfig({ behavior: 'append', properties: { className: ['heading-anchor'], - 'aria-hidden': 'true', - tabIndex: -1, + ariaLabel: 'Permalink', }, - content: { type: 'text', value: '#' }, + // Glyph rendered via CSS ::before so it doesn't leak into the TOC + // when astro:content extracts heading.text from the rendered HTML. + content: [], }, ], ], diff --git a/package.json b/package.json index daba35a..90763b8 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,11 @@ "typecheck": "astro check", "lint": "prettier --check \"astro.config.mjs\" \"src/**/*.{astro,ts,md,css}\" \"scripts/*.mjs\" \"*.md\" \"*.json\"", "format": "prettier --write \"astro.config.mjs\" \"src/**/*.{astro,ts,md,css}\" \"scripts/*.mjs\" \"*.md\" \"*.json\"", - "format:check": "prettier --check \"astro.config.mjs\" \"src/**/*.{astro,ts,md,css}\" \"scripts/*.mjs\" \"*.md\" \"*.json\"", "build": "astro build", "preview": "astro preview", "qa:no-js": "node scripts/check-no-js.mjs", "qa:overflow": "node scripts/check-overflow.mjs", - "qa": "npm run build && npm run qa:no-js && npm run qa:overflow" + "qa": "npm run typecheck && npm run lint && npm run build && npm run qa:no-js && npm run qa:overflow" }, "repository": { "type": "git", diff --git a/src/components/ArticleList.astro b/src/components/ArticleList.astro index 8f18e33..79caaff 100644 --- a/src/components/ArticleList.astro +++ b/src/components/ArticleList.astro @@ -15,8 +15,9 @@ const { posts, showYear = true, currentTag } = Astro.props;
    { - posts.map((post) => { + posts.map((post, index) => { const href = articlePath(post); + const isFirst = index === 0; return (
  1. ); diff --git a/src/components/EntryThumbnail.astro b/src/components/EntryThumbnail.astro index 54cddff..111ec36 100644 --- a/src/components/EntryThumbnail.astro +++ b/src/components/EntryThumbnail.astro @@ -31,8 +31,9 @@ const { } = Astro.props; const Tag = href ? 'a' : 'div'; -const resolvedFallback: FallbackFormat = - fallbackFormat ?? (src.format === 'png' ? 'png' : 'jpg'); +// Listing thumbnails are screenshots with no required transparency; force JPG +// fallback to avoid shipping multi-hundred-KB PNG derivatives. +const resolvedFallback: FallbackFormat = fallbackFormat ?? 'jpg'; const isDecorativeLink = Boolean(href) && decorative; --- diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 76c5a5d..5de389b 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -3,14 +3,8 @@ import { navItems, site } from '../lib/site'; const year = new Date().getFullYear(); -// Local fallback: Agent 3 hasn't shipped `footerItems`/`footerOnly` yet, so we -// derive footer items locally. Footer mirrors Header (Home filtered out) and -// adds Tags + RSS. -const footerNavItems = [ - ...navItems.filter((item) => item.href !== '/'), - { href: '/tags/', label: 'Tags' }, - { href: '/rss.xml', label: 'RSS' }, -]; +// Footer shows all nav items except Home (which is implicit via the site title). +const footerNavItems = navItems.filter((item) => item.href !== '/'); --- diff --git a/src/components/Header.astro b/src/components/Header.astro index 43e485e..ca7da05 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -3,28 +3,28 @@ import { navItems, site } from '../lib/site'; const current = Astro.url.pathname; -function isCurrent(href: string) { - if (href === '/') return current === '/'; - return current.startsWith(href); +// Exact match for the current page; section match (descendant URLs) for +// ancestor links. `aria-current="page"` is reserved for the exact page, +// `"true"` indicates an ancestor section. +function currentState(href: string): 'page' | 'true' | undefined { + if (current === href) return 'page'; + if (href !== '/' && current.startsWith(href)) return 'true'; + return undefined; } -// Local fallback: Agent 3 hasn't shipped `footerItems`/`footerOnly` yet, so we -// derive header items locally. Header shows nav (minus Home) + Tags. RSS lives -// in the header as a dedicated icon link. -const headerNavItems = [ - ...navItems.filter((item) => item.href !== '/'), - { href: '/tags/', label: 'Tags' }, -]; +// Header shows nav items except Home and footer-only entries. RSS lives as a +// dedicated icon link to the right of the nav. +const headerNavItems = navItems.filter((item) => item.href !== '/' && !item.footerOnly); ---