review round 2: deterministic checks (npm run check: build + zero-dep link checker; tsconfig with docs-recommended include/exclude, astro check dropped — OOMs at 12GB on a 200-entry collection), sitemap static pages derived from route files, non-recursive content glob (id/slug trap), --header-h token, honest CDN comments (sveltia UI fonts), preview.css drift (prose a wrap, img block, h1 size)
This commit is contained in:
Generated
+999
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -6,9 +6,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview"
|
"preview": "astro preview",
|
||||||
|
"check": "astro build && node scripts/check-links.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^7.2.10"
|
"astro": "^7.2.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.10",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Deterministic internal-link check over the built site (zero dependencies).
|
||||||
|
// Run AFTER `astro build`: scans dist/**/*.html for root-relative href/src
|
||||||
|
// values and fails (exit 1) if any target has no file in dist/. External
|
||||||
|
// URLs, mailto:, data: and pure-#fragment links are out of scope.
|
||||||
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const DIST = new URL('../dist', import.meta.url).pathname;
|
||||||
|
if (!existsSync(DIST)) { console.error('dist/ not found — run `npm run build` first'); process.exit(1); }
|
||||||
|
|
||||||
|
const htmlFiles = [];
|
||||||
|
(function walk(dir) {
|
||||||
|
for (const name of readdirSync(dir)) {
|
||||||
|
const p = join(dir, name);
|
||||||
|
if (statSync(p).isDirectory()) walk(p);
|
||||||
|
else if (name.endsWith('.html')) htmlFiles.push(p);
|
||||||
|
}
|
||||||
|
})(DIST);
|
||||||
|
|
||||||
|
const resolves = (path) => {
|
||||||
|
const clean = decodeURI(path.split(/[?#]/)[0]);
|
||||||
|
if (clean === '/') return true;
|
||||||
|
return ['', '.html', '/index.html'].some((suffix) =>
|
||||||
|
existsSync(join(DIST, clean.replace(/\/$/, '') + suffix)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let broken = 0;
|
||||||
|
for (const file of htmlFiles) {
|
||||||
|
const html = readFileSync(file, 'utf8');
|
||||||
|
for (const [, , url] of html.matchAll(/\s(href|src)="(\/[^"]*)"/g)) {
|
||||||
|
if (!resolves(url)) {
|
||||||
|
console.error(`broken: ${url} (in ${file.slice(DIST.length + 1)})`);
|
||||||
|
broken++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(broken ? `${broken} broken internal link(s)` : `OK — ${htmlFiles.length} pages, all internal links resolve`);
|
||||||
|
process.exit(broken ? 1 : 0);
|
||||||
@@ -5,7 +5,9 @@ import { glob } from 'astro/loaders';
|
|||||||
// This is exactly what Sveltia CMS edits — the CMS writes these files, Astro
|
// This is exactly what Sveltia CMS edits — the CMS writes these files, Astro
|
||||||
// renders them to static HTML at build time.
|
// renders them to static HTML at build time.
|
||||||
const news = defineCollection({
|
const news = defineCollection({
|
||||||
loader: glob({ pattern: '**/*.md', base: './src/content/news' }),
|
// Top-level only ('*.md', not '**'): entry ids feed a non-rest [slug]
|
||||||
|
// route, and an id from a subfolder would contain '/' and break it.
|
||||||
|
loader: glob({ pattern: '*.md', base: './src/content/news' }),
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ const readMin = readMinOf(post);
|
|||||||
let observer;
|
let observer;
|
||||||
const observe = () => {
|
const observe = () => {
|
||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
// Narrow band below the sticky header (56px) — Starlight's rootMargin trick
|
// Narrow band below the sticky header (--header-h 56px + 12px slack;
|
||||||
|
// JS can't read the token cheaply, keep in sync) — Starlight's rootMargin trick
|
||||||
const top = 68, band = 64;
|
const top = 68, band = 64;
|
||||||
observer = new IntersectionObserver(onIntersect, {
|
observer = new IntersectionObserver(onIntersect, {
|
||||||
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
||||||
|
|||||||
@@ -13,10 +13,17 @@ export async function GET(context) {
|
|||||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
const pages = await getCollection('pages');
|
const pages = await getCollection('pages');
|
||||||
|
|
||||||
// Go-live note: /login and /dashboard are mock pages — drop them from this
|
// Static pages come from the actual route files, so a new src/pages/*.astro
|
||||||
// list (or gate them) before robots.txt stops disallowing everything.
|
// can never be forgotten here. 404 is excluded; /admin (subdirectory) is
|
||||||
|
// intentionally unlisted. Go-live note: /login and /dashboard are mock
|
||||||
|
// pages — filter them out before robots.txt stops disallowing everything.
|
||||||
|
const staticPages = Object.keys(import.meta.glob('./*.astro'))
|
||||||
|
.map((p) => p.slice(1, -'.astro'.length))
|
||||||
|
.filter((p) => p !== '/404')
|
||||||
|
.map((p) => (p === '/index' ? '/' : p))
|
||||||
|
.sort();
|
||||||
const urls = [
|
const urls = [
|
||||||
'/', '/news', '/proposals', '/dashboard', '/support', '/search', '/login',
|
...staticPages, '/news',
|
||||||
...Array.from({ length: newsPageCount(news.length) - 1 }, (_, i) => `/news/${i + 2}`),
|
...Array.from({ length: newsPageCount(news.length) - 1 }, (_, i) => `/news/${i + 2}`),
|
||||||
...pages.map((p) => `/pages/${p.id}`),
|
...pages.map((p) => `/pages/${p.id}`),
|
||||||
].map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`);
|
].map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
--space-m: 24px;
|
--space-m: 24px;
|
||||||
--space-l: clamp(24px, 2vw + 16px, 40px);
|
--space-l: clamp(24px, 2vw + 16px, 40px);
|
||||||
--space-xl: clamp(56px, 7vw, 120px); /* section rhythm */
|
--space-xl: clamp(56px, 7vw, 120px); /* section rhythm */
|
||||||
|
--header-h: 56px; /* sticky header; anchors, TOC rail, progress bar derive from it */
|
||||||
--space-band: clamp(72px, 10vw, 160px); /* hero / auth / landing-head padding */
|
--space-band: clamp(72px, 10vw, 160px); /* hero / auth / landing-head padding */
|
||||||
/* Elevation — navy-tinted, soft/large/low-alpha; the ONLY two shadows allowed */
|
/* Elevation — navy-tinted, soft/large/low-alpha; the ONLY two shadows allowed */
|
||||||
--shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.04), 0 8px 24px rgba(0, 0, 74, 0.06);
|
--shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.04), 0 8px 24px rgba(0, 0, 74, 0.06);
|
||||||
@@ -121,7 +122,7 @@ html { -webkit-text-size-adjust: 100%; }
|
|||||||
it — so each typed character walked the page toward 0. Scoping the offset
|
it — so each typed character walked the page toward 0. Scoping the offset
|
||||||
to the in-content anchor targets fixes typing and keeps every jump
|
to the in-content anchor targets fixes typing and keeps every jump
|
||||||
(TOC headings, skip link → #main) landing below the header. */
|
(TOC headings, skip link → #main) landing below the header. */
|
||||||
main, main [id] { scroll-margin-top: 76px; }
|
main, main [id] { scroll-margin-top: calc(var(--header-h) + 20px); }
|
||||||
/* Guard, not a solution (DESIGN.md): every wide element is contained at its own
|
/* Guard, not a solution (DESIGN.md): every wide element is contained at its own
|
||||||
level (tables scroll in their box, long words wrap); clip only catches
|
level (tables scroll in their box, long words wrap); clip only catches
|
||||||
regressions so one offender can never remove the page gutters. `clip`, unlike
|
regressions so one offender can never remove the page gutters. `clip`, unlike
|
||||||
@@ -197,7 +198,7 @@ img { max-width: 100%; height: auto; display: block; }
|
|||||||
-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.nav { display: flex; align-items: center; gap: 4px; min-height: 56px; flex-wrap: nowrap; }
|
.nav { display: flex; align-items: center; gap: 4px; min-height: var(--header-h); flex-wrap: nowrap; }
|
||||||
.nav > nav { display: flex; align-items: center; gap: 4px; }
|
.nav > nav { display: flex; align-items: center; gap: 4px; }
|
||||||
/* One-row guarantee: everything keeps its natural size except the spacer
|
/* One-row guarantee: everything keeps its natural size except the spacer
|
||||||
(links are single words, they can never wrap) */
|
(links are single words, they can never wrap) */
|
||||||
@@ -568,9 +569,9 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
|||||||
.article-layout { grid-template-columns: minmax(0, 70ch) 15rem; column-gap: var(--space-xl); justify-content: space-between; }
|
.article-layout { grid-template-columns: minmax(0, 70ch) 15rem; column-gap: var(--space-xl); justify-content: space-between; }
|
||||||
.toc--inline { display: none; }
|
.toc--inline { display: none; }
|
||||||
.toc--rail {
|
.toc--rail {
|
||||||
display: block; position: sticky; top: 80px; align-self: start;
|
display: block; position: sticky; top: calc(var(--header-h) + 24px); align-self: start;
|
||||||
margin-top: calc(var(--space-l) + 44px); /* aligns with the article h1 */
|
margin-top: calc(var(--space-l) + 44px); /* aligns with the article h1 */
|
||||||
max-height: calc(100dvh - 104px); overflow: auto;
|
max-height: calc(100dvh - var(--header-h) - 48px); overflow: auto;
|
||||||
border-left: 1px solid var(--border); padding-left: var(--space-m);
|
border-left: 1px solid var(--border); padding-left: var(--space-m);
|
||||||
}
|
}
|
||||||
/* Active rule segment: 2px Cherenkov over the hairline, tracking the link */
|
/* Active rule segment: 2px Cherenkov over the hairline, tracking the link */
|
||||||
@@ -705,7 +706,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
|
|||||||
/* Auth (login mock) — nebula backdrop + floating card; the ONLY navy band
|
/* Auth (login mock) — nebula backdrop + floating card; the ONLY navy band
|
||||||
besides the home hero */
|
besides the home hero */
|
||||||
.auth-band {
|
.auth-band {
|
||||||
min-height: calc(100dvh - 56px); position: relative;
|
min-height: calc(100dvh - var(--header-h)); position: relative;
|
||||||
display: grid; place-items: center; padding: var(--space-band) var(--gutter);
|
display: grid; place-items: center; padding: var(--space-band) var(--gutter);
|
||||||
background: var(--nebula), var(--galaxy);
|
background: var(--nebula), var(--galaxy);
|
||||||
}
|
}
|
||||||
@@ -819,7 +820,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
|
|||||||
/* Article reading progress — functional state, same Cherenkov budget line
|
/* Article reading progress — functional state, same Cherenkov budget line
|
||||||
as the TOC scrollspy rule (navigation feedback, not decoration). */
|
as the TOC scrollspy rule (navigation feedback, not decoration). */
|
||||||
.read-progress {
|
.read-progress {
|
||||||
position: fixed; top: 56px; left: 0; width: 100%; height: 3px; z-index: 9;
|
position: fixed; top: var(--header-h); left: 0; width: 100%; height: 3px; z-index: 9;
|
||||||
background: var(--cherenkov); transform-origin: 0 50%;
|
background: var(--cherenkov); transform-origin: 0 50%;
|
||||||
animation: progress linear both; animation-timeline: scroll(root);
|
animation: progress linear both; animation-timeline: scroll(root);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/base"
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user