handoff pass: vendored sveltia bundle, astro pinned ^7.2.10, root README + refreshed deploy runbook, review fixes (coverless card grid, shared NewsCard/lib, real site origin, nginx security headers, linger step, dead config removed, preview.css sync, tsconfig)
This commit is contained in:
+15
-13
@@ -4,17 +4,19 @@ import Base from '../layouts/Base.astro';
|
||||
<Base title="CTAO Science Portal — Page not found" description="This page does not exist">
|
||||
{/* Document archetype (DESIGN.md): the document IS the page — h1, one line,
|
||||
recovery links, and a real GET search form as the way forward. */}
|
||||
<article class="container article">
|
||||
<h1>Page not found</h1>
|
||||
<div class="prose">
|
||||
<p>The address may be mistyped or the page may have moved — go to the
|
||||
<a href="/">home page</a> or browse <a href="/news">all news</a>.</p>
|
||||
</div>
|
||||
<form class="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="nf-q">Search news</label>
|
||||
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
||||
<input id="nf-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
</form>
|
||||
</article>
|
||||
<div class="container article-layout">
|
||||
<article class="article">
|
||||
<h1>Page not found</h1>
|
||||
<div class="prose">
|
||||
<p>The address may be mistyped or the page may have moved — go to the
|
||||
<a href="/">home page</a> or browse <a href="/news">all news</a>.</p>
|
||||
</div>
|
||||
<form class="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="nf-q">Search news</label>
|
||||
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
||||
<input id="nf-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
// Editor entry point (/admin). Sveltia CMS loads its config from ./config.yml
|
||||
// (served statically from public/admin/config.yml). The external bundle is kept
|
||||
// inline so Astro doesn't try to process/bundle it. CTAO branding on the sign-in
|
||||
// screen comes from the officially supported `logo_url` config option.
|
||||
// (served statically from public/admin/config.yml). The bundle is vendored in
|
||||
// public/vendor/ so the editor CODE never auto-updates from a CDN (updates are
|
||||
// deliberate — see README); note the bundle still fetches its own UI fonts
|
||||
// from jsDelivr at runtime. Kept is:inline so Astro doesn't process/bundle it.
|
||||
// CTAO branding on the sign-in screen comes from the documented `logo` option.
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -25,7 +27,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<noscript><p>The CTAO content editor requires JavaScript.<br />Please enable it, or return to the <a href="/">Science Portal</a>.</p></noscript>
|
||||
<script is:inline src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
|
||||
<script is:inline src="/vendor/sveltia-cms.js"></script>
|
||||
<script is:inline>
|
||||
// Officially supported API: article previews rendered with portal-like styles.
|
||||
window.CMS?.registerPreviewStyle?.('/admin/preview.css');
|
||||
|
||||
+2
-15
@@ -1,15 +1,11 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../layouts/Base.astro';
|
||||
import NewsCard from '../components/NewsCard.astro';
|
||||
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0, 3);
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const iso = (d) => d.toISOString().slice(0, 10);
|
||||
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
|
||||
// category/author are identical across all articles, so cards omit them (NN/g).
|
||||
const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
|
||||
// Services per the Science Portal spec. Tiles carry no status chrome — honesty
|
||||
// lives at the interaction point (REQUIREMENTS §5): each target page opens with
|
||||
// its own whisper badge. Planned services stay in the grid but subdued (muted,
|
||||
@@ -91,16 +87,7 @@ const planned = ['Scheduling', 'Science alerts'];
|
||||
<a class="more" href="/news">All news →</a>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{posts.map((post) => (
|
||||
<article class="card">
|
||||
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" loading="lazy" />}
|
||||
<div class="body">
|
||||
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
|
||||
<p class="desc">{post.data.description}</p>
|
||||
<div class="meta"><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></span><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin(post)} min read</span></div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{posts.map((post) => <NewsCard post={post} />)}
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import NewsCard from '../../components/NewsCard.astro';
|
||||
import { PAGE_FIRST as FIRST, PAGE_REST as REST } from '../../lib/news.js';
|
||||
|
||||
// Paginated archive; the [...page] rest route makes page 1 the bare /news URL,
|
||||
// then /news/2 … /news/N. Custom slicing instead of paginate(): page 1 holds
|
||||
// 25 items (1 featured lead + 24 grid = even 3-column rows), later pages 24 —
|
||||
// paginate() cannot vary page size.
|
||||
// FIRST items (1 featured lead + 24 grid = even 3-column rows), later pages
|
||||
// REST — paginate() cannot vary page size. Chunk sizes live in lib/news.js,
|
||||
// shared with sitemap.xml.js.
|
||||
export async function getStaticPaths() {
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
|
||||
);
|
||||
const FIRST = 25, REST = 24;
|
||||
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;
|
||||
@@ -34,11 +36,6 @@ export async function getStaticPaths() {
|
||||
|
||||
const { page } = Astro.props;
|
||||
const first = page.currentPage === 1;
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const iso = (d) => d.toISOString().slice(0, 10);
|
||||
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
|
||||
// category/author are identical across all articles, so cards omit them (NN/g).
|
||||
const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
|
||||
// Windowed page list — 1 … n-1 n n+1 … last (0 marks an ellipsis)
|
||||
const nums = [];
|
||||
for (let n = 1; n <= page.lastPage; n++) {
|
||||
@@ -61,18 +58,7 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
<section class="container page">
|
||||
<h2 class="sr-only">{first ? 'All news' : `All news — page ${page.currentPage}`}</h2>
|
||||
<div class="grid">
|
||||
{page.data.map((post, i) => (
|
||||
<article class={first && i === 0 ? 'card card--featured' : 'card'}>
|
||||
{post.data.cover && (
|
||||
<img class="cover" src={encodeURI(post.data.cover)} alt="" loading={first && i === 0 ? 'eager' : 'lazy'} />
|
||||
)}
|
||||
<div class="body">
|
||||
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
|
||||
<p class="desc">{post.data.description}</p>
|
||||
<div class="meta"><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></span><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin(post)} min read</span></div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{page.data.map((post, i) => <NewsCard post={post} featured={first && i === 0} />)}
|
||||
</div>
|
||||
{page.lastPage > 1 && (
|
||||
<nav class="pagination" aria-label="News pages">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { fmt, readMin as readMinOf } from '../../lib/news.js';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Date-sorted so each article knows its sequential neighbours (prev/next
|
||||
@@ -17,17 +18,18 @@ export async function getStaticPaths() {
|
||||
const { post, newer, older } = Astro.props;
|
||||
const { Content, headings } = await render(post);
|
||||
const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 220));
|
||||
const readMin = readMinOf(post);
|
||||
---
|
||||
<Base title={`${post.data.title} — CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)} lang={post.data.lang}>
|
||||
<Base title={`${post.data.title} — CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)}>
|
||||
{/* Reading progress (scroll-driven CSS, no JS) — styled only where
|
||||
animation-timeline is supported; elsewhere it stays an empty div. */}
|
||||
<div class="read-progress" aria-hidden="true"></div>
|
||||
{/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed <details>
|
||||
under the title below that. CSS shows exactly one of the two (DESIGN.md). */}
|
||||
{/* lang sits on the article, not <html>: the chrome (nav/footer) stays
|
||||
English for screen readers even when the article body is not. */}
|
||||
<div class="container article-layout">
|
||||
<article class="article">
|
||||
<article class="article" lang={post.data.lang}>
|
||||
<a class="back" href="/news">← All news</a>
|
||||
{/* Editorial anatomy (Guardian/BBC/Reuters convention): headline →
|
||||
standfirst → byline/meta → lead image → body. The standfirst is the
|
||||
|
||||
@@ -11,13 +11,17 @@ const { page } = Astro.props;
|
||||
const { Content } = await render(page);
|
||||
---
|
||||
<Base title={`${page.data.title} — CTAO Science Portal`} description={page.data.description}>
|
||||
<article class="container article">
|
||||
{/* Static pages are footer-reached documents; same back affordance as
|
||||
articles ("← All news") so deep links aren't dead ends. */}
|
||||
<a class="back" href="/">← Home</a>
|
||||
<h1>{page.data.title}</h1>
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
{/* Same wrapper pattern as news/[slug] (never .container and .article on
|
||||
one element — both set max-width and would depend on rule order). */}
|
||||
<div class="container article-layout">
|
||||
<article class="article">
|
||||
{/* Static pages are footer-reached documents; same back affordance as
|
||||
articles ("← All news") so deep links aren't dead ends. */}
|
||||
<a class="back" href="/">← Home</a>
|
||||
<h1>{page.data.title}</h1>
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
@@ -7,7 +7,7 @@ const esc = (s = '') =>
|
||||
s.replace(/[<>&'"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' })[c]);
|
||||
|
||||
export async function GET(context) {
|
||||
const site = context.site ?? new URL('https://portal.ctao.org');
|
||||
const site = context.site; // always set in astro.config.mjs
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.slice(0, 30);
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// Sitemap — hand-rolled static endpoint (no @astrojs/sitemap dependency; our URL
|
||||
// set is fully known at build time). /admin is intentionally excluded.
|
||||
import { getCollection } from 'astro:content';
|
||||
import { PAGE_FIRST, PAGE_REST } from '../lib/news.js';
|
||||
|
||||
// Same chunk math as src/pages/news/[...page].astro (keep in sync): page 1
|
||||
// holds 25 items (featured lead + 24), later pages 24 — so the page count
|
||||
// here can never drift from the archive's.
|
||||
const FIRST = 25, REST = 24;
|
||||
const newsPageCount = (n) => (n <= FIRST ? 1 : 1 + Math.ceil((n - FIRST) / REST));
|
||||
// Chunk sizes are imported from lib/news.js — the same values the
|
||||
// /news/[...page] route slices with, so the page count here cannot drift.
|
||||
const newsPageCount = (n) => (n <= PAGE_FIRST ? 1 : 1 + Math.ceil((n - PAGE_FIRST) / PAGE_REST));
|
||||
|
||||
export async function GET(context) {
|
||||
const site = context.site ?? new URL('https://portal.ctao.org');
|
||||
const site = context.site; // always set in astro.config.mjs
|
||||
const news = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
const pages = await getCollection('pages');
|
||||
|
||||
// Go-live note: /login and /dashboard are mock pages — drop them from this
|
||||
// list (or gate them) before robots.txt stops disallowing everything.
|
||||
const urls = [
|
||||
'/', '/news', '/proposals', '/dashboard', '/support', '/search', '/login',
|
||||
...Array.from({ length: newsPageCount(news.length) - 1 }, (_, i) => `/news/${i + 2}`),
|
||||
|
||||
@@ -18,6 +18,7 @@ const channels = [
|
||||
{/* One page-level whisper badge — the tiles below stay clean (they are
|
||||
not interactive; honesty lives here, at the point users arrive). */}
|
||||
<span class="badge-mock">Mock — target: SUSS User Support system</span>
|
||||
<h2 class="sr-only">Support channels</h2>
|
||||
<div class="grid">
|
||||
{channels.map((c) => (
|
||||
<article class="card tile">
|
||||
|
||||
Reference in New Issue
Block a user