Preact + TypeScript + Tailwind CSS v4 + Vite 7 template with built-in multi-page routing, shadcn-style UI components, TanStack Query/Form/Table/Virtual/Store examples, static error pages, dark mode, and a Task-based CI/CD pipeline.
  • TypeScript 91.3%
  • CSS 6.8%
  • JavaScript 1%
  • HTML 0.9%
Find a file Use this template
izy 1b83dcbb08
All checks were successful
Build / build (push) Successful in 27s
Add TanStack libraries, error pages, and central page config
- Add @tanstack/react-query, react-form, react-table, react-virtual,
  and store with preact/compat aliases for tests
- Add error-pages Vite plugin generating static HTML for 15 HTTP codes
- Extract page metadata into src/pages/config.ts as single source of
  truth, consumed by multiPage plugin and App.tsx at runtime
- Add About, QueryExample, FormExample, TableExample, VirtualExample
  page components
- Add useSearchParams hook; update Link to use ROUTE_PATHS for SPA
  vs real navigation fallback
- Switch CI runner to ubuntu-latest; add robots.txt

Signed-off-by: izy <izy@izy.sh>
2026-05-18 16:42:51 +00:00
.forgejo/workflows Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
plugins Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
public Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
src Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
.gitignore init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
.rules Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
eslint.config.js init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
index.html init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
LICENSE init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
package.json Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
pnpm-lock.yaml Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
pnpm-workspace.yaml init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
README.md Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
taskfile.yml init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
tsconfig.json init: scaffold minimal Preact 10 project template 2026-05-17 02:27:33 +00:00
vite.config.ts Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00
vitest.config.ts Add TanStack libraries, error pages, and central page config 2026-05-18 16:42:51 +00:00

Preact Template

A minimal Preact project template with TypeScript, Tailwind CSS v4, and Vite 7.

Stack

  • Framework: Preact 10 + TypeScript
  • Build Tool: Vite 7
  • Styling: Tailwind CSS v4 (via @tailwindcss/vite)
  • Fonts: JetBrains Mono (via @fontsource, enabled by default in index.css)
  • Icons: Lucide (via lucide-preact)
  • UI Components: Minimal shadcn-style components in src/components/ui/

Architecture

public/                  # Vite static asset directory
│   robots.txt           # Allow all crawlers, sitemap reference
src/
├── App.tsx              # Route dispatcher (path-based routing via meta tag + events)
├── App.test.tsx         # App routing, navigation, focus, and validation tests
├── main.tsx             # Preact entry point with ErrorBoundary + QueryClientProvider
├── ErrorFallback.tsx    # Error boundary fallback UI
├── index.css            # Single source of truth for all styles
├── vite-end.d.ts        # Vite client type reference
├── pages/
│   ├── config.ts        # Central page metadata — THE single source of truth
│   ├── Home.tsx         # Home page component
│   ├── Home.test.tsx    # Home page tests
│   ├── About.tsx        # About page component
│   ├── QueryExample.tsx # TanStack Query example
│   ├── FormExample.tsx  # TanStack Form example
│   ├── TableExample.tsx # TanStack Table example
│   ├── VirtualExample.tsx # TanStack Virtual example
│   └── NotFound.tsx     # 404 page component
├── components/
│   └── ui/              # shadcn-style UI components (Button, Alert, Link)
│       └── link.test.tsx
├── hooks/
│   └── use-search-params.ts  # Query string parsing via URLSearchParams
├── lib/                 # Utility functions (utils.ts + utils.test.ts)
└── assets/              # Images, favicon

plugins/
├── multi-page.ts        # Vite plugin that generates per-route HTML with meta injection
├── multi-page.test.ts   # Tests for escapeHtml, getFaviconType, findPageConfig, resolvePage, injectMeta
└── error-pages.ts       # Vite plugin that generates static error pages (dist/errors/)

Routing & Multi-Page

The app uses path-based routing via window.location.pathname in src/App.tsx.

For client-side navigation, use the <Link> component from @/components/ui/link. It wraps <a> tags with history.pushState, dispatches a custom app-navigate event, and scrolls to top. Non-root links should use trailing slashes (/about/, /query-example/). The App.tsx route dispatcher reads the x-page meta tag for the initial route, strips trailing slashes for lookup, then switches to window.location.pathname for subsequent navigations. Unknown routes render the NotFound page.

When a link points to a path not matching ROUTE_PATHS (checked with and without trailing slash), the <Link> component falls back to a real browser navigation — the server handles it natively.

On every route change, App.tsx updates <title>, <meta name="description">, and <link rel="icon"> from src/pages/config.ts.

The multiPage Vite plugin (plugins/multi-page.ts) reads page metadata from src/pages/config.ts (imported by vite.config.ts) and generates per-route HTML files with injected <title>, <meta description>, <meta name="x-page">, and <link rel="icon"> tags.

Each page config supports:

  • title, description, favicon — all optional. Omitted fields fall back to the "/" page's values.
  • faviconType — optional MIME type. Inferred from file extension when not set.
  • head — array of additional <head> strings (e.g., Open Graph meta tags). Not HTML-escaped. Root page entries are inherited, then page-specific entries appended.

To add a new page:

  1. Add an entry to src/pages/config.ts
  2. Import the component in App.tsx and add it to the routes object

Error Pages

The error-pages plugin (plugins/error-pages.ts) generates pure HTML error pages at build time into dist/errors/. No JavaScript — just styled HTML matching the app's theme. 15 error codes covered (400504). Messages are randomly picked per build.

TanStack Libraries

The template includes five TanStack libraries. All work through Preact's React compatibility layer (preact/compat in tsconfig.json paths).

Library Use Works via
@tanstack/react-query Fetching, caching, syncing server data. useQuery, useMutation, etc. preact/compat
@tanstack/react-table Headless tables — sorting, filtering, pagination. You render every <td>. preact/compat
@tanstack/react-form Headless forms — validation, dirty tracking, submission. preact/compat
@tanstack/react-virtual Virtualized scrolling for large lists. Only renders visible DOM nodes. preact/compat
@tanstack/store Tiny reactive store (~1KB). Framework-agnostic core. Direct (no compat needed)

Wrapper pattern: QueryClientProvider wraps the app in main.tsx. Other TanStack libraries are used directly in components — no additional setup needed.

See src/pages/QueryExample.tsx, src/pages/FormExample.tsx, src/pages/TableExample.tsx, and src/pages/VirtualExample.tsx for working examples of each library.

Styling System

Single CSS file: src/index.css contains:

  • Tailwind CSS imports
  • Custom CSS variables for the theme (OKLCH color space)
  • Custom @keyframes animations (add your own here)
  • Dark mode via prefers-color-scheme and .dark class (add to <html> for manual toggle). Add .light to <html> to override the OS dark preference and force light mode.

Custom color palette (OKLCH):

  • --background / --foreground: Base surface and text colors
  • --primary: Brand primary color
  • --card: Card surface color
  • --destructive: Error/danger color

DO NOT create additional CSS files. All styling goes in index.css or inline Tailwind classes.

Build Configuration

Vite config (vite.config.ts):

  • Preact plugin for JSX transformation
  • Tailwind CSS v4 via @tailwindcss/vite
  • Multi-page HTML generation via plugins/multi-page.ts
  • Error page generation via plugins/error-pages.ts
  • Image optimization via vite-plugin-image-optimizer (85% quality)
  • Terser minification (drop console, 2-pass compression)
  • Lightning CSS for CSS minification
  • Path alias: @/src/

TypeScript: "strict": true enabled; --noCheck flag used in build script for faster builds, type correctness enforced via task typecheck. The tsconfig.json include covers src/, plugins/, vite.config.ts, and vitest.config.ts — all TypeScript files in the repo.

Build System

All build and dependency tasks are managed via Task (taskfile.yml). The build task handles dependency installation automatically as a prerequisite.

task                  # Build and deploy to staging (default)
task build            # Build project (output in dist/)
task dev              # Start Vite dev server
task typecheck        # Run TypeScript type checking (no emit)
task lint             # Run ESLint
task test             # Run tests
task test-watch       # Run tests in watch mode
task preview          # Preview production build locally
task install          # Install all dependencies (PHP + Node)
task install-php      # Install PHP dependencies via Composer
task install-node     # Install Node.js dependencies via pnpm
task update           # Update all dependencies
task update-php       # Update PHP dependencies (Composer)
task update-node      # Update Node.js dependencies (pnpm)
task audit            # Audit all dependencies for vulnerabilities
task audit-php        # Audit PHP dependencies (Composer)
task audit-node       # Audit Node.js dependencies (pnpm)
task clean            # Remove build artifacts, Node deps, and PHP vendors
task deploy           # Build and deploy to production
task sync-staging     # Sync build output to ../staging/
task sync-prod        # Sync build output to ../public/

Task Summary

Task Description
default Build and deploy to staging
build Build project (installs deps first)
kill Kill process on port 5173
dev Start Vite dev server
typecheck Run TypeScript type checking (no emit)
lint Run ESLint
test Run tests (single run)
test-watch Run tests in watch mode
preview Preview production build locally
install Install both PHP and Node.js dependencies
install-php Install PHP dependencies (Composer)
install-node Install Node.js dependencies (pnpm)
update Update all dependencies
update-php Update PHP dependencies (Composer)
update-node Update Node.js dependencies (pnpm)
audit Audit all dependencies for vulnerabilities
audit-php Audit PHP dependencies (Composer)
audit-node Audit Node.js dependencies (pnpm)
clean Remove build artifacts, Node dependencies, and PHP vendors
deploy Build and deploy to production
sync-staging Sync build output to ../staging/
sync-prod Sync build output to ../public/

Development

For quick iteration:

pnpm dev        # Dev server at http://localhost:5173
pnpm build      # Build to dist/
pnpm typecheck  # TypeScript type checking
pnpm lint       # ESLint check
pnpm test       # Run tests
pnpm test:watch # Run tests in watch mode
pnpm preview    # Preview production build

Making Changes

  1. Edit source files in src/
  2. Test with pnpm dev or task build
  3. The build output goes to dist/

Dependency Management

To add/remove dependencies:

  1. Edit package.json (Node.js) or composer.json (PHP)
  2. Run task install to reinstall
  3. Test the build works before committing

PHP dependencies are discovered automatically — any composer.json found under public/ (the Vite static directory, excluding vendor/ directories) will be processed.

Conventions

Preact vs React

  • Import from preact and preact/hooks, NOT react
  • Use react-error-boundary for error boundaries (Preact compatibility via aliases). The ErrorFallback component uses FallbackProps (where error is unknown) and safely extracts the message via error instanceof Error. It renders for both dev and production builds.
  • The Root wrapper in main.tsx uses a resetKey state on ErrorBoundary. When the user clicks "Try Again", onReset increments the key, forcing a full remount of the entire component tree to clear corrupted state.
  • Button and Alert components use @radix-ui/react-slot with asChild pattern for polymorphic rendering (works via the reactpreact/compat alias). A Comp: any cast with eslint-disable bridges the React Slot ref type and Preact's native ref types.

Linting

ESLint is configured via eslint.config.js (flat config format) with:

  • @eslint/js recommended rules
  • typescript-eslint recommended rules
  • Browser globals
  • dist/, node_modules/, .pnpm-store/, and public/**/vendor/ are ignored

Run pnpm lint (or task audit-node) to check for issues.

Tailwind Configuration

  • CSS variables: Defined in :root and @theme blocks in index.css

Adding UI Components

Follow shadcn-style patterns:

  1. Components go in src/components/ui/
  2. Use class-variance-authority for variant props
  3. Use tailwind-merge via @/lib/utils for className merging
  4. Export component with TypeScript interface

Icon Usage

Icons via lucide-preact. Import specific icons to avoid bundle bloat.

Image Optimization

SVG and raster images in src/assets/ are automatically optimized during build via vite-plugin-image-optimizer (uses Sharp for rasters, SVGO for vectors).

Testing

  • Test runner: Vitest with jsdom environment.
  • Component tests: @testing-library/preact for rendering and querying.
  • Test files are co-located with components (e.g., src/pages/Home.test.tsx).
  • Use vi.resetModules() in beforeEach when testing modules with module-scope side effects (e.g., App.tsx reads x-page meta at import time).
  • Use waitFor() for assertions after async state changes (e.g., after dispatching navigation events). Preact batches updates and useEffect runs asynchronously.
  • Use vi.spyOn(console, 'warn').mockImplementation(() => {}) to suppress expected dev-mode warnings, with .mockRestore() in a finally block.
  • Prefer accessible queries (getByRole, getByLabelText) over getByTestId.
  • Run pnpm test for a single run or pnpm test:watch for watch mode.

What NOT to Do

  • Don't create additional CSS files (index.css is the single source of truth)
  • Don't duplicate color variables or animation definitions
  • Don't add animation libraries (define custom keyframes in index.css instead)
  • Don't add a tailwind.config.js (Tailwind v4 is configured via CSS @theme block)
  • Don't add a router library — use window.location.pathname in App.tsx
  • Don't edit build output files in dist/ directly — they are generated by the build plugins
  • Don't use @apply for theme tokens — use CSS variables directly (e.g. border-color: var(--border))
  • Don't use console.log or console.debug — they are stripped in production builds
  • Don't add generic documentation files that don't reflect actual codebase state