From f61441407052db463f73bc845a0215eb3aa5ab24 Mon Sep 17 00:00:00 2001 From: Tudor Date: Wed, 2 Sep 2026 16:22:52 +0100 Subject: [PATCH] feat(about): give the site a named author MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site had no author, no statement of why it exists and nobody accountable for its numbers, which is most of why it reads as machine generated. The page states plainly that its author is not an education expert. The credibility claim is lived experience — a parent going through primary admissions — plus stated provenance for every figure, which is true and cannot be undermined by someone noticing there is no teaching qualification behind it. First name only: the Person JSON-LD carries no familyName, worksFor or affiliation, and a test asserts it stays that way. The footer gains a fourth column, with a tablet breakpoint so four columns pair up rather than crushing before the 768px collapse. The nav is deliberately untouched — its mobile tab bar already carries four items. public/brand/tudor.jpg is NOT in this commit. The page references it and will show a broken image until the photograph is supplied; a stock portrait would defeat the entire point of the work. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM --- e2e/tests/journeys.spec.ts | 29 ++++ .../__tests__/app/aboutMetadata.test.ts | 34 +++++ .../app/(frontend)/about/About.module.css | 83 +++++++++++ nextjs-app/app/(frontend)/about/page.tsx | 129 ++++++++++++++++++ nextjs-app/components/Footer.module.css | 11 +- nextjs-app/components/Footer.tsx | 12 ++ nextjs-app/lib/jsonld.ts | 34 +++++ 7 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 nextjs-app/__tests__/app/aboutMetadata.test.ts create mode 100644 nextjs-app/app/(frontend)/about/About.module.css create mode 100644 nextjs-app/app/(frontend)/about/page.tsx create mode 100644 nextjs-app/lib/jsonld.ts diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 98ac194..49cecac 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -2554,3 +2554,32 @@ test('the destinations section never claims a pupil stayed at this school', asyn const text = (await section.textContent()) ?? ''; expect(text).not.toMatch(/stayed on (here|at this school)/i); }); + +/** + * The About page and the blog exist to give the site a named human author. + * These journeys assert the load-bearing parts of that — a name, a face, the + * honesty claim, and a resolvable Person entity — rather than exact copy, + * which will be edited. + */ +test('the about page names a human author and is reachable from the footer', async ({ page }) => { + await page.goto('/'); + const aboutLink = page.locator('footer a[href="/about"]'); + await expect(aboutLink).toBeVisible(); + await aboutLink.click(); + await page.waitForURL(/\/about$/); + + await expect(page.getByRole('heading', { level: 1 })).toContainText('Tudor'); + await expect(page.locator('img[alt*="Tudor"]')).toBeVisible(); + + // The credibility claim is lived experience plus stated provenance, not + // expertise. If this sentence ever disappears the positioning has drifted. + await expect(page.getByText(/not an education expert/i)).toBeVisible(); + + const jsonLd = await page + .locator('script[type="application/ld+json"]') + .first() + .textContent(); + expect(jsonLd).toContain('"Person"'); + // First name only — a surname here would be the one place it leaks. + expect(jsonLd).not.toMatch(/familyName/); +}); diff --git a/nextjs-app/__tests__/app/aboutMetadata.test.ts b/nextjs-app/__tests__/app/aboutMetadata.test.ts new file mode 100644 index 0000000..192b811 --- /dev/null +++ b/nextjs-app/__tests__/app/aboutMetadata.test.ts @@ -0,0 +1,34 @@ +import { metadata } from '@/app/(frontend)/about/page'; +import { personJsonLd, organizationJsonLd } from '@/lib/jsonld'; + +describe('/about metadata', () => { + it('canonicalises to the bare path', () => { + expect(metadata.alternates?.canonical) + .toBe('https://www.schoolcompare.co.uk/about'); + }); +}); + +describe('author structured data', () => { + it('describes a Person with a first name and a photo', () => { + const person = personJsonLd(); + expect(person['@type']).toBe('Person'); + expect(person.name).toBe('Tudor'); + expect(person.image).toBe('https://www.schoolcompare.co.uk/brand/tudor.jpg'); + expect(person.url).toBe('https://www.schoolcompare.co.uk/about'); + }); + + it('never publishes a surname or an employer', () => { + // Author identity constraint: first name only. A surname here would be + // the one place it leaks, since JSON-LD is machine-read and archived. + const serialised = JSON.stringify(personJsonLd()); + expect(serialised).not.toMatch(/familyName|Sitaru/i); + expect(serialised).not.toMatch(/worksFor|affiliation/i); + }); + + it('describes the site as an Organization the Person authors for', () => { + const org = organizationJsonLd(); + expect(org['@type']).toBe('Organization'); + expect(org.name).toBe('schoolcompare'); + expect(org.url).toBe('https://www.schoolcompare.co.uk'); + }); +}); diff --git a/nextjs-app/app/(frontend)/about/About.module.css b/nextjs-app/app/(frontend)/about/About.module.css new file mode 100644 index 0000000..09dafcb --- /dev/null +++ b/nextjs-app/app/(frontend)/about/About.module.css @@ -0,0 +1,83 @@ +.page { + max-width: 42rem; + margin: 0 auto; + padding: 2.5rem 1.25rem 4rem; +} + +.header { + display: flex; + align-items: center; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.portrait { + border-radius: 50%; + border: 2px solid var(--border); + object-fit: cover; + flex-shrink: 0; +} + +.kicker { + font-family: var(--font-ui); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--brand); + margin: 0 0 0.35rem; +} + +.heading { + font-family: var(--font-display); + font-size: clamp(1.5rem, 4vw, 2rem); + font-weight: 700; + line-height: 1.2; + color: var(--text-primary); + margin: 0; +} + +.subheading { + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 600; + color: var(--text-primary); + margin: 2.25rem 0 0.75rem; +} + +.prose p { + font-family: var(--font-ui); + font-size: 1rem; + line-height: 1.7; + color: var(--text-secondary); + margin: 0 0 1.1rem; +} + +/* The opening paragraph carries the page. Larger, and in the primary ink + rather than the secondary, so it reads as a voice rather than as body copy. */ +.lede { + font-size: 1.125rem; + color: var(--text-primary); +} + +.prose .lede { + font-size: 1.125rem; + color: var(--text-primary); +} + +.link { + color: var(--brand); + font-weight: 600; +} + +.link:hover { + color: var(--brand-strong); +} + +@media (max-width: 480px) { + .header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } +} diff --git a/nextjs-app/app/(frontend)/about/page.tsx b/nextjs-app/app/(frontend)/about/page.tsx new file mode 100644 index 0000000..8dea136 --- /dev/null +++ b/nextjs-app/app/(frontend)/about/page.tsx @@ -0,0 +1,129 @@ +import type { Metadata } from 'next'; +import Image from 'next/image'; +import { absoluteUrl } from '@/lib/site'; +import { personJsonLd, organizationJsonLd } from '@/lib/jsonld'; +import styles from './About.module.css'; + +export const metadata: Metadata = { + title: 'About', + description: + 'Who builds schoolcompare, why it exists, and where its numbers come from.', + alternates: { canonical: absoluteUrl('/about') }, +}; + +export default function AboutPage() { + const jsonLd = { + '@context': 'https://schema.org', + '@graph': [personJsonLd(), organizationJsonLd()], + }; + + return ( +
+