Skip to content

Commit 2b1409a

Browse files
committed
Add topic pages; improve SEO, fonts, metadata
Introduce a topics listing page and per-topic pages (src/pages/topics/index.astro, src/pages/topics/[id].astro) that group posts by month and include CollectionPage JSON-LD. Update BaseLayout to improve SEO and performance: revise default title, add Organization + WebSite JSON-LD, change font loading to preload + non-blocking stylesheet with noscript fallback, and set AnalyticsConsent to client:idle. Enhance blog article JSON-LD with image, dateModified and publisher logo. Update site copy in src/lib/site.ts and change topic links to /topics/:id/ in the topics index.
1 parent addc7c2 commit 2b1409a

5 files changed

Lines changed: 161 additions & 7 deletions

File tree

src/layouts/BaseLayout.astro

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface Props {
2727
}
2828
2929
const {
30-
title = `${SITE.name} — An open archive for the engineers building modern AI`,
30+
title = `${SITE.name} — Open community blog on machine learning systems & infrastructure`,
3131
description = SITE.description,
3232
canonical = new URL(Astro.url.pathname, SITE.url).toString(),
3333
image = '/og-default.png',
@@ -41,6 +41,33 @@ const fullTitle = title.includes(SITE.name) ? title : `${title} — ${SITE.name}
4141
const imageUrl = image.startsWith('http') ? image : new URL(image, SITE.url).toString();
4242
const currentPath = Astro.url.pathname;
4343
const gaMeasurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
44+
45+
const siteJsonLd = [
46+
{
47+
'@context': 'https://schema.org',
48+
'@type': 'Organization',
49+
name: SITE.name,
50+
url: SITE.url,
51+
logo: new URL('/favicon-96x96.png', SITE.url).toString(),
52+
description: SITE.description,
53+
sameAs: [SITE.github, 'https://x.com/MLSystemsDev'],
54+
},
55+
{
56+
'@context': 'https://schema.org',
57+
'@type': 'WebSite',
58+
name: SITE.name,
59+
url: SITE.url,
60+
description: SITE.description,
61+
potentialAction: {
62+
'@type': 'SearchAction',
63+
target: {
64+
'@type': 'EntryPoint',
65+
urlTemplate: `${SITE.url}/search?q={search_term_string}`,
66+
},
67+
'query-input': 'required name=search_term_string',
68+
},
69+
},
70+
];
4471
---
4572

4673
<!doctype html>
@@ -113,13 +140,26 @@ const gaMeasurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
113140
<meta name="twitter:image" content={imageUrl} />
114141
<meta name="twitter:creator" content={SITE.twitter} />
115142

116-
<!-- Fonts (preconnect + display=swap for performance) -->
143+
<!-- Fonts — preloaded non-blocking. Stylesheet swaps in after first paint. -->
117144
<link rel="preconnect" href="https://fonts.googleapis.com" />
118145
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
119146
<link
147+
rel="preload"
148+
as="style"
120149
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
150+
/>
151+
<link
121152
rel="stylesheet"
153+
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
154+
media="print"
155+
onload="this.media='all'"
122156
/>
157+
<noscript>
158+
<link
159+
rel="stylesheet"
160+
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
161+
/>
162+
</noscript>
123163

124164
<!-- Favicons -->
125165
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
@@ -134,7 +174,8 @@ const gaMeasurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
134174
<!-- Sitemap -->
135175
<link rel="sitemap" href="/sitemap-index.xml" />
136176

137-
<!-- JSON-LD structured data -->
177+
<!-- JSON-LD structured data: site-wide Organization + WebSite -->
178+
<script is:inline type="application/ld+json" set:html={JSON.stringify(siteJsonLd)} />
138179
{jsonLd && <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />}
139180

140181
<!-- Google Analytics (gtag.js) with Consent Mode v2 (denied by default) -->
@@ -179,6 +220,6 @@ const gaMeasurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
179220
<Footer />
180221
</div>
181222
<SearchModal client:idle />
182-
<AnalyticsConsent measurementId={gaMeasurementId} client:load />
223+
<AnalyticsConsent measurementId={gaMeasurementId} client:idle />
183224
</body>
184225
</html>

src/lib/site.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const SITE = {
55
domain: 'mlsystems.dev',
66
url: 'https://mlsystems.dev',
77
description:
8-
'An open archive for the engineers and researchers building modern machine learning systems.',
8+
'Open community writing about machine learning systems — inference engines, training infrastructure, GPU memory, embeddings, and deployments. Articles, primers, and case studies from anyone learning or building in the space.',
99
tagline: 'Machine learning, from kernels to clusters.',
1010
author: 'The ML Systems Community',
1111
twitter: '@MLSystemsDev',

src/pages/blog/[slug].astro

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,16 @@ const jsonLd = {
4747
'@type': 'Article',
4848
headline: post.data.title,
4949
description: post.data.summary,
50+
image: new URL(ogImage, SITE.url).toString(),
5051
datePublished: dateISO,
52+
dateModified: dateISO,
5153
author: authors.map((a) => ({ '@type': 'Person', name: a.data.name })),
52-
publisher: { '@type': 'Organization', name: SITE.name, url: SITE.url },
54+
publisher: {
55+
'@type': 'Organization',
56+
name: SITE.name,
57+
url: SITE.url,
58+
logo: { '@type': 'ImageObject', url: new URL('/favicon-96x96.png', SITE.url).toString() },
59+
},
5360
mainEntityOfPage: { '@type': 'WebPage', '@id': canonical },
5461
keywords: post.data.tags?.join(', '),
5562
articleSection: post.data.topic,

src/pages/topics/[id].astro

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
import { getCollection, getEntries } from 'astro:content';
3+
import BaseLayout from '@/layouts/BaseLayout.astro';
4+
import { TOPICS, formatDate, formatMonth, sortPostsByDate } from '@/lib/data';
5+
import { SITE } from '@/lib/site';
6+
7+
export async function getStaticPaths() {
8+
return TOPICS.map((t) => ({ params: { id: t.id }, props: { topic: t } }));
9+
}
10+
11+
interface Props {
12+
topic: (typeof TOPICS)[number];
13+
}
14+
const { topic } = Astro.props;
15+
16+
const allRaw = (await getCollection('posts', ({ data }) => !data.draft))
17+
.filter((p) => p.data.topicId === topic.id)
18+
.sort(sortPostsByDate);
19+
20+
const posts = await Promise.all(
21+
allRaw.map(async (p) => ({ ...p, authors: await getEntries(p.data.authors) })),
22+
);
23+
24+
const grouped: Record<string, typeof posts> = {};
25+
posts.forEach((a) => {
26+
const m = formatMonth(a.data.date.toISOString());
27+
if (!grouped[m]) grouped[m] = [];
28+
grouped[m].push(a);
29+
});
30+
31+
const canonical = `${SITE.url}/topics/${topic.id}/`;
32+
33+
const jsonLd = {
34+
'@context': 'https://schema.org',
35+
'@type': 'CollectionPage',
36+
name: `${topic.name} — ${SITE.name}`,
37+
description: topic.desc,
38+
url: canonical,
39+
isPartOf: { '@type': 'WebSite', name: SITE.name, url: SITE.url },
40+
mainEntity: {
41+
'@type': 'ItemList',
42+
itemListElement: posts.map((p, i) => ({
43+
'@type': 'ListItem',
44+
position: i + 1,
45+
url: `${SITE.url}/blog/${p.id}/`,
46+
name: p.data.title,
47+
})),
48+
},
49+
};
50+
---
51+
52+
<BaseLayout
53+
title={`${topic.name} — articles on ${topic.name.toLowerCase()}`}
54+
description={`${topic.desc} — articles, primers, and case studies on ${topic.name.toLowerCase()} from the ML Systems community.`}
55+
image="/og/page/topics.png"
56+
jsonLd={jsonLd}
57+
>
58+
<div class="container blog-shell">
59+
<div class="blog-head">
60+
<div>
61+
<div class="eyebrow page-eyebrow">Topic</div>
62+
<h1 class="blog-h1">{topic.name}.</h1>
63+
<p class="page-lede" style="max-width: 680px;">{topic.desc}</p>
64+
</div>
65+
</div>
66+
67+
{
68+
posts.length === 0 ? (
69+
<div style="padding: 80px 0; text-align: center; color: var(--ink-3);">
70+
No articles in this topic yet. Be the first to{' '}
71+
<a href="/contribute" style="color: var(--accent);">
72+
write one
73+
</a>
74+
.
75+
</div>
76+
) : (
77+
Object.entries(grouped).map(([month, items]) => (
78+
<div class="blog-month-group" style="margin-bottom: 40px;">
79+
<div class="eyebrow" style="margin-bottom: 4px; color: var(--ink-3);">
80+
{month}
81+
</div>
82+
<div class="blog-list">
83+
{items.map((a) => (
84+
<a href={`/blog/${a.id}`} class="blog-row" data-topic={a.data.topicId}>
85+
<div class="blog-row-body">
86+
<div class="blog-row-title-line">
87+
<h3 class="blog-row-title">{a.data.title}</h3>
88+
{a.data.tags?.slice(0, 2).map((t) => (
89+
<span class="blog-row-tag">#{t}</span>
90+
))}
91+
</div>
92+
<p class="blog-row-summary">{a.data.summary}</p>
93+
</div>
94+
<div class="blog-row-meta">
95+
<strong>{a.authors.map((au) => au.data.name).join(' · ')}</strong>
96+
<span>{formatDate(a.data.date.toISOString())}</span>
97+
</div>
98+
</a>
99+
))}
100+
</div>
101+
</div>
102+
))
103+
)
104+
}
105+
</div>
106+
</BaseLayout>

src/pages/topics/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const topicCounts = countPostsByTopic(posts);
2424
<div class="topics-list" style="margin-top: 16px;">
2525
{
2626
TOPICS.map((t, i) => (
27-
<a href={`/blog?topic=${t.id}`} class="topic-row">
27+
<a href={`/topics/${t.id}/`} class="topic-row">
2828
<span class="topic-num">{String(i + 1).padStart(2, '0')}</span>
2929
<h3 class="topic-name">{t.name}</h3>
3030
<span class="topic-desc">{t.desc}</span>

0 commit comments

Comments
 (0)