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 (
-
);
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);
---
Skip to content