From 9f47ccaa2360da2d6b512283b26161ffe2ce51c9 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 12:57:26 +0000 Subject: [PATCH 1/2] Give every page its own meta description Pattern pages use the same opening-paragraph summary the catalog shows; extractSummary moves to lib/markdown so both share it. Other pages use their intro text, stored once and rendered by the page too. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b16-89b6-7301-9d0c-e6ff7f681d32 Co-authored-by: Lada Kesseler --- website/app/[category]/[slug]/page.tsx | 6 +- website/app/[category]/page.tsx | 3 +- website/app/contributors/[authorId]/page.tsx | 7 ++- website/app/contributors/page.tsx | 6 +- website/app/page.tsx | 14 +++-- website/app/pattern-catalog/page.tsx | 51 +--------------- website/app/talk/page.tsx | 1 + website/lib/markdown.ts | 48 +++++++++++++++ website/tests/unit/components/pages.test.tsx | 62 +++++++++++++++++++- 9 files changed, 136 insertions(+), 62 deletions(-) diff --git a/website/app/[category]/[slug]/page.tsx b/website/app/[category]/[slug]/page.tsx index c7419dd..f1ecfa9 100644 --- a/website/app/[category]/[slug]/page.tsx +++ b/website/app/[category]/[slug]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import { getPatternBySlug, getPatternSlugs, titleToSlug } from "@/lib/markdown"; +import { extractSummary, getPatternBySlug, getPatternSlugs, titleToSlug } from "@/lib/markdown"; import { getCategoryConfig, isValidCategory } from "@/app/lib/category-config"; import { basePath } from "@/lib/config"; import { PatternCategory, PatternContent } from "@/lib/types"; @@ -57,7 +57,9 @@ export async function generateMetadata({ params }: PatternPageProps): Promise

{pageTitle}

-

- Thank you to all the contributors who have documented patterns, anti-patterns, and obstacles. -

+

{intro}

Want to contribute? Read the{" "}

Augmented Coding Patterns

-

- A collection of emerging patterns, anti-patterns and obstacles for effective AI-augmented software development. - Learn how to collaborate successfully with AI coding assistants and avoid common pitfalls. -

+

{intro}

diff --git a/website/app/pattern-catalog/page.tsx b/website/app/pattern-catalog/page.tsx index b701032..99fbdbc 100644 --- a/website/app/pattern-catalog/page.tsx +++ b/website/app/pattern-catalog/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import styles from "./page.module.css"; import { PATTERN_CATALOG_GROUPS } from "./constants"; -import { getAllPatterns } from "@/lib/markdown"; +import { extractSummary, getAllPatterns } from "@/lib/markdown"; import { getAuthorById } from "@/lib/authors"; import CatalogView from "./CatalogView"; import { CatalogGroupData, CatalogPreviewItem } from "./types"; @@ -11,56 +11,9 @@ const catalogTitle = "Complete Catalog"; export const metadata: Metadata = { title: catalogTitle, + description: "Every pattern, anti-pattern, and obstacle in one filterable catalog, each with a short summary and its authors.", }; -function extractSummary(markdown: string): string | undefined { - const lines = markdown.split("\n"); - const summaryLines: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - - if (trimmed.length === 0) { - if (summaryLines.length > 0) { - break; - } - continue; - } - - if (/^#{1,6}\s/.test(trimmed)) { - continue; - } - - if (/^[-*]\s/.test(trimmed)) { - continue; - } - - if (/^>/.test(trimmed)) { - continue; - } - - const plain = trimmed - .replace(/\*\*(.*?)\*\*/g, "$1") - .replace(/_(.*?)_/g, "$1") - .replace(/`(.*?)`/g, "$1") - .replace(/\[(.*?)]\((.*?)\)/g, "$1"); - - summaryLines.push(plain); - - if (summaryLines.join(" ").length >= 220) { - break; - } - } - - const summary = summaryLines.join(" ").trim(); - - if (!summary) { - return undefined; - } - - return summary.length > 240 ? `${summary.slice(0, 237).trimEnd()}…` : summary; -} - function resolveAuthorNames(authorIds: string[] | undefined): string[] { if (!authorIds || authorIds.length === 0) { return []; diff --git a/website/app/talk/page.tsx b/website/app/talk/page.tsx index 577bc2b..c7aedf5 100644 --- a/website/app/talk/page.tsx +++ b/website/app/talk/page.tsx @@ -16,6 +16,7 @@ const currentMapTitle = "Patterns for Coding with AI"; export const metadata: Metadata = { title: currentMapTitle, + description: "Interactive maps of the patterns, anti-patterns, and obstacles from the Augmented Coding talks, with links to the recorded walkthroughs.", }; type MapIndex = Record; diff --git a/website/lib/markdown.ts b/website/lib/markdown.ts index 8925817..ed12653 100644 --- a/website/lib/markdown.ts +++ b/website/lib/markdown.ts @@ -61,6 +61,54 @@ function validateSlug(slug: string): void { } } +export function extractSummary(markdown: string): string | undefined { + const lines = markdown.split('\n') + const summaryLines: string[] = [] + + for (const line of lines) { + const trimmed = line.trim() + + if (trimmed.length === 0) { + if (summaryLines.length > 0) { + break + } + continue + } + + if (/^#{1,6}\s/.test(trimmed)) { + continue + } + + if (/^[-*]\s/.test(trimmed)) { + continue + } + + if (/^>/.test(trimmed)) { + continue + } + + const plain = trimmed + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/_(.*?)_/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\[(.*?)]\((.*?)\)/g, '$1') + + summaryLines.push(plain) + + if (summaryLines.join(' ').length >= 220) { + break + } + } + + const summary = summaryLines.join(' ').trim() + + if (!summary) { + return undefined + } + + return summary.length > 240 ? `${summary.slice(0, 237).trimEnd()}…` : summary +} + export function slugToTitleCase(slug: string): string { return slug .split('-') diff --git a/website/tests/unit/components/pages.test.tsx b/website/tests/unit/components/pages.test.tsx index a9a67bd..6a9278f 100644 --- a/website/tests/unit/components/pages.test.tsx +++ b/website/tests/unit/components/pages.test.tsx @@ -1,9 +1,9 @@ import { render, screen } from '@testing-library/react' import { metadata as siteMetadata } from '@/app/layout' -import Home from '@/app/page' +import Home, { metadata as homeMetadata } from '@/app/page' import CategoryPage, { generateMetadata as generateCategoryMetadata } from '@/app/[category]/page' import PatternPage, { generateMetadata as generatePatternMetadata } from '@/app/[category]/[slug]/page' -import { metadata as contributorsMetadata } from '@/app/contributors/page' +import ContributorsPage, { metadata as contributorsMetadata } from '@/app/contributors/page' import { generateMetadata as generateContributorMetadata } from '@/app/contributors/[authorId]/page' import { metadata as patternCatalogMetadata } from '@/app/pattern-catalog/page' import { metadata as talkMetadata } from '@/app/talk/page' @@ -24,6 +24,7 @@ jest.mock('@/lib/markdown', () => ({ getAllPatterns: jest.fn(() => []), getPatternBySlug: jest.fn(), titleToSlug: jest.requireActual('@/lib/markdown').titleToSlug, + extractSummary: jest.requireActual('@/lib/markdown').extractSummary, })) jest.mock('@/lib/contributors', () => ({ @@ -122,6 +123,63 @@ describe('Page titles', () => { }) }) +describe('Page descriptions', () => { + afterEach(() => { + mockGetPatternSlugs.mockReset() + mockGetPatternBySlug.mockReset() + mockGetContributorDetailById.mockReset() + }) + + it('describes the home page with its intro text', () => { + mockGetPatternSlugs.mockReturnValue([]) + render() + + expect(homeMetadata.description).toBe(screen.getByText(/^A collection of emerging patterns/).textContent) + }) + + it('describes a pattern page with the opening paragraph of the pattern as plain text', async () => { + mockGetPatternBySlug.mockReturnValue({ + title: 'Answer Injection', + slug: 'answer-injection', + category: 'anti-patterns', + content: '## Problem\n\nPutting **the answer** you expect into the [prompt](/x/) makes the model echo it.\n\nSecond paragraph.', + rawContent: '' + }) + const params = Promise.resolve({ category: 'anti-patterns', slug: 'answer-injection' }) + + expect((await generatePatternMetadata({ params })).description) + .toBe('Putting the answer you expect into the prompt makes the model echo it.') + }) + + it('describes a category page with the intro text it shows', async () => { + const params = Promise.resolve({ category: 'obstacles' }) + render(await CategoryPage({ params: Promise.resolve({ category: 'obstacles' }) })) + + expect((await generateCategoryMetadata({ params })).description) + .toBe(screen.getByText(/^Understand the inherent limitations/).textContent) + }) + + it('describes the contributors page with its intro text', () => { + render() + + expect(contributorsMetadata.description).toBe(screen.getByText(/^Thank you to all the contributors/).textContent) + }) + + it('describes a contributor page by naming the contributor', async () => { + mockGetContributorDetailById.mockReturnValue({ name: 'Ivett Ördög' } as ContributorDetail) + const params = Promise.resolve({ authorId: 'ivett_ordog' }) + + expect((await generateContributorMetadata({ params })).description).toContain('Ivett Ördög') + }) + + it('gives the pattern catalog and talk pages distinct descriptions of their own', () => { + const descriptions = [homeMetadata, patternCatalogMetadata, talkMetadata].map(m => m.description) + + expect(descriptions.every(Boolean)).toBe(true) + expect(new Set(descriptions).size).toBe(descriptions.length) + }) +}) + describe('Home Page', () => { beforeEach(() => { mockGetPatternSlugs.mockImplementation((category) => { From cd8e7e1e07e2f385830ef401a24442596318174c Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 14:35:31 +0000 Subject: [PATCH 2/2] Summarise patterns from their first real paragraph extractSummary skipped headings, lists and quotes but not a leading "Related:" or "Closely related:" line, so Feedback Loop was described by its related-reading link instead of its opening paragraph. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b16-89b6-7301-9d0c-e6ff7f681d32 Co-authored-by: Lada Kesseler --- website/lib/markdown.ts | 4 ++++ website/tests/unit/markdown.test.ts | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/website/lib/markdown.ts b/website/lib/markdown.ts index ed12653..40d6bef 100644 --- a/website/lib/markdown.ts +++ b/website/lib/markdown.ts @@ -87,6 +87,10 @@ export function extractSummary(markdown: string): string | undefined { continue } + if (/^(closely\s+)?related:/i.test(trimmed)) { + continue + } + const plain = trimmed .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/_(.*?)_/g, '$1') diff --git a/website/tests/unit/markdown.test.ts b/website/tests/unit/markdown.test.ts index 06a5e9f..7bab4a4 100644 --- a/website/tests/unit/markdown.test.ts +++ b/website/tests/unit/markdown.test.ts @@ -1,6 +1,6 @@ import * as fs from 'fs' import * as path from 'path' -import { getPatternSlugs, getAllPatterns, slugToTitleCase, titleToSlug } from '@/lib/markdown' +import { getPatternSlugs, getAllPatterns, slugToTitleCase, titleToSlug, extractSummary } from '@/lib/markdown' import * as relationships from '@/lib/relationships' import { existingPattern } from '../support/existing-pattern' @@ -621,3 +621,24 @@ AI defaults to silent compliance.` }) }) }) + +describe('extractSummary', () => { + it('summarises with the first real paragraph, skipping a leading related-reading line', () => { + const content = [ + 'Closely related: ["Engineer the Harness"](https://example.com/harness)', + '', + '## Pattern', + 'Set up automated feedback, give AI permission to iterate.', + '', + 'Later paragraph.', + ].join('\n') + + expect(extractSummary(content)).toBe('Set up automated feedback, give AI permission to iterate.') + }) + + it('keeps an opening paragraph that comes before a related line', () => { + const content = 'Names beat descriptions. Always.\n\nRelated: [Catalog](https://example.com/)' + + expect(extractSummary(content)).toBe('Names beat descriptions. Always.') + }) +})