feat: content-UX — header search, metadata de-noise, icon/microcopy coherence, article composition
This commit is contained in:
@@ -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
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user