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
52 changes: 10 additions & 42 deletions src/components/BlogFilter.astro
Original file line number Diff line number Diff line change
@@ -1,57 +1,25 @@
---
// Topic filter chips. Each chip is a real anchor — works for direct URLs,
// crawlers, and JS-disabled clients. A small enhancement script intercepts
// clicks for an instant in-page filter (no reload).
// Topic chips — each links to its topic hub page; "All" is the paginated archive.

import { TOPICS } from '@/lib/data';
import { TOPICS } from '@/lib/topics';

interface Props {
activeTopic: string;
allCount: number;
topicCounts: Record<string, number>;
}
const { activeTopic, allCount, topicCounts } = Astro.props;
const { allCount, topicCounts } = Astro.props;

// Topic pages are only generated for topics with posts — hide empty ones.
const visibleTopics = TOPICS.filter((t) => (topicCounts[t.id] ?? 0) > 0);
---

<div class="blog-filters" style="margin-bottom: 40px;" data-blog-filter>
<a
href="/blog"
class:list={['chip', 'chip--interactive', { 'is-active': activeTopic === 'all' }]}
data-topic="all"
>
All ({allCount})
</a>
<div class="blog-filters" style="margin-bottom: 40px;">
<a href="/blog" class="chip chip--interactive is-active">All ({allCount})</a>
{
TOPICS.map((t) => (
<a
href={`/blog?topic=${t.id}`}
class:list={['chip', 'chip--interactive', { 'is-active': activeTopic === t.id }]}
data-topic={t.id}
>
visibleTopics.map((t) => (
<a href={`/topics/${t.id}`} class="chip chip--interactive">
{t.name} ({topicCounts[t.id] ?? 0})
</a>
))
}
</div>

<script>
// Enhancement: intercept chip clicks, update URL + active state without reload.
// Falls back to plain navigation if JS is disabled.
document.addEventListener('click', (e) => {
const link = (e.target as HTMLElement).closest<HTMLAnchorElement>(
'[data-blog-filter] a[data-topic]',
);
if (!link) return;
e.preventDefault();
const topic = link.dataset.topic!;
const newUrl = topic === 'all' ? '/blog' : `/blog?topic=${topic}`;
history.pushState(null, '', newUrl);
document
.querySelectorAll<HTMLAnchorElement>('[data-blog-filter] a[data-topic]')
.forEach((a) => {
a.classList.toggle('is-active', a.dataset.topic === topic);
});
// Trigger the filter script in blog/index.astro
window.dispatchEvent(new PopStateEvent('popstate'));
});
</script>
107 changes: 107 additions & 0 deletions src/components/Pager.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
import type { Page } from 'astro';

interface Props {
page: Page<unknown>;
}
const { page } = Astro.props;

const base =
page.url.first ??
(page.currentPage === 1 ? page.url.current : page.url.current.replace(/\/\d+$/, ''));
const urlFor = (n: number) => (n === 1 ? base : `${base}/${n}`);

// First, last, and a one-page window around the current page; gaps become ellipses.
const shown = [
...new Set(
[1, page.currentPage - 1, page.currentPage, page.currentPage + 1, page.lastPage].filter(
(n) => n >= 1 && n <= page.lastPage,
),
),
].sort((a, b) => a - b);

const items: (number | '…')[] = [];
shown.forEach((n, i) => {
if (i > 0 && n - shown[i - 1] > 1) items.push('…');
items.push(n);
});
---

{
page.lastPage > 1 && (
<nav class="pager" aria-label="Pagination">
{page.url.prev ? (
<a class="pager-link" href={page.url.prev}>
← Newer
</a>
) : (
<span aria-hidden="true" />
)}
<span class="pager-pages">
{items.map((it) =>
it === '…' ? (
<span class="pager-gap" aria-hidden="true">
</span>
) : it === page.currentPage ? (
<span class="pager-num" aria-current="page">
{it}
</span>
) : (
<a class="pager-num" href={urlFor(it)}>
{it}
</a>
),
)}
</span>
{page.url.next ? (
<a class="pager-link" href={page.url.next}>
Older →
</a>
) : (
<span aria-hidden="true" />
)}
</nav>
)
}

<style>
.pager {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 56px;
padding-top: 24px;
border-top: 1px solid var(--line);
font-family: var(--font-mono);
font-size: 13px;
}
.pager-link {
color: var(--accent);
}
.pager-link:hover {
text-decoration: underline;
}
.pager-pages {
display: flex;
align-items: center;
gap: 6px;
}
.pager-num {
min-width: 28px;
padding: 3px 6px;
text-align: center;
border-radius: var(--radius-pill);
color: var(--ink-3);
}
a.pager-num:hover {
color: var(--accent);
}
.pager-num[aria-current='page'] {
color: var(--accent);
border: 1px solid var(--accent-soft);
}
.pager-gap {
color: var(--ink-3);
}
</style>
3 changes: 2 additions & 1 deletion src/components/PostRow.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { formatDate, topicName } from '@/lib/data';
import { formatDate } from '@/lib/data';
import { topicName } from '@/lib/topics';
import type { PostWithAuthors } from '@/lib/posts';

interface Props {
Expand Down
18 changes: 9 additions & 9 deletions src/components/SearchInline.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import { useEffect, useMemo, useRef, useState } from 'react';
import { TOPICS } from '@/lib/data';
import { loadPagefind, type PagefindResultData } from '@/lib/pagefind';

type Topic = { id: string; name: string; desc: string };

type Group = 'topic' | 'article' | 'tool' | 'author' | 'page';

type RowItem = {
Expand Down Expand Up @@ -43,7 +44,7 @@ function buildMeta(group: Group, meta: PagefindResultData['meta']): string {
return parts.join(' · ');
}

export default function SearchInline() {
export default function SearchInline({ topics }: { topics: Topic[] }) {
const [query, setQuery] = useState('');
const [pageResults, setPageResults] = useState<PagefindResultData[]>([]);
const [loading, setLoading] = useState(false);
Expand All @@ -69,17 +70,16 @@ export default function SearchInline() {
const topicMatches = useMemo<RowItem[]>(() => {
const q = query.trim().toLowerCase();
if (!q) return [];
return TOPICS.filter(
(t) => t.name.toLowerCase().includes(q) || t.desc.toLowerCase().includes(q),
)
return topics
.filter((t) => t.name.toLowerCase().includes(q) || t.desc.toLowerCase().includes(q))
.slice(0, 3)
.map((t) => ({
group: 'topic' as const,
url: `/blog?topic=${t.id}`,
url: `/topics/${t.id}`,
title: t.name,
excerpt: t.desc,
}));
}, [query]);
}, [query, topics]);

const rows = useMemo<RowItem[]>(() => {
const fromPagefind: RowItem[] = pageResults.map((r) => {
Expand Down Expand Up @@ -210,8 +210,8 @@ export default function SearchInline() {
<div className="search-section">
<div className="search-section-label">Browse by topic</div>
<div className="search-topic-chips">
{TOPICS.map((t) => (
<a key={t.id} href={`/blog?topic=${t.id}`} className="chip chip--interactive">
{topics.map((t) => (
<a key={t.id} href={`/topics/${t.id}`} className="chip chip--interactive">
{t.name}
</a>
))}
Expand Down
14 changes: 10 additions & 4 deletions src/components/SearchModal.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
---
import { TOPICS } from '@/lib/data';
import { TOPICS } from '@/lib/topics';

const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals'];
---

<!-- Topics for the client-side quick matcher — collections aren't readable in the browser. -->
<script type="application/json" data-search-topics set:html={JSON.stringify(TOPICS)} />

<div
class="search-modal"
role="dialog"
Expand Down Expand Up @@ -63,7 +66,7 @@ const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals'];
<div class="search-topic-chips">
{
TOPICS.map((t) => (
<a href={`/blog?topic=${t.id}`} class="chip chip--interactive">
<a href={`/topics/${t.id}`} class="chip chip--interactive">
{t.name}
</a>
))
Expand Down Expand Up @@ -104,9 +107,12 @@ const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals'];
</div>

<script>
import { TOPICS } from '@/lib/data';
import { loadPagefind } from '@/lib/pagefind';

const TOPICS: { id: string; name: string; desc: string }[] = JSON.parse(
document.querySelector('[data-search-topics]')?.textContent ?? '[]',
);

type Group = 'topic' | 'article' | 'tool' | 'author' | 'page';
type RowItem = { group: Group; url: string; title: string; excerpt?: string; meta?: string };

Expand Down Expand Up @@ -167,7 +173,7 @@ const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals'];
.slice(0, 3)
.map((t) => ({
group: 'topic' as const,
url: `/blog?topic=${t.id}`,
url: `/topics/${t.id}`,
title: t.name,
excerpt: t.desc,
}));
Expand Down
31 changes: 29 additions & 2 deletions src/content/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { readdirSync } from 'node:fs';
import { defineCollection, reference, z } from 'astro:content';
import { glob } from 'astro/loaders';
import { TOPIC_IDS } from '@/lib/data';

// Topic ids come from the data files themselves, so post frontmatter is
// validated against whatever topics exist — no code change to add one.
const TOPIC_IDS = readdirSync(new URL('./topics', import.meta.url))
.filter((f) => f.endsWith('.json'))
.map((f) => f.replace(/\.json$/, '')) as [string, ...string[]];

const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
Expand Down Expand Up @@ -66,4 +72,25 @@ const tools = defineCollection({
}),
});

export const collections = { posts, authors, tools };
const topics = defineCollection({
loader: glob({ pattern: '*.json', base: './src/content/topics' }),
schema: z.object({
order: z.number().int(),
name: z.string(),
desc: z.string(),
}),
});

const externalTools = defineCollection({
loader: glob({ pattern: '*.json', base: './src/content/external-tools' }),
schema: z.object({
order: z.number().int(),
name: z.string(),
source: z.string(),
desc: z.string(),
href: z.string().url(),
category: z.enum(['Tokenization', 'Memory & VRAM', 'Architecture', 'Training & Scaling']),
}),
});

export const collections = { posts, authors, tools, topics, externalTools };
8 changes: 8 additions & 0 deletions src/content/external-tools/apxml-vram-calculator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 4,
"name": "APXML VRAM Calculator",
"source": "APXML",
"desc": "Inference and fine-tuning VRAM calculator covering Nvidia GPUs and Apple Silicon. Good for picking hardware for a target model.",
"href": "https://apxml.com/tools/vram-calculator",
"category": "Memory & VRAM"
}
8 changes: 8 additions & 0 deletions src/content/external-tools/chinchilla-scaling.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 6,
"name": "Chinchilla Scaling Calculator",
"source": "Nathan Godey",
"desc": "Enter a model size, get the Chinchilla-optimal training-token count per Hoffmann et al. 2022 — with an interactive params-vs-tokens chart.",
"href": "https://nathangodey.github.io/posts/scaling/",
"category": "Training & Scaling"
}
8 changes: 8 additions & 0 deletions src/content/external-tools/llm-visualization.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 5,
"name": "LLM Visualization",
"source": "Brendan Bycroft",
"desc": "A 3D, animated walk through the entire forward pass of a nano-GPT model, layer by layer. The clearest mental model of how a transformer works.",
"href": "https://bbycroft.net/llm",
"category": "Architecture"
}
8 changes: 8 additions & 0 deletions src/content/external-tools/llm-vram-calculator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 3,
"name": "LLM Model VRAM Calculator",
"source": "NyxKrage · Hugging Face",
"desc": "Widely-referenced inference VRAM estimator for popular open-source models with quantization and context-length sliders.",
"href": "https://huggingface.co/spaces/NyxKrage/LLM-Model-VRAM-Calculator",
"category": "Memory & VRAM"
}
8 changes: 8 additions & 0 deletions src/content/external-tools/tiktokenizer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 2,
"name": "Tiktokenizer",
"source": "dqbd",
"desc": "OpenAI-focused tokenizer playground. Visualize cl100k, o200k, and legacy encodings with per-token highlights.",
"href": "https://tiktokenizer.vercel.app/",
"category": "Tokenization"
}
8 changes: 8 additions & 0 deletions src/content/external-tools/tokenizer-playground.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"order": 1,
"name": "The Tokenizer Playground",
"source": "Xenova · Hugging Face",
"desc": "Tokenize the same text with GPT-4, Claude, LLaMA, Mistral, Gemma, and more — switch tokenizers instantly, or load any Hugging Face tokenizer. Runs in the browser.",
"href": "https://huggingface.co/spaces/Xenova/the-tokenizer-playground",
"category": "Tokenization"
}
5 changes: 5 additions & 0 deletions src/content/topics/agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"order": 8,
"name": "Agents",
"desc": "Planning, tool use, multi-agent systems, memory, and orchestration."
}
1 change: 1 addition & 0 deletions src/content/topics/architecture.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "order": 3, "name": "Architecture", "desc": "Transformers, MoE, SSMs, hybrids, and what's next." }
5 changes: 5 additions & 0 deletions src/content/topics/distributed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"order": 4,
"name": "Distributed Training",
"desc": "FSDP, tensor parallel, pipeline parallel, sequence parallel."
}
5 changes: 5 additions & 0 deletions src/content/topics/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"order": 9,
"name": "Evaluation",
"desc": "Benchmarks, harnesses, contamination, signal vs noise."
}
Loading
Loading