Skip to content

Commit 5f8b211

Browse files
joaoh82claude
andauthored
feat(web): technical SEO foundation for sqlritedb.com (SQLR-32) (#118)
- Per-route titles / descriptions / canonicals on / and /docs - OpenGraph + Twitter cards (siteName, locale, summary_large_image) restated at the page level since Next 15 shallow-merges these - Auto-generated OG + Twitter images via next/og ImageResponse, shared `OgFrame` JSX in src/lib/og.tsx; brand mark inlined as SVG so Satori's dynamic-font fetcher doesn't 400 on the ▸ glyph - app/sitemap.ts + app/robots.ts metadata routes; sitemap pointer in robots and SITE.url canonical in both - JSON-LD: SoftwareApplication on landing, BreadcrumbList on /docs - Site-wide robots policy, keywords, author, and a placeholder verification block in the root layout for GSC + Bing tokens - Heading hierarchy: footer column h4→h3, /docs TOC h4→h2 (with aria-label) so headings descend in order - web/README.md SEO surface section documenting each piece Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a35ff2f commit 5f8b211

14 files changed

Lines changed: 480 additions & 20 deletions

web/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ extracted into its own repository later without rewrites.
1818
- `/` — landing (hero with animated REPL, features, architecture, roadmap, SDK switcher, SQL surface, desktop showcase, blog series, footer)
1919
- `/docs` — Getting Started page (sticky sidebar nav + on-page TOC)
2020

21+
## SEO surface
22+
23+
Each public route ships full search/social metadata. The pieces:
24+
25+
- **Per-route `<title>` + `<meta name="description">`** — declared via the
26+
Next App-Router `metadata` export on each `page.tsx` (and a site-wide
27+
template in [`src/app/layout.tsx`](src/app/layout.tsx)).
28+
- **Canonical URL**`alternates.canonical` on every page; prevents the
29+
`/docs` tree (and any future hash/query variants) from being treated as
30+
duplicates.
31+
- **OpenGraph + Twitter Card** — full set of `og:*` and `twitter:*` tags per
32+
route. Heads-up: Next 15 does **not** deep-merge `openGraph` / `twitter`
33+
between layout and page, so site-wide fields (`siteName`, `card`,
34+
`site`/`creator`) are restated on each page-level override.
35+
- **Auto-generated OG images** — every page has a sibling
36+
`opengraph-image.tsx` + `twitter-image.tsx` rendered via
37+
`next/og`'s `ImageResponse` at the edge. Layout lives in
38+
[`src/lib/og.tsx`](src/lib/og.tsx) so each route just supplies a
39+
page-specific eyebrow / title / subtitle. The brand mark is inlined as
40+
SVG (Satori's dynamic-font fallback 400s on uncommon glyphs).
41+
- **`/sitemap.xml` + `/robots.txt`** — Next 15 metadata routes
42+
([`src/app/sitemap.ts`](src/app/sitemap.ts),
43+
[`src/app/robots.ts`](src/app/robots.ts)). Add a route to the `ROUTES`
44+
list when shipping a new page.
45+
- **JSON-LD structured data**`SoftwareApplication` schema on the landing
46+
page, `BreadcrumbList` on `/docs`. Validate via Google's
47+
[Rich Results Test](https://search.google.com/test/rich-results).
48+
- **Search Console verification** — fill in the placeholder tokens in
49+
`metadata.verification` ([`src/app/layout.tsx`](src/app/layout.tsx)) once
50+
Google Search Console + Bing Webmaster Tools issue them.
51+
52+
The canonical site URL + Twitter handle live in
53+
[`src/lib/site.ts`](src/lib/site.ts) (`SITE.url`, `SITE.twitterHandle`) —
54+
update both there if the domain or handle ever changes.
55+
2156
## Local development
2257

2358
```sh
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ImageResponse } from "next/og";
2+
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";
3+
4+
export const runtime = "edge";
5+
export const alt = "SQLRite docs — getting started";
6+
export const size = OG_SIZE;
7+
export const contentType = OG_CONTENT_TYPE;
8+
9+
export default function OgImage() {
10+
return new ImageResponse(
11+
(
12+
<OgFrame
13+
eyebrow="docs · getting started"
14+
title="From cargo install to a persistent on-disk database in ten minutes."
15+
subtitle="REPL, transactions, JOINs, prepared statements, vector + BM25 search, MCP, and six language SDKs."
16+
/>
17+
),
18+
{ ...size },
19+
);
20+
}

web/src/app/docs/page.tsx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,59 @@ import { Footer } from "@/components/footer";
44
import { Nav } from "@/components/nav";
55
import { SITE } from "@/lib/site";
66

7+
const TITLE = "Getting started with SQLRite";
8+
const DESCRIPTION =
9+
"A ten-minute tour from cargo install to a persistent on-disk SQLRite database — REPL, transactions, JOINs, vector search, BM25 full-text, and six language SDKs.";
10+
711
export const metadata: Metadata = {
8-
title: "Getting started · SQLRite docs",
9-
description:
10-
"A ten-minute tour from cargo install to a persistent on-disk SQLRite database with real transactions, vector search, and full-text search.",
12+
title: TITLE,
13+
description: DESCRIPTION,
14+
alternates: { canonical: "/docs" },
15+
openGraph: {
16+
type: "article",
17+
siteName: "SQLRite",
18+
locale: "en_US",
19+
url: `${SITE.url}/docs`,
20+
title: TITLE,
21+
description: DESCRIPTION,
22+
},
23+
twitter: {
24+
card: "summary_large_image",
25+
site: SITE.twitterHandle,
26+
creator: SITE.twitterHandle,
27+
title: TITLE,
28+
description: DESCRIPTION,
29+
},
30+
};
31+
32+
const breadcrumbJsonLd = {
33+
"@context": "https://schema.org",
34+
"@type": "BreadcrumbList",
35+
itemListElement: [
36+
{
37+
"@type": "ListItem",
38+
position: 1,
39+
name: "Home",
40+
item: SITE.url,
41+
},
42+
{
43+
"@type": "ListItem",
44+
position: 2,
45+
name: "Docs",
46+
item: `${SITE.url}/docs`,
47+
},
48+
],
1149
};
1250

1351
export default function DocsPage() {
1452
return (
1553
<>
54+
<script
55+
type="application/ld+json"
56+
dangerouslySetInnerHTML={{
57+
__html: JSON.stringify(breadcrumbJsonLd),
58+
}}
59+
/>
1660
<Nav variant="docs" />
1761
<div className="docs-shell">
1862
<aside className="docs-side">
@@ -733,8 +777,8 @@ export default function DocsPage() {
733777
</div>
734778
</main>
735779

736-
<aside className="toc">
737-
<h4>On this page</h4>
780+
<aside className="toc" aria-label="On this page">
781+
<h2 className="toc-title">On this page</h2>
738782
<a href="#install">Install</a>
739783
<a href="#first-db">Your first database</a>
740784
<a href="#repl">Using the REPL</a>

web/src/app/docs/twitter-image.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ImageResponse } from "next/og";
2+
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";
3+
4+
export const runtime = "edge";
5+
export const alt = "SQLRite docs — getting started";
6+
export const size = OG_SIZE;
7+
export const contentType = OG_CONTENT_TYPE;
8+
9+
export default function TwitterImage() {
10+
return new ImageResponse(
11+
(
12+
<OgFrame
13+
eyebrow="docs · getting started"
14+
title="From cargo install to a persistent on-disk database in ten minutes."
15+
subtitle="REPL, transactions, JOINs, prepared statements, vector + BM25 search, MCP, and six language SDKs."
16+
/>
17+
),
18+
{ ...size },
19+
);
20+
}

web/src/app/globals.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,7 @@ footer {
12341234
grid-template-columns: 1fr 1fr;
12351235
}
12361236
}
1237-
.foot-col h4 {
1237+
.foot-col h3 {
12381238
font-size: 11px;
12391239
font-family: var(--font-mono);
12401240
text-transform: uppercase;
@@ -1332,7 +1332,7 @@ footer {
13321332
overflow-y: auto;
13331333
}
13341334
.docs-side h4,
1335-
.toc h4 {
1335+
.toc .toc-title {
13361336
font-family: var(--font-mono);
13371337
font-size: 11px;
13381338
color: var(--color-fg-dim);

web/src/app/layout.tsx

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from "next";
22
import { Inter, JetBrains_Mono } from "next/font/google";
3+
import { SITE } from "@/lib/site";
34
import "./globals.css";
45

56
const inter = Inter({
@@ -16,22 +17,68 @@ const jetbrainsMono = JetBrains_Mono({
1617
display: "swap",
1718
});
1819

20+
const DEFAULT_TITLE = "SQLRite — embedded SQL database, built in Rust";
21+
const DEFAULT_DESCRIPTION =
22+
"An embedded SQL database modeled after SQLite, built from scratch in Rust. Single-file format, real B-tree, WAL, transactions, vector search, full-text search, and six language SDKs.";
23+
1924
export const metadata: Metadata = {
20-
title: "SQLRite — embedded SQL database, built in Rust",
21-
description:
22-
"An embedded SQL database modeled after SQLite, built from scratch in Rust. Single-file format, real B-tree, WAL, transactions, vector search, full-text search, and six language SDKs.",
23-
metadataBase: new URL("https://sqlrite.dev"),
25+
metadataBase: new URL(SITE.url),
26+
title: {
27+
default: DEFAULT_TITLE,
28+
template: "%s · SQLRite",
29+
},
30+
description: DEFAULT_DESCRIPTION,
31+
applicationName: "SQLRite",
32+
authors: [{ name: "Joao Henrique Machado Silva", url: SITE.socials.github }],
33+
keywords: [
34+
"SQLRite",
35+
"Rust SQLite",
36+
"embedded database",
37+
"embedded SQL",
38+
"Rust database",
39+
"vector search",
40+
"HNSW",
41+
"BM25",
42+
"full-text search",
43+
"WAL",
44+
"B-tree",
45+
"MCP",
46+
],
2447
openGraph: {
25-
title: "SQLRite — embedded SQL database, built in Rust",
26-
description:
27-
"Single-file embedded SQL engine in Rust — B-tree, WAL, transactions, HNSW vector search, BM25 full-text, six language SDKs.",
2848
type: "website",
49+
siteName: "SQLRite",
50+
locale: "en_US",
51+
url: SITE.url,
52+
title: DEFAULT_TITLE,
53+
description: DEFAULT_DESCRIPTION,
2954
},
3055
twitter: {
3156
card: "summary_large_image",
32-
title: "SQLRite — embedded SQL database, built in Rust",
33-
description:
34-
"Single-file embedded SQL engine in Rust — B-tree, WAL, transactions, HNSW vector search, BM25 full-text, six language SDKs.",
57+
site: SITE.twitterHandle,
58+
creator: SITE.twitterHandle,
59+
title: DEFAULT_TITLE,
60+
description: DEFAULT_DESCRIPTION,
61+
},
62+
robots: {
63+
index: true,
64+
follow: true,
65+
googleBot: {
66+
index: true,
67+
follow: true,
68+
"max-image-preview": "large",
69+
"max-snippet": -1,
70+
"max-video-preview": -1,
71+
},
72+
},
73+
alternates: {
74+
canonical: "/",
75+
},
76+
// Search-engine ownership tokens. Fill these in once Google Search Console
77+
// and Bing Webmaster Tools verification is complete (the values are short
78+
// opaque strings issued by each tool).
79+
verification: {
80+
// google: "<google-site-verification-token>",
81+
// other: { "msvalidate.01": "<bing-verification-token>" },
3582
},
3683
};
3784

web/src/app/opengraph-image.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ImageResponse } from "next/og";
2+
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";
3+
4+
export const runtime = "edge";
5+
export const alt = "SQLRite — embedded SQL database, built in Rust";
6+
export const size = OG_SIZE;
7+
export const contentType = OG_CONTENT_TYPE;
8+
9+
export default function OgImage() {
10+
return new ImageResponse(
11+
(
12+
<OgFrame
13+
eyebrow="embedded · open source"
14+
title="An embedded SQL database, built from scratch in Rust."
15+
subtitle="Single-file engine. B-tree, WAL, transactions, JOINs, vector + full-text search, six SDKs."
16+
/>
17+
),
18+
{ ...size },
19+
);
20+
}

web/src/app/page.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from "next";
12
import { Architecture } from "@/components/architecture";
23
import { Benchmarks } from "@/components/benchmarks";
34
import { Blog } from "@/components/blog";
@@ -10,10 +11,77 @@ import { Nav } from "@/components/nav";
1011
import { Roadmap } from "@/components/roadmap";
1112
import { SDKShowcase } from "@/components/sdk-showcase";
1213
import { SQLRef } from "@/components/sql-ref";
14+
import { SITE } from "@/lib/site";
15+
16+
const TITLE = "SQLRite — embedded SQL database, built in Rust";
17+
const DESCRIPTION =
18+
"Single-file embedded SQL engine in Rust. Real B-tree, WAL, transactions, JOINs, aggregates, HNSW vector search, BM25 full-text, plus six language SDKs.";
19+
20+
export const metadata: Metadata = {
21+
title: { absolute: TITLE },
22+
description: DESCRIPTION,
23+
alternates: { canonical: "/" },
24+
// openGraph and twitter aren't deep-merged with the parent layout in Next
25+
// 15 — providing either at the page level fully replaces the layout's
26+
// version, so site-wide fields (siteName, twitter card, etc.) have to be
27+
// restated here.
28+
openGraph: {
29+
type: "website",
30+
siteName: "SQLRite",
31+
locale: "en_US",
32+
url: SITE.url,
33+
title: TITLE,
34+
description: DESCRIPTION,
35+
},
36+
twitter: {
37+
card: "summary_large_image",
38+
site: SITE.twitterHandle,
39+
creator: SITE.twitterHandle,
40+
title: TITLE,
41+
description: DESCRIPTION,
42+
},
43+
};
44+
45+
// JSON-LD describing SQLRite as a SoftwareApplication. Helps search engines
46+
// surface a rich card (name + description + repo + license) and gives LLM
47+
// crawlers a structured handle on what this project is.
48+
const softwareApplicationJsonLd = {
49+
"@context": "https://schema.org",
50+
"@type": "SoftwareApplication",
51+
name: "SQLRite",
52+
description: DESCRIPTION,
53+
applicationCategory: "DeveloperApplication",
54+
applicationSubCategory: "DatabaseApplication",
55+
operatingSystem: "macOS, Windows, Linux, Web (WASM)",
56+
softwareVersion: SITE.version,
57+
url: SITE.url,
58+
downloadUrl: SITE.releasesLatest,
59+
codeRepository: SITE.repo,
60+
programmingLanguage: "Rust",
61+
license: "https://opensource.org/licenses/MIT",
62+
offers: {
63+
"@type": "Offer",
64+
price: "0",
65+
priceCurrency: "USD",
66+
},
67+
author: {
68+
"@type": "Person",
69+
name: "Joao Henrique Machado Silva",
70+
url: SITE.socials.github,
71+
},
72+
};
1373

1474
export default function Home() {
1575
return (
1676
<>
77+
<script
78+
type="application/ld+json"
79+
// Stringified ahead of render — the script's content is static, so
80+
// this is not a user-controlled injection vector.
81+
dangerouslySetInnerHTML={{
82+
__html: JSON.stringify(softwareApplicationJsonLd),
83+
}}
84+
/>
1785
<Nav />
1886
<Hero />
1987
<Features />

web/src/app/robots.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { MetadataRoute } from "next";
2+
import { SITE } from "@/lib/site";
3+
4+
export default function robots(): MetadataRoute.Robots {
5+
return {
6+
rules: [
7+
{
8+
userAgent: "*",
9+
allow: "/",
10+
},
11+
],
12+
sitemap: `${SITE.url}/sitemap.xml`,
13+
host: SITE.url,
14+
};
15+
}

0 commit comments

Comments
 (0)