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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Public origin used for canonical links and Open Graph / Twitter cards.
# No trailing slash. Read at build time by src/lib/site.ts.
# On Vercel this defaults to the project's production domain, so setting it is
# optional there; set it to pin a custom domain.
VITE_SITE_URL=https://example.com
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ dist
dist-ssr
*.local

# Local env files (keep the example)
.env
.env.*
!.env.example

# TanStack Router (generated)
src/routeTree.gen.ts

Expand Down
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
A minimal starter for content-driven websites: a landing page and an MDX blog,
server-rendered and statically prerendered. Fork it and make it yours.

One click to your own copy:

**Vercel**

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/labset/website-template&project-name=website-template&repository-name=website-template)

**Cloudflare Workers**

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/labset/website-template)

Build settings, config files, and Cloudflare Pages setup are under
[Deploy](#deploy).

## Stack

- **React 19** + **TypeScript**
Expand Down Expand Up @@ -86,12 +99,41 @@ ships as crawlable HTML with content already in the markup, plus an explicit
`/404`. The build writes static assets to `dist/client/` and the SSR runtime to
`dist/server/`; deploy `dist/client/` for a static host.

## Deploy

The output in `dist/client/` is plain static files, so any static host works.
Everywhere, the **build command** is `pnpm build` and the **publish directory**
is `dist/client`. Set `VITE_SITE_URL` to your origin (see [SEO](#seo)).

### Vercel

Use the button at the top (or import the repo). Config lives in `vercel.json`
(`outputDirectory: dist/client`, `framework: null`).

### Cloudflare Workers

Use the button at the top. Config lives in `wrangler.jsonc` as an assets-only
Worker (no server code) serving `dist/client`, with
`not_found_handling: "404-page"` for the prerendered `404.html`. Cloudflare runs
`pnpm build`, then `wrangler deploy`.

### Cloudflare Pages

No one-click button — Cloudflare's deploy button is Workers-only. In the
dashboard, **Create → Pages → Connect to Git**, then set **Build command**
`pnpm build` and **Build output directory** `dist/client` (Framework preset:
none). Pages serves `404.html` automatically. `wrangler.jsonc` is not used by
Pages.

## SEO

`src/lib/site.ts` holds `SITE_URL`, `SITE_NAME`, and `socialMeta()` (Open Graph
+ Twitter tags). Each route sets its own `<title>`, description, and a
self-referencing canonical link in `head()`. **Set `SITE_URL` to your deployed
origin** before shipping.
self-referencing canonical link in `head()`. `SITE_URL` comes from the
**`VITE_SITE_URL`** env var (see `.env.example`) — set it to your deployed
origin before shipping. On Vercel it defaults to the project's production
domain, so deploys via the button above work out of the box; set
`VITE_SITE_URL` to pin a custom domain.

## Development

Expand Down
10 changes: 7 additions & 3 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Shared site-wide constants used for canonical URLs and social cards.
// Change SITE_URL to your deployed origin. No trailing slash; build paths as
// `${SITE_URL}/blog/...`.
export const SITE_URL = 'https://example.com'
// SITE_URL comes from the VITE_SITE_URL env var (set it to your deployed
// origin); on Vercel it defaults to the project's production domain — see
// vite.config.ts. Falls back to https://example.com when unset. Any trailing
// slash is stripped so paths build cleanly as `${SITE_URL}/blog/...`.
export const SITE_URL = (
import.meta.env.VITE_SITE_URL ?? 'https://example.com'
).replace(/\/+$/, '')
export const SITE_NAME = 'website-template'
export const SITE_DESCRIPTION =
'A minimal starter built with TanStack Start, React, Tailwind, and shadcn — server-rendered, type-safe, and ready to grow.'
Expand Down
6 changes: 6 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "pnpm build",
"outputDirectory": "dist/client",
"framework": null
}
8 changes: 8 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import tailwindcss from '@tailwindcss/vite'
import mdx from '@mdx-js/rollup'
import path from 'path'

// Default the public site origin to the Vercel production domain when
// VITE_SITE_URL isn't set explicitly, so the "Deploy with Vercel" button
// produces correct canonical/OG URLs with no configuration. An explicit
// VITE_SITE_URL always wins. See src/lib/site.ts.
if (!process.env.VITE_SITE_URL && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
process.env.VITE_SITE_URL = `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
}

// https://vite.dev/config/
export default defineConfig({
plugins: [
Expand Down
13 changes: 13 additions & 0 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
// Cloudflare Workers config for serving the prerendered static site.
// Used by the "Deploy to Cloudflare" button and `wrangler deploy`. No Worker
// script (`main`) is needed — this is an assets-only Worker. Cloudflare runs
// the `build` script from package.json, then serves ./dist/client.
"name": "website-template",
"compatibility_date": "2026-07-25",
"assets": {
"directory": "./dist/client",
// Serve the prerendered dist/client/404.html for unmatched URLs.
"not_found_handling": "404-page"
}
}