astro check restored: tsconfig excludes public/ (the 2 MB vendored CMS bundle was what exhausted the heap) and extends tsconfigs/strict; type annotations in template scripts, z from astro/zod; exact version pins; compressHTML true (lossless whitespace, no words glued to links) and explicit trailingSlash/build.format; README: upgrading section
This commit is contained in:
@@ -29,4 +29,5 @@ presentation. Pages are `.astro` templates (plain HTML).
|
||||
(astro.config.mjs) — the default minifier breaks `animation-timeline`.
|
||||
- `--header-h` drives the sticky-header offsets (anchors, TOC rail, reading
|
||||
progress); change the header height only through the token.
|
||||
- Deterministic gate before pushing: `npm run check` (build + link check).
|
||||
- Deterministic gate before pushing: `npm run check` (types + build + link
|
||||
check, a few seconds).
|
||||
|
||||
@@ -50,9 +50,12 @@ git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../ctao-co
|
||||
./scripts/link-content.sh # copies news/pages/uploads into the dev tree
|
||||
npm run dev # http://localhost:4321
|
||||
npm run build # static output in dist/
|
||||
npm run check # deterministic gate: build + internal-link check
|
||||
npm run check # deterministic gate: astro check (types) + build + internal-link check
|
||||
```
|
||||
|
||||
`tsconfig.json` excludes `public/` on purpose: the vendored CMS bundle there is
|
||||
2 MB of minified JS and would make `astro check` run out of memory.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
@@ -76,3 +79,17 @@ Saving commits to `main`; the machine polls and republishes automatically
|
||||
Note for styling work: `public/admin/preview.css` mirrors the `.prose` rules
|
||||
from `global.css` so the editor preview matches the site 1:1 — keep them in
|
||||
sync (both files carry a KEEP IN SYNC comment).
|
||||
|
||||
## Upgrading
|
||||
|
||||
Versions are pinned exactly; an upgrade is a deliberate change, never a side
|
||||
effect of `npm install`. Astro ships security fixes only for the current and
|
||||
one previous major, so plan one upgrade per major rather than skipping several.
|
||||
|
||||
```
|
||||
npx @astrojs/upgrade # moves astro and official integrations together
|
||||
npm run check # types + build + links must pass
|
||||
npm run preview # verify the real dist/ output, not the dev server
|
||||
```
|
||||
|
||||
No experimental flags in `astro.config.mjs`: they can change in minor releases.
|
||||
|
||||
@@ -6,6 +6,14 @@ export default defineConfig({
|
||||
// Public origin — used to build absolute URLs (canonical, og:*, RSS,
|
||||
// sitemap). Change when the portal moves to its production domain.
|
||||
site: 'https://astro.isl-dev.grid.cyfronet.pl',
|
||||
// URL contract, pinned explicitly (these are the defaults): changing either
|
||||
// later rewrites every public URL. nginx serves /path/ via try_files $uri/.
|
||||
trailingSlash: 'ignore',
|
||||
build: { format: 'directory' },
|
||||
// Lossless whitespace handling instead of the v7 default 'jsx', which drops
|
||||
// line breaks around inline elements and silently glues words to links when
|
||||
// a template line wraps before <a>. Templates render as written.
|
||||
compressHTML: true,
|
||||
// Built-in prefetch on every internal link (no per-link attributes needed).
|
||||
// Default 'hover' strategy: near-instant navigation without the bandwidth
|
||||
// cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on
|
||||
|
||||
Generated
+3
-3
@@ -8,11 +8,11 @@
|
||||
"name": "ctao-portal",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"astro": "^7.2.10"
|
||||
"astro": "7.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"typescript": "^6.0.3"
|
||||
"@astrojs/check": "0.9.10",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/check": {
|
||||
|
||||
+4
-4
@@ -7,13 +7,13 @@
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro build && node scripts/check-links.mjs"
|
||||
"check": "astro check && astro build && node scripts/check-links.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^7.2.10"
|
||||
"astro": "7.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"typescript": "^6.0.3"
|
||||
"@astrojs/check": "0.9.10",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { z } from 'astro/zod';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
// News/articles live as Markdown files in src/content/news/*.md.
|
||||
|
||||
@@ -11,7 +11,7 @@ const canonical = new URL(Astro.url.pathname, site);
|
||||
// LinkedIn) don't render SVG, so those articles fall back to the brand card.
|
||||
const ogImage = new URL(image && !image.endsWith('.svg') ? image : '/brand/og-card.jpg', site);
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href));
|
||||
const isActive = (href: string) => (href === '/' ? path === '/' : path.startsWith(href));
|
||||
// Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5).
|
||||
// Order: serial-position effect (NN/g) — News (universal entry) first, Support
|
||||
// (help convention) last; Data/Proposals by task frequency; Dashboard is the
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function getStaticPaths() {
|
||||
const chunks = [posts.slice(0, FIRST)];
|
||||
for (let i = FIRST; i < posts.length; i += REST) chunks.push(posts.slice(i, i + REST));
|
||||
const lastPage = chunks.length;
|
||||
const hrefOf = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
const hrefOf = (n: number) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
return chunks.map((data, i) => {
|
||||
const n = i + 1;
|
||||
return {
|
||||
@@ -42,7 +42,7 @@ for (let n = 1; n <= page.lastPage; n++) {
|
||||
if (n === 1 || n === page.lastPage || Math.abs(n - page.currentPage) <= 1) nums.push(n);
|
||||
else if (nums.at(-1) !== 0) nums.push(0);
|
||||
}
|
||||
const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
const hrefFor = (n: number) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
---
|
||||
<Base
|
||||
title={first ? 'CTAO Science Portal - News' : `CTAO Science Portal - News, page ${page.currentPage}`}
|
||||
|
||||
+11
-10
@@ -96,10 +96,10 @@ const readMin = readMinOf(post);
|
||||
// enters it is mapped back to its governing h2/h3, whose TOC link(s) get
|
||||
// aria-current="true" (set in BOTH renderings — inline <details> and rail).
|
||||
// Progressive enhancement: without JS the TOC is plain working anchors.
|
||||
const links = [...document.querySelectorAll('.toc a[href^="#"]')];
|
||||
const links = [...document.querySelectorAll<HTMLAnchorElement>('.toc a[href^="#"]')];
|
||||
if (links.length) {
|
||||
let current = [];
|
||||
const setCurrent = (id) => {
|
||||
let current: HTMLAnchorElement[] = [];
|
||||
const setCurrent = (id: string | null) => {
|
||||
const next = id ? links.filter((a) => decodeURIComponent(a.hash.slice(1)) === id) : [];
|
||||
if (next[0] === current[0]) return;
|
||||
for (const a of current) a.removeAttribute('aria-current');
|
||||
@@ -108,35 +108,36 @@ const readMin = readMinOf(post);
|
||||
};
|
||||
const blocks = [...document.querySelectorAll('.prose > *')];
|
||||
// Governing heading = the block itself or the nearest h2/h3[id] above it
|
||||
const headingFor = (el) => {
|
||||
const headingFor = (el: Element) => {
|
||||
for (let i = blocks.indexOf(el); i >= 0; i--) {
|
||||
if (blocks[i].matches('h2[id], h3[id]')) return blocks[i].id;
|
||||
}
|
||||
return null; // intro before the first heading — nothing highlighted
|
||||
};
|
||||
const onIntersect = (entries) => {
|
||||
const onIntersect = (entries: IntersectionObserverEntry[]) => {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) { setCurrent(headingFor(e.target)); break; }
|
||||
}
|
||||
};
|
||||
let observer;
|
||||
let observer: IntersectionObserver | undefined;
|
||||
const observe = () => {
|
||||
observer?.disconnect();
|
||||
// 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;
|
||||
observer = new IntersectionObserver(onIntersect, {
|
||||
const io = new IntersectionObserver(onIntersect, {
|
||||
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
||||
});
|
||||
blocks.forEach((b) => observer.observe(b));
|
||||
observer = io;
|
||||
blocks.forEach((b) => io.observe(b));
|
||||
};
|
||||
observe();
|
||||
let timer;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
addEventListener('resize', () => { clearTimeout(timer); timer = setTimeout(observe, 200); });
|
||||
// Short final section: at page bottom, the last heading wins
|
||||
addEventListener('scroll', () => {
|
||||
if (innerHeight + scrollY >= document.documentElement.scrollHeight - 4) {
|
||||
setCurrent(headingFor(blocks.at(-1)));
|
||||
setCurrent(headingFor(blocks[blocks.length - 1]));
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["dist", "public"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user