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:

exportconst 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 commandpnpm build and Build output directorydist/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

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages