Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions website/app/[category]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -57,7 +57,9 @@ export async function generateMetadata({ params }: PatternPageProps): Promise<Me
}

const resolved = resolvePattern(category as PatternCategory, slug);
return resolved ? { title: resolved.pattern.title } : {};
return resolved
? { title: resolved.pattern.title, description: extractSummary(resolved.pattern.content) }
: {};
}

interface ResolvedPattern {
Expand Down
3 changes: 2 additions & 1 deletion website/app/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export async function generateMetadata({ params }: CategoryPageProps): Promise<M
return {};
}

return { title: getCategoryConfig(category as PatternCategory).labelPlural };
const config = getCategoryConfig(category as PatternCategory);
return { title: config.labelPlural, description: config.listDescription };
}

export default async function CategoryPage({ params }: CategoryPageProps) {
Expand Down
7 changes: 6 additions & 1 deletion website/app/contributors/[authorId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export async function generateMetadata({ params }: ContributorDetailPageProps):
const { authorId } = await params;
const contributor = getContributorDetailById(authorId);

return contributor ? { title: contributor.name } : {};
return contributor
? {
title: contributor.name,
description: `Patterns, anti-patterns, and obstacles contributed by ${contributor.name}.`,
}
: {};
}

export default async function ContributorDetailPage({ params }: ContributorDetailPageProps) {
Expand Down
6 changes: 3 additions & 3 deletions website/app/contributors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { siteConfig } from "@/config/site";
import styles from "./page.module.css";

const pageTitle = "Contributors";
const intro = "Thank you to all the contributors who have documented patterns, anti-patterns, and obstacles.";

export const metadata: Metadata = {
title: pageTitle,
description: intro,
};

export default function ContributorsPage() {
Expand All @@ -18,9 +20,7 @@ export default function ContributorsPage() {
<div className={styles.container}>
<header className={styles.header}>
<h1 className={styles.title}>{pageTitle}</h1>
<p className={styles.description}>
Thank you to all the contributors who have documented patterns, anti-patterns, and obstacles.
</p>
<p className={styles.description}>{intro}</p>
<p className={styles.description}>
Want to contribute? Read the{" "}
<Link
Expand Down
14 changes: 10 additions & 4 deletions website/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import type { Metadata } from "next";
import Link from "next/link";
import styles from "./page.module.css";
import { getPatternSlugs, getAllPatterns } from "@/lib/markdown";
import { getGraphData } from "@/lib/graph-data";
import RelationshipGraph from "./components/RelationshipGraph";
import SearchBar from "./components/SearchBar";

const intro =
"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.";

export const metadata: Metadata = {
description: intro,
};

export default function Home() {
const patternsCount = getPatternSlugs("patterns").length;
const antiPatternsCount = getPatternSlugs("anti-patterns").length;
Expand All @@ -20,10 +29,7 @@ export default function Home() {
<div className={styles.container}>
<section className={styles.hero}>
<h1>Augmented Coding Patterns</h1>
<p>
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.
</p>
<p>{intro}</p>
<div className={styles.searchContainer}>
<SearchBar patterns={allPatterns} />
</div>
Expand Down
51 changes: 2 additions & 49 deletions website/app/pattern-catalog/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 [];
Expand Down
1 change: 1 addition & 0 deletions website/app/talk/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { name: string; category: string; slug: string }>;
Expand Down
52 changes: 52 additions & 0 deletions website/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,58 @@ 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
}

if (/^(closely\s+)?related:/i.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('-')
Expand Down
62 changes: 60 additions & 2 deletions website/tests/unit/components/pages.test.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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', () => ({
Expand Down Expand Up @@ -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(<Home />)

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(<ContributorsPage />)

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) => {
Expand Down
23 changes: 22 additions & 1 deletion website/tests/unit/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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.')
})
})
Loading