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
21 changes: 21 additions & 0 deletions .config/mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[tools]
node = "24"
pnpm = "11"

[tasks.clean]
description = "clean it"
run = "rm -rf node_modules && rm -rf dist"

[tasks.tidy]
description = "tidy it"
run = "pnpm install"

[tasks.lint]
description = "lint it"
run = "pnpm lint"
depends = ["tidy"]

[tasks.build]
description = "build it"
run = "pnpm build"
depends = ["lint"]
24 changes: 24 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: setup
description: Install the toolchain with mise and restore pnpm caches.

runs:
using: composite
steps:
- name: Install mise and pinned tools
uses: jdx/mise-action@v4.2.0
with:
version: 2026.7.6
cache: true

- name: Resolve pnpm store directory
id: pnpm
shell: bash
run: echo "dir=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- name: Cache pnpm store
uses: actions/cache@v6.1.0
with:
path: ${{ steps.pnpm.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: build

on:
push:
branches: [main]
pull_request:

concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7.0.0
- uses: ./.github/actions/setup
- run: mise run build
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ dist
dist-ssr
*.local

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

# TanStack Start
.tanstack
.nitro
.output

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
166 changes: 106 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,121 @@
# React + TypeScript + Vite
# @labset/website-template

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
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.

Currently, two official plugins are available:
## Stack

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
- **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`

## React Compiler
## Structure

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
```
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:

```mdx
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:

## Expanding the ESLint configuration
<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.

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
## SEO

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
`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.

// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
## Development

// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
Using pnpm:

```sh
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
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
Or via the mise tasks (`.config/mise.toml`), which pin Node + pnpm:

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

## Adding shadcn components

```sh
pnpm dlx shadcn@latest add <component>
```

Components land in `src/components/ui/`. The registry style and aliases are
configured in `components.json`.
25 changes: 25 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-luma",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ export default defineConfig([
globals: globals.browser,
},
},
{
// shadcn primitives co-locate their cva variants with the component.
files: ['src/components/ui/**/*.tsx'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
])
13 changes: 0 additions & 13 deletions index.html

This file was deleted.

21 changes: 17 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
{
"name": "website-template",
"name": "@labset/website-template",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"dev": "vite dev",
"build": "vite build && tsc -b",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@base-ui/react": "^1.6.0",
"@fontsource-variable/inter": "^5.3.0",
"@tailwindcss/vite": "^4.3.3",
"@tanstack/react-router": "^1.170.18",
"@tanstack/react-start": "^1.168.32",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.26.0",
"react": "^19.2.7",
"react-dom": "^19.2.7"
"react-dom": "^19.2.7",
"shadcn": "^4.14.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@mdx-js/rollup": "^3.1.1",
"@types/node": "^24.13.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
Expand Down
Loading