Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@semianalysisai/inferencex-constants": "workspace:*",
"@semianalysisai/inferencex-db": "workspace:*",
"@tailwindcss/typography": "^0.5.19",
"@tanstack/react-query": "^5.91.0",
"@tanstack/react-query-persist-client": "^5.90.25",
"@vercel/analytics": "^2.0.1",
Expand All @@ -52,6 +53,7 @@
"posthog-js": "^1.362.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-markdown": "^10.1.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
Expand Down
137 changes: 137 additions & 0 deletions packages/app/src/app/blog/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ImageResponse } from 'next/og';

import { BLOG_POSTS, getReadingTime } from '@/components/blog/blog-data';

export const alt = 'InferenceX Blog';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export function generateStaticParams() {
return BLOG_POSTS.map((post) => ({ slug: post.slug }));
}

export default async function OgImage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = BLOG_POSTS.find((p) => p.slug === slug);

if (!post) {
return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#09090b',
color: '#fafafa',
fontSize: 48,
fontWeight: 700,
}}
>
InferenceX Blog
</div>,
size,
);
}

const readingTime = getReadingTime(post.content);
const tags = post.tags?.slice(0, 3) ?? [];
const truncatedExcerpt =
post.excerpt.length > 160 ? post.excerpt.slice(0, 157) + '...' : post.excerpt;
const formattedDate = new Date(post.date + 'T00:00:00').toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});

return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#09090b',
padding: '60px 72px',
}}
>
<div
style={{
display: 'flex',
fontSize: 22,
fontWeight: 700,
color: '#a1a1aa',
letterSpacing: '-0.02em',
}}
>
InferenceX Blog — SemiAnalysis
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div
style={{
display: 'flex',
fontSize: post.title.length > 60 ? 44 : 52,
fontWeight: 700,
color: '#fafafa',
lineHeight: 1.15,
letterSpacing: '-0.03em',
maxWidth: '1050px',
}}
>
{post.title}
</div>
<div
style={{
display: 'flex',
fontSize: 22,
color: '#a1a1aa',
lineHeight: 1.4,
maxWidth: '900px',
}}
>
{truncatedExcerpt}
</div>
</div>

<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
<div style={{ display: 'flex', fontSize: 20, color: '#fafafa', fontWeight: 600 }}>
{post.author}
</div>
<div style={{ display: 'flex', fontSize: 18, color: '#71717a' }}>{formattedDate}</div>
<div style={{ display: 'flex', fontSize: 18, color: '#71717a' }}>
{`${readingTime} min read`}
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{tags.map((tag) => (
<div
key={tag}
style={{
display: 'flex',
fontSize: 16,
color: '#3b82f6',
backgroundColor: 'rgba(59,130,246,0.15)',
padding: '4px 14px',
borderRadius: '6px',
fontWeight: 500,
}}
>
{tag}
</div>
))}
</div>
</div>
</div>,
size,
);
}
137 changes: 137 additions & 0 deletions packages/app/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

import { BlogPostContent } from '@/components/blog/blog-post-content';
import { BLOG_POSTS, getReadingTime } from '@/components/blog/blog-data';
import {
AUTHOR_HANDLE,
AUTHOR_NAME,
AUTHOR_URL,
OG_IMAGE,
SITE_NAME,
SITE_URL,
} from '@semianalysisai/inferencex-constants';

type Params = { slug: string };

export function generateStaticParams(): Params[] {
return BLOG_POSTS.map((post) => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { slug } = await params;
const post = BLOG_POSTS.find((p) => p.slug === slug);
if (!post) return {};

const ogImage = post.coverImage ?? `${SITE_URL}/blog/${post.slug}/opengraph-image`;

return {
title: post.title,
description: post.excerpt,
keywords: post.tags,
authors: [{ name: post.author }],
alternates: {
canonical: `${SITE_URL}/blog/${post.slug}`,
},
openGraph: {
title: `${post.title} | ${SITE_NAME} Blog`,
description: post.excerpt,
url: `${SITE_URL}/blog/${post.slug}`,
siteName: SITE_NAME,
type: 'article',
publishedTime: `${post.date}T00:00:00Z`,
...(post.modifiedDate && { modifiedTime: `${post.modifiedDate}T00:00:00Z` }),
authors: post.author.split(', '),
tags: post.tags,
images: [{ url: ogImage, width: 1200, height: 630, alt: post.title }],
locale: 'en_US',
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [ogImage],
creator: AUTHOR_HANDLE,
site: AUTHOR_HANDLE,
},
};
}

export default async function BlogPostPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const post = BLOG_POSTS.find((p) => p.slug === slug);
if (!post) notFound();

const readingTime = getReadingTime(post.content);

const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: SITE_NAME,
item: SITE_URL,
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: `${SITE_URL}/blog`,
},
{
'@type': 'ListItem',
position: 3,
name: post.title,
item: `${SITE_URL}/blog/${post.slug}`,
},
],
},
{
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
url: `${SITE_URL}/blog/${post.slug}`,
datePublished: `${post.date}T00:00:00Z`,
...(post.modifiedDate && { dateModified: `${post.modifiedDate}T00:00:00Z` }),
wordCount: post.content.trim().split(/\s+/).length,
timeRequired: `PT${readingTime}M`,
author: post.author.split(', ').map((name) => ({
'@type': 'Person',
name,
})),
publisher: {
'@type': 'Organization',
name: AUTHOR_NAME,
url: AUTHOR_URL,
logo: { '@type': 'ImageObject', url: OG_IMAGE },
},
image: post.coverImage ?? `${SITE_URL}/blog/${post.slug}/opengraph-image`,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${SITE_URL}/blog/${post.slug}`,
},
...(post.tags && {
keywords: post.tags.join(', '),
articleSection: post.tags[0],
}),
isPartOf: {
'@type': 'Blog',
'@id': `${SITE_URL}/blog`,
name: `${SITE_NAME} Blog`,
},
inLanguage: 'en-US',
},
],
};

return (
<>
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
<BlogPostContent post={post} readingTime={readingTime} />
</>
);
}
91 changes: 91 additions & 0 deletions packages/app/src/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Metadata } from 'next';

import { BlogContent } from '@/components/blog/blog-content';
import { BLOG_POSTS } from '@/components/blog/blog-data';
import {
AUTHOR_NAME,
AUTHOR_URL,
OG_IMAGE,
SITE_NAME,
SITE_URL,
} from '@semianalysisai/inferencex-constants';

const BLOG_DESCRIPTION =
'Deep dives into inference benchmarking, GPU performance, and the economics of AI compute from the InferenceX team at SemiAnalysis.';

export const metadata: Metadata = {
title: 'Blog',
description: BLOG_DESCRIPTION,
alternates: {
canonical: `${SITE_URL}/blog`,
types: { 'application/rss+xml': `${SITE_URL}/feed.xml` },
},
openGraph: {
title: `Blog | ${SITE_NAME} by ${AUTHOR_NAME}`,
description: BLOG_DESCRIPTION,
url: `${SITE_URL}/blog`,
siteName: SITE_NAME,
type: 'website',
locale: 'en_US',
},
};

export default function BlogPage() {
const sorted = [...BLOG_POSTS].sort((a, b) => b.date.localeCompare(a.date));

const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: SITE_NAME,
item: SITE_URL,
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: `${SITE_URL}/blog`,
},
],
},
{
'@type': 'Blog',
'@id': `${SITE_URL}/blog`,
name: `${SITE_NAME} Blog`,
description: BLOG_DESCRIPTION,
url: `${SITE_URL}/blog`,
publisher: {
'@type': 'Organization',
name: AUTHOR_NAME,
url: AUTHOR_URL,
logo: { '@type': 'ImageObject', url: OG_IMAGE },
},
inLanguage: 'en-US',
blogPost: sorted.map((post) => ({
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
url: `${SITE_URL}/blog/${post.slug}`,
datePublished: `${post.date}T00:00:00Z`,
...(post.modifiedDate && { dateModified: `${post.modifiedDate}T00:00:00Z` }),
author: post.author.split(', ').map((name) => ({
'@type': 'Person',
name,
})),
})),
},
],
};

return (
<>
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
<BlogContent />
</>
);
}
Loading