Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f6adc0
feat: add blog infrastructure (pages, components, sitemap, nav link)
adibarra Mar 19, 2026
a158758
feat: add blog SEO (JSON-LD, RSS feed, dynamic OG images, enriched me…
adibarra Mar 19, 2026
a11bb56
Merge branch 'master' into feat/blog-infra
adibarra Mar 21, 2026
0ac758c
remove blog infrastructure in preparation for MDX rewrite
adibarra Mar 21, 2026
c514c26
add next-mdx-remote, shiki, gray-matter dependencies
adibarra Mar 21, 2026
0abd489
add blog helpers library with MDX compilation and Shiki highlighting
adibarra Mar 21, 2026
3a937b0
add MDX components map for blog
adibarra Mar 21, 2026
e36b7e0
add blog listing page
adibarra Mar 21, 2026
6fc8da0
add blog post page with MDX rendering and Shiki highlighting
adibarra Mar 21, 2026
b72b667
add dynamic OG image generation for blog posts
adibarra Mar 21, 2026
d40fb4f
add RSS feed for blog
adibarra Mar 21, 2026
369199e
wire blog into sitemap, layout RSS link, and header nav
adibarra Mar 21, 2026
0fc8571
remove unused react-markdown dependency
adibarra Mar 21, 2026
7236152
add blog smoke tests (vitest RSS feed, Cypress E2E)
adibarra Mar 21, 2026
41da042
add llms.txt and llms-full.txt for LLM discoverability
adibarra Mar 21, 2026
80ab1a4
add 10 OG image variants with preview page at /blog/og-preview
adibarra Mar 21, 2026
238eb10
add 5 more OG variants (v11-v15) with exact SemiAnalysis brand palette
adibarra Mar 21, 2026
0ab91f9
add 5 fully branded OG variants (v16-v20) matching GSA presentation s…
adibarra Mar 21, 2026
1f632c6
add 5 thumbnail-optimized OG variants (v21-v25) with huge text and mi…
adibarra Mar 22, 2026
284e4e2
render OG preview at actual platform sizes (Twitter, Slack, iMessage)
adibarra Mar 22, 2026
f8e44a2
fix OG preview images rendering at correct pixel sizes
adibarra Mar 22, 2026
25f2319
fix OG preview layout: full size on own row, platform sizes below
adibarra Mar 22, 2026
c80f0a0
fix OG preview overflow breaking site layout
adibarra Mar 22, 2026
e6d926d
fix OG preview: use lazy loading, inline width, overflow-x-auto for t…
adibarra Mar 22, 2026
58bf72f
OG variants: bump text sizes for thumbnail legibility, replace SemiAn…
adibarra Mar 22, 2026
6252482
Merge branch 'master' into feat/blog-infra
adibarra Mar 22, 2026
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
27 changes: 27 additions & 0 deletions packages/app/content/blog/hello-world.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: Hello World
author: SemiAnalysis
date: '2026-03-21'
excerpt: Our first blog post on InferenceX.
tags:
- announcement
---

# Hello World

Welcome to the **InferenceX** blog. We'll be sharing insights on AI inference benchmarking, GPU performance analysis, and the latest trends in ML infrastructure.

## What is InferenceX?

InferenceX is the open-source AI inference benchmark dashboard by SemiAnalysis. We compare GPU performance for LLM inference across NVIDIA, AMD, and more.

```python
# Example: running a benchmark
from inferencex import Benchmark

bench = Benchmark(model="llama-3.1-70b", gpu="H100")
result = bench.run(concurrency=64)
print(f"Throughput: {result.tokens_per_second} tok/s")
```

Stay tuned for more posts.
57 changes: 57 additions & 0 deletions packages/app/cypress/e2e/blog.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
describe('Blog', () => {
describe('Blog listing page', () => {
before(() => {
cy.visit('/blog');
});

it('renders the blog page with heading', () => {
cy.get('h1').should('contain.text', 'Blog');
});

it('displays at least one blog post card', () => {
cy.get('article').should('have.length.gte', 1);
});

it('post cards have titles and excerpts', () => {
cy.get('article')
.first()
.within(() => {
cy.get('h2').should('exist').and('not.be.empty');
cy.get('p').should('exist');
});
});

it('post cards link to individual posts', () => {
cy.get('a[href^="/blog/"]').should('have.length.gte', 1);
});
});

describe('Blog post page', () => {
before(() => {
cy.visit('/blog/hello-world');
});

it('renders the post title', () => {
cy.get('h1').should('contain.text', 'Hello World');
});

it('displays post metadata', () => {
cy.contains('SemiAnalysis').should('exist');
cy.contains('min read').should('exist');
});

it('renders the article content', () => {
cy.get('article.prose').should('exist');
cy.get('article.prose').should('contain.text', 'InferenceX');
});

it('renders code blocks with syntax highlighting', () => {
cy.get('article.prose pre').should('exist');
cy.get('article.prose code').should('exist');
});

it('has a back link to the blog listing', () => {
cy.get('a[href="/blog"]').should('exist');
});
});
});
1 change: 1 addition & 0 deletions packages/app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
transpilePackages: ['@semianalysisai/inferencex-constants'],
serverExternalPackages: ['shiki'],
};

export default nextConfig;
6 changes: 6 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@semianalysisai/inferencex-constants": "workspace:*",
"@semianalysisai/inferencex-db": "workspace:*",
"@shikijs/rehype": "^4.0.2",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.91.0",
"@vercel/analytics": "^2.0.1",
"@vercel/blob": "^2.3.1",
Expand All @@ -44,13 +46,16 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"gray-matter": "^4.0.3",
"lodash-es": "^4.17.23",
"lucide-react": "^0.577.0",
"next": "16.2.0",
"next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"posthog-js": "^1.362.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
Expand All @@ -59,6 +64,7 @@
"@types/adm-zip": "^0.5.8",
"@types/d3": "^7.4.3",
"@types/lodash-es": "^4.17.12",
"@types/mdast": "^4.0.4",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
78 changes: 78 additions & 0 deletions packages/app/src/app/api/og-preview/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ImageResponse } from 'next/og';
import type { NextRequest } from 'next/server';

import { getPostBySlug } from '@/lib/blog';

import { renderOgImage as v1 } from '@/app/blog/[slug]/og-variants/v1-current';
import { renderOgImage as v2 } from '@/app/blog/[slug]/og-variants/v2-circuit-corners';
import { renderOgImage as v3 } from '@/app/blog/[slug]/og-variants/v3-grid-overlay';
import { renderOgImage as v4 } from '@/app/blog/[slug]/og-variants/v4-left-stripe';
import { renderOgImage as v5 } from '@/app/blog/[slug]/og-variants/v5-diagonal-blocks';
import { renderOgImage as v6 } from '@/app/blog/[slug]/og-variants/v6-top-bar';
import { renderOgImage as v7 } from '@/app/blog/[slug]/og-variants/v7-right-panel';
import { renderOgImage as v8 } from '@/app/blog/[slug]/og-variants/v8-bottom-circuit';
import { renderOgImage as v9 } from '@/app/blog/[slug]/og-variants/v9-scattered-nodes';
import { renderOgImage as v10 } from '@/app/blog/[slug]/og-variants/v10-border-frame';
import { renderOgImage as v11 } from '@/app/blog/[slug]/og-variants/v11-brand-corners';
import { renderOgImage as v12 } from '@/app/blog/[slug]/og-variants/v12-brand-grid';
import { renderOgImage as v13 } from '@/app/blog/[slug]/og-variants/v13-brand-left-panel';
import { renderOgImage as v14 } from '@/app/blog/[slug]/og-variants/v14-brand-frame-gold';
import { renderOgImage as v15 } from '@/app/blog/[slug]/og-variants/v15-brand-split';
import { renderOgImage as v16 } from '@/app/blog/[slug]/og-variants/v16-slide-title';
import { renderOgImage as v17 } from '@/app/blog/[slug]/og-variants/v17-slide-content';
import { renderOgImage as v18 } from '@/app/blog/[slug]/og-variants/v18-slide-hybrid';
import { renderOgImage as v19 } from '@/app/blog/[slug]/og-variants/v19-slide-keynote';
import { renderOgImage as v20 } from '@/app/blog/[slug]/og-variants/v20-slide-full';
import { renderOgImage as v21 } from '@/app/blog/[slug]/og-variants/v21-compact-bold';
import { renderOgImage as v22 } from '@/app/blog/[slug]/og-variants/v22-gold-title-bar';
import { renderOgImage as v23 } from '@/app/blog/[slug]/og-variants/v23-bold-circuit';
import { renderOgImage as v24 } from '@/app/blog/[slug]/og-variants/v24-gold-split-bold';
import { renderOgImage as v25 } from '@/app/blog/[slug]/og-variants/v25-gold-accent-stripe';

const variants: Record<string, (meta: any) => Promise<ImageResponse>> = {
v1,
v2,
v3,
v4,
v5,
v6,
v7,
v8,
v9,
v10,
v11,
v12,
v13,
v14,
v15,
v16,
v17,
v18,
v19,
v20,
v21,
v22,
v23,
v24,
v25,
};

export async function GET(request: NextRequest) {
const slug = request.nextUrl.searchParams.get('slug') ?? 'hello-world';
const variant = request.nextUrl.searchParams.get('v') ?? 'v1';

const result = getPostBySlug(slug);
if (!result) {
return new Response('Post not found', { status: 404 });
}

const render = variants[variant];
if (!render) {
return new Response(
`Unknown variant: ${variant}. Available: ${Object.keys(variants).join(', ')}`,
{ status: 400 },
);
}

return await render(result.meta);
}
81 changes: 81 additions & 0 deletions packages/app/src/app/blog/[slug]/og-variants/v1-current.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* V1: Current — Plain dark background, no pattern.
* Clean and minimal, text-only layout.
*/
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

import { ImageResponse } from 'next/og';

import type { BlogPostMeta } from '@/lib/blog';

export const size = { width: 1200, height: 630 };

export async function renderOgImage(meta: BlogPostMeta) {
const logoSrc = `data:image/png;base64,${(await readFile(join(process.cwd(), 'public/logo.png'))).toString('base64')}`;
const titleSize = meta.title.length > 60 ? 48 : meta.title.length > 40 ? 56 : 64;

return new ImageResponse(
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
width: '100%',
height: '100%',
backgroundColor: '#18181b',
color: '#fafafa',
padding: 60,
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<img src={logoSrc} height={32} />
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ fontSize: titleSize, fontWeight: 700, lineHeight: 1.2 }}>{meta.title}</div>
<div
style={{
fontSize: 28,
color: '#a1a1aa',
lineHeight: 1.4,
maxHeight: 80,
overflow: 'hidden',
}}
>
{meta.excerpt.length > 140 ? meta.excerpt.slice(0, 140) + '...' : meta.excerpt}
</div>
</div>

<div style={{ display: 'flex', gap: 24, fontSize: 24, color: '#a1a1aa' }}>
<span>{meta.author}</span>
<span>·</span>
<span>
{new Date(meta.date + 'T00:00:00Z').toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
})}
</span>
<span>·</span>
<span>{meta.readingTime} min read</span>
{meta.tags &&
meta.tags.slice(0, 3).map((tag) => (
<span
key={tag}
style={{
backgroundColor: '#27272a',
padding: '4px 12px',
borderRadius: 9999,
fontSize: 20,
}}
>
{tag}
</span>
))}
</div>
</div>,
size,
);
}
Loading