2026-09-02 16:28:58 +01:00
|
|
|
import type { Metadata } from 'next';
|
|
|
|
|
import Link from 'next/link';
|
2026-09-08 17:12:25 +01:00
|
|
|
import { notFound } from 'next/navigation';
|
2026-09-02 16:28:58 +01:00
|
|
|
import { getCachedPayload } from '@/lib/payload';
|
|
|
|
|
import { absoluteUrl } from '@/lib/site';
|
2026-09-08 17:12:25 +01:00
|
|
|
import { getFlags } from '@/lib/flags';
|
2026-09-02 16:28:58 +01:00
|
|
|
import styles from './Blog.module.css';
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Dynamic, not ISR.
|
|
|
|
|
*
|
|
|
|
|
* This route has no dynamic params, so Next prerenders it at build time — and
|
|
|
|
|
* CI builds the image with no database reachable, which fails the build. It is
|
|
|
|
|
* a single indexed query against Postgres on the same Docker network, so
|
|
|
|
|
* rendering per request is cheap, and it means a newly published post appears
|
|
|
|
|
* here immediately rather than waiting on a revalidation.
|
|
|
|
|
*/
|
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
|
|
|
|
export const metadata: Metadata = {
|
|
|
|
|
title: 'Blog',
|
|
|
|
|
description:
|
|
|
|
|
'Notes on what school performance data shows, and what it does not.',
|
|
|
|
|
alternates: { canonical: absoluteUrl('/blog') },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function formatDate(value: string) {
|
|
|
|
|
return new Date(value).toLocaleDateString('en-GB', {
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
month: 'long',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default async function BlogIndexPage() {
|
2026-09-08 17:12:25 +01:00
|
|
|
const flags = await getFlags();
|
|
|
|
|
if (flags.blog !== true) notFound();
|
|
|
|
|
|
2026-09-02 16:28:58 +01:00
|
|
|
const payload = await getCachedPayload();
|
|
|
|
|
const { docs } = await payload.find({
|
|
|
|
|
collection: 'posts',
|
|
|
|
|
where: { _status: { equals: 'published' } },
|
|
|
|
|
sort: '-publishedAt',
|
|
|
|
|
limit: 50,
|
|
|
|
|
depth: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles.page}>
|
|
|
|
|
<header className={styles.header}>
|
|
|
|
|
<p className={styles.kicker}>Blog</p>
|
|
|
|
|
<h1 className={styles.heading}>Notes on the numbers</h1>
|
|
|
|
|
<p className={styles.standfirst}>
|
|
|
|
|
What school performance data shows, what it doesn't, and how to
|
|
|
|
|
read it without being misled. Written by{' '}
|
2026-09-08 17:12:25 +01:00
|
|
|
{/* Plain text when about_page is dark: the two flags are
|
|
|
|
|
independent, so this link would otherwise point at a 404. */}
|
|
|
|
|
{flags.about_page === true
|
|
|
|
|
? <Link href="/about" className={styles.link}>Tudor</Link>
|
|
|
|
|
: 'Tudor'}.
|
2026-09-02 16:28:58 +01:00
|
|
|
</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{docs.length === 0 ? (
|
|
|
|
|
<p className={styles.empty}>No posts yet.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<ul className={styles.list}>
|
|
|
|
|
{docs.map((post) => (
|
|
|
|
|
<li key={post.id} className={styles.item}>
|
|
|
|
|
<time className={styles.date} dateTime={String(post.publishedAt)}>
|
|
|
|
|
{formatDate(String(post.publishedAt))}
|
|
|
|
|
</time>
|
|
|
|
|
<h2 className={styles.itemTitle}>
|
|
|
|
|
<Link href={`/blog/${post.slug}`} className={styles.itemLink}>
|
|
|
|
|
{post.title}
|
|
|
|
|
</Link>
|
|
|
|
|
</h2>
|
|
|
|
|
<p className={styles.excerpt}>{post.excerpt}</p>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|