diff --git a/docs/bench/architecture/read-path.md b/docs/bench/architecture/read-path.md index 9c361714..0ac984d3 100644 --- a/docs/bench/architecture/read-path.md +++ b/docs/bench/architecture/read-path.md @@ -123,7 +123,7 @@ order is the curated `GROUP_ORDER` in `queries.ts`: Compression, Compression Size, Random Access, Clickbench, TPC-H (NVMe/S3) SF=1, SF=10, SF=100 (alternating), TPC-DS (NVMe) SF=1, -Statistical and Population Genetics, PolarSignals Profiling, +Statistical and Population Genetics, Polar Signals Profiling, fineweb (NVMe/S3), Appian (NVMe) ``` diff --git a/scripts/parity/README.md b/scripts/parity/README.md index 88a3b818..f5d50004 100644 --- a/scripts/parity/README.md +++ b/scripts/parity/README.md @@ -120,7 +120,7 @@ JSON path with both values: ```text /api/group/{slug}?n=50: body differs $.charts[3].series["datafusion:parquet"][41]: 1.25 != 1.26 /api/chart/{slug}?n=100: cache-control "public, s-maxage=300, stale-while-revalidate=3600" != "no-store" -/chart/{slug}: title "TPC-H Q1 - Vortex Benchmarks" != "Vortex Benchmarks" +/chart/{slug}: title "TPC-H Q1 | Vortex" != "Benchmarks | Vortex" ``` When two bodies parse to the same JSON but differ in bytes, the line gives the byte offset and the diff --git a/src/app/(bench)/bench/chart/[slug]/page.tsx b/src/app/(bench)/bench/chart/[slug]/page.tsx index efdb5c2f..cb6eee6d 100644 --- a/src/app/(bench)/bench/chart/[slug]/page.tsx +++ b/src/app/(bench)/bench/chart/[slug]/page.tsx @@ -114,7 +114,7 @@ const getChart = cache(async (slug: string, n: string | null): Promise { const base = await requestBenchBase(); return { - title: 'Vortex Benchmarks', + title: 'Benchmarks | Vortex', description: BENCH_DESCRIPTION, // The canonical and Open Graph URLs are bench's public ones, written out // in full. No `metadataBase`: it would also resolve the icons' paths, which // belong to the host serving the page, against bench.vortex.dev. - openGraph: openGraph('Vortex Benchmarks', BENCH_DESCRIPTION, `${BENCH_URL}/`), + openGraph: openGraph('Benchmarks | Vortex', BENCH_DESCRIPTION, `${BENCH_URL}/`), // Theme-aware favicons, ported from `render.rs::favicon_links`: the black // sigil on light-mode tabs, the white sigil on dark-mode tabs, with the dark // sigil as the unmediated fallback (and the apple-touch icon). The files are diff --git a/src/app/(bench)/bench/methodology/page.tsx b/src/app/(bench)/bench/methodology/page.tsx index 99f67f04..d6634bb1 100644 --- a/src/app/(bench)/bench/methodology/page.tsx +++ b/src/app/(bench)/bench/methodology/page.tsx @@ -8,7 +8,7 @@ import { BenchHeader } from '@/bench/components/BenchHeader'; import { METHODOLOGY } from '@/bench/lib/methodology'; import { BENCH_URL, openGraph } from '@/bench/lib/site-files'; -const TITLE = 'Methodology - Vortex Benchmarks'; +const TITLE = 'Methodology | Vortex'; const DESCRIPTION = 'How the Vortex benchmarks run: the machines, storage, engines, and formats, how each ' + 'result is measured, and how the summaries are scored.'; diff --git a/src/app/(site)/blog/[slug]/opengraph-image.tsx b/src/app/(site)/blog/[slug]/opengraph-image.tsx index a5710655..e1db7910 100644 --- a/src/app/(site)/blog/[slug]/opengraph-image.tsx +++ b/src/app/(site)/blog/[slug]/opengraph-image.tsx @@ -1,6 +1,7 @@ import { notFound } from "next/navigation"; import { getAllSlugs, getPostBySlug } from "@/lib/blog"; -import { OG_CONTENT_TYPE, OG_SIZE, renderPostOgImage } from "@/lib/og"; +import { siteName } from "@/lib/constants"; +import { OG_CONTENT_TYPE, OG_SIZE, renderOgCard } from "@/lib/og"; import { formatPostDate } from "@/lib/posts"; export const alt = "Vortex blog post"; @@ -22,9 +23,11 @@ export default async function OgImage({ const post = getPostBySlug(slug); if (!post) notFound(); - return renderPostOgImage({ - slug, - title: post.title, - date: formatPostDate(post.date) + return renderOgCard({ + seed: slug, + eyebrow: `${siteName} blog`, + title: [post.title], + face: "display", + foot: formatPostDate(post.date) }); } diff --git a/src/app/(site)/blog/[slug]/page.tsx b/src/app/(site)/blog/[slug]/page.tsx index 1b8f1608..f02dfbc3 100644 --- a/src/app/(site)/blog/[slug]/page.tsx +++ b/src/app/(site)/blog/[slug]/page.tsx @@ -28,17 +28,17 @@ export async function generateMetadata({ if (!post) { return { - title: "Post Not Found | Vortex Blog" + title: "Post not found | Vortex" }; } const description = post.excerpt || `Read "${post.title}" on the Vortex blog`; return { - title: `${post.title} | Vortex Blog`, + title: `${post.title} | Vortex`, description, openGraph: { - title: `${post.title} | Vortex Blog`, + title: `${post.title} | Vortex`, description, siteName, url: `${baseUrl}/blog/${slug}`, diff --git a/src/app/(site)/blog/opengraph-image.tsx b/src/app/(site)/blog/opengraph-image.tsx index cae9177c..23ca238c 100644 --- a/src/app/(site)/blog/opengraph-image.tsx +++ b/src/app/(site)/blog/opengraph-image.tsx @@ -1,12 +1,16 @@ -import { OG_CONTENT_TYPE, OG_SIZE, renderOgImage } from "@/lib/og"; +import { siteName } from "@/lib/constants"; +import { OG_CONTENT_TYPE, OG_SIZE, renderOgCard } from "@/lib/og"; -export const alt = "Vortex Blog"; +export const alt = "Vortex blog"; export const size = OG_SIZE; export const contentType = OG_CONTENT_TYPE; export default function OgImage() { - return renderOgImage({ - title: "Blog", - description: "Updates and insights from the Vortex team." + return renderOgCard({ + seed: "blog", + eyebrow: siteName, + title: ["Blog"], + face: "display", + foot: "Updates and insights from the Vortex team." }); } diff --git a/src/app/(site)/opengraph-image.tsx b/src/app/(site)/opengraph-image.tsx index 006ff75e..4b33e5a3 100644 --- a/src/app/(site)/opengraph-image.tsx +++ b/src/app/(site)/opengraph-image.tsx @@ -1,13 +1,18 @@ -import { OG_CONTENT_TYPE, OG_SIZE, renderOgImage } from "@/lib/og"; +import { siteName } from "@/lib/constants"; +import { OG_CONTENT_TYPE, OG_SIZE, renderOgCard } from "@/lib/og"; -export const alt = "Vortex — an extensible, SOTA columnar file format"; +export const alt = "Vortex, the frontier file format."; export const size = OG_SIZE; export const contentType = OG_CONTENT_TYPE; +// What Vortex is, over the homepage's standfirst, in the homepage's mono +// voice. The card does not repeat the homepage's headline. export default function OgImage() { - return renderOgImage({ - title: "An extensible, SOTA columnar file format", - description: - "Tools for working with compressed Apache Arrow arrays in-memory, on-disk, and over-the-wire." + return renderOgCard({ + seed: "home", + eyebrow: siteName, + title: ["The frontier", "file format"], + face: "mono", + foot: "An open-source columnar file format for fast scans, random access, and compression." }); } diff --git a/src/app/(site)/rss/[type]/route.ts b/src/app/(site)/rss/[type]/route.ts index 0c0184df..2f7969f2 100644 --- a/src/app/(site)/rss/[type]/route.ts +++ b/src/app/(site)/rss/[type]/route.ts @@ -11,7 +11,6 @@ export async function GET( // JSON Feed marks a link item (`external_url`). In RSS and Atom a link item // reads as our own post, so the three feeds would disagree. const posts = getAllPosts(); - const date = new Date(); const feed = new Feed({ title: "Vortex Blog", @@ -19,7 +18,9 @@ export async function GET( id: baseUrl, link: baseUrl, favicon: `${baseUrl}/favicon.ico`, - copyright: `All rights reserved ${date.getFullYear()}, Vortex`, + // The posts are CC-BY-4.0 (REUSE.toml), and the project is a Series of + // LF Projects, LLC. + copyright: "CC-BY-4.0, Vortex a Series of LF Projects, LLC", generator: "rss feed", feedLinks: { rss2: `${baseUrl}/rss/feed.xml`, diff --git a/src/bench/components/Chart.tsx b/src/bench/components/Chart.tsx index 210cef80..c4367df0 100644 --- a/src/bench/components/Chart.tsx +++ b/src/bench/components/Chart.tsx @@ -435,7 +435,7 @@ export function Chart({ slug, name, index, groupSlug, initialPayload }: ChartIsl step={1} defaultValue={100} data-role="scope-slider" - aria-label="Custom commit window" + aria-label="Show commits" ref={sliderRef} /> diff --git a/src/bench/components/GroupNav.test.tsx b/src/bench/components/GroupNav.test.tsx index 184bf0b7..a1e14f63 100644 --- a/src/bench/components/GroupNav.test.tsx +++ b/src/bench/components/GroupNav.test.tsx @@ -12,7 +12,7 @@ import { GroupNav, jumpToGroup, type GroupNavItem } from '@/bench/components/Gro const GROUPS: GroupNavItem[] = [ { name: 'Random Access', slug: 'random_access.abc' }, - { name: 'PolarSignals Profiling', slug: 'qmg.polar' }, + { name: 'Polar Signals Profiling', slug: 'qmg.polar' }, ]; describe('GroupNav markup', () => { @@ -25,7 +25,7 @@ describe('GroupNav markup', () => { expect(html).not.toContain('group-nav-panel--open'); expect(html).toContain('id="group-nav-panel"'); expect(html).toContain('>Random Access'); - expect(html).toContain('>PolarSignals Profiling'); + expect(html).toContain('>Polar Signals Profiling'); }); it('marks the whole menu as needing JavaScript, since neither its toggle nor its entries work without it', () => { @@ -49,7 +49,7 @@ describe('jumpToGroup', () => { document.body.innerHTML = `
-
PolarSignals
+
Polar Signals
Random Access
@@ -139,7 +139,7 @@ describe('GroupNav jump-menu clicks', () => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; document.body.innerHTML = `
-
PolarSignals
+
Polar Signals
Random Access
diff --git a/src/bench/components/SummaryCard.tsx b/src/bench/components/SummaryCard.tsx index c51bad4f..4b4aa858 100644 --- a/src/bench/components/SummaryCard.tsx +++ b/src/bench/components/SummaryCard.tsx @@ -28,7 +28,7 @@ function seriesTitle(item: SeriesRanking, coverageUnit: string): string { const label = displaySeriesLabel(item.name); return item.measured >= item.total ? label - : `${label} - measured in ${item.measured} of ${item.total} ${coverageUnit}; the rest scored by the missing-series penalty`; + : `${label} · measured in ${item.measured} of ${item.total} ${coverageUnit} · the rest scored by the missing-series penalty`; } /** diff --git a/src/bench/lib/anchor.test.ts b/src/bench/lib/anchor.test.ts index 75098764..73c91e17 100644 --- a/src/bench/lib/anchor.test.ts +++ b/src/bench/lib/anchor.test.ts @@ -37,6 +37,10 @@ describe('groupAnchors', () => { ]); }); + it('keeps the anchor a renamed group had, so shared links still land on it', () => { + expect(groupAnchor('Polar Signals Profiling')).toBe('polarsignals-profiling'); + }); + it('keeps the full production group list collision-free', () => { const names = [ 'Compression', @@ -55,7 +59,7 @@ describe('groupAnchors', () => { 'TPC-DS (S3) (SF=100)', 'Random Access', 'Statistical and Population Genetics', - 'PolarSignals Profiling', + 'Polar Signals Profiling', 'fineweb [nvme]', 'fineweb [s3]', 'appian [nvme]', diff --git a/src/bench/lib/anchor.ts b/src/bench/lib/anchor.ts index 15befe9c..f9fd9c37 100644 --- a/src/bench/lib/anchor.ts +++ b/src/bench/lib/anchor.ts @@ -11,13 +11,27 @@ * URL. The API keeps the opaque slug. */ +/** + * Anchors that stay as they were when a group's name changed, so links that + * readers already shared still land on the group. The group was named + * "PolarSignals Profiling" until its owners' spelling replaced it. + */ +const KEPT_ANCHORS: Readonly> = { + 'Polar Signals Profiling': 'polarsignals-profiling', +}; + /** * Derive a URL anchor from a group display name: lowercase, every run of * non-alphanumerics collapsed to a single `-`, leading/trailing dashes * trimmed. `TPC-H (NVMe) (SF=1)` becomes `tpc-h-nvme-sf-1`. A name with no - * alphanumerics at all falls back to `group` so the anchor is never empty. + * alphanumerics at all falls back to `group` so the anchor is never empty. A + * renamed group keeps its old anchor (`KEPT_ANCHORS`). */ export function groupAnchor(name: string): string { + const kept = KEPT_ANCHORS[name]; + if (kept !== undefined) { + return kept; + } const anchor = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') diff --git a/src/bench/lib/data-cache.test.ts b/src/bench/lib/data-cache.test.ts index 8a063e1b..3a01b286 100644 --- a/src/bench/lib/data-cache.test.ts +++ b/src/bench/lib/data-cache.test.ts @@ -69,9 +69,9 @@ describe('data-cache wrappers', () => { expect(BENCH_DATA_TAG).toBe('bench-data'); expect(DATA_CACHE_BACKSTOP_SECONDS).toBe(86400); expect(cacheCalls.map((call) => call.keyParts)).toContainEqual([ - 'data-cache:group-charts:v10:n100', + 'data-cache:group-charts:v11:n100', ]); - expect(cacheCalls.map((call) => call.keyParts)).toContainEqual(['data-cache:groups:v10']); + expect(cacheCalls.map((call) => call.keyParts)).toContainEqual(['data-cache:groups:v11']); for (const call of cacheCalls) { expect(call.options.tags).toEqual([BENCH_DATA_TAG]); expect(call.options.revalidate).toBe(DATA_CACHE_BACKSTOP_SECONDS); diff --git a/src/bench/lib/data-cache.ts b/src/bench/lib/data-cache.ts index db595fae..e9e03b25 100644 --- a/src/bench/lib/data-cache.ts +++ b/src/bench/lib/data-cache.ts @@ -56,7 +56,7 @@ const CACHE_OPTIONS = { tags: [BENCH_DATA_TAG], revalidate: DATA_CACHE_BACKSTOP_ * code can serve payloads the old code computed until the next ingest. * It is independent of the producer-facing benchmark schema version. */ -const GROUP_PAYLOAD_CACHE_VERSION = 'v10'; +const GROUP_PAYLOAD_CACHE_VERSION = 'v11'; // The default last-100 group bundle, keyed by group slug. The slug is the cache // key (an `unstable_cache` argument), so one wrapper covers every group. A diff --git a/src/bench/lib/descriptions.test.ts b/src/bench/lib/descriptions.test.ts index 2af41cf2..7cba09bd 100644 --- a/src/bench/lib/descriptions.test.ts +++ b/src/bench/lib/descriptions.test.ts @@ -28,10 +28,10 @@ describe('groupDescription', () => { 'analytics data', ); expect(groupDescription('Statistical and Population Genetics')).toBe( - 'A suite of Statistical and Population genetics queries using the gnomAD dataset', + 'A suite of statistical and population genetics queries using the gnomAD dataset', ); - expect(groupDescription('PolarSignals Profiling')).toBe( - 'Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer performance ' + + expect(groupDescription('Polar Signals Profiling')).toBe( + 'Profiling data benchmark modeled on Polar Signals/Parca, exercising scan-layer performance ' + 'with projection and filter pushdown on deeply nested schemas', ); }); diff --git a/src/bench/lib/descriptions.ts b/src/bench/lib/descriptions.ts index db581ac2..5fe7d638 100644 --- a/src/bench/lib/descriptions.ts +++ b/src/bench/lib/descriptions.ts @@ -61,10 +61,10 @@ function staticDescription(name: string): string | null { 'analytics data' ); case 'Statistical and Population Genetics': - return 'A suite of Statistical and Population genetics queries using the gnomAD dataset'; - case 'PolarSignals Profiling': + return 'A suite of statistical and population genetics queries using the gnomAD dataset'; + case 'Polar Signals Profiling': return ( - 'Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer ' + + 'Profiling data benchmark modeled on Polar Signals/Parca, exercising scan-layer ' + 'performance with projection and filter pushdown on deeply nested schemas' ); default: diff --git a/src/bench/lib/groups.test.ts b/src/bench/lib/groups.test.ts index bf2d6661..3784b8d7 100644 --- a/src/bench/lib/groups.test.ts +++ b/src/bench/lib/groups.test.ts @@ -1252,16 +1252,16 @@ describe('groupNameQuery v2-name special-cases (PR-5.0.5, no DB)', () => { const name = groupNameQuery('statpopgen', null, null, 'nvme'); expect(name).toBe('Statistical and Population Genetics'); expect(groupDescription(name)).toBe( - 'A suite of Statistical and Population genetics queries using the gnomAD dataset', + 'A suite of statistical and population genetics queries using the gnomAD dataset', ); }); it('maps polarsignals to its v2 name and attaches the profiling description', () => { // Same null scale_factor as statpopgen in production. const name = groupNameQuery('polarsignals', null, null, 'nvme'); - expect(name).toBe('PolarSignals Profiling'); + expect(name).toBe('Polar Signals Profiling'); expect(groupDescription(name)).toBe( - 'Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer ' + + 'Profiling data benchmark modeled on Polar Signals/Parca, exercising scan-layer ' + 'performance with projection and filter pushdown on deeply nested schemas', ); }); diff --git a/src/bench/lib/methodology.ts b/src/bench/lib/methodology.ts index 29d2f75b..28f308dd 100644 --- a/src/bench/lib/methodology.ts +++ b/src/bench/lib/methodology.ts @@ -26,14 +26,14 @@ export interface MethodologySection { export const SCORES = { query: 'the geometric mean, over the queries, of the series’ time divided by the fastest ' + - 'series’ time on that query. 1.00x is the fastest; lower is better.', + 'series’ time on that query. 1.00× is the fastest. Lower is better.', /** No vector search results reach bench yet; `llms.txt` prints this only if some do. */ vectorSearch: 'the geometric mean, over the thresholds, of the series’ time divided by the fastest ' + - 'series’ time at that threshold. 1.00x is the fastest; lower is better.', + 'series’ time at that threshold. 1.00× is the fastest. Lower is better.', randomAccess: 'the geometric mean, over the datasets, of the series’ time divided by the fastest ' + - 'series’ time on that dataset. 1.00x is the fastest; lower is better.', + 'series’ time on that dataset. 1.00× is the fastest. Lower is better.', throughput: 'the geometric mean, over the datasets, of the format’s throughput divided by ' + 'Parquet’s. Higher is better.', @@ -49,8 +49,8 @@ export const METHODOLOGY: readonly MethodologySection[] = [ 'Every push to the develop branch of https://github.com/vortex-data/vortex runs the ' + 'compression, string encoding, random access, and SQL query benchmarks, and bench ' + 'records their results. A nightly run at 00:00 UTC adds TPC-H at scale factor 100 ' + - 'and TPC-DS at scale factors 10 and 100. Benchmarks run on pull requests only ' + - 'comment on the pull request; their results never reach bench.', + 'and TPC-DS at scale factors 10 and 100. Benchmarks that run on pull requests only ' + + 'comment on the pull request. Their results never reach bench.', ], }, { @@ -68,7 +68,7 @@ export const METHODOLOGY: readonly MethodologySection[] = [ text: [ 'Queries run on DataFusion 55 and DuckDB 2.0. The formats are Vortex (its default ' + 'and compact write strategies), Apache Parquet, Lance 13, Arrow IPC, DuckDB’s own ' + - 'format, and, for random access, LitData; not every suite runs every format. The ' + + 'format, and, for random access, LitData. Not every suite runs every format. The ' + 'compression and file size benchmarks write Parquet with parquet-rs 59 and zstd at ' + 'level 3. ClickBench reads its published Parquet files.', ], @@ -77,12 +77,12 @@ export const METHODOLOGY: readonly MethodologySection[] = [ title: 'How a result is measured', text: [ 'SQL suites drop the operating system’s page cache before each query, run it ' + - 'several times, and record the median time. Most suites run each query 5 times; ' + + 'several times, and record the median time. Most suites run each query 5 times. ' + 'TPC-H at scale factors 1 and 10 and Appian run it 10 times, TPC-DS at scale ' + 'factor 100 runs it 3 times, and the Vortex queries run it 100 times.', 'Random access drops the page cache before each dataset, format, access pattern, ' + 'and open mode, repeats the lookups for 10 seconds, and records the median. Hot ' + - 'results keep one file open after a one-second untimed warm-up; cold results open ' + + 'results keep one file open after a one-second untimed warm-up. Cold results open ' + 'the file again, footer included, for each lookup.', 'The compression benchmark drops the page cache before each dataset, writes and ' + 'scans it 5 times, and records the fastest run.', diff --git a/src/bench/lib/queries.test.ts b/src/bench/lib/queries.test.ts index 93791150..fe0b16f4 100644 --- a/src/bench/lib/queries.test.ts +++ b/src/bench/lib/queries.test.ts @@ -344,7 +344,7 @@ describe.skipIf(!databaseAvailable)('chartPayload (testcontainers Postgres)', () describe('compareGroupSortKey canonical group ordering (no DB)', () => { it('orders NVMe and S3 Random Access directly before Clickbench', () => { const sorted = [ - 'PolarSignals Profiling', + 'Polar Signals Profiling', 'Random Access (S3)', 'Random Access (NVMe)', 'Clickbench', @@ -355,7 +355,7 @@ describe('compareGroupSortKey canonical group ordering (no DB)', () => { 'Random Access (NVMe)', 'Random Access (S3)', 'Clickbench', - 'PolarSignals Profiling', + 'Polar Signals Profiling', ]); expect(sorted.indexOf('Random Access (S3)') + 1).toBe(sorted.indexOf('Clickbench')); }); @@ -376,10 +376,10 @@ describe('compareGroupSortKey canonical group ordering (no DB)', () => { 'fineweb [s3]', 'appian [nvme]', 'fineweb [nvme]', - 'PolarSignals Profiling', + 'Polar Signals Profiling', ].sort(compareGroupSortKey); expect(sorted).toEqual([ - 'PolarSignals Profiling', + 'Polar Signals Profiling', 'fineweb [nvme]', 'fineweb [s3]', 'appian [nvme]', diff --git a/src/bench/lib/queries.ts b/src/bench/lib/queries.ts index ae8b463b..5688f605 100644 --- a/src/bench/lib/queries.ts +++ b/src/bench/lib/queries.ts @@ -1174,7 +1174,7 @@ const GROUP_ORDER: readonly string[] = [ 'TPC-DS (S3) (SF=10)', 'TPC-DS (S3) (SF=100)', 'Statistical and Population Genetics', - 'PolarSignals Profiling', + 'Polar Signals Profiling', 'fineweb [nvme]', 'fineweb [s3]', 'appian [nvme]', @@ -1208,7 +1208,8 @@ export function compareGroupSortKey(a: string, b: string): number { * - `tpcds` + storage + scale_factor -> `TPC-DS (NVMe) (SF=1)`, * - `clickbench` -> `Clickbench`, * - `statpopgen` -> `Statistical and Population Genetics` (v2 `src/config.js`), - * - `polarsignals` -> `PolarSignals Profiling` (v2 `src/config.js`), + * - `polarsignals` -> `Polar Signals Profiling` (v2 `src/config.js` wrote + * `PolarSignals`; the anchor keeps that spelling, `anchor.ts`), * - anything else -> the legacy `dataset[/variant] sf=N [storage]` shape. * * The `statpopgen`/`polarsignals` names are exactly the keys `groupDescription` @@ -1241,7 +1242,7 @@ export function groupNameQuery( } else if (dataset === 'statpopgen') { base = 'Statistical and Population Genetics'; } else if (dataset === 'polarsignals') { - base = 'PolarSignals Profiling'; + base = 'Polar Signals Profiling'; } if (base !== null) { return datasetVariant !== null ? `${base} / ${datasetVariant}` : base; diff --git a/src/bench/lib/server-smoke.test.ts b/src/bench/lib/server-smoke.test.ts index af7be498..de40cbd7 100644 --- a/src/bench/lib/server-smoke.test.ts +++ b/src/bench/lib/server-smoke.test.ts @@ -161,7 +161,7 @@ describe.skipIf(!databaseAvailable || !BUILD_PRESENT)( expect(html).toContain('data-role="theme-toggle" data-requires-js="true"'); }); - it('serves the chart permalink page with the v3 title and meta line', async () => { + it('serves the chart permalink page with its page title and meta line', async () => { const slug = chartKeyToSlug(QUERY_Q1); const raw = await (await fetch(`${BASE}/chart/${slug}`)).text(); // React's streaming SSR separates adjacent text expressions with @@ -173,7 +173,7 @@ describe.skipIf(!databaseAvailable || !BUILD_PRESENT)( previous = html; html = html.replace(//g, ''); } - expect(html).toContain('tpch sf=1 Q1 [nvme] - Vortex Benchmarks'); + expect(html).toContain('tpch sf=1 Q1 [nvme] | Vortex'); expect(html).toContain('class="chart-meta"'); expect(html).toContain('unit: '); expect(html).toContain('2 series'); diff --git a/src/components/MDXRenderer.tsx b/src/components/MDXRenderer.tsx index 075f59d4..d97db34c 100644 --- a/src/components/MDXRenderer.tsx +++ b/src/components/MDXRenderer.tsx @@ -2,6 +2,7 @@ // 'unsafe-eval', which is safe only while this runs on the server, so make // importing it from a Client Component a build error. import "server-only"; +import NextLink from "next/link"; import * as runtime from "react/jsx-runtime"; import { mdxComponents } from "@/components/blog/mdx-components"; import { WarningIcon } from "@/components/icons"; @@ -20,13 +21,18 @@ export function MDXRenderer({ code }: MDXRendererProps) { const Component = compileMDXComponent(code); return ; } catch (error) { + // The error goes to the server log, not the page: App strings never + // shows a raw exception as the message. console.error("Error rendering MDX:", error); return (
-

This post did not render

-

{String(error)}

+

This post did not render.

+

+ Try again later, or go to the blog + . +

); diff --git a/src/components/newsletter-form/index.tsx b/src/components/newsletter-form/index.tsx index e61956a4..bf205c58 100644 --- a/src/components/newsletter-form/index.tsx +++ b/src/components/newsletter-form/index.tsx @@ -1,49 +1,76 @@ "use client"; -import { type FormEvent, useId, useState } from "react"; +import { type FormEvent, useEffect, useId, useRef, useState } from "react"; type FormState = "idle" | "loading" | "success" | "error"; +/** + * What the reader sees when a sign-up fails: what did not happen, then what + * to do. The server's own error text and any exception stay out of it, so a + * 502's HTML or "Failed to fetch" never reaches the page. + */ +export function subscribeError(status: number | null): string { + if (status === null) { + return "You are not subscribed. Check your connection and try again."; + } + if (status === 400) { + return "You are not subscribed. Check the address and try again."; + } + if (status === 429) { + return "You are not subscribed. Try again in a few minutes."; + } + return "You are not subscribed. Try again later."; +} + /** * The blog's newsletter sign-up, in the design system's Field card: the * email field and the primary `vx-btn` in a `vx-field-row`, and the error * under it in `vx-field-error`. + * + * Focus stays in the form while a request runs: the field turns read-only + * and the button `aria-disabled`, because a `disabled` control that has + * focus drops it to the page. On success the form goes, so focus moves to + * the confirmation. The confirmation's live region is in the page from the + * start, empty, so a screen reader announces the text when it arrives. */ export const NewsletterForm = () => { const [email, setEmail] = useState(""); const [state, setState] = useState("idle"); const [errorMessage, setErrorMessage] = useState(""); + const doneRef = useRef(null); const id = useId(); + const loading = state === "loading"; + + useEffect(() => { + if (state === "success") doneRef.current?.focus(); + }, [state]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - if (!email.trim()) return; + if (loading || !email.trim()) return; setState("loading"); setErrorMessage(""); + let status: number | null; try { const response = await fetch("/api/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim() }) }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error || "Failed to subscribe"); + status = response.status; + if (response.ok) { + setState("success"); + setEmail(""); + return; } - - setState("success"); - setEmail(""); - } catch (error) { - setState("error"); - setErrorMessage( - error instanceof Error ? error.message : "Something went wrong" - ); + } catch { + status = null; } + setState("error"); + setErrorMessage(subscribeError(status)); }; return ( @@ -60,11 +87,15 @@ export const NewsletterForm = () => {

Get each new post by email.

- {state === "success" ? ( -

- You are subscribed. -

- ) : ( +

+ {state === "success" ? "You are subscribed." : ""} +

+ {state !== "success" && (