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`.
|
(astro.config.mjs) — the default minifier breaks `animation-timeline`.
|
||||||
- `--header-h` drives the sticky-header offsets (anchors, TOC rail, reading
|
- `--header-h` drives the sticky-header offsets (anchors, TOC rail, reading
|
||||||
progress); change the header height only through the token.
|
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
|
./scripts/link-content.sh # copies news/pages/uploads into the dev tree
|
||||||
npm run dev # http://localhost:4321
|
npm run dev # http://localhost:4321
|
||||||
npm run build # static output in dist/
|
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
|
## Layout
|
||||||
|
|
||||||
| Path | What |
|
| 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
|
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
|
from `global.css` so the editor preview matches the site 1:1 — keep them in
|
||||||
sync (both files carry a KEEP IN SYNC comment).
|
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,
|
// Public origin — used to build absolute URLs (canonical, og:*, RSS,
|
||||||
// sitemap). Change when the portal moves to its production domain.
|
// sitemap). Change when the portal moves to its production domain.
|
||||||
site: 'https://astro.isl-dev.grid.cyfronet.pl',
|
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).
|
// Built-in prefetch on every internal link (no per-link attributes needed).
|
||||||
// Default 'hover' strategy: near-instant navigation without the bandwidth
|
// Default 'hover' strategy: near-instant navigation without the bandwidth
|
||||||
// cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on
|
// cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on
|
||||||
|
|||||||
Generated
+3
-3
@@ -8,11 +8,11 @@
|
|||||||
"name": "ctao-portal",
|
"name": "ctao-portal",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^7.2.10"
|
"astro": "7.2.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "0.9.10",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/check": {
|
"node_modules/@astrojs/check": {
|
||||||
|
|||||||
+4
-4
@@ -7,13 +7,13 @@
|
|||||||
"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"
|
"check": "astro check && astro build && node scripts/check-links.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^7.2.10"
|
"astro": "7.2.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "0.9.10",
|
||||||
"typescript": "^6.0.3"
|
"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';
|
import { glob } from 'astro/loaders';
|
||||||
|
|
||||||
// News/articles live as Markdown files in src/content/news/*.md.
|
// 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.
|
// 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 ogImage = new URL(image && !image.endsWith('.svg') ? image : '/brand/og-card.jpg', site);
|
||||||
const path = Astro.url.pathname;
|
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).
|
// Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5).
|
||||||
// Order: serial-position effect (NN/g) — News (universal entry) first, Support
|
// Order: serial-position effect (NN/g) — News (universal entry) first, Support
|
||||||
// (help convention) last; Data/Proposals by task frequency; Dashboard is the
|
// (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)];
|
const chunks = [posts.slice(0, FIRST)];
|
||||||
for (let i = FIRST; i < posts.length; i += REST) chunks.push(posts.slice(i, i + REST));
|
for (let i = FIRST; i < posts.length; i += REST) chunks.push(posts.slice(i, i + REST));
|
||||||
const lastPage = chunks.length;
|
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) => {
|
return chunks.map((data, i) => {
|
||||||
const n = i + 1;
|
const n = i + 1;
|
||||||
return {
|
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);
|
if (n === 1 || n === page.lastPage || Math.abs(n - page.currentPage) <= 1) nums.push(n);
|
||||||
else if (nums.at(-1) !== 0) nums.push(0);
|
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
|
<Base
|
||||||
title={first ? 'CTAO Science Portal - News' : `CTAO Science Portal - News, page ${page.currentPage}`}
|
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
|
// 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).
|
// aria-current="true" (set in BOTH renderings — inline <details> and rail).
|
||||||
// Progressive enhancement: without JS the TOC is plain working anchors.
|
// 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) {
|
if (links.length) {
|
||||||
let current = [];
|
let current: HTMLAnchorElement[] = [];
|
||||||
const setCurrent = (id) => {
|
const setCurrent = (id: string | null) => {
|
||||||
const next = id ? links.filter((a) => decodeURIComponent(a.hash.slice(1)) === id) : [];
|
const next = id ? links.filter((a) => decodeURIComponent(a.hash.slice(1)) === id) : [];
|
||||||
if (next[0] === current[0]) return;
|
if (next[0] === current[0]) return;
|
||||||
for (const a of current) a.removeAttribute('aria-current');
|
for (const a of current) a.removeAttribute('aria-current');
|
||||||
@@ -108,35 +108,36 @@ const readMin = readMinOf(post);
|
|||||||
};
|
};
|
||||||
const blocks = [...document.querySelectorAll('.prose > *')];
|
const blocks = [...document.querySelectorAll('.prose > *')];
|
||||||
// Governing heading = the block itself or the nearest h2/h3[id] above it
|
// 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--) {
|
for (let i = blocks.indexOf(el); i >= 0; i--) {
|
||||||
if (blocks[i].matches('h2[id], h3[id]')) return blocks[i].id;
|
if (blocks[i].matches('h2[id], h3[id]')) return blocks[i].id;
|
||||||
}
|
}
|
||||||
return null; // intro before the first heading — nothing highlighted
|
return null; // intro before the first heading — nothing highlighted
|
||||||
};
|
};
|
||||||
const onIntersect = (entries) => {
|
const onIntersect = (entries: IntersectionObserverEntry[]) => {
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (e.isIntersecting) { setCurrent(headingFor(e.target)); break; }
|
if (e.isIntersecting) { setCurrent(headingFor(e.target)); break; }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let observer;
|
let observer: IntersectionObserver | undefined;
|
||||||
const observe = () => {
|
const observe = () => {
|
||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
// Narrow band below the sticky header (--header-h 56px + 12px slack;
|
// 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
|
// 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, {
|
const io = new IntersectionObserver(onIntersect, {
|
||||||
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
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();
|
observe();
|
||||||
let timer;
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||||
addEventListener('resize', () => { clearTimeout(timer); timer = setTimeout(observe, 200); });
|
addEventListener('resize', () => { clearTimeout(timer); timer = setTimeout(observe, 200); });
|
||||||
// Short final section: at page bottom, the last heading wins
|
// Short final section: at page bottom, the last heading wins
|
||||||
addEventListener('scroll', () => {
|
addEventListener('scroll', () => {
|
||||||
if (innerHeight + scrollY >= document.documentElement.scrollHeight - 4) {
|
if (innerHeight + scrollY >= document.documentElement.scrollHeight - 4) {
|
||||||
setCurrent(headingFor(blocks.at(-1)));
|
setCurrent(headingFor(blocks[blocks.length - 1]));
|
||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/base",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
"exclude": ["dist"]
|
"exclude": ["dist", "public"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user