Skip to content

Commit 31a2a79

Browse files
authored
feat: add blog infrastructure (#17)
* feat: add blog infrastructure (pages, components, sitemap, nav link) * feat: add blog SEO (JSON-LD, RSS feed, dynamic OG images, enriched metadata) * remove blog infrastructure in preparation for MDX rewrite * add next-mdx-remote, shiki, gray-matter dependencies * add blog helpers library with MDX compilation and Shiki highlighting * add MDX components map for blog * add blog listing page * add blog post page with MDX rendering and Shiki highlighting * add dynamic OG image generation for blog posts * add RSS feed for blog * wire blog into sitemap, layout RSS link, and header nav * remove unused react-markdown dependency * add blog smoke tests (vitest RSS feed, Cypress E2E) * add llms.txt and llms-full.txt for LLM discoverability * add 10 OG image variants with preview page at /blog/og-preview * add 5 more OG variants (v11-v15) with exact SemiAnalysis brand palette * add 5 fully branded OG variants (v16-v20) matching GSA presentation slide style * add 5 thumbnail-optimized OG variants (v21-v25) with huge text and minimal details * render OG preview at actual platform sizes (Twitter, Slack, iMessage) * fix OG preview images rendering at correct pixel sizes * fix OG preview layout: full size on own row, platform sizes below * fix OG preview overflow breaking site layout * fix OG preview: use lazy loading, inline width, overflow-x-auto for thumbnails * OG variants: bump text sizes for thumbnail legibility, replace SemiAnalysis text with logo * triple logo height in all OG variants for legibility * OG variants: move logo to top-right, increase title sizes * OG variants: bump secondary text to 36-42px, fix contrast for WCAG AA at thumbnail sizes * OG overhaul: keep v11/v12/v13/v15, consolidate logos to brand/, rename excerpt to subtitle, remove author field, WCAG text sizes and contrast * blog OG: consolidate to single circuit-sidebar design with extracted tiles * remove OG preview page and API route * move pattern SVGs to brand/ * blog list: match site layout with Card wrappers and full-width container * blog: match site-wide Card/container layout, use 265 WPM reading time * blog post: add semantic HTML, subtitle, and inline tags * rename "Blog" display text to "Articles" across site * fix AUTHOR_HANDLE to correct Twitter handle @semianalysis_ * mock blog posts in tests instead of using real content * add slugify to sanitize blog slugs * fix E2E test heading and replace hardcoded author strings with AUTHOR_NAME * fix heading hierarchy: use h2 for blog page titles to match media/quotes * add active nav link highlighting based on current pathname * add blog analytics tracking for post clicks and back navigation * add share buttons to blog posts and content links to footer * remove unused coverImage frontmatter field * add next/previous post navigation to blog posts * add reading progress bar to blog posts * add table of contents with heading anchors to blog posts * add tag filtering to blog list and clickable tags on posts * use article title as X share text on blog posts * use brand color for active nav links (blue/gold per theme) * fix tags: plain spans on post page, back inside card on list page * include h1 headings in table of contents * strip fenced code blocks before extracting TOC headings * use replaceAll for code block stripping * remove Content section from footer * reorder llms.txt: site info, links, then articles * ToC: sticky sidebar at title level on ultrawides, active highlight, grayed-out passed sections * ToC: JS-based sidebar positioning, inline fallback when not enough space * fix ToC sidebar: defer position calculation until after hydration * fix data-blog-section attribute rendering * always show TOC expanded, remove collapsible details * align sidebar TOC with title height, follow scroll via ref * add lorem ipsum sections to test post for scroll testing * replace test post with full lorem ipsum content * double test post length with deeper h3 nesting throughout * add two more lorem ipsum test posts, update existing title and subtitle * activate last TOC heading when scrolled to bottom of page * add twitter:site, article:modified_time, and content:encoded to RSS feed * use brand color for progress bar, fix AUTHOR_HANDLE casing * remove content:encoded from RSS feed to avoid raw MDX/HTML in XML * add 32px scroll offset when clicking TOC headings * add Shiki dual-theme CSS for syntax highlighting in light/dark mode * re-add collapsible TOC on mobile, expanded on desktop * add lorem ipsum comments to all code blocks * collapsible TOC on non-ultrawide, default closed with hint text * fix progress bar: use rect.top for accurate scroll tracking, handle short articles * use brand color on blog post title hover * add Figure component and placeholder images with lorem ipsum captions * fix TOC sidebar initial position on page load * increase footer top padding for breathing room * add InferenceX v2 article crosspost from Substack with 90 images * limit figure images to 66% width on desktop, full width on mobile * add paywall notice and Substack subscribe CTA at end of v2 article * add blurred paywall teaser and subscribe CTA at end of v2 article * add Blur MDX component for paywall teaser section * rename v2 article slug to match title * add InferenceMAX v1 open source inference benchmarking article * remove auto-inserted curly quotes from blog blockquotes * remove placeholder posts, tweak blog article styling and add copyright footer * move copyright footer inside article prose * deduplicate heading IDs using parent section prefix * add hash navigation for blog article headings * add copy-link icon on heading hover for blog articles * polish heading link: show "Link copied" with fade-out, no underline * add remark-gfm for strikethrough and GFM support in blog * fix escaped asterisks in multiplication expression * add width/height, lazy loading and async decoding to blog figure images * add lazy loading to blog images, fix inline code wrapping on mobile * eagerly load first blog image, preconnect to Substack CDN * guard against null pathname in header isActive check * fix gpu-specs e2e tests to use updated logo watermark path * update blog e2e test to use real article slug after placeholder removal * auto-scroll sidebar TOC to keep active heading visible * add track() and error handling to heading link, add extractHeadings tests * add getAdjacentPosts tests, use node: protocol for fs/path imports * docs: add blog infrastructure documentation * add frontmatter and llms to vscode spellcheck dictionary
1 parent 85755bb commit 31a2a79

57 files changed

Lines changed: 5003 additions & 104 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"dotenv",
2626
"esbuild",
2727
"evals",
28+
"frontmatter",
2829
"gptoss",
2930
"gsutil",
3031
"hyperscaler",
@@ -38,6 +39,7 @@
3839
"kimi",
3940
"kimik",
4041
"lefthook",
42+
"llms",
4143
"maxage",
4244
"minimaxm",
4345
"mooncake",

AGENTS.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ pnpm test:e2e # Cypress E2E tests
3434
```
3535
packages/
3636
├── app/ # Next.js frontend (@semianalysisai/inferencex-app)
37+
│ ├── content/blog/ # MDX blog posts (frontmatter + content)
3738
│ └── src/
3839
│ ├── app/ # Pages, layouts, API routes (/api/v1/*)
40+
│ │ └── blog/ # Blog list + [slug] post pages, OG image generation
3941
│ ├── components/ # Tab sections: inference/, evaluation/, historical-trends/,
40-
│ │ # throughput-calculator/, reliability/, gpu-specs/, ui/
42+
│ │ # throughput-calculator/, reliability/, gpu-specs/, blog/, ui/
4143
│ ├── hooks/api/ # React Query hooks (use-benchmarks, use-availability, etc.)
42-
│ └── lib/ # Utilities, constants, d3-chart/, chart-utils, data-mappings
43-
├── constants/ # Shared constants (GPU keys, model mappings)
44+
│ └── lib/ # Utilities, constants, d3-chart/, chart-utils, blog, data-mappings
45+
├── constants/ # Shared constants (GPU keys, model mappings, SEO)
4446
└── db/ # DB layer, ETL, migrations, queries, ingest scripts
4547
```
4648

@@ -66,6 +68,15 @@ API routes (`packages/app/src/app/api/v1/`):
6668

6769
**API routes return raw DB data** — no presentation logic. Frontend handles all transformations.
6870

71+
Static content routes (no DB):
72+
73+
- `/blog` — blog listing (statically generated from MDX files in `content/blog/`)
74+
- `/blog/[slug]` — blog post page with MDX rendering and OG image generation
75+
- `/feed.xml` — RSS 2.0 feed
76+
- `/llms.txt` — LLM-readable site index
77+
- `/llms-full.txt` — full article content for LLM ingestion
78+
- `/sitemap.xml` — dynamic sitemap (includes blog posts)
79+
6980
## Code Style & Tooling
7081

7182
- **Linter**: oxlint — `pnpm lint` / `pnpm lint:fix`
@@ -87,7 +98,7 @@ All interactive elements should have `track()` from `@/lib/analytics` (autocaptu
8798

8899
**Convention**: `[section]_[action]` — e.g., `latency_zoom_reset`, `calculator_bar_selected`, `tab_changed`
89100

90-
**Prefixes**: `latency_`, `interactivity_`, `gpu_timeseries_`, `inference_`, `calculator_`, `evaluation_`, `reliability_`, `tab_`, `selector_`
101+
**Prefixes**: `latency_`, `interactivity_`, `gpu_timeseries_`, `inference_`, `calculator_`, `evaluation_`, `reliability_`, `tab_`, `selector_`, `blog_`, `social_`
91102

92103
## Tab Structure
93104

@@ -118,6 +129,25 @@ Order: `inference` → `evaluation` → `historical` → `calculator` → `relia
118129
6. Add disagg caveat banner in `ChartDisplay.tsx` for per-GPU or per-MW metrics (animated amber `border-l-2` banner pattern)
119130
7. Expose in UI state: `InferenceContext.tsx`
120131

132+
### Add a new blog post
133+
134+
1. Create `packages/app/content/blog/<slug>.mdx` with frontmatter: `title`, `subtitle`, `date` (required), `tags`, `modifiedDate` (optional)
135+
2. Write content using Markdown + custom MDX components (`Figure`, `Blur`)
136+
3. No code changes needed — the post automatically appears in the blog list, sitemap, RSS feed, llms.txt, and gets a generated OG image
137+
138+
See [Blog](./docs/blog.md) for content format, available MDX components, and design details.
139+
140+
### Modify blog components
141+
142+
- Blog library (posts, headings, reading time): `src/lib/blog.ts`
143+
- Blog list page: `src/app/blog/page.tsx`
144+
- Blog post page: `src/app/blog/[slug]/page.tsx`
145+
- MDX components: `src/components/blog/mdx-components.tsx`
146+
- TOC sidebar: `src/components/blog/blog-toc.tsx`
147+
- OG image generation: `src/app/blog/[slug]/og-image-render.tsx`
148+
- RSS feed: `src/app/feed.xml/route.ts`
149+
- SEO constants: `packages/constants/src/seo.ts`
150+
121151
### Add a new model or GPU
122152

123153
**First ask for the PR / GitHub Actions run URL** — see [Adding Entities](./docs/adding-entities.md) for the full workflow. Never ask other questions before getting the URL.
@@ -144,6 +174,7 @@ Detailed design rationale (the "why" and "how", not the "what") lives in [docs/]
144174
- **[Testing](./docs/testing.md)** — Requirements, quality standards, pre-commit checklist
145175
- **[Data Transforms](./docs/data-transforms.md)** — BenchmarkRow → AggDataEntry → InferenceData pipeline, hardware key construction, derived metrics
146176
- **[State Ownership](./docs/state-ownership.md)** — Context provider state map, availability filtering cascade, comparison dates, URL params
177+
- **[Blog](./docs/blog.md)** — MDX content system, SEO features, TOC sidebar, reading progress, analytics events
147178

148179
## Claude AI Agents
149180

docs/blog.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Blog Infrastructure
2+
3+
## Why MDX + Static Generation
4+
5+
Blog posts are MDX files in `packages/app/content/blog/`, compiled at build time via `next-mdx-remote`. This was chosen over a CMS or database because:
6+
7+
- **No runtime dependency**: Posts are part of the repo, versioned in git, reviewed in PRs. No CMS outage can break the blog.
8+
- **MDX flexibility**: Authors can embed custom React components (`Figure`, `Blur`) alongside Markdown. This matters for image-heavy technical articles with captions, paywall teasers, and code blocks.
9+
- **Static generation**: `generateStaticParams()` pre-renders all post pages. No server-side rendering at request time.
10+
11+
Syntax highlighting uses Shiki with dual light/dark themes (CSS class-based switching, not runtime theme detection).
12+
13+
## Content Format
14+
15+
```yaml
16+
# Frontmatter (required: title, subtitle, date)
17+
title: string
18+
subtitle: string
19+
date: YYYY-MM-DD
20+
modifiedDate?: YYYY-MM-DD # Used in sitemap and JSON-LD
21+
tags?: string[] # Used for filtering on /blog and in RSS categories
22+
```
23+
24+
Slug is derived from the filename (e.g., `my-post.mdx` -> `my-post`), not from frontmatter. Reading time is calculated at 265 WPM.
25+
26+
## MDX Components Available to Authors
27+
28+
| Component | Usage | Notes |
29+
| ---------------------------------------------- | -------------------------------- | --------------------------------------------------------------------------------- |
30+
| `# / ## / ###` | Headings with auto-generated IDs | IDs are deduped: second `## Details` under `## Results` becomes `results-details` |
31+
| `[text](url)` | Links | Internal links use `<Link>`, external get `target="_blank"` |
32+
| `![alt](src)` | Images | Rendered via `next/image` with lazy loading (first image is eager) |
33+
| `<Figure src="..." alt="..." caption="..." />` | Captioned figures | Uses `<img>` (not `next/image`) for external URLs |
34+
| `<Blur>...</Blur>` | Paywall teaser blur overlay | Content is blurred, unselectable, and not clickable |
35+
36+
Heading ID deduplication: if two headings share a slug, the second gets prefixed with its parent heading's slug (e.g., `overview-details`). If no parent exists, a level suffix is appended (`intro-2`).
37+
38+
## Blog Library (`src/lib/blog.ts`)
39+
40+
| Function | Purpose |
41+
| ------------------------- | --------------------------------------------------------- |
42+
| `getAllPosts()` | All posts sorted newest-first |
43+
| `getPostBySlug(slug)` | Single post meta + raw MDX content |
44+
| `getAdjacentPosts(slug)` | `{ prev, next }` — prev is older, next is newer |
45+
| `extractHeadings(rawMdx)` | h1-h3 headings with unique IDs (strips code blocks first) |
46+
| `slugify(raw)` | URL-safe slug generation |
47+
| `getReadingTime(content)` | Word count / 265, minimum 1 minute |
48+
49+
## SEO Features
50+
51+
### Dynamic OG Images (`/blog/[slug]/opengraph-image.tsx`)
52+
53+
1200x630px images generated at build time with `next/og` (Satori). Design: decorative tile sidebar + dark content panel with title, subtitle, date, and logo. Title font size scales (56-72px) based on length for readability at thumbnail sizes.
54+
55+
### RSS Feed (`/feed.xml`)
56+
57+
RSS 2.0 with Dublin Core and Atom extensions. Includes all posts with title, link, description, creator, pubDate, categories. Cached 1 hour.
58+
59+
### LLM Discovery (`/llms.txt`, `/llms-full.txt`)
60+
61+
- `/llms.txt`: Site description + article index with titles, URLs, and subtitles
62+
- `/llms-full.txt`: Full raw MDX content of every post, for LLM context ingestion
63+
64+
### Sitemap Integration
65+
66+
Blog index at priority 0.8 (weekly), individual posts at priority 0.7 (monthly, uses `modifiedDate` if present).
67+
68+
### JSON-LD
69+
70+
- `/blog` page: `Blog` schema
71+
- `/blog/[slug]` page: `BlogPosting` schema (headline, author, publisher, dates, wordCount, timeRequired)
72+
73+
## UI Components
74+
75+
### Table of Contents (`blog-toc.tsx`)
76+
77+
Two modes based on available viewport space:
78+
79+
- **Sidebar** (>= 240px right of content): Fixed position, follows scroll via imperative DOM updates in a scroll handler. Active heading tracked via `IntersectionObserver` with `rootMargin: '0px 0px -80% 0px'`. Falls back to last heading when scrolled to page bottom.
80+
- **Inline** (narrow screens): Collapsible `<details>` card.
81+
82+
The sidebar position is calculated relative to the `[data-blog-section]` element and updated on scroll/resize.
83+
84+
### Reading Progress Bar (`reading-progress-bar.tsx`)
85+
86+
Fixed-top 0.5px bar tracking scroll position within the `<article>` element. Fires milestone events at 25/50/75/100% thresholds (each fires only once per page load).
87+
88+
### Heading Links (`heading-link.tsx`)
89+
90+
Copy-to-clipboard button shown on heading hover. State cycle: idle -> copied ("Link copied" text) -> fade out -> idle.
91+
92+
### Post Navigation (`blog-post-nav.tsx`)
93+
94+
Previous (older) / Next (newer) post links with title display. Uses `getAdjacentPosts()`.
95+
96+
## Analytics Events
97+
98+
All blog analytics use the `blog_` prefix per the `[section]_[action]` convention:
99+
100+
| Event | Trigger |
101+
| ------------------------------------------------ | ---------------------------- |
102+
| `blog_post_clicked` | Click post card on list page |
103+
| `blog_toc_clicked` | Click TOC heading |
104+
| `blog_read_milestone` | Scroll past 25/50/75/100% |
105+
| `blog_heading_link_copied` | Copy heading link |
106+
| `blog_nav_prev` / `blog_nav_next` | Click prev/next post |
107+
| `blog_back_clicked` | Click back to articles |
108+
| `blog_tag_filtered` | Click tag filter |
109+
| `social_share_twitter` / `social_share_linkedin` | Click share buttons |
110+
111+
## Adding a New Blog Post
112+
113+
1. Create `packages/app/content/blog/<slug>.mdx` with required frontmatter (`title`, `subtitle`, `date`)
114+
2. Add optional `tags` and `modifiedDate` frontmatter
115+
3. Write content using standard Markdown + available MDX components
116+
4. The post automatically appears in: blog list, sitemap, RSS feed, llms.txt, OG image generation
117+
5. No code changes needed — just the MDX file

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ Design rationale and non-obvious conventions. See [CLAUDE.md](../CLAUDE.md) for
1414
- [Testing](./testing.md) — Requirements, quality standards, pre-commit checklist
1515
- [Data Transforms](./data-transforms.md) — Full pipeline from BenchmarkRow to RenderableGraph: type hierarchy, hardware key construction, derived metrics, memoization strategy
1616
- [State Ownership](./state-ownership.md) — Which context owns which state, availability filtering cascade, comparison date mechanics, URL param sync
17+
- [Blog](./blog.md) — MDX content system, SEO features (OG images, RSS, llms.txt, JSON-LD), TOC sidebar, reading progress, heading links, analytics events

0 commit comments

Comments
 (0)