Skip to content

Repository files navigation

@labset/website-template

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

Cloudflare Workers

Deploy to Cloudflare

Build settings, config files, and Cloudflare Pages setup are under Deploy.

Stack

  • React 19 + TypeScript
  • TanStack Start (SSR + static prerendering) on TanStack Router file-based routing, built with Vite
  • Tailwind CSS v4 (@tailwindcss/vite) with OKLCH design tokens + light/dark themes
  • shadcn components on Base UI primitives (base-luma style)
  • MDX content layer (@mdx-js/rollup)
  • Inter via @fontsource-variable/inter, icons from lucide-react

Structure

src/
├── components/                 # grouped by domain, not loose
│   ├── ui/                     # shadcn primitives: button, card, badge, sheet, separator
│   ├── mdx/                    # MDX element styling + custom components (callout)
│   ├── layout/                 # site chrome: root-document, site-layout, header,
│   │                           #   footer, theme-toggle, not-found
│   ├── home/                   # landing-page + its sections (hero, features,
│   │                           #   latest-posts)
│   └── blog/                   # blog-index, blog-post, blog-post-page, post-card
├── content/
│   ├── posts.ts                # import.meta.glob loader + meta validation
│   ├── authors.ts              # author registry
│   └── posts/YYYY/MM/*.mdx     # the posts themselves
├── providers/
│   └── theme/                  # context, provider, hook (light/dark/system)
├── lib/
│   ├── site.ts                 # SITE_URL/SITE_NAME + socialMeta() helper
│   └── utils.ts                # cn() class-merge helper
├── routes/
│   ├── __root.tsx              # document head: meta, theme script, canonical strategy
│   ├── index.tsx               # → LandingPage
│   ├── 404.tsx                 # prerendered to dist/client/404.html
│   └── blog/
│       ├── index.tsx           # → BlogIndex
│       └── $slug.tsx           # → BlogPostPage
├── router.tsx                  # getRouter() for TanStack Start
├── index.css                   # Tailwind import + theme tokens
├── mdx-env.d.ts                # types for *.mdx modules
└── routeTree.gen.ts            # generated by the Start/Router plugin (gitignored)

Each file has a single role. Routes are thin — they wire the URL, load data, set head() metadata, and point component: at a dedicated component. There is no index.html; the document is rendered by __root.tsx + root-document.tsx.

Add a page by dropping a file in src/routes/; the plugin regenerates routeTree.gen.ts on the next dev/build.

Writing posts

Posts are MDX files under src/content/posts/YYYY/MM/. The filename is the slug (hello.mdx/blog/hello); the folders only keep things tidy. Each post exports a meta object, validated at build time:

export const meta = {
  title: 'Hello, world',
  description: 'A one-line summary used in listings and social cards.',
  author: 'hasnae',            // must exist in src/content/authors.ts
  date: '2026-07-24',          // YYYY-MM-DD, drives sort order
  readingTime: '4 min read',
  tags: ['intro'],
}

Write **Markdown** here. Drop in React components when you need them:

<Callout>Posts are MDX, so components work inline.</Callout>

Markdown elements are styled by src/components/mdx/mdx-components.tsx; add custom components (like Callout) alongside it. Add authors in src/content/authors.ts.

Rendering

The site is statically prerendered at build time (prerender + crawlLinks in vite.config.ts), so every route reachable from the homepage 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).

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(). 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

Using pnpm:

pnpm dev       # start the dev server (HMR)
pnpm build     # build + prerender, then type-check (vite build && tsc -b)
pnpm preview   # serve the production build locally
pnpm lint      # run ESLint

Or via the mise tasks (.config/mise.toml), which pin Node + pnpm:

mise run lint    # tidy → lint
mise run build   # build + prerender + type-check

Adding shadcn components

pnpm dlx shadcn@latest add <component>

Components land in src/components/ui/. The registry style and aliases are configured in components.json.

About

Website starter : landing page + MDX blog on TanStack Start, React 19, Tailwind v4 & shadcn.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages