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
4 changes: 3 additions & 1 deletion blogs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ title: "Tuning Reth for payments: how we hit 21,200 TPS"
excerpt: "One or two sentences shown on the index and as the post's lede."
date: 2026-06-02
category: technical # network-upgrades | events | technical | case-studies
# Use an inline list when a post belongs to multiple categories:
# category: [technical, case-studies]
featured: true # optional — pins the post to the hero card on /blog
---
```

`category` must be one of the four slugs above (the build fails loudly otherwise). At most one post should be `featured`; if none is, the newest post takes the hero card.
`category` must be one of the four slugs above or an inline list of those slugs (the build fails loudly otherwise). At most one post should be `featured`; if none is, the newest post takes the hero card.

## Body

Expand Down
2 changes: 1 addition & 1 deletion blogs/human-authorization-in-agentic-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Human Authorization in Agentic Workflows"
excerpt: "How Tempo enables AI-accelerated development securely"
date: 2026-08-17
category: case-studies
category: [technical, case-studies]
---

At Tempo, we are building more workflows where software can move quickly on a person's behalf. That is useful, but it can also be risky.
Expand Down
11 changes: 8 additions & 3 deletions src/marketing/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ export default function BlogPostPage({ params }: { params: { slug: string } }) {
</Link>

<div className="mt-8 flex flex-wrap items-center gap-3">
<span className="whitespace-nowrap border border-line-strong px-2.5 py-[3px] font-mono text-[11px] text-foreground/50 uppercase tracking-[0.02em]">
{categoryBySlug(post.category).badge}
</span>
{post.categories.map((category) => (
<span
key={category}
className="whitespace-nowrap border border-line-strong px-2.5 py-[3px] font-mono text-[11px] text-foreground/50 uppercase tracking-[0.02em]"
>
{categoryBySlug(category).badge}
</span>
))}
<span className="font-mono text-[12px] text-foreground/40 uppercase tracking-[0.02em]">
{formatDate(post.date)}
</span>
Expand Down
13 changes: 9 additions & 4 deletions src/marketing/app/blog/_components/PostExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Filter = (typeof filters)[number]['slug']

export default function PostExplorer({ posts }: { posts: PostMeta[] }) {
const [active, setActive] = useState<Filter>('all')
const visible = active === 'all' ? posts : posts.filter((p) => p.category === active)
const visible = active === 'all' ? posts : posts.filter((p) => p.categories.includes(active))

return (
<section>
Expand Down Expand Up @@ -48,9 +48,14 @@ export default function PostExplorer({ posts }: { posts: PostMeta[] }) {
className="group flex flex-col gap-2.5 border-line border-b px-5 py-6 transition-colors hover:bg-surface-block lg:px-8"
>
<span className="flex flex-wrap items-center gap-3">
<span className="whitespace-nowrap border border-line-strong px-2.5 py-[3px] font-mono text-[11px] text-foreground/50 uppercase tracking-[0.02em]">
{categoryBySlug(post.category).badge}
</span>
{post.categories.map((category) => (
<span
key={category}
className="whitespace-nowrap border border-line-strong px-2.5 py-[3px] font-mono text-[11px] text-foreground/50 uppercase tracking-[0.02em]"
>
{categoryBySlug(category).badge}
</span>
))}
{isNew(post.date) && (
<span className="whitespace-nowrap border border-indicator-green px-2.5 py-[3px] font-mono text-[11px] text-indicator-green uppercase tracking-[0.02em]">
New
Expand Down
1 change: 1 addition & 0 deletions src/marketing/app/blog/_lib/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type PostMeta = {
excerpt: string
date: string
category: CategorySlug
categories: CategorySlug[]
authors: string
featured: boolean
}
Expand Down
1 change: 1 addition & 0 deletions src/marketing/app/blog/_lib/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type Post = PostMeta & { html: string }
const posts: Post[] = renderedPosts.map((post) => ({
...post,
category: post.category as CategorySlug,
categories: post.categories as CategorySlug[],
}))

export function getAllPosts(): Post[] {
Expand Down
1 change: 1 addition & 0 deletions src/marketing/app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function BlogPage() {
excerpt: post.excerpt,
date: post.date,
category: post.category,
categories: post.categories,
authors: post.authors,
featured: post.featured,
}))
Expand Down
26 changes: 20 additions & 6 deletions src/marketing/blogPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { unified } from 'unified'
import type { Plugin } from 'vite'

// Blog content lives as dev-managed markdown files in /blogs at the repo root.
// Frontmatter schema: title, excerpt, date (YYYY-MM-DD), category, optional
// Frontmatter schema: title, excerpt, date (YYYY-MM-DD), category (a slug or
// inline list of slugs), optional
// authors, and an optional `featured: true` to pin a post to the hero card.
//
// Markdown is rendered to HTML here, in Node, at build/dev time, so the heavy
Expand Down Expand Up @@ -66,6 +67,7 @@ export type RenderedPost = {
excerpt: string
date: string
category: string
categories: string[]
authors: string
featured: boolean
html: string
Expand Down Expand Up @@ -97,8 +99,9 @@ const processor = unified()
.use(rehypeStringify)

// Minimal frontmatter parser for our simple schema (quoted strings, an
// unquoted YYYY-MM-DD date, a category slug, and an optional boolean). Avoids
// pulling in gray-matter + js-yaml for a handful of known keys.
// unquoted YYYY-MM-DD date, a category slug or inline list of slugs, and an
// optional boolean). Avoids pulling in gray-matter + js-yaml for a handful of
// known keys.
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw)
if (!match) return { data: {}, content: raw }
Expand All @@ -123,14 +126,24 @@ function parseFrontmatter(raw: string): { data: Record<string, string>; content:
return { data, content: match[2] }
}

function parseCategories(value: string): string[] {
const list = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1) : value
return list
.split(',')
.map((category) => category.trim().replace(/^(['"])(.*)\1$/, '$2'))
.filter(Boolean)
}

async function renderPost(filename: string): Promise<SearchablePost> {
const slug = filename.replace(/\.md$/, '')
const raw = fs.readFileSync(path.join(BLOGS_DIR, filename), 'utf8')
const { data, content } = parseFrontmatter(raw)

if (!CATEGORY_SLUGS.includes(data.category)) {
const categories = [...new Set(parseCategories(data.category ?? ''))]
const unknownCategories = categories.filter((category) => !CATEGORY_SLUGS.includes(category))
if (categories.length === 0 || unknownCategories.length > 0) {
throw new Error(
`blogs/${filename}: unknown category "${data.category}". ` +
`blogs/${filename}: unknown category "${unknownCategories[0] ?? data.category}". ` +
`Expected one of: ${CATEGORY_SLUGS.join(', ')}`,
)
}
Expand All @@ -142,7 +155,8 @@ async function renderPost(filename: string): Promise<SearchablePost> {
title: data.title,
excerpt: data.excerpt,
date: data.date,
category: data.category,
category: categories[0],
categories,
authors: data.authors ?? '',
featured: data.featured === 'true',
html,
Expand Down
1 change: 1 addition & 0 deletions src/marketing/next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module 'virtual:blog-posts' {
excerpt: string
date: string
category: string
categories: string[]
authors: string
featured: boolean
html: string
Expand Down
Loading