# SQLRite website
Marketing + docs site for [SQLRite](https://github.com/joaoh82/rust_sqlite).
Lives inside the same repo as the engine for now; it is intentionally
self-contained (independent `package.json`, no Cargo coupling) so it can be
extracted into its own repository later without rewrites.
## Stack
- Next.js 15 (App Router) + React 19
- TypeScript (strict)
- Tailwind CSS v4 (CSS-first `@theme` config in [`src/app/globals.css`](src/app/globals.css))
- shadcn/ui infrastructure (`components.json` + `cn` helper) — components are added on demand
- `lucide-react` for icons
## Pages
- `/` — 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)
- `/blog` — index of long-form posts pulled from `content/blog/*.mdx`
- `/blog/[slug]` — per-post detail page (MDX rendered server-side, `Article` JSON-LD, breadcrumb JSON-LD, dynamic OG image, prev/next navigation)
- `/blog/tags/[tag]` — tag pages (one per unique frontmatter tag)
- `/blog/rss.xml` — RSS 2.0 feed
## 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`, `Blog` on `/blog`, and
`BlogPosting` + `BreadcrumbList` on each `/blog/<slug>`. 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
cd web
npm install
npm run dev # http://localhost:3000
```
Other commands:
```sh
npm run build # production build
npm run typecheck # tsc --noEmit
npm run lint # next lint (ESLint)
```
## Project structure
```
web/
├── content/
│ └── blog/ # MDX posts (one .mdx file per post; frontmatter at top)
├── src/
│ ├── app/
│ │ ├── globals.css # design tokens + utility CSS (ports the original design's styles.css)
│ │ ├── layout.tsx # root layout, fonts (Inter + JetBrains Mono via next/font)
│ │ ├── page.tsx # landing
│ │ ├── docs/page.tsx # /docs
│ │ ├── blog/ # /blog index, [slug] detail, tags/[tag], rss.xml
│ │ ├── sitemap.ts # /sitemap.xml — enumerates static + per-post + per-tag URLs
│ │ └── robots.ts # /robots.txt
│ ├── components/ # one .tsx per landing section (hero, features, roadmap, …)
│ └── lib/
│ ├── blog.ts # MDX loader: frontmatter parsing, post enumeration, tag helpers
│ ├── og.tsx # shared OpenGraph frame
│ ├── site.ts # SITE constants (version, repo URL, social links)
│ └── utils.ts # shadcn cn() helper
└── components.json # shadcn/ui config
```
## Blog
The blog is content-driven. Posts live as `.mdx` files in
[`content/blog/`](content/blog) and are rendered server-side via
[`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote).
Frontmatter is parsed by `gray-matter`.
### Adding a post
Create `content/blog/<slug>.mdx`:
```mdx
---
title: "Your post title"
description: "One-sentence description used in <meta>, OG, RSS."
publishedAt: "2026-05-10" # ISO date, sorts the index
updatedAt: "2026-05-12" # optional
author: "Joao Henrique Machado Silva"
tags: ["sqlrite", "rust"] # also drives /blog/tags/[tag]
primaryKeyword: "rust sql engine" # optional, for SEO bookkeeping
---
Body text in Markdown / MDX.
```
Then:
- The post is automatically picked up by `/blog`, `/blog/<slug>`,
every relevant `/blog/tags/<tag>`, the RSS feed, and the sitemap.
- An OG image is generated dynamically from the title +
description at `/blog/<slug>/opengraph-image`.
- `BlogPosting` JSON-LD and `BreadcrumbList` JSON-LD are injected
on the detail page.
### Required frontmatter validity
`src/lib/blog.ts` validates frontmatter at load time and throws if
`title`, `description`, `publishedAt`, `author`, or `tags` is
missing / wrong-typed. The build will fail fast in CI rather than
shipping a half-broken post.
### MDX caveats
`<` and `{` in prose can confuse the MDX parser. Wrap them in
backticks or escape (`<`, `\{`). The MDX renderer auto-routes
internal `[link](/foo)` markdown links through `next/link`; external
links open in a new tab via `rel="noreferrer"`.
### Code block highlighting
Code is tokenized at build time by [Shiki](https://shiki.style/). The
shared theme + helper live in
[`src/lib/highlight.ts`](src/lib/highlight.ts) and use
`createCssVariablesTheme()` so Shiki emits inline styles like
`color: var(--shiki-token-keyword)`. Each surface that needs
highlighting then maps the `--shiki-*` variables onto the blog's
palette tokens (`--color-kw`, `--color-str`, `--color-num`, …) in
[`src/app/globals.css`](src/app/globals.css) — adjust colors there,
not in the components. Two consumers:
- **Blog MDX** (`src/components/blog-mdx.tsx`) — uses
[`rehype-pretty-code`](https://github.com/rehype-pretty/rehype-pretty-code)
inside the `MDXRemote` pipeline. Inline `` `code` `` keeps its chip
styling (`bypassInlineCode: true`); fences without a language tag
fall back to `plaintext` so a missing language never breaks the
build. Mapping lives on `.blog-article-body pre`.
- **SDK showcase** (`src/components/sdk-showcase.tsx` —
server-rendered, paired with a small client wrapper for the tab
state) pre-renders each language snippet with `highlightCode()` and
embeds the resulting HTML inside `.code-body`. Mapping lives on
`.code-body`.
The design tokens (colors, typography, spacing) live in `globals.css`'s
`@theme` block. The page-level CSS (sections, terminal, feature grid,
roadmap timeline, etc.) is intentionally hand-rolled — it ports the
prototype's `styles.css` 1:1 rather than reaching for component-library
abstractions.
## Responsive design
The site is mobile-first and verified at 375px (iPhone SE), 390px
(iPhone 14), 768px (iPad), and 1024px+. Key conventions:
- **Breakpoints** live at the bottom of [`src/app/globals.css`](src/app/globals.css):
900px (tablet), 760px (mobile nav cutover), 640px (phone), and 380px
(very small phones). Section-level grids declare their own breakpoints
inline near their styles (features, bench bars, footer, etc.).
- **Nav** ([`src/components/nav.tsx`](src/components/nav.tsx)) is a
client component. Below 760px the inline links collapse into a 44×44
hamburger that opens a full-width drawer; Esc closes; the body scroll
is locked while open.
- **Docs** ([`src/app/docs/page.tsx`](src/app/docs/page.tsx)) hides the
desktop sidebar and on-page TOC under 1000px and 720px respectively
and shows a sticky `<details>`-driven section list in their place.
- **Tap targets** — primary buttons (`.btn`), the hamburger, install-bar
copy, mobile menu links, and the docs section toggle are all ≥ 44px
tall on phones. Footer / docs sidebar inline nav links stay at ~36px,
which is the common compromise for dense navigation lists.
- **Horizontal scroll** is guarded globally with `html { overflow-x:
clip }`. We use `clip` instead of `hidden` so `position: sticky` keeps
working for the nav and the docs sidebar. Long URLs / unbroken tokens
in prose use `overflow-wrap: anywhere` so they don't blow out the
viewport.
- **Tables and code blocks** scroll horizontally inside their container
(`overflow-x: auto`); the SQL surface table on `/` reflows into
stacked cards under 640px since its second column is a long pill list.
- **Viewport / theme color** — set via the `viewport` export in
[`src/app/layout.tsx`](src/app/layout.tsx); the dark `#0b0c0e`
`themeColor` keeps mobile browser chrome from flashing white.
When adding new sections, declare the breakpoint logic alongside the
section's styles rather than at the bottom of the file — it keeps the
section self-contained and the global breakpoint block reserved for
typography / spacing baseline tweaks.
## Updating the version
The displayed version is in [`src/lib/site.ts`](src/lib/site.ts). Update it
when the engine cuts a new release.
## Deploying
The site is a static-friendly Next.js app and deploys to Vercel out of the
box. Point Vercel at the `web/` directory:
- **Root Directory:** `web`
- **Framework Preset:** Next.js (auto-detected)
- No environment variables required.
For other hosts, `next build` produces a standard Next.js output suitable
for any Node-friendly runtime.
## License
MIT — same as the rest of the repo.