diff --git a/Makefile b/Makefile index ffaac61..4b92fbb 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,7 @@ preview: ## @setup Serve the built site, search included, on the development por @echo "Built site at $(SITE_URL)" .PHONY: og-image -og-image: ## @setup Draw the link-preview card into public/og-image.png +og-image: ## @setup Draw the link-preview cards into public/og-image.png and public/og/ @docker run --rm -u $(HOST_USER) -e HOME=/tmp -v $(CURDIR):/app -w /app python:3.12-slim \ sh -c "pip install --quiet --no-cache-dir --user pillow==11.3.0 && python tools/og-image.py" diff --git a/README.md b/README.md index f48719e..d3ad72e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ To serve the built site, search included, as it is published, run: make preview ``` -To draw the link-preview card into `public/og-image.png`, run: +To draw the link-preview cards, `public/og-image.png` and one per library in `public/og/`, run the command +below. Run it again after adding a library to the catalog: ```bash make og-image diff --git a/public/og/building-blocks.png b/public/og/building-blocks.png new file mode 100644 index 0000000..d3a9f0f Binary files /dev/null and b/public/og/building-blocks.png differ diff --git a/public/og/collection.png b/public/og/collection.png new file mode 100644 index 0000000..9a90aa4 Binary files /dev/null and b/public/og/collection.png differ diff --git a/public/og/country.png b/public/og/country.png new file mode 100644 index 0000000..13ff336 Binary files /dev/null and b/public/og/country.png differ diff --git a/public/og/currency.png b/public/og/currency.png new file mode 100644 index 0000000..8817ee7 Binary files /dev/null and b/public/og/currency.png differ diff --git a/public/og/docker-container.png b/public/og/docker-container.png new file mode 100644 index 0000000..53fc8f2 Binary files /dev/null and b/public/og/docker-container.png differ diff --git a/public/og/encoder.png b/public/og/encoder.png new file mode 100644 index 0000000..eed0293 Binary files /dev/null and b/public/og/encoder.png differ diff --git a/public/og/environment-variable.png b/public/og/environment-variable.png new file mode 100644 index 0000000..88542fc Binary files /dev/null and b/public/og/environment-variable.png differ diff --git a/public/og/http-correlation-id.png b/public/og/http-correlation-id.png new file mode 100644 index 0000000..26bb6c2 Binary files /dev/null and b/public/og/http-correlation-id.png differ diff --git a/public/og/http-error-handler.png b/public/og/http-error-handler.png new file mode 100644 index 0000000..13eb1ac Binary files /dev/null and b/public/og/http-error-handler.png differ diff --git a/public/og/http-health-check.png b/public/og/http-health-check.png new file mode 100644 index 0000000..82d076b Binary files /dev/null and b/public/og/http-health-check.png differ diff --git a/public/og/http-logging.png b/public/og/http-logging.png new file mode 100644 index 0000000..e70c668 Binary files /dev/null and b/public/og/http-logging.png differ diff --git a/public/og/http-query.png b/public/og/http-query.png new file mode 100644 index 0000000..eeafb6e Binary files /dev/null and b/public/og/http-query.png differ diff --git a/public/og/http.png b/public/og/http.png new file mode 100644 index 0000000..24feee1 Binary files /dev/null and b/public/og/http.png differ diff --git a/public/og/ksuid.png b/public/og/ksuid.png new file mode 100644 index 0000000..c4019a3 Binary files /dev/null and b/public/og/ksuid.png differ diff --git a/public/og/logger.png b/public/og/logger.png new file mode 100644 index 0000000..3b43f6d Binary files /dev/null and b/public/og/logger.png differ diff --git a/public/og/mapper.png b/public/og/mapper.png new file mode 100644 index 0000000..ff9270b Binary files /dev/null and b/public/og/mapper.png differ diff --git a/public/og/math.png b/public/og/math.png new file mode 100644 index 0000000..ab963a2 Binary files /dev/null and b/public/og/math.png differ diff --git a/public/og/outbox.png b/public/og/outbox.png new file mode 100644 index 0000000..20f77aa Binary files /dev/null and b/public/og/outbox.png differ diff --git a/public/og/time.png b/public/og/time.png new file mode 100644 index 0000000..b227478 Binary files /dev/null and b/public/og/time.png differ diff --git a/public/og/value-object.png b/public/og/value-object.png new file mode 100644 index 0000000..1cd5a78 Binary files /dev/null and b/public/og/value-object.png differ diff --git a/src/components/LibraryPage.astro b/src/components/LibraryPage.astro index 8de0ac7..2c0852f 100644 --- a/src/components/LibraryPage.astro +++ b/src/components/LibraryPage.astro @@ -38,6 +38,8 @@ const relations = [ description={description} path={`/${data.name}/`} kind="article" + image={{ path: `/og/${data.name}.png`, alt: packageName }} + markdown={`/${data.name}.md`} structuredData={{ '@graph': [ { diff --git a/src/content/markdown.test.ts b/src/content/markdown.test.ts new file mode 100644 index 0000000..cb5f386 --- /dev/null +++ b/src/content/markdown.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { demote, libraryMarkdown } from './markdown' + +describe('the Markdown of a library', () => { + it('opens with what the library is and how to install it, then carries its documentation', () => { + // Given a library with its documentation + const library = { + name: 'time', + title: 'Time', + description: 'Models time as immutable value objects for PHP.', + version: '2.7.2', + body: '\n## Overview\n\nInstants and durations.\n', + } + + // When its Markdown is drawn + const markdown = libraryMarkdown(library, 'https://tiny-blocks.github.io/time/') + + // Then it names the package, its version and its page, then the documentation follows + expect(markdown).toBe( + [ + '# Time', + '', + '> Models time as immutable value objects for PHP.', + '', + 'The Composer package `tiny-blocks/time`, at version 2.7.2, documented at https://tiny-blocks.github.io/time/.', + '', + 'Install it with `composer require tiny-blocks/time`.', + '', + '## Overview', + '', + 'Instants and durations.', + '', + ].join('\n'), + ) + }) +}) + +describe('a document moved one heading level down', () => { + it('moves every heading down and leaves the lines of a code block alone', () => { + // Given a document with headings and a code block whose comment opens with a hash + const markdown = ['# Encoder', '', '## How to use', '', '```bash', '# Output: Hello', '```'].join('\n') + + // When it is moved down + const demoted = demote(markdown) + + // Then the headings move and the comment stays + expect(demoted).toBe(['## Encoder', '', '### How to use', '', '```bash', '# Output: Hello', '```'].join('\n')) + }) +}) diff --git a/src/content/markdown.ts b/src/content/markdown.ts new file mode 100644 index 0000000..1d645f2 --- /dev/null +++ b/src/content/markdown.ts @@ -0,0 +1,45 @@ +/** + * The documentation of a library as plain Markdown, for the language models and agents that read the site + * rather than render it: what the library is, how to install it, then its documentation as its page shows it. + */ + +export interface MarkdownSource { + readonly name: string + readonly title: string + readonly description: string + readonly version: string + readonly body: string +} + +/** Answers the Markdown of a library, whose page is at the given address. */ +export function libraryMarkdown(library: MarkdownSource, page: string): string { + const packageName = `tiny-blocks/${library.name}` + return [ + `# ${library.title}`, + '', + `> ${library.description}`, + '', + `The Composer package \`${packageName}\`, at version ${library.version}, documented at ${page}.`, + '', + `Install it with \`composer require ${packageName}\`.`, + '', + library.body.trim(), + '', + ].join('\n') +} + +const FENCE = /^\s*(```|~~~)/ + +/** Moves every heading one level down, so a document nests under a heading of its own, code left alone. */ +export function demote(markdown: string): string { + let fenced = false + return markdown + .split('\n') + .map((line) => { + if (FENCE.test(line)) { + fenced = !fenced + } + return !fenced && /^#{1,5} /.test(line) ? `#${line}` : line + }) + .join('\n') +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 68a1b5d..ac4c655 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -19,14 +19,27 @@ interface Props { noindex?: boolean /** The schema.org description of the page, published as JSON-LD for search engines. */ structuredData?: Record + /** The link-preview card of the page from the root, and what it shows. The card of the site otherwise. */ + image?: { path: string; alt: string } + /** The address from the root of the same page as plain Markdown, for language models and agents. */ + markdown?: string } -const { title, description, path, kind = 'website', noindex = false, structuredData } = Astro.props +const { + title, + description, + path, + kind = 'website', + noindex = false, + structuredData, + image = { path: '/og-image.png', alt: SITE.name }, + markdown, +} = Astro.props const documentTitle = title ? `${title} ยท ${SITE.name}` : SITE.meta.title const documentDescription = description ?? SITE.meta.description const origin = Astro.site ?? Astro.url.origin const canonical = path === null ? null : new URL(path, origin).href -const ogImage = new URL('/og-image.png', origin).href +const ogImage = new URL(image.path, origin).href /** Escapes `<` so nothing in the data can close the script element it is published in. */ const jsonLd = structuredData ? JSON.stringify({ '@context': 'https://schema.org', ...structuredData }).replace(/ - + + {markdown ? : null} {jsonLd ?