- TypeScript 91.3%
- CSS 6.8%
- JavaScript 1%
- HTML 0.9%
|
All checks were successful
Build / build (push) Successful in 27s
- 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> |
||
|---|---|---|
| .forgejo/workflows | ||
| plugins | ||
| public | ||
| src | ||
| .gitignore | ||
| .rules | ||
| eslint.config.js | ||
| index.html | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| taskfile.yml | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
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 inindex.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:
- Add an entry to
src/pages/config.ts - Import the component in
App.tsxand add it to theroutesobject
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 (400–504). 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
@keyframesanimations (add your own here) - Dark mode via
prefers-color-schemeand.darkclass (add to<html>for manual toggle). Add.lightto<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
- Edit source files in
src/ - Test with
pnpm devortask build - The build output goes to
dist/
Dependency Management
To add/remove dependencies:
- Edit
package.json(Node.js) orcomposer.json(PHP) - Run
task installto reinstall - 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
preactandpreact/hooks, NOTreact - Use
react-error-boundaryfor error boundaries (Preact compatibility via aliases). TheErrorFallbackcomponent usesFallbackProps(whereerrorisunknown) and safely extracts the message viaerror instanceof Error. It renders for both dev and production builds. - The
Rootwrapper inmain.tsxuses aresetKeystate onErrorBoundary. When the user clicks "Try Again",onResetincrements the key, forcing a full remount of the entire component tree to clear corrupted state. - Button and Alert components use
@radix-ui/react-slotwithasChildpattern for polymorphic rendering (works via thereact→preact/compatalias). AComp: anycast with eslint-disable bridges the ReactSlotref type and Preact's native ref types.
Linting
ESLint is configured via eslint.config.js (flat config format) with:
@eslint/jsrecommended rulestypescript-eslintrecommended rules- Browser globals
dist/,node_modules/,.pnpm-store/, andpublic/**/vendor/are ignored
Run pnpm lint (or task audit-node) to check for issues.
Tailwind Configuration
- CSS variables: Defined in
:rootand@themeblocks inindex.css
Adding UI Components
Follow shadcn-style patterns:
- Components go in
src/components/ui/ - Use
class-variance-authorityfor variant props - Use
tailwind-mergevia@/lib/utilsfor className merging - 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
jsdomenvironment. - Component tests:
@testing-library/preactfor rendering and querying. - Test files are co-located with components (e.g.,
src/pages/Home.test.tsx). - Use
vi.resetModules()inbeforeEachwhen testing modules with module-scope side effects (e.g.,App.tsxreadsx-pagemeta at import time). - Use
waitFor()for assertions after async state changes (e.g., after dispatching navigation events). Preact batches updates anduseEffectruns asynchronously. - Use
vi.spyOn(console, 'warn').mockImplementation(() => {})to suppress expected dev-mode warnings, with.mockRestore()in afinallyblock. - Prefer accessible queries (
getByRole,getByLabelText) overgetByTestId. - Run
pnpm testfor a single run orpnpm test:watchfor watch mode.
What NOT to Do
- Don't create additional CSS files (
index.cssis the single source of truth) - Don't duplicate color variables or animation definitions
- Don't add animation libraries (define custom keyframes in
index.cssinstead) - Don't add a
tailwind.config.js(Tailwind v4 is configured via CSS@themeblock) - Don't add a router library — use
window.location.pathnameinApp.tsx - Don't edit build output files in
dist/directly — they are generated by the build plugins - Don't use
@applyfor theme tokens — use CSS variables directly (e.g.border-color: var(--border)) - Don't use
console.logorconsole.debug— they are stripped in production builds - Don't add generic documentation files that don't reflect actual codebase state