Skip to content

Commit 7639005

Browse files
thegdsksglinr
andcommitted
feat: expand /docs with recipes, migration guides, and more FAQ
Co-Authored-By: Glinr <bot@glincker.com>
1 parent 03961de commit 7639005

9 files changed

Lines changed: 349 additions & 19 deletions

‎src/app/docs/page.tsx‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ import { getCategoryCounts, getFormattedIconCount } from "@/lib/icons";
55
import { SidebarShell } from "@/components/layout/sidebar-shell";
66
import { JsonLd } from "@/components/json-ld";
77
import { FRAMEWORK_GUIDES, BEST_PRACTICES, TRICKS, FAQ_ITEMS } from "@/lib/docs-content";
8+
import { RECIPES } from "@/lib/docs-recipes";
9+
import { MIGRATION_GUIDES } from "@/lib/docs-migration";
810
import { FrameworkGuideSection } from "@/components/docs/framework-guide-section";
911
import { BestPracticesSection } from "@/components/docs/best-practices-section";
1012
import { TricksSection } from "@/components/docs/tricks-section";
13+
import { RecipesSection } from "@/components/docs/recipes-section";
14+
import { MigrationSection } from "@/components/docs/migration-section";
1115
import { FaqSection } from "@/components/docs/faq-section";
1216
import { DocsNav, type DocsNavItem } from "@/components/docs/docs-nav";
1317

1418
const count = getFormattedIconCount();
1519

1620
export const metadata: Metadata = {
17-
title: "Docs & Guides - theSVG",
18-
description: `How to use ${count}+ brand SVG icons in React, Vue, Svelte, React Native, plain HTML, the CLI, the REST API, and AI assistants via MCP. Best practices, tricks, and FAQ.`,
21+
title: "Docs & Guides",
22+
description: `How to use ${count}+ brand SVG icons in React, Vue, Svelte, React Native, plain HTML, the CLI, the REST API, and AI assistants via MCP. Recipes, best practices, tricks, migration guides, and FAQ.`,
1923
keywords: [
2024
"theSVG docs",
2125
"SVG icon library usage guide",
@@ -27,10 +31,15 @@ export const metadata: Metadata = {
2731
"brand icon CLI",
2832
"MCP server icons",
2933
"SVG icon FAQ",
34+
"tech stack badge icons",
35+
"brand icon picker React",
36+
"migrate from Simple Icons",
37+
"Next.js Image SVG icons",
38+
"accessible icon button",
3039
],
3140
openGraph: {
32-
title: "Docs & Guides - theSVG",
33-
description: `How to use ${count}+ brand SVG icons across every major framework, the CDN, the CLI, the REST API, and AI assistants.`,
41+
title: "Docs & Guides",
42+
description: `How to use ${count}+ brand SVG icons across every major framework, the CDN, the CLI, the REST API, and AI assistants. Recipes, best practices, and migration guides included.`,
3443
siteName: "theSVG",
3544
},
3645
alternates: {
@@ -41,8 +50,10 @@ export const metadata: Metadata = {
4150
const NAV_ITEMS: DocsNavItem[] = [
4251
{ id: "getting-started", label: "Getting started" },
4352
...FRAMEWORK_GUIDES.map((g) => ({ id: g.id, label: g.label })),
53+
{ id: "recipes", label: "Recipes" },
4454
{ id: "best-practices", label: "Best practices" },
4555
{ id: "tricks", label: "Tricks" },
56+
{ id: "migration", label: "Migrating" },
4657
{ id: "faq", label: "FAQ" },
4758
];
4859

@@ -81,19 +92,21 @@ export default function DocsPage() {
8192
</div>
8293
<h1 className="mb-2 text-2xl font-bold sm:text-3xl">Docs &amp; Guides</h1>
8394
<p className="max-w-3xl text-muted-foreground">
84-
Everything for using {count}+ brand icons: framework guides, best practices,
85-
tricks, and FAQ, all on one page.
95+
Everything for using {count}+ brand icons: framework guides, real-world recipes,
96+
best practices, tricks, migration guides, and FAQ, all on one page.
8697
</p>
8798
</div>
8899

89-
{/* Sticky in-page nav */}
90-
<div className="sticky top-0 z-10 -mx-4 mb-8 border-b border-border/40 bg-background/90 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 dark:border-white/[0.06]">
100+
{/* Sticky in-page nav. Offset below the mobile floating top bar
101+
and the desktop header (both fixed/sticky with a higher
102+
z-index) so it doesn't scroll underneath them. */}
103+
<div className="sticky top-16 z-20 -mx-4 mb-8 border-b border-border/40 bg-background/90 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:top-[calc(4.25rem+var(--banner-h,0px))] dark:border-white/[0.06]">
91104
<DocsNav items={NAV_ITEMS} />
92105
</div>
93106

94107
<div className="space-y-14">
95108
{/* Getting started */}
96-
<section id="getting-started" className="scroll-mt-24">
109+
<section id="getting-started" className="scroll-mt-32">
97110
<h2 className="mb-3 text-lg font-semibold">Getting started</h2>
98111
<p className="max-w-2xl text-sm text-muted-foreground">
99112
Pick the guide that matches how you&apos;re building. A handful of known icons in
@@ -108,8 +121,10 @@ export default function DocsPage() {
108121
<FrameworkGuideSection key={guide.id} guide={guide} />
109122
))}
110123

124+
<RecipesSection items={RECIPES} />
111125
<BestPracticesSection items={BEST_PRACTICES} />
112126
<TricksSection items={TRICKS} />
127+
<MigrationSection items={MIGRATION_GUIDES} />
113128
<FaqSection items={FAQ_ITEMS} />
114129
</div>
115130
</div>

‎src/components/docs/best-practices-section.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { BestPractice } from "@/lib/docs-content";
33

44
export function BestPracticesSection({ items }: { items: BestPractice[] }) {
55
return (
6-
<section id="best-practices" className="scroll-mt-24">
6+
<section id="best-practices" className="scroll-mt-32">
77
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
88
<Lightbulb className="h-4 w-4 text-orange-500" />
99
Best practices

‎src/components/docs/framework-guide-section.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DocsCodeBlock } from "@/components/docs/docs-code-block";
55

66
export function FrameworkGuideSection({ guide }: { guide: FrameworkGuide }) {
77
return (
8-
<section id={guide.id} className="scroll-mt-24">
8+
<section id={guide.id} className="scroll-mt-32">
99
<div className="mb-3 flex flex-wrap items-center gap-2">
1010
<h3 className="text-base font-semibold">{guide.label}</h3>
1111
{guide.packageName && (
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Link from "next/link";
2+
import { ArrowLeftRight } from "lucide-react";
3+
import type { MigrationGuide } from "@/lib/docs-migration";
4+
import { DocsCodeBlock } from "@/components/docs/docs-code-block";
5+
6+
export function MigrationSection({ items }: { items: MigrationGuide[] }) {
7+
return (
8+
<section id="migration" className="scroll-mt-32">
9+
<h2 className="mb-1.5 flex items-center gap-2 text-lg font-semibold">
10+
<ArrowLeftRight className="h-4 w-4 text-orange-500" />
11+
Migrating from other libraries
12+
</h2>
13+
<p className="mb-4 max-w-2xl text-sm text-muted-foreground">
14+
Full feature-by-feature comparison lives on{" "}
15+
<Link href="/compare" className="text-orange-600 underline underline-offset-2 dark:text-orange-400">
16+
/compare
17+
</Link>
18+
. These are the short, practical version.
19+
</p>
20+
<div className="space-y-8">
21+
{items.map((guide) => (
22+
<div key={guide.id} id={guide.id} className="scroll-mt-32">
23+
<h3 className="mb-1.5 text-sm font-semibold">{guide.title}</h3>
24+
<p className="mb-3 max-w-2xl text-xs leading-relaxed text-muted-foreground">
25+
{guide.summary}
26+
</p>
27+
<ul className="mb-3 list-disc space-y-1 pl-4 text-xs leading-relaxed text-muted-foreground">
28+
{guide.points.map((point) => (
29+
<li key={point}>{point}</li>
30+
))}
31+
</ul>
32+
{guide.snippet && <DocsCodeBlock snippet={guide.snippet} />}
33+
</div>
34+
))}
35+
</div>
36+
</section>
37+
);
38+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Wrench } from "lucide-react";
2+
import type { Recipe } from "@/lib/docs-recipes";
3+
import { DocsCodeBlock } from "@/components/docs/docs-code-block";
4+
5+
export function RecipesSection({ items }: { items: Recipe[] }) {
6+
return (
7+
<section id="recipes" className="scroll-mt-32">
8+
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
9+
<Wrench className="h-4 w-4 text-orange-500" />
10+
Recipes
11+
</h2>
12+
<div className="space-y-8">
13+
{items.map((recipe) => (
14+
<div key={recipe.id} id={recipe.id} className="scroll-mt-32">
15+
<h3 className="mb-1.5 text-sm font-semibold">{recipe.title}</h3>
16+
<p className="mb-3 max-w-2xl text-xs leading-relaxed text-muted-foreground">
17+
{recipe.description}
18+
</p>
19+
<div className="space-y-3">
20+
{recipe.snippets.map((snippet) => (
21+
<DocsCodeBlock key={snippet.label} snippet={snippet} />
22+
))}
23+
</div>
24+
</div>
25+
))}
26+
</div>
27+
</section>
28+
);
29+
}

‎src/components/docs/tricks-section.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DocsCodeBlock } from "@/components/docs/docs-code-block";
44

55
export function TricksSection({ items }: { items: Trick[] }) {
66
return (
7-
<section id="tricks" className="scroll-mt-24">
7+
<section id="tricks" className="scroll-mt-32">
88
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold">
99
<Sparkles className="h-4 w-4 text-orange-500" />
1010
Tricks

‎src/lib/docs-content.ts‎

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@
99
* packages/cli) — verify against those before changing an API shape.
1010
*/
1111

12+
import { readFileSync } from "fs";
13+
import { join } from "path";
1214
import type { SnippetFormat } from "@/lib/code-snippets";
1315

16+
/**
17+
* The release workflow (.github/workflows/release.yml) tags releases as
18+
* `thesvg@<version>`, taken from packages/thesvg/package.json's own
19+
* version field at publish time. Reading that same field here means the
20+
* jsDelivr pin example below tracks whatever was actually last released,
21+
* instead of a hardcoded tag that goes stale the next time a version bumps.
22+
*/
23+
const THESVG_PKG_VERSION: string = JSON.parse(
24+
readFileSync(join(process.cwd(), "packages/thesvg/package.json"), "utf-8"),
25+
).version;
26+
const JSDELIVR_PINNED_REF = `thesvg@${THESVG_PKG_VERSION}`;
27+
1428
export interface DocsSnippet {
1529
label: string;
1630
code: string;
@@ -42,7 +56,7 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
4256
{
4357
label: "Automatic dark mode (no JS)",
4458
format: "html",
45-
code: `<picture>\n <source media="(prefers-color-scheme: dark)" srcset="https://thesvg.org/icons/github/light.svg" />\n <img src="https://thesvg.org/icons/github/dark.svg" width="24" alt="GitHub" />\n</picture>`,
59+
code: `<picture>\n <source media="(prefers-color-scheme: dark)" srcset="https://thesvg.org/icons/github/light.svg" />\n <img src="https://thesvg.org/icons/github/dark.svg" width="24" height="24" alt="GitHub" />\n</picture>`,
4660
},
4761
],
4862
},
@@ -85,14 +99,20 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
8599
format: "vue",
86100
code: `<script setup>\nimport { Github, Figma } from "@thesvg/vue";\n</script>\n\n<template>\n <Github width="24" height="24" />\n <Figma width="32" height="32" aria-label="Figma" />\n</template>`,
87101
},
102+
{
103+
label: "Nuxt 3 (explicit import, no auto-import config needed)",
104+
format: "vue",
105+
code: `<script setup>\n// @thesvg/vue is a plain npm package, not a Nuxt module -\n// import it like any other component, no nuxt.config changes required.\nimport { Github } from "@thesvg/vue";\n</script>\n\n<template>\n <Github width="24" height="24" />\n</template>`,
106+
},
88107
],
89108
},
90109
{
91110
id: "svelte",
92111
label: "Svelte",
93112
packageName: "@thesvg/svelte",
94113
npmUrl: "https://www.npmjs.com/package/@thesvg/svelte",
95-
summary: "Typed components for Svelte 4 and 5.",
114+
summary:
115+
"Typed components for Svelte 4 and 5. Plain markup with no browser-only APIs, so it renders identically during SvelteKit SSR and in the client - no onMount guard or client-only wrapper needed.",
96116
snippets: [
97117
{ label: "Install", format: "cli", code: `npm install @thesvg/svelte` },
98118
{
@@ -119,6 +139,11 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
119139
format: "react",
120140
code: `import { Github, Figma } from '@thesvg/react-native';\n\nexport function MyComponent() {\n return (\n <>\n <Github size={24} />\n <Figma size={32} color="#3b82f6" />\n </>\n );\n}`,
121141
},
142+
{
143+
label: "Expo Router screen",
144+
format: "react",
145+
code: `import { View } from 'react-native';\nimport { Github, Figma } from '@thesvg/react-native';\n\nexport default function Screen() {\n return (\n <View style={{ flexDirection: 'row', gap: 16 }}>\n <Github size={28} />\n <Figma size={28} />\n </View>\n );\n}`,
146+
},
122147
],
123148
},
124149
{
@@ -136,6 +161,11 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
136161
code: `npx @thesvg/cli add github vercel nextjs --format jsx --dir ./components/icons`,
137162
},
138163
{ label: "Search the catalog", format: "cli", code: `npx @thesvg/cli search "version control"` },
164+
{
165+
label: "In a Dockerfile / CI build step",
166+
format: "cli",
167+
code: `# Vendor icons at image build time instead of fetching them at runtime\nRUN npx --yes @thesvg/cli add github vercel nextjs --dir /app/public/icons`,
168+
},
139169
],
140170
},
141171
{
@@ -150,7 +180,7 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
150180
{
151181
label: "jsDelivr mirror (recommended for high traffic)",
152182
format: "cli",
153-
code: `curl "https://cdn.jsdelivr.net/gh/glincker/thesvg@main/src/data/icons.json"`,
183+
code: `curl "https://cdn.jsdelivr.net/gh/glincker/thesvg@${JSDELIVR_PINNED_REF}/src/data/icons.json"`,
154184
},
155185
],
156186
},
@@ -160,12 +190,12 @@ export const FRAMEWORK_GUIDES: FrameworkGuide[] = [
160190
packageName: "@thesvg/mcp-server",
161191
npmUrl: "https://www.npmjs.com/package/@thesvg/mcp-server",
162192
summary:
163-
"A local MCP server (stdio) that gives Claude Desktop, Claude Code, and Cursor tools to search and fetch icons directly.",
193+
"A local MCP server (stdio) that gives AI assistants tools to search and fetch icons directly. The same mcpServers JSON block works in any MCP-compatible client's config file (Claude Desktop, Claude Code, Cursor, Windsurf) - check your client's docs for where that config file lives.",
164194
snippets: [
165195
{
166196
label: "MCP config (Claude Desktop, Cursor, etc.)",
167197
format: "mcp",
168-
code: `{\n "mcpServers": {\n "thesvg": {\n "command": "npx",\n "args": ["@thesvg/mcp-server"]\n }\n }\n}`,
198+
code: `{\n "mcpServers": {\n "thesvg": {\n "command": "npx",\n "args": ["-y", "@thesvg/mcp-server"]\n }\n }\n}`,
169199
},
170200
],
171201
},
@@ -191,7 +221,7 @@ export const BEST_PRACTICES: BestPractice[] = [
191221
},
192222
{
193223
title: "Pin the jsDelivr mirror to a release, not @main",
194-
body: "cdn.jsdelivr.net/gh/glincker/thesvg@main/... tracks the moving main branch. Pin to a released version (e.g. @3.3.8) in production so an upstream rename or removal can't silently change what your app renders.",
224+
body: `cdn.jsdelivr.net/gh/glincker/thesvg@main/... tracks the moving main branch. Pin to a released git tag (e.g. @${JSDELIVR_PINNED_REF}, the current release) in production so an upstream rename or removal can't silently change what your app renders.`,
195225
},
196226
{
197227
title: "Use <picture> for dark mode, not JS",
@@ -225,7 +255,7 @@ export const TRICKS: Trick[] = [
225255
code: {
226256
label: "onError fallback",
227257
format: "react",
228-
code: `<img\n src={\`https://thesvg.org/icons/\${slug}/default.svg\`}\n onError={(e) => { e.currentTarget.src = "/fallback-icon.svg"; }}\n alt={slug}\n/>`,
258+
code: `// "/fallback-icon.svg" is a placeholder you provide - swap in a real\n// asset from your own public/ folder.\n<img\n src={\`https://thesvg.org/icons/\${slug}/default.svg\`}\n onError={(e) => {\n e.currentTarget.onerror = null; // stop if the fallback itself 404s\n e.currentTarget.src = "/fallback-icon.svg";\n }}\n alt={slug}\n/>`,
229259
},
230260
},
231261
{
@@ -246,6 +276,24 @@ export const TRICKS: Trick[] = [
246276
code: `npx @thesvg/cli add github vercel nextjs tailwindcss vscode --format jsx --dir ./src/icons`,
247277
},
248278
},
279+
{
280+
title: "Preload a hero logo to avoid a flash",
281+
body: "A brand icon that's visible immediately on page load (a hero logo, an above-the-fold integration badge) benefits from a preload hint so the browser fetches it before it's needed, instead of waiting for the <img> tag to be discovered during layout.",
282+
code: {
283+
label: "Preload hint",
284+
format: "html",
285+
code: `<link rel="preload" as="image" href="https://thesvg.org/icons/github/default.svg" />`,
286+
},
287+
},
288+
{
289+
title: "Search the catalog client-side without a package",
290+
body: "Building a custom icon picker? Fetch the manifest once, then filter it in memory - no search package, no per-keystroke network request.",
291+
code: {
292+
label: "Client-side filter",
293+
format: "react",
294+
code: `const res = await fetch('https://thesvg.org/api/registry.json');\nconst { icons } = await res.json();\n\nconst matches = icons.filter((icon) =>\n icon.title.toLowerCase().includes(query.toLowerCase()) ||\n icon.aliases.some((a) => a.toLowerCase().includes(query.toLowerCase()))\n);`,
295+
},
296+
},
249297
];
250298

251299
export interface FaqItem {
@@ -289,4 +337,39 @@ export const FAQ_ITEMS: FaqItem[] = [
289337
answer:
290338
"A handful of known icons in a React, Vue, or Svelte app: use the matching framework package — it's tree-shaken and typed. An open-ended or large set (a picker, a directory): use the CDN <img> pattern instead, no bundle cost regardless of count. Want icons checked into your own repo: use the CLI.",
291339
},
340+
{
341+
question: "My icon is showing a broken image / 404. What's wrong?",
342+
answer:
343+
"Almost always a slug mismatch — the URL path uses the exact slug from /api/registry.json, which isn't always the brand's display name (spaces, dots, and casing are normalized). Search the site or check the registry for the exact slug rather than guessing one from the brand name.",
344+
},
345+
{
346+
question: "Does theSVG work with Next.js's <Image> component?",
347+
answer:
348+
"Not out of the box. Next's built-in image optimizer doesn't rasterize SVGs by default, so <Image> either needs the unoptimized prop for that image, or dangerouslyAllowSVG set on the domain in next.config. For most icon use cases a plain <img> tag (or a framework package component) is simpler and avoids the optimizer entirely.",
349+
},
350+
{
351+
question: "Is there a TypeScript type listing every icon slug?",
352+
answer:
353+
"No exported union type of all slugs — each icon is its own named export (Github, Figma, ...), so importing an icon that doesn't exist is a module-not-found error at build time, not a string-literal type error. If you need slugs as data (e.g. from an API response), registry.json's slug field is a plain string.",
354+
},
355+
{
356+
question: "How is theSVG different from Simple Icons, svgl, or Iconify?",
357+
answer:
358+
"Short version: theSVG has more brand icons with more variants per icon (color, mono, light, dark, wordmark) and a wider toolchain (framework packages, CLI, REST API, MCP server). Simple Icons is mono-only but has years of community trust; svgl is Svelte-focused with no npm package; Iconify aggregates 150+ icon sets but isn't brand-focused. Full side-by-side comparison at /compare.",
359+
},
360+
{
361+
question: "Do the framework packages support server-side rendering (SSR)?",
362+
answer:
363+
"Yes. Every framework package (@thesvg/react, @thesvg/vue, @thesvg/svelte) renders plain markup with no browser-only APIs, so they're SSR-safe by default - no client-only wrapper or hydration guard needed. @thesvg/react components specifically work as React Server Components with no \"use client\" directive.",
364+
},
365+
{
366+
question: "Can I self-host the icons without crediting theSVG?",
367+
answer:
368+
"The codebase and tooling are MIT-licensed, so no attribution is legally required to self-host. The brand marks themselves stay the property of their trademark holders regardless of where you host the files — see /legal. A link back is appreciated but not required.",
369+
},
370+
{
371+
question: "Why does the same icon look different in dark mode?",
372+
answer:
373+
"If you're rendering the default variant everywhere, that's expected - it's a fixed brand color, not theme-aware. For a variant that adapts to the page theme, use light/dark with a <picture> element (see Best practices above), or the mono variant with an inline component so it inherits currentColor.",
374+
},
292375
];

0 commit comments

Comments
 (0)