feat: content-UX — header search, metadata de-noise, icon/microcopy coherence, article composition

This commit is contained in:
ctao
2026-07-25 07:32:11 +02:00
parent a8833ce736
commit 419e9a2dd1
12 changed files with 361 additions and 119 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ const tickets = [
<p class="notice">Search and download in the <a href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener" class="external">Data Explorer</a> (external, LUX team).</p>
</div>
<div class="panel">
<h2 class="panel-title">Help-desk tickets</h2>
<h2 class="panel-title">Support tickets</h2>
<ul class="rows">
{tickets.map((t) => (
<li>
+14 -17
View File
@@ -7,6 +7,9 @@ const posts = (await getCollection('news', ({ data }) => !data.draft))
.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; unbuilt ones are labelled, never faked.
const services = [
{ title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.', badge: 'mock' },
@@ -24,23 +27,18 @@ const services = [
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
<h1>CTAO Science Portal</h1>
<p>Exploring the Universe at the Highest Energies</p>
<form class="hero-search" role="search" action="/search" method="get">
<label class="sr-only" for="home-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="home-q" name="q" type="search" placeholder="Search news and announcements…" autocomplete="off" />
<button class="btn" type="submit">Search</button>
{/* Live suggestions (plain links, Tab-accessible); Enter still submits to /search */}
<ul class="suggest" id="home-suggest"></ul>
<p class="sr-only" id="home-suggest-status" role="status" aria-live="polite"></p>
</form>
{/* Search lives in the header (one search location); the hero carries the
two core scientist tasks per SPEC §3.1 instead — classic hero-CTA pair. */}
<div class="hero-cta">
<a class="btn external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a>
<a class="btn btn--ghost" href="/proposals">Submit a proposal</a>
</div>
{posts[0] && (
<p class="hero-teaser">Latest: <a href={`/news/${posts[0].id}`}>{posts[0].data.title}</a></p>
)}
</div>
</section>
<script is:inline src="/search-client.js"></script>
<script is:inline>
newsSearch({ input: 'home-q', list: 'home-suggest', status: 'home-suggest-status', limit: 6, unit: 'suggestion' });
</script>
{/* Surface rhythm (DESIGN.md): services on Moon Gray, editorial news on white */}
<section class="band--moon">
<div class="container page">
@@ -56,7 +54,7 @@ const services = [
<p class="desc">{s.desc}</p>
{s.badge === 'mock' && <span class="badge-mock">Mock</span>}
{s.badge === 'ext' && <span class="badge-ext">External — LUX team</span>}
{s.badge === 'tbd' && <span class="badge-ext">Planned (TBD)</span>}
{s.badge === 'tbd' && <span class="badge-ext">Planned — not yet available</span>}
</article>
))}
</div>
@@ -73,10 +71,9 @@ const services = [
<article class="card">
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" loading="lazy" />}
<div class="body">
<span class="cat">{post.data.category}</span>
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
<p class="desc">{post.data.description}</p>
<div class="meta">{post.data.author} · <time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></div>
<div class="meta"><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time> · {readMin(post)} min read</div>
</div>
</article>
))}
+3 -4
View File
@@ -8,11 +8,10 @@ import Base from '../layouts/Base.astro';
<img class="auth-logo" src="/brand/CTAO_Logo_positive.svg" alt="CTAO" />
<span class="badge-mock">Mock — AAI integration pending</span>
<h1>Sign in</h1>
<p class="muted">One account for all CTAO services. Authentication is handled by the
central <strong>CTAO AAI</strong> (single sign-on) — the portal never sees your password.</p>
{/* Auth pages are scanned, not read — one line, no reassurance prose */}
<p class="muted">Use your CTAO account — single sign-on via CTAO AAI.</p>
<button class="btn btn-wide" type="button">Continue with your CTAO account</button>
<p class="notice">No account yet? <a>Register via CTAO AAI</a> · Access level follows
your CTAO role and permissions.</p>
<p class="notice">No account? <a>Register via CTAO AAI</a></p>
</div>
</section>
</Base>
+5 -3
View File
@@ -15,6 +15,9 @@ 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++) {
@@ -32,7 +35,7 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
<div class="container hero">
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
<h1>News &amp; Announcements</h1>
<p>Exploring the Universe at the Highest Energies</p>
<p>Science highlights and updates from the observatory and its partners</p>
</div>
</section>
@@ -45,10 +48,9 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
<img class="cover" src={encodeURI(post.data.cover)} alt="" loading={first && i === 0 ? 'eager' : 'lazy'} />
)}
<div class="body">
<span class="cat">{post.data.category}</span>
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
<p class="desc">{post.data.description}</p>
<div class="meta">{post.data.author} · <time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></div>
<div class="meta"><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time> · {readMin(post)} min read</div>
</div>
</article>
))}
+29 -4
View File
@@ -3,14 +3,22 @@ import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
export async function getStaticPaths() {
const posts = await getCollection('news', ({ data }) => !data.draft);
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
// Date-sorted so each article knows its sequential neighbours (prev/next
// pattern of every editorial site — keeps readers in the content flow).
const posts = (await getCollection('news', ({ data }) => !data.draft)).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
);
return posts.map((post, i) => ({
params: { slug: post.id },
props: { post, newer: posts[i - 1] ?? null, older: posts[i + 1] ?? null },
}));
}
const { post } = Astro.props;
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));
---
<Base title={`${post.data.title} — CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)}>
{/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed <details>
@@ -19,7 +27,8 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
<article class="article">
<a class="back" href="/news">← All news</a>
<h1>{post.data.title}</h1>
<div class="meta"><span class="cat">{post.data.category}</span><span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time></div>
{/* Meta order: category · author · date · reading time */}
<div class="meta"><span class="cat">{post.data.category}</span><span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time>·<span>{readMin} min read</span></div>
{toc.length >= 3 && (
<details class="toc toc--inline">
<summary>On this page</summary>
@@ -34,6 +43,22 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
<div class="prose">
<Content />
</div>
{(newer || older) && (
<nav class="post-nav" aria-label="More news">
{newer && (
<a class="post-nav-prev" rel="prev" href={`/news/${newer.id}`}>
<small>← Newer</small>
<span class="post-nav-title">{newer.data.title}</span>
</a>
)}
{older && (
<a class="post-nav-next" rel="next" href={`/news/${older.id}`}>
<small>Older →</small>
<span class="post-nav-title">{older.data.title}</span>
</a>
)}
</nav>
)}
</article>
{toc.length >= 3 && (
<nav class="toc toc--rail" aria-label="On this page">
+3 -3
View File
@@ -7,14 +7,14 @@ import Base from '../layouts/Base.astro';
<div class="container hero">
<div class="eyebrow">Proposal Submission</div>
<h1>Observation proposals</h1>
<p>Submitting observation proposals, including ToO / MWL requests</p>
<p>Submit observation proposals, including ToO and MWL requests</p>
</div>
</section>
<section class="band--moon">
<div class="container page">
<span class="badge-mock">Mock — no submission logic</span>
<span class="badge-ext">Target: Proposal Handling System (APC team) integrated into the portal</span>
<span class="badge-ext">Planned integration: Proposal Handling System (APC team)</span>
<div class="panel panel--narrow">
<div class="form-row">
<label for="f-title">Proposal title</label>
@@ -42,7 +42,7 @@ import Base from '../layouts/Base.astro';
</div>
<div class="form-row">
<label for="f-just">Scientific justification</label>
<textarea id="f-just" rows="4" placeholder="Brief description of the observation goal..."></textarea>
<textarea id="f-just" rows="4" placeholder="Brief description of the observation goal…"></textarea>
</div>
<button class="btn" type="button">Submit proposal</button>
<p class="notice">Demonstration mock — the form does not store any data.</p>
+1 -1
View File
@@ -26,7 +26,7 @@ import Base from '../layouts/Base.astro';
</section>
</Base>
<script is:inline src="/search-client.js"></script>
{/* search-client.js is loaded once by the Base layout (header search) */}
<script is:inline>
const run = newsSearch({ input: 'q', list: 'search-results', status: 'search-status', limit: 20, hint: 'Type at least 2 characters.' });
// Support /search?q=… deep links (hero search form, footer, bookmarks)
+1 -1
View File
@@ -14,7 +14,7 @@ const channels = [
<div class="container hero">
<div class="eyebrow">User services</div>
<h1>User Support</h1>
<p>Help desk, FAQ / troubleshooting guide, user forum and mailing lists</p>
<p>Help desk, FAQ, user forum and mailing lists</p>
</div>
</section>