From 767bab0adfce822778c0bf83c6c21c3811f414be Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:07:49 -0700 Subject: [PATCH 1/2] Add upload image compression, fix playground scroll trap, paginate blog and topic pages --- src/components/BlogFilter.astro | 50 ++------- src/components/Pager.astro | 55 ++++++++++ src/components/SearchInline.tsx | 4 +- src/components/SearchModal.astro | 4 +- src/pages/blog/[...page].astro | 79 ++++++++++++++ src/pages/blog/index.astro | 102 ------------------ src/pages/index.astro | 2 +- src/pages/playground/index.astro | 14 +++ src/pages/search.astro | 2 +- .../{[id].astro => [id]/[...page].astro} | 54 +++++----- src/write/editor/blocks/FigureBlock.tsx | 5 +- src/write/editor/blocks/GalleryBlock.tsx | 8 +- src/write/storage/optimizeImage.test.ts | 25 +++++ src/write/storage/optimizeImage.ts | 38 +++++++ 14 files changed, 264 insertions(+), 178 deletions(-) create mode 100644 src/components/Pager.astro create mode 100644 src/pages/blog/[...page].astro delete mode 100644 src/pages/blog/index.astro rename src/pages/topics/{[id].astro => [id]/[...page].astro} (66%) create mode 100644 src/write/storage/optimizeImage.test.ts create mode 100644 src/write/storage/optimizeImage.ts diff --git a/src/components/BlogFilter.astro b/src/components/BlogFilter.astro index 51ed049..639dde2 100644 --- a/src/components/BlogFilter.astro +++ b/src/components/BlogFilter.astro @@ -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'; interface Props { - activeTopic: string; allCount: number; topicCounts: Record; } -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); --- -
- - All ({allCount}) - + - - diff --git a/src/components/Pager.astro b/src/components/Pager.astro new file mode 100644 index 0000000..e1f97e6 --- /dev/null +++ b/src/components/Pager.astro @@ -0,0 +1,55 @@ +--- +import type { Page } from 'astro'; + +interface Props { + page: Page; +} +const { page } = Astro.props; +--- + +{ + page.lastPage > 1 && ( + + ) +} + + diff --git a/src/components/SearchInline.tsx b/src/components/SearchInline.tsx index b81fd66..559268c 100644 --- a/src/components/SearchInline.tsx +++ b/src/components/SearchInline.tsx @@ -75,7 +75,7 @@ export default function SearchInline() { .slice(0, 3) .map((t) => ({ group: 'topic' as const, - url: `/blog?topic=${t.id}`, + url: `/topics/${t.id}`, title: t.name, excerpt: t.desc, })); @@ -211,7 +211,7 @@ export default function SearchInline() {
Browse by topic
{TOPICS.map((t) => ( - + {t.name} ))} diff --git a/src/components/SearchModal.astro b/src/components/SearchModal.astro index 5d80217..bb9c993 100644 --- a/src/components/SearchModal.astro +++ b/src/components/SearchModal.astro @@ -63,7 +63,7 @@ const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals'];
{ TOPICS.map((t) => ( - + {t.name} )) @@ -167,7 +167,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, })); diff --git a/src/pages/blog/[...page].astro b/src/pages/blog/[...page].astro new file mode 100644 index 0000000..5097085 --- /dev/null +++ b/src/pages/blog/[...page].astro @@ -0,0 +1,79 @@ +--- +import type { GetStaticPaths, Page } from 'astro'; +import { getCollection } from 'astro:content'; +import BaseLayout from '@/layouts/BaseLayout.astro'; +import BlogFilter from '@/components/BlogFilter.astro'; +import Pager from '@/components/Pager.astro'; +import PostRow from '@/components/PostRow.astro'; +import { TOPICS, formatMonth, sortPostsByDate } from '@/lib/data'; +import { resolvePostAuthors, type PostWithAuthors } from '@/lib/posts'; + +export const getStaticPaths = (async ({ paginate }) => { + const raw = (await getCollection('posts', ({ data }) => !data.draft)).sort(sortPostsByDate); + const all = await resolvePostAuthors(raw); + const topicCounts: Record = {}; + TOPICS.forEach((t) => { + topicCounts[t.id] = all.filter((p) => p.data.topicId === t.id).length; + }); + return paginate(all, { pageSize: 24, props: { topicCounts } }); +}) satisfies GetStaticPaths; + +interface Props { + page: Page; + topicCounts: Record; +} +const { page, topicCounts } = Astro.props; + +const grouped: Record = {}; +page.data.forEach((a) => { + const m = formatMonth(a.data.date); + if (!grouped[m]) grouped[m] = []; + grouped[m].push(a); +}); +--- + + 1 ? `All articles — page ${page.currentPage}` : 'All articles'} + description="The complete archive of ML Systems articles — long-form writing on inference, training, architecture, quantization, RAG, and more." + image="/og/page/blog.png" +> +
+
+
+
The Archive
+

All articles.

+
+
+ + + + { + Object.entries(grouped).map(([month, items]) => ( +
+
+ {month} +
+
+ {items.map((a) => ( + + ))} +
+
+ )) + } + + { + page.data.length === 0 && ( +
+ No articles yet. Be the first to{' '} + + write one + + . +
+ ) + } + + +
+
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro deleted file mode 100644 index f701066..0000000 --- a/src/pages/blog/index.astro +++ /dev/null @@ -1,102 +0,0 @@ ---- -import { getCollection } from 'astro:content'; -import BaseLayout from '@/layouts/BaseLayout.astro'; -import BlogFilter from '@/components/BlogFilter.astro'; -import PostRow from '@/components/PostRow.astro'; -import { TOPICS, formatMonth, sortPostsByDate } from '@/lib/data'; -import { resolvePostAuthors } from '@/lib/posts'; - -const url = new URL(Astro.request.url); -const activeTopic = url.searchParams.get('topic') || 'all'; - -const allRaw = (await getCollection('posts', ({ data }) => !data.draft)).sort(sortPostsByDate); -const all = await resolvePostAuthors(allRaw); - -const topicCounts: Record = {}; -TOPICS.forEach((t) => { - topicCounts[t.id] = all.filter((p) => p.data.topicId === t.id).length; -}); - -const grouped: Record = {}; -all.forEach((a) => { - const m = formatMonth(a.data.date); - if (!grouped[m]) grouped[m] = []; - grouped[m].push(a); -}); ---- - - -
-
-
-
The Archive
-

All articles.

-
-
- - - - { - Object.entries(grouped).map(([month, items]) => ( -
-
- {month} -
-
- {items.map((a) => ( - - ))} -
-
- )) - } - - -
-
- - - diff --git a/src/pages/index.astro b/src/pages/index.astro index 58b26ce..14bbe4a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -220,7 +220,7 @@ const featuredTools = allTools {t.totalCount > TOPIC_PREVIEW_LIMIT && ( - + All {t.totalCount} articles in {t.name} → )} diff --git a/src/pages/playground/index.astro b/src/pages/playground/index.astro index b964e6f..6ad8d24 100644 --- a/src/pages/playground/index.astro +++ b/src/pages/playground/index.astro @@ -487,4 +487,18 @@ const catalogTools = allTools.sort((a, b) => { if (empty) empty.hidden = visible > 0; }); }); + + // Hand wheel scrolling off to the page when the catalog box hits an edge, + // so the pointer resting inside it never traps the user (macOS scroll latching). + document.querySelectorAll('.tools-catalog-scroll').forEach((box) => { + box.addEventListener( + 'wheel', + (e) => { + const atTop = e.deltaY < 0 && box.scrollTop <= 0; + const atBottom = e.deltaY > 0 && box.scrollTop + box.clientHeight >= box.scrollHeight - 1; + if (atTop || atBottom) window.scrollBy(0, e.deltaY); + }, + { passive: true }, + ); + }); diff --git a/src/pages/search.astro b/src/pages/search.astro index 7a09cb2..0f5dd59 100644 --- a/src/pages/search.astro +++ b/src/pages/search.astro @@ -23,7 +23,7 @@ import { TOPICS } from '@/lib/data';

Interactive search needs JavaScript. Browse by topic instead:

- {TOPICS.map((t) => {t.name})} + {TOPICS.map((t) => {t.name})}
diff --git a/src/pages/topics/[id].astro b/src/pages/topics/[id]/[...page].astro similarity index 66% rename from src/pages/topics/[id].astro rename to src/pages/topics/[id]/[...page].astro index b790c58..2e5e604 100644 --- a/src/pages/topics/[id].astro +++ b/src/pages/topics/[id]/[...page].astro @@ -1,39 +1,38 @@ --- +import type { GetStaticPaths, Page } from 'astro'; import { getCollection } from 'astro:content'; import BaseLayout from '@/layouts/BaseLayout.astro'; +import Pager from '@/components/Pager.astro'; import PostRow from '@/components/PostRow.astro'; import { TOPICS, formatMonth, sortPostsByDate } from '@/lib/data'; -import { resolvePostAuthors } from '@/lib/posts'; +import { resolvePostAuthors, type PostWithAuthors } from '@/lib/posts'; import { SITE } from '@/lib/site'; -export async function getStaticPaths() { - const posts = await getCollection('posts', ({ data }) => !data.draft); - const active = new Set(posts.map((p) => p.data.topicId)); - return TOPICS.filter((t) => active.has(t.id)).map((t) => ({ - params: { id: t.id }, - props: { topic: t }, - })); -} +export const getStaticPaths = (async ({ paginate }) => { + const all = await getCollection('posts', ({ data }) => !data.draft); + const paths = []; + for (const topic of TOPICS) { + const raw = all.filter((p) => p.data.topicId === topic.id).sort(sortPostsByDate); + const posts = await resolvePostAuthors(raw); + paths.push(...paginate(posts, { params: { id: topic.id }, pageSize: 24, props: { topic } })); + } + return paths; +}) satisfies GetStaticPaths; interface Props { + page: Page; topic: (typeof TOPICS)[number]; } -const { topic } = Astro.props; - -const allRaw = (await getCollection('posts', ({ data }) => !data.draft)) - .filter((p) => p.data.topicId === topic.id) - .sort(sortPostsByDate); - -const posts = await resolvePostAuthors(allRaw); +const { page, topic } = Astro.props; -const grouped: Record = {}; -posts.forEach((a) => { +const grouped: Record = {}; +page.data.forEach((a) => { const m = formatMonth(a.data.date); if (!grouped[m]) grouped[m] = []; grouped[m].push(a); }); -const canonical = `${SITE.url}/topics/${topic.id}`; +const canonical = `${SITE.url}${page.url.current}`; const collectionJsonLd = { '@context': 'https://schema.org', @@ -44,9 +43,9 @@ const collectionJsonLd = { isPartOf: { '@type': 'WebSite', name: SITE.name, url: SITE.url }, mainEntity: { '@type': 'ItemList', - itemListElement: posts.map((p, i) => ({ + itemListElement: page.data.map((p, i) => ({ '@type': 'ListItem', - position: i + 1, + position: page.start + i + 1, url: `${SITE.url}/blog/${p.id}`, name: p.data.title, })), @@ -59,13 +58,18 @@ const breadcrumbJsonLd = { itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: SITE.url }, { '@type': 'ListItem', position: 2, name: 'Topics', item: `${SITE.url}/topics` }, - { '@type': 'ListItem', position: 3, name: topic.name, item: canonical }, + { '@type': 'ListItem', position: 3, name: topic.name, item: `${SITE.url}/topics/${topic.id}` }, ], }; + +const title = + page.currentPage > 1 + ? `${topic.name}: guides, primers & case studies — page ${page.currentPage}` + : `${topic.name}: guides, primers & case studies`; --- { - posts.length === 0 ? ( + page.data.length === 0 ? (
No articles in this topic yet. Be the first to{' '} @@ -102,5 +106,7 @@ const breadcrumbJsonLd = { )) ) } + +
diff --git a/src/write/editor/blocks/FigureBlock.tsx b/src/write/editor/blocks/FigureBlock.tsx index 97b042e..00fd95f 100644 --- a/src/write/editor/blocks/FigureBlock.tsx +++ b/src/write/editor/blocks/FigureBlock.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { createReactBlockSpec } from '@blocknote/react'; import { addAsset, getAssetUrl, removeAsset } from '../../storage/assets'; +import { optimizeImage } from '../../storage/optimizeImage'; // preview is a relative width so sizes stay distinct in the narrow editor column. const SIZES: { key: string; label: string; width: number; preview: string }[] = [ @@ -48,9 +49,9 @@ export const createFigureBlock = createReactBlockSpec( { + onChange={async (e) => { const file = e.target.files?.[0]; - if (file) setProps({ fileName: addAsset(file) }); + if (file) setProps({ fileName: addAsset(await optimizeImage(file)) }); }} />
diff --git a/src/write/editor/blocks/GalleryBlock.tsx b/src/write/editor/blocks/GalleryBlock.tsx index 5cfa537..840b5b9 100644 --- a/src/write/editor/blocks/GalleryBlock.tsx +++ b/src/write/editor/blocks/GalleryBlock.tsx @@ -1,5 +1,6 @@ import { createReactBlockSpec } from '@blocknote/react'; import { addAsset, getAssetUrl, removeAsset } from '../../storage/assets'; +import { optimizeImage } from '../../storage/optimizeImage'; function parseList(value: string): string[] { try { @@ -75,12 +76,13 @@ export const createGalleryBlock = createReactBlockSpec( type="file" accept="image/*" multiple - onChange={(e) => { + onChange={async (e) => { const files = [...(e.target.files ?? [])]; + e.target.value = ''; if (files.length === 0) return; - const added = files.map((f) => addAsset(f)); + const optimized = await Promise.all(files.map(optimizeImage)); + const added = optimized.map((f) => addAsset(f)); update([...fileNames, ...added], [...alts, ...added.map(() => '')]); - e.target.value = ''; }} />
diff --git a/src/write/storage/optimizeImage.test.ts b/src/write/storage/optimizeImage.test.ts new file mode 100644 index 0000000..6ba0614 --- /dev/null +++ b/src/write/storage/optimizeImage.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { optimizeImage } from './optimizeImage'; + +function fakeFile(name: string, type: string, bytes: number): File { + return new File([new Uint8Array(bytes)], name, { type }); +} + +describe('optimizeImage', () => { + it('passes small images through untouched', async () => { + const file = fakeFile('photo.png', 'image/png', 100 * 1024); + expect(await optimizeImage(file)).toBe(file); + }); + + it('skips gifs and svgs regardless of size', async () => { + const gif = fakeFile('anim.gif', 'image/gif', 900 * 1024); + const svg = fakeFile('chart.svg', 'image/svg+xml', 900 * 1024); + expect(await optimizeImage(gif)).toBe(gif); + expect(await optimizeImage(svg)).toBe(svg); + }); + + it('falls back to the original when decoding is unavailable', async () => { + const file = fakeFile('big.png', 'image/png', 900 * 1024); + expect(await optimizeImage(file)).toBe(file); + }); +}); diff --git a/src/write/storage/optimizeImage.ts b/src/write/storage/optimizeImage.ts new file mode 100644 index 0000000..671678b --- /dev/null +++ b/src/write/storage/optimizeImage.ts @@ -0,0 +1,38 @@ +// Optional compression pass in front of addAsset(). To drop the feature, delete +// this file and unwrap the optimizeImage() calls in FigureBlock and GalleryBlock. + +const SKIP_UNDER_BYTES = 400 * 1024; +const MAX_EDGE = 2400; +const QUALITY = 0.85; + +// GIFs would lose animation; SVGs are text and already small. +const SKIP_TYPES = new Set(['image/gif', 'image/svg+xml']); + +export async function optimizeImage(file: File): Promise { + if (file.size < SKIP_UNDER_BYTES || SKIP_TYPES.has(file.type)) return file; + try { + const bitmap = await createImageBitmap(file); + const scale = Math.min(1, MAX_EDGE / Math.max(bitmap.width, bitmap.height)); + const canvas = document.createElement('canvas'); + canvas.width = Math.round(bitmap.width * scale); + canvas.height = Math.round(bitmap.height * scale); + const ctx = canvas.getContext('2d'); + if (!ctx) { + bitmap.close(); + return file; + } + ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height); + bitmap.close(); + + const blob = await new Promise((resolve) => + canvas.toBlob((b) => resolve(b), 'image/webp', QUALITY), + ); + // Keep the original when the browser can't emit webp or re-encoding didn't shrink it. + if (!blob || blob.type !== 'image/webp' || blob.size >= file.size) return file; + + const stem = file.name.replace(/\.[^.]+$/, '') || 'image'; + return new File([blob], `${stem}.webp`, { type: 'image/webp' }); + } catch { + return file; + } +} From e2ed63cde60b320ba6c7df265012cd7eb3d1bbd0 Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:17:22 -0700 Subject: [PATCH 2/2] Move topics and external tools to content collections, add write-portal preview/paste/suggestions, contribute cleanup --- src/components/BlogFilter.astro | 2 +- src/components/Pager.astro | 60 +++- src/components/PostRow.astro | 3 +- src/components/SearchInline.tsx | 14 +- src/components/SearchModal.astro | 10 +- src/content/config.ts | 31 +- .../external-tools/apxml-vram-calculator.json | 8 + .../external-tools/chinchilla-scaling.json | 8 + .../external-tools/llm-visualization.json | 8 + .../external-tools/llm-vram-calculator.json | 8 + src/content/external-tools/tiktokenizer.json | 8 + .../external-tools/tokenizer-playground.json | 8 + src/content/topics/agents.json | 5 + src/content/topics/architecture.json | 1 + src/content/topics/distributed.json | 5 + src/content/topics/evals.json | 5 + src/content/topics/inference.json | 5 + src/content/topics/mlops.json | 5 + src/content/topics/multimodal.json | 5 + src/content/topics/quantization.json | 1 + src/content/topics/rag.json | 5 + src/content/topics/training.json | 5 + src/lib/data.ts | 145 +-------- src/lib/topics.ts | 30 ++ src/pages/authors/[handle]/index.astro | 3 +- src/pages/blog/[...page].astro | 5 +- src/pages/blog/[slug].astro | 3 +- src/pages/contribute/index.astro | 18 +- src/pages/index.astro | 3 +- src/pages/og/post/[slug].png.ts | 2 +- src/pages/playground/index.astro | 6 +- src/pages/rss.xml.ts | 3 +- src/pages/search.astro | 4 +- src/pages/topics/[id]/[...page].astro | 5 +- src/pages/topics/index.astro | 2 +- src/pages/write.astro | 2 +- src/styles/global.css | 3 + src/write/WritePortal.tsx | 80 ++++- src/write/dialogs/PublishDialog.tsx | 23 +- src/write/editor/editor-theme.css | 136 ++++++++ src/write/editor/usePasteImages.ts | 31 ++ src/write/preview/PreviewPane.tsx | 297 ++++++++++++++++++ src/write/serialize/toMdx.ts | 4 +- src/write/serialize/validate.ts | 18 ++ 44 files changed, 833 insertions(+), 200 deletions(-) create mode 100644 src/content/external-tools/apxml-vram-calculator.json create mode 100644 src/content/external-tools/chinchilla-scaling.json create mode 100644 src/content/external-tools/llm-visualization.json create mode 100644 src/content/external-tools/llm-vram-calculator.json create mode 100644 src/content/external-tools/tiktokenizer.json create mode 100644 src/content/external-tools/tokenizer-playground.json create mode 100644 src/content/topics/agents.json create mode 100644 src/content/topics/architecture.json create mode 100644 src/content/topics/distributed.json create mode 100644 src/content/topics/evals.json create mode 100644 src/content/topics/inference.json create mode 100644 src/content/topics/mlops.json create mode 100644 src/content/topics/multimodal.json create mode 100644 src/content/topics/quantization.json create mode 100644 src/content/topics/rag.json create mode 100644 src/content/topics/training.json create mode 100644 src/lib/topics.ts create mode 100644 src/write/editor/usePasteImages.ts create mode 100644 src/write/preview/PreviewPane.tsx diff --git a/src/components/BlogFilter.astro b/src/components/BlogFilter.astro index 639dde2..c3d68ab 100644 --- a/src/components/BlogFilter.astro +++ b/src/components/BlogFilter.astro @@ -1,7 +1,7 @@ --- // 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 { allCount: number; diff --git a/src/components/Pager.astro b/src/components/Pager.astro index e1f97e6..bc0985d 100644 --- a/src/components/Pager.astro +++ b/src/components/Pager.astro @@ -5,6 +5,26 @@ interface Props { page: Page; } 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); +}); --- { @@ -17,8 +37,22 @@ const { page } = Astro.props; ) : (
+ {it} + + ), + )} {page.url.next ? ( @@ -48,8 +82,26 @@ const { page } = Astro.props; .pager-link:hover { text-decoration: underline; } - .pager-count { + .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); - letter-spacing: 0.08em; } diff --git a/src/components/PostRow.astro b/src/components/PostRow.astro index 869a408..b432653 100644 --- a/src/components/PostRow.astro +++ b/src/components/PostRow.astro @@ -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 { diff --git a/src/components/SearchInline.tsx b/src/components/SearchInline.tsx index 559268c..d228ceb 100644 --- a/src/components/SearchInline.tsx +++ b/src/components/SearchInline.tsx @@ -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 = { @@ -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([]); const [loading, setLoading] = useState(false); @@ -69,9 +70,8 @@ export default function SearchInline() { const topicMatches = useMemo(() => { 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, @@ -79,7 +79,7 @@ export default function SearchInline() { title: t.name, excerpt: t.desc, })); - }, [query]); + }, [query, topics]); const rows = useMemo(() => { const fromPagefind: RowItem[] = pageResults.map((r) => { @@ -210,7 +210,7 @@ export default function SearchInline() {
Browse by topic
- {TOPICS.map((t) => ( + {topics.map((t) => ( {t.name} diff --git a/src/components/SearchModal.astro b/src/components/SearchModal.astro index bb9c993..fabe56f 100644 --- a/src/components/SearchModal.astro +++ b/src/components/SearchModal.astro @@ -1,9 +1,12 @@ --- -import { TOPICS } from '@/lib/data'; +import { TOPICS } from '@/lib/topics'; const HINT_TERMS = ['attention', 'quantization', 'vLLM', 'FSDP', 'evals']; --- + +