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
35 changes: 35 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ extracted into its own repository later without rewrites.
- `/` — landing (hero with animated REPL, features, architecture, roadmap, SDK switcher, SQL surface, desktop showcase, blog series, footer)
- `/docs` — Getting Started page (sticky sidebar nav + on-page TOC)

## SEO surface

Each public route ships full search/social metadata. The pieces:

- **Per-route `<title>` + `<meta name="description">`** — declared via the
Next App-Router `metadata` export on each `page.tsx` (and a site-wide
template in [`src/app/layout.tsx`](src/app/layout.tsx)).
- **Canonical URL** — `alternates.canonical` on every page; prevents the
`/docs` tree (and any future hash/query variants) from being treated as
duplicates.
- **OpenGraph + Twitter Card** — full set of `og:*` and `twitter:*` tags per
route. Heads-up: Next 15 does **not** deep-merge `openGraph` / `twitter`
between layout and page, so site-wide fields (`siteName`, `card`,
`site`/`creator`) are restated on each page-level override.
- **Auto-generated OG images** — every page has a sibling
`opengraph-image.tsx` + `twitter-image.tsx` rendered via
`next/og`'s `ImageResponse` at the edge. Layout lives in
[`src/lib/og.tsx`](src/lib/og.tsx) so each route just supplies a
page-specific eyebrow / title / subtitle. The brand mark is inlined as
SVG (Satori's dynamic-font fallback 400s on uncommon glyphs).
- **`/sitemap.xml` + `/robots.txt`** — Next 15 metadata routes
([`src/app/sitemap.ts`](src/app/sitemap.ts),
[`src/app/robots.ts`](src/app/robots.ts)). Add a route to the `ROUTES`
list when shipping a new page.
- **JSON-LD structured data** — `SoftwareApplication` schema on the landing
page, `BreadcrumbList` on `/docs`. Validate via Google's
[Rich Results Test](https://search.google.com/test/rich-results).
- **Search Console verification** — fill in the placeholder tokens in
`metadata.verification` ([`src/app/layout.tsx`](src/app/layout.tsx)) once
Google Search Console + Bing Webmaster Tools issue them.

The canonical site URL + Twitter handle live in
[`src/lib/site.ts`](src/lib/site.ts) (`SITE.url`, `SITE.twitterHandle`) —
update both there if the domain or handle ever changes.

## Local development

```sh
Expand Down
20 changes: 20 additions & 0 deletions web/src/app/docs/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ImageResponse } from "next/og";
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";

export const runtime = "edge";
export const alt = "SQLRite docs — getting started";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;

export default function OgImage() {
return new ImageResponse(
(
<OgFrame
eyebrow="docs · getting started"
title="From cargo install to a persistent on-disk database in ten minutes."
subtitle="REPL, transactions, JOINs, prepared statements, vector + BM25 search, MCP, and six language SDKs."
/>
),
{ ...size },
);
}
54 changes: 49 additions & 5 deletions web/src/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,59 @@ import { Footer } from "@/components/footer";
import { Nav } from "@/components/nav";
import { SITE } from "@/lib/site";

const TITLE = "Getting started with SQLRite";
const DESCRIPTION =
"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.";

export const metadata: Metadata = {
title: "Getting started · SQLRite docs",
description:
"A ten-minute tour from cargo install to a persistent on-disk SQLRite database with real transactions, vector search, and full-text search.",
title: TITLE,
description: DESCRIPTION,
alternates: { canonical: "/docs" },
openGraph: {
type: "article",
siteName: "SQLRite",
locale: "en_US",
url: `${SITE.url}/docs`,
title: TITLE,
description: DESCRIPTION,
},
twitter: {
card: "summary_large_image",
site: SITE.twitterHandle,
creator: SITE.twitterHandle,
title: TITLE,
description: DESCRIPTION,
},
};

const breadcrumbJsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: SITE.url,
},
{
"@type": "ListItem",
position: 2,
name: "Docs",
item: `${SITE.url}/docs`,
},
],
};

export default function DocsPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbJsonLd),
}}
/>
<Nav variant="docs" />
<div className="docs-shell">
<aside className="docs-side">
Expand Down Expand Up @@ -733,8 +777,8 @@ export default function DocsPage() {
</div>
</main>

<aside className="toc">
<h4>On this page</h4>
<aside className="toc" aria-label="On this page">
<h2 className="toc-title">On this page</h2>
<a href="#install">Install</a>
<a href="#first-db">Your first database</a>
<a href="#repl">Using the REPL</a>
Expand Down
20 changes: 20 additions & 0 deletions web/src/app/docs/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ImageResponse } from "next/og";
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";

export const runtime = "edge";
export const alt = "SQLRite docs — getting started";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;

export default function TwitterImage() {
return new ImageResponse(
(
<OgFrame
eyebrow="docs · getting started"
title="From cargo install to a persistent on-disk database in ten minutes."
subtitle="REPL, transactions, JOINs, prepared statements, vector + BM25 search, MCP, and six language SDKs."
/>
),
{ ...size },
);
}
4 changes: 2 additions & 2 deletions web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1234,7 +1234,7 @@ footer {
grid-template-columns: 1fr 1fr;
}
}
.foot-col h4 {
.foot-col h3 {
font-size: 11px;
font-family: var(--font-mono);
text-transform: uppercase;
Expand Down Expand Up @@ -1332,7 +1332,7 @@ footer {
overflow-y: auto;
}
.docs-side h4,
.toc h4 {
.toc .toc-title {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-fg-dim);
Expand Down
67 changes: 57 additions & 10 deletions web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { SITE } from "@/lib/site";
import "./globals.css";

const inter = Inter({
Expand All @@ -16,22 +17,68 @@ const jetbrainsMono = JetBrains_Mono({
display: "swap",
});

const DEFAULT_TITLE = "SQLRite — embedded SQL database, built in Rust";
const DEFAULT_DESCRIPTION =
"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.";

export const metadata: Metadata = {
title: "SQLRite — embedded SQL database, built in Rust",
description:
"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.",
metadataBase: new URL("https://sqlrite.dev"),
metadataBase: new URL(SITE.url),
title: {
default: DEFAULT_TITLE,
template: "%s · SQLRite",
},
description: DEFAULT_DESCRIPTION,
applicationName: "SQLRite",
authors: [{ name: "Joao Henrique Machado Silva", url: SITE.socials.github }],
keywords: [
"SQLRite",
"Rust SQLite",
"embedded database",
"embedded SQL",
"Rust database",
"vector search",
"HNSW",
"BM25",
"full-text search",
"WAL",
"B-tree",
"MCP",
],
openGraph: {
title: "SQLRite — embedded SQL database, built in Rust",
description:
"Single-file embedded SQL engine in Rust — B-tree, WAL, transactions, HNSW vector search, BM25 full-text, six language SDKs.",
type: "website",
siteName: "SQLRite",
locale: "en_US",
url: SITE.url,
title: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
},
twitter: {
card: "summary_large_image",
title: "SQLRite — embedded SQL database, built in Rust",
description:
"Single-file embedded SQL engine in Rust — B-tree, WAL, transactions, HNSW vector search, BM25 full-text, six language SDKs.",
site: SITE.twitterHandle,
creator: SITE.twitterHandle,
title: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
alternates: {
canonical: "/",
},
// Search-engine ownership tokens. Fill these in once Google Search Console
// and Bing Webmaster Tools verification is complete (the values are short
// opaque strings issued by each tool).
verification: {
// google: "<google-site-verification-token>",
// other: { "msvalidate.01": "<bing-verification-token>" },
},
};

Expand Down
20 changes: 20 additions & 0 deletions web/src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ImageResponse } from "next/og";
import { OG_CONTENT_TYPE, OG_SIZE, OgFrame } from "@/lib/og";

export const runtime = "edge";
export const alt = "SQLRite — embedded SQL database, built in Rust";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;

export default function OgImage() {
return new ImageResponse(
(
<OgFrame
eyebrow="embedded · open source"
title="An embedded SQL database, built from scratch in Rust."
subtitle="Single-file engine. B-tree, WAL, transactions, JOINs, vector + full-text search, six SDKs."
/>
),
{ ...size },
);
}
68 changes: 68 additions & 0 deletions web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { Architecture } from "@/components/architecture";
import { Benchmarks } from "@/components/benchmarks";
import { Blog } from "@/components/blog";
Expand All @@ -10,10 +11,77 @@ import { Nav } from "@/components/nav";
import { Roadmap } from "@/components/roadmap";
import { SDKShowcase } from "@/components/sdk-showcase";
import { SQLRef } from "@/components/sql-ref";
import { SITE } from "@/lib/site";

const TITLE = "SQLRite — embedded SQL database, built in Rust";
const DESCRIPTION =
"Single-file embedded SQL engine in Rust. Real B-tree, WAL, transactions, JOINs, aggregates, HNSW vector search, BM25 full-text, plus six language SDKs.";

export const metadata: Metadata = {
title: { absolute: TITLE },
description: DESCRIPTION,
alternates: { canonical: "/" },
// openGraph and twitter aren't deep-merged with the parent layout in Next
// 15 — providing either at the page level fully replaces the layout's
// version, so site-wide fields (siteName, twitter card, etc.) have to be
// restated here.
openGraph: {
type: "website",
siteName: "SQLRite",
locale: "en_US",
url: SITE.url,
title: TITLE,
description: DESCRIPTION,
},
twitter: {
card: "summary_large_image",
site: SITE.twitterHandle,
creator: SITE.twitterHandle,
title: TITLE,
description: DESCRIPTION,
},
};

// JSON-LD describing SQLRite as a SoftwareApplication. Helps search engines
// surface a rich card (name + description + repo + license) and gives LLM
// crawlers a structured handle on what this project is.
const softwareApplicationJsonLd = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "SQLRite",
description: DESCRIPTION,
applicationCategory: "DeveloperApplication",
applicationSubCategory: "DatabaseApplication",
operatingSystem: "macOS, Windows, Linux, Web (WASM)",
softwareVersion: SITE.version,
url: SITE.url,
downloadUrl: SITE.releasesLatest,
codeRepository: SITE.repo,
programmingLanguage: "Rust",
license: "https://opensource.org/licenses/MIT",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
author: {
"@type": "Person",
name: "Joao Henrique Machado Silva",
url: SITE.socials.github,
},
};

export default function Home() {
return (
<>
<script
type="application/ld+json"
// Stringified ahead of render — the script's content is static, so
// this is not a user-controlled injection vector.
dangerouslySetInnerHTML={{
__html: JSON.stringify(softwareApplicationJsonLd),
}}
/>
<Nav />
<Hero />
<Features />
Expand Down
15 changes: 15 additions & 0 deletions web/src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MetadataRoute } from "next";
import { SITE } from "@/lib/site";

export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
},
],
sitemap: `${SITE.url}/sitemap.xml`,
host: SITE.url,
};
}
Loading
Loading