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** + +[![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** @@ -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" + } +}