diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..eb2278c
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/.gitignore b/.gitignore
index 564eb6b..e608033 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index a0d6fc1..05a9267 100644
--- a/README.md
+++ b/README.md
@@ -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**
+
+[](https://vercel.com/new/clone?repository-url=https://github.com/labset/website-template&project-name=website-template&repository-name=website-template)
+
+**Cloudflare Workers**
+
+[](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**
@@ -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 `
`, 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
diff --git a/src/lib/site.ts b/src/lib/site.ts
index 65e7749..afe5d05 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -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.'
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..5305dd5
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "buildCommand": "pnpm build",
+ "outputDirectory": "dist/client",
+ "framework": null
+}
diff --git a/vite.config.ts b/vite.config.ts
index 8beb961..ca8c357 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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: [
diff --git a/wrangler.jsonc b/wrangler.jsonc
new file mode 100644
index 0000000..897908a
--- /dev/null
+++ b/wrangler.jsonc
@@ -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"
+ }
+}