mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
Add velox-docs: config, nav, theme, search.json, and pages
This commit is contained in:
parent
7463f9d9ca
commit
25884d2a09
29 changed files with 6728 additions and 0 deletions
8
velox-docs/config.yml
Normal file
8
velox-docs/config.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# mdcms v0.3 | DO NOT REMOVE THIS COMMENT
|
||||||
|
sitename: Velox Framework
|
||||||
|
sitedescription: The high-performance TypeScript web framework
|
||||||
|
navigation: sidebar
|
||||||
|
nav-position: left
|
||||||
|
search: true
|
||||||
|
footer: "© 2026 Velox Contributors. Licensed under MIT."
|
||||||
|
theme: theme.yml
|
||||||
227
velox-docs/nav.yml
Normal file
227
velox-docs/nav.yml
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
# nav.yml — generated by mdcms.py
|
||||||
|
sections:
|
||||||
|
- code: getting-started
|
||||||
|
defaultname: Getting Started
|
||||||
|
sort: 100
|
||||||
|
pagesvisibility: visible
|
||||||
|
|
||||||
|
- code: core-concepts
|
||||||
|
defaultname: Core Concepts
|
||||||
|
sort: 200
|
||||||
|
pagesvisibility: visible
|
||||||
|
|
||||||
|
- code: api-reference
|
||||||
|
defaultname: API Reference
|
||||||
|
sort: 300
|
||||||
|
pagesvisibility: visible
|
||||||
|
|
||||||
|
- code: guides
|
||||||
|
defaultname: Guides
|
||||||
|
sort: 400
|
||||||
|
pagesvisibility: visible
|
||||||
|
|
||||||
|
- code: deployment
|
||||||
|
defaultname: Deployment
|
||||||
|
sort: 500
|
||||||
|
pagesvisibility: visible
|
||||||
|
|
||||||
|
pages:
|
||||||
|
- file: pages/index.md
|
||||||
|
title: Introduction
|
||||||
|
section-id: getting-started
|
||||||
|
sort: 100
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Introduction
|
||||||
|
|
||||||
|
- file: pages/installation.md
|
||||||
|
title: Installation
|
||||||
|
section-id: getting-started
|
||||||
|
sort: 110
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Installation
|
||||||
|
|
||||||
|
- file: pages/quick-start.md
|
||||||
|
title: Quick Start
|
||||||
|
section-id: getting-started
|
||||||
|
sort: 120
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Quick Start
|
||||||
|
|
||||||
|
- file: pages/project-structure.md
|
||||||
|
title: Project Structure
|
||||||
|
section-id: getting-started
|
||||||
|
sort: 130
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Project Structure
|
||||||
|
|
||||||
|
- file: pages/configuration.md
|
||||||
|
title: Configuration
|
||||||
|
section-id: getting-started
|
||||||
|
sort: 140
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Configuration
|
||||||
|
|
||||||
|
- file: pages/routing.md
|
||||||
|
title: Routing
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 100
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Routing
|
||||||
|
|
||||||
|
- file: pages/components.md
|
||||||
|
title: Components
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 110
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Components
|
||||||
|
|
||||||
|
- file: pages/state-management.md
|
||||||
|
title: State Management
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 120
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: State Management
|
||||||
|
|
||||||
|
- file: pages/data-fetching.md
|
||||||
|
title: Data Fetching
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 130
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Data Fetching
|
||||||
|
|
||||||
|
- file: pages/middleware.md
|
||||||
|
title: Middleware
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 140
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Middleware
|
||||||
|
|
||||||
|
- file: pages/layouts.md
|
||||||
|
title: Layouts
|
||||||
|
section-id: core-concepts
|
||||||
|
sort: 150
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Layouts
|
||||||
|
|
||||||
|
- file: pages/api-router.md
|
||||||
|
title: Router API
|
||||||
|
section-id: api-reference
|
||||||
|
sort: 100
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Router API
|
||||||
|
|
||||||
|
- file: pages/api-components.md
|
||||||
|
title: Component API
|
||||||
|
section-id: api-reference
|
||||||
|
sort: 110
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Component API
|
||||||
|
|
||||||
|
- file: pages/api-server.md
|
||||||
|
title: Server API
|
||||||
|
section-id: api-reference
|
||||||
|
sort: 120
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Server API
|
||||||
|
|
||||||
|
- file: pages/api-config.md
|
||||||
|
title: Config API
|
||||||
|
section-id: api-reference
|
||||||
|
sort: 130
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Config API
|
||||||
|
|
||||||
|
- file: pages/api-hooks.md
|
||||||
|
title: Hooks API
|
||||||
|
section-id: api-reference
|
||||||
|
sort: 140
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Hooks API
|
||||||
|
|
||||||
|
- file: pages/guide-auth.md
|
||||||
|
title: Authentication
|
||||||
|
section-id: guides
|
||||||
|
sort: 100
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Authentication
|
||||||
|
|
||||||
|
- file: pages/guide-database.md
|
||||||
|
title: Database Integration
|
||||||
|
section-id: guides
|
||||||
|
sort: 110
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Database Integration
|
||||||
|
|
||||||
|
- file: pages/guide-testing.md
|
||||||
|
title: Testing
|
||||||
|
section-id: guides
|
||||||
|
sort: 120
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Testing
|
||||||
|
|
||||||
|
- file: pages/guide-i18n.md
|
||||||
|
title: Internationalisation
|
||||||
|
section-id: guides
|
||||||
|
sort: 130
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Internationalisation
|
||||||
|
|
||||||
|
- file: pages/guide-performance.md
|
||||||
|
title: Performance
|
||||||
|
section-id: guides
|
||||||
|
sort: 140
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Performance
|
||||||
|
|
||||||
|
- file: pages/deploy-vercel.md
|
||||||
|
title: Deploy to Vercel
|
||||||
|
section-id: deployment
|
||||||
|
sort: 100
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Deploy to Vercel
|
||||||
|
|
||||||
|
- file: pages/deploy-cloudflare.md
|
||||||
|
title: Deploy to Cloudflare
|
||||||
|
section-id: deployment
|
||||||
|
sort: 110
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Deploy to Cloudflare
|
||||||
|
|
||||||
|
- file: pages/deploy-docker.md
|
||||||
|
title: Docker
|
||||||
|
section-id: deployment
|
||||||
|
sort: 120
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Docker
|
||||||
|
|
||||||
|
- file: pages/deploy-self-hosted.md
|
||||||
|
title: Self-Hosted
|
||||||
|
section-id: deployment
|
||||||
|
sort: 130
|
||||||
|
variants: [en]
|
||||||
|
titles:
|
||||||
|
en: Self-Hosted
|
||||||
270
velox-docs/pages/api-components.md
Normal file
270
velox-docs/pages/api-components.md
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
---
|
||||||
|
title: Component API
|
||||||
|
sort: 110
|
||||||
|
section-id: api-reference
|
||||||
|
keywords: defineComponent, ref, computed, watch, component API, reactive
|
||||||
|
description: Complete reference for the Velox component API — defineComponent, ref, computed, and watch
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Component API
|
||||||
|
|
||||||
|
This page documents the core component primitives exported from `velox/client`.
|
||||||
|
|
||||||
|
## `signal(initialValue)`
|
||||||
|
|
||||||
|
Creates a reactive signal — a value container that triggers DOM updates when changed.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal } from 'velox/client';
|
||||||
|
|
||||||
|
const count = signal(0);
|
||||||
|
count.value; // read: 0
|
||||||
|
count.value = 5; // write: triggers reactive updates
|
||||||
|
count.peek(); // read without tracking (no reactive subscription)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Signature
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function signal<T>(initialValue: T): Signal<T>;
|
||||||
|
|
||||||
|
interface Signal<T> {
|
||||||
|
value: T;
|
||||||
|
peek(): T;
|
||||||
|
subscribe(listener: (value: T) => void): () => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `computed(fn)`
|
||||||
|
|
||||||
|
Creates a derived reactive value. Re-evaluates lazily when accessed and a dependency has changed.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, computed } from 'velox/client';
|
||||||
|
|
||||||
|
const price = signal(100);
|
||||||
|
const taxRate = signal(0.2);
|
||||||
|
const total = computed(() => price.value * (1 + taxRate.value));
|
||||||
|
|
||||||
|
total.value; // 120
|
||||||
|
price.value = 200;
|
||||||
|
total.value; // 240
|
||||||
|
```
|
||||||
|
|
||||||
|
Computed signals are read-only — attempting to set `.value` throws.
|
||||||
|
|
||||||
|
## `effect(fn)`
|
||||||
|
|
||||||
|
Registers a reactive side effect. Runs immediately, then again whenever its signal dependencies change.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, effect } from 'velox/client';
|
||||||
|
|
||||||
|
const query = signal('');
|
||||||
|
|
||||||
|
const dispose = effect(() => {
|
||||||
|
console.log('query =', query.value);
|
||||||
|
// runs now, then on every change to query.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up manually (effects inside components clean up on unmount):
|
||||||
|
dispose();
|
||||||
|
```
|
||||||
|
|
||||||
|
Return a cleanup function from the effect to run before the next execution or on disposal:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
fetchData(query.value, controller.signal);
|
||||||
|
return () => controller.abort();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `batch(fn)`
|
||||||
|
|
||||||
|
Groups multiple signal writes into a single reactive update pass:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { batch } from 'velox/client';
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
a.value = 1;
|
||||||
|
b.value = 2;
|
||||||
|
c.value = 3;
|
||||||
|
// Only one round of re-renders happens
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `untrack(fn)`
|
||||||
|
|
||||||
|
Execute a function without tracking its signal reads as dependencies:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, effect, untrack } from 'velox/client';
|
||||||
|
|
||||||
|
const a = signal(1);
|
||||||
|
const b = signal(2);
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
// Only subscribes to `a`, not `b`
|
||||||
|
const result = a.value + untrack(() => b.value);
|
||||||
|
console.log(result);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Hooks
|
||||||
|
|
||||||
|
Lifecycle hooks must be called synchronously during component initialisation (similar to React rules of hooks, but without the runtime check overhead).
|
||||||
|
|
||||||
|
### `onMount(fn)`
|
||||||
|
|
||||||
|
Called after the component's DOM is inserted into the document:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { onMount } from 'velox/client';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// safe to access DOM, start timers, etc.
|
||||||
|
const input = document.querySelector('#my-input') as HTMLInputElement;
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `onCleanup(fn)`
|
||||||
|
|
||||||
|
Called before the component unmounts, or before an effect runs again. Use to cancel subscriptions, abort requests, and clear timers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { onMount, onCleanup } from 'velox/client';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const timer = setInterval(tick, 1000);
|
||||||
|
onCleanup(() => clearInterval(timer));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `onDestroy(fn)`
|
||||||
|
|
||||||
|
Like `onCleanup`, but only runs when the component is permanently destroyed (not before re-renders):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { onDestroy } from 'velox/client';
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
analytics.trackLeave(route);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `createContext` / `useContext`
|
||||||
|
|
||||||
|
Create a context for dependency injection without prop drilling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createContext, useContext } from 'velox/client';
|
||||||
|
|
||||||
|
// Create context with a default value
|
||||||
|
const ThemeContext = createContext<'light' | 'dark'>('light');
|
||||||
|
|
||||||
|
// Provide a value to a subtree
|
||||||
|
<ThemeContext.Provider value="dark">
|
||||||
|
<App />
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
|
||||||
|
// Consume in any descendant
|
||||||
|
function Button() {
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
return <button class={`btn--${theme}`}>Click</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `ref()`
|
||||||
|
|
||||||
|
Create a DOM element reference:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref, onMount } from 'velox/client';
|
||||||
|
|
||||||
|
export default function TextInput() {
|
||||||
|
const inputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <input ref={inputRef} type="text" />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `defineComponent(options)`
|
||||||
|
|
||||||
|
The explicit component definition API — useful when you need named components for debugging or when defining components programmatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineComponent, signal } from 'velox/client';
|
||||||
|
|
||||||
|
const Counter = defineComponent({
|
||||||
|
name: 'Counter',
|
||||||
|
props: {
|
||||||
|
initialValue: { type: Number, default: 0 },
|
||||||
|
step: { type: Number, default: 1 },
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const count = signal(props.initialValue);
|
||||||
|
const increment = () => { count.value += props.step; };
|
||||||
|
const decrement = () => { count.value -= props.step; };
|
||||||
|
|
||||||
|
return { count, increment, decrement };
|
||||||
|
},
|
||||||
|
render({ count, increment, decrement }) {
|
||||||
|
return (
|
||||||
|
<div class="counter">
|
||||||
|
<button onClick={decrement}>−</button>
|
||||||
|
<span>{count}</span>
|
||||||
|
<button onClick={increment}>+</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Counter;
|
||||||
|
```
|
||||||
|
|
||||||
|
## `lazy(loader)`
|
||||||
|
|
||||||
|
Lazily load a component — its code is only downloaded when the component is first rendered:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { lazy } from 'velox/client';
|
||||||
|
|
||||||
|
const HeavyChart = lazy(() => import('./HeavyChart.tsx'));
|
||||||
|
|
||||||
|
// Use in JSX — shows nothing while loading by default
|
||||||
|
<HeavyChart data={data} />
|
||||||
|
|
||||||
|
// With a Suspense boundary for a loading state
|
||||||
|
import { Suspense } from 'velox';
|
||||||
|
|
||||||
|
<Suspense fallback={<Skeleton />}>
|
||||||
|
<HeavyChart data={data} />
|
||||||
|
</Suspense>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `memo(component)`
|
||||||
|
|
||||||
|
Wrap a component in `memo` to skip re-renders when props have not changed (shallow equality):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { memo } from 'velox/client';
|
||||||
|
|
||||||
|
const ExpensiveList = memo(function ExpensiveList({ items }: { items: string[] }) {
|
||||||
|
return <ul>{items.map(i => <li>{i}</li>)}</ul>;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepts a custom equality function as the second argument:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MyComponent = memo(Component, (prev, next) => prev.id === next.id);
|
||||||
|
```
|
||||||
276
velox-docs/pages/api-config.md
Normal file
276
velox-docs/pages/api-config.md
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
---
|
||||||
|
title: Config API
|
||||||
|
sort: 130
|
||||||
|
section-id: api-reference
|
||||||
|
keywords: config API, velox.config.ts, defineConfig, types, TypeScript reference
|
||||||
|
description: Full TypeScript type reference for velox.config.ts and all configuration options
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Config API
|
||||||
|
|
||||||
|
This page provides the complete TypeScript type reference for `velox.config.ts`. All types are exported from the `velox` package.
|
||||||
|
|
||||||
|
## `defineConfig(config)`
|
||||||
|
|
||||||
|
The primary export. Accepts a `VeloxConfig` object and returns it with full type checking:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// VeloxConfig object
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass a function for dynamic configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default defineConfig(async (env) => {
|
||||||
|
const secrets = await loadSecrets();
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
name: 'My App',
|
||||||
|
baseUrl: secrets.BASE_URL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `VeloxConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface VeloxConfig {
|
||||||
|
app?: AppConfig;
|
||||||
|
server?: ServerConfig;
|
||||||
|
build?: BuildConfig;
|
||||||
|
routes?: RoutesConfig;
|
||||||
|
assets?: AssetsConfig;
|
||||||
|
css?: CSSConfig;
|
||||||
|
i18n?: I18nConfig;
|
||||||
|
middleware?: MiddlewareConfig[];
|
||||||
|
plugins?: VeloxPlugin[];
|
||||||
|
experimental?: ExperimentalConfig;
|
||||||
|
errorHandler?: ErrorHandler;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `AppConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AppConfig {
|
||||||
|
/** Application display name. Used in page titles and error pages. */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Canonical base URL. Required in production. Example: 'https://example.com' */
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
/** Default locale for i18n. Default: 'en' */
|
||||||
|
defaultLocale?: string;
|
||||||
|
|
||||||
|
/** Whether to append trailing slashes to all routes. Default: false */
|
||||||
|
trailingSlash?: boolean;
|
||||||
|
|
||||||
|
/** Custom 404 page route. Default: '_error' */
|
||||||
|
notFoundPage?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `ServerConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ServerConfig {
|
||||||
|
/** TCP port. Default: 3700 */
|
||||||
|
port?: number;
|
||||||
|
|
||||||
|
/** Bind hostname. Default: 'localhost' */
|
||||||
|
host?: string;
|
||||||
|
|
||||||
|
/** HTTPS configuration for the development server */
|
||||||
|
https?: {
|
||||||
|
cert: string; // path to PEM certificate
|
||||||
|
key: string; // path to PEM private key
|
||||||
|
};
|
||||||
|
|
||||||
|
/** CORS policy */
|
||||||
|
cors?: {
|
||||||
|
origin: string | string[] | ((origin: string) => boolean);
|
||||||
|
credentials?: boolean;
|
||||||
|
methods?: string[];
|
||||||
|
allowedHeaders?: string[];
|
||||||
|
exposedHeaders?: string[];
|
||||||
|
maxAge?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Compression. Default: true in production */
|
||||||
|
compress?: boolean;
|
||||||
|
|
||||||
|
/** Trust proxy headers (X-Forwarded-For, etc.). Default: false */
|
||||||
|
trustProxy?: boolean | number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `BuildConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BuildConfig {
|
||||||
|
/**
|
||||||
|
* Deployment target.
|
||||||
|
* - 'node': Outputs a Node.js server (default)
|
||||||
|
* - 'edge': Outputs a Web-API-compatible edge bundle
|
||||||
|
* - 'static': Full static export — no server required
|
||||||
|
*/
|
||||||
|
target: 'node' | 'edge' | 'static';
|
||||||
|
|
||||||
|
/** Output directory. Default: '.velox/output' */
|
||||||
|
outDir?: string;
|
||||||
|
|
||||||
|
/** Source map generation. Default: false in production */
|
||||||
|
sourcemap?: boolean | 'external' | 'inline';
|
||||||
|
|
||||||
|
/** Minify output. Default: true in production */
|
||||||
|
minify?: boolean;
|
||||||
|
|
||||||
|
/** Enable code splitting. Default: true */
|
||||||
|
splitting?: boolean;
|
||||||
|
|
||||||
|
/** Emit a bundle analysis HTML report */
|
||||||
|
analyze?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes to explicitly pre-render as static HTML.
|
||||||
|
* Supports globs. e.g. ['/blog/*', '/about']
|
||||||
|
*/
|
||||||
|
prerender?: string[];
|
||||||
|
|
||||||
|
/** External dependencies (not bundled). */
|
||||||
|
external?: string[];
|
||||||
|
|
||||||
|
/** Environment variables to inline into the client bundle */
|
||||||
|
define?: Record<string, string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `RoutesConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RoutesConfig {
|
||||||
|
/** Routes directory relative to project root. Default: 'routes' */
|
||||||
|
dir?: string;
|
||||||
|
|
||||||
|
/** File extensions treated as routes. Default: ['.velox', '.tsx'] */
|
||||||
|
extensions?: string[];
|
||||||
|
|
||||||
|
/** Glob patterns to exclude from routing */
|
||||||
|
exclude?: string[];
|
||||||
|
|
||||||
|
/** Prefix all generated routes with this base path */
|
||||||
|
base?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `AssetsConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AssetsConfig {
|
||||||
|
/** Static assets directory. Default: 'public' */
|
||||||
|
publicDir?: string;
|
||||||
|
|
||||||
|
imageOptimisation?: {
|
||||||
|
enabled: boolean;
|
||||||
|
/** Output formats to generate. Default: ['webp'] */
|
||||||
|
formats?: ('webp' | 'avif' | 'jpeg' | 'png')[];
|
||||||
|
/** JPEG/WebP/AVIF quality 1–100. Default: 80 */
|
||||||
|
quality?: number;
|
||||||
|
/** Maximum width in pixels before downscaling */
|
||||||
|
maxWidth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
fonts?: {
|
||||||
|
/** Emit <link rel="preload"> for fonts. Default: true */
|
||||||
|
preload?: boolean;
|
||||||
|
/** Unicode range subsets. Example: ['latin', 'latin-ext'] */
|
||||||
|
subsets?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `I18nConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface I18nConfig {
|
||||||
|
/** Supported locale codes. Example: ['en', 'fr', 'de'] */
|
||||||
|
locales: string[];
|
||||||
|
|
||||||
|
/** Default locale. Default: 'en' */
|
||||||
|
defaultLocale: string;
|
||||||
|
|
||||||
|
/** Strategy for locale in URL. Default: 'prefix-except-default' */
|
||||||
|
routing?: 'prefix' | 'prefix-except-default' | 'domain';
|
||||||
|
|
||||||
|
/** Domain mapping for 'domain' routing strategy */
|
||||||
|
domains?: Record<string, string>;
|
||||||
|
|
||||||
|
/** Path to translation files. Default: 'messages' */
|
||||||
|
messagesDir?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `MiddlewareConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MiddlewareConfig {
|
||||||
|
/** Route path glob to match. Use '*' for global middleware */
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
/** Path to the middleware module (relative to project root) */
|
||||||
|
handler: string;
|
||||||
|
|
||||||
|
/** Execution order (lower runs first). Default: 0 */
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `ExperimentalConfig`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ExperimentalConfig {
|
||||||
|
/** Enable CSS View Transitions for route changes */
|
||||||
|
viewTransitions?: boolean;
|
||||||
|
|
||||||
|
/** Enable fine-grained island hydration */
|
||||||
|
partialHydration?: boolean;
|
||||||
|
|
||||||
|
/** Enable compatibility shim for React components */
|
||||||
|
reactCompat?: boolean;
|
||||||
|
|
||||||
|
/** Enable server actions (form + RPC) */
|
||||||
|
serverActions?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface VeloxPlugin {
|
||||||
|
name: string;
|
||||||
|
setup?(build: VeloxBuild): void | Promise<void>;
|
||||||
|
transform?(code: string, id: string): string | { code: string; map?: string } | null;
|
||||||
|
resolveId?(id: string, importer?: string): string | null;
|
||||||
|
load?(id: string): string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a plugin:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function myPlugin(): VeloxPlugin {
|
||||||
|
return {
|
||||||
|
name: 'my-plugin',
|
||||||
|
transform(code, id) {
|
||||||
|
if (!id.endsWith('.myext')) return null;
|
||||||
|
return { code: transformMyExtension(code) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
222
velox-docs/pages/api-hooks.md
Normal file
222
velox-docs/pages/api-hooks.md
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
---
|
||||||
|
title: Hooks API
|
||||||
|
sort: 140
|
||||||
|
section-id: api-reference
|
||||||
|
keywords: hooks, useRequest, useSession, useCookies, useEnv, server hooks
|
||||||
|
description: Reference for Velox server-side hooks — useRequest, useSession, useCookies, and useEnv
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hooks API
|
||||||
|
|
||||||
|
Velox provides server-side hooks for accessing request context, sessions, cookies, and environment variables within route server blocks and middleware. These hooks are only available in server-side contexts.
|
||||||
|
|
||||||
|
## `useRequest()`
|
||||||
|
|
||||||
|
Returns the current `VeloxRequest` object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useRequest } from 'velox/server';
|
||||||
|
|
||||||
|
const request = useRequest();
|
||||||
|
|
||||||
|
const method = request.method;
|
||||||
|
const pathname = new URL(request.url).pathname;
|
||||||
|
const userAgent = request.headers.get('User-Agent');
|
||||||
|
```
|
||||||
|
|
||||||
|
This is equivalent to the `request` variable that is automatically available in server blocks, but `useRequest()` is useful in helper functions that are called from a server block without threading `request` through manually.
|
||||||
|
|
||||||
|
### Full Request Object Reference
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface VeloxRequest extends Request {
|
||||||
|
params: Record<string, string>; // route dynamic params
|
||||||
|
query: URLSearchParams; // parsed query string
|
||||||
|
cookies: RequestCookies; // parsed cookies
|
||||||
|
context: Map<string, unknown>; // middleware-set values
|
||||||
|
ip: string | null; // client IP address
|
||||||
|
geo: GeoInfo | null; // geographic info (if available)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useSession()`
|
||||||
|
|
||||||
|
Reads and writes the server-managed session. Sessions are stored server-side (in memory, Redis, or a database depending on your `session.store` configuration) and identified by a signed cookie.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useSession } from 'velox/server';
|
||||||
|
|
||||||
|
const session = await useSession<{ userId: string; role: string }>();
|
||||||
|
|
||||||
|
// Read
|
||||||
|
const userId = session.data.userId;
|
||||||
|
const role = session.data.role;
|
||||||
|
|
||||||
|
// Write — persists changes to the session store
|
||||||
|
await session.set('userId', '123');
|
||||||
|
await session.set('role', 'admin');
|
||||||
|
|
||||||
|
// Update multiple at once
|
||||||
|
await session.update({ userId: '123', role: 'admin' });
|
||||||
|
|
||||||
|
// Destroy the session (logout)
|
||||||
|
await session.destroy();
|
||||||
|
|
||||||
|
// Regenerate session ID (after privilege change — prevents fixation)
|
||||||
|
await session.regenerate();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Configuration
|
||||||
|
|
||||||
|
Configure the session store in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
import { RedisSessionStore } from '@velox/session-redis';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
session: {
|
||||||
|
secret: process.env.SESSION_SECRET!,
|
||||||
|
cookieName: 'velox.session',
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
store: new RedisSessionStore({
|
||||||
|
url: process.env.REDIS_URL!,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useCookies()`
|
||||||
|
|
||||||
|
Read and write cookies. Returns a `CookieJar` object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCookies } from 'velox/server';
|
||||||
|
|
||||||
|
const cookies = useCookies();
|
||||||
|
|
||||||
|
// Read
|
||||||
|
const theme = cookies.get('theme')?.value ?? 'light';
|
||||||
|
const hasConsented = cookies.get('consent')?.value === 'true';
|
||||||
|
|
||||||
|
// Write (adds Set-Cookie header to the response)
|
||||||
|
cookies.set('theme', 'dark', {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 365 * 24 * 60 * 60, // 1 year
|
||||||
|
sameSite: 'lax',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
cookies.delete('old-cookie', { path: '/' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cookie Options
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `domain` | `string` | Cookie domain |
|
||||||
|
| `path` | `string` | Cookie path. Default: `/` |
|
||||||
|
| `maxAge` | `number` | Expiry in seconds |
|
||||||
|
| `expires` | `Date` | Expiry as a date |
|
||||||
|
| `httpOnly` | `boolean` | Prevent JavaScript access |
|
||||||
|
| `secure` | `boolean` | HTTPS only |
|
||||||
|
| `sameSite` | `'strict' \| 'lax' \| 'none'` | SameSite policy |
|
||||||
|
|
||||||
|
## `useEnv(key, defaultValue?)`
|
||||||
|
|
||||||
|
Type-safe environment variable access:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useEnv } from 'velox/server';
|
||||||
|
|
||||||
|
const dbUrl = useEnv('DATABASE_URL'); // throws if not set
|
||||||
|
const port = useEnv('PORT', '3700'); // returns default if not set
|
||||||
|
const debug = useEnv.boolean('DEBUG', false); // parse as boolean
|
||||||
|
const timeout = useEnv.number('TIMEOUT', 30); // parse as number
|
||||||
|
```
|
||||||
|
|
||||||
|
### `useEnv` Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `useEnv(key)` | Return string value, throw if missing |
|
||||||
|
| `useEnv(key, default)` | Return string value or default |
|
||||||
|
| `useEnv.boolean(key, default?)` | Parse as boolean (`'true'` / `'1'` = `true`) |
|
||||||
|
| `useEnv.number(key, default?)` | Parse as float |
|
||||||
|
| `useEnv.json(key, default?)` | Parse as JSON |
|
||||||
|
| `useEnv.url(key, default?)` | Validate and return as URL string |
|
||||||
|
|
||||||
|
## `useHeaders()`
|
||||||
|
|
||||||
|
Access and modify response headers from within a server block or middleware:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useHeaders } from 'velox/server';
|
||||||
|
|
||||||
|
const headers = useHeaders();
|
||||||
|
|
||||||
|
// Read request headers
|
||||||
|
const accept = headers.request.get('Accept');
|
||||||
|
|
||||||
|
// Set response headers
|
||||||
|
headers.response.set('Cache-Control', 'public, max-age=3600');
|
||||||
|
headers.response.set('X-Custom-Header', 'value');
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useLocale()`
|
||||||
|
|
||||||
|
Returns the current request locale (resolved by the i18n middleware):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useLocale } from 'velox/server';
|
||||||
|
|
||||||
|
const { locale, locales, defaultLocale } = useLocale();
|
||||||
|
|
||||||
|
// locale: 'fr' (current request locale)
|
||||||
|
// locales: ['en', 'fr', 'de'] (all supported locales)
|
||||||
|
// defaultLocale: 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useAuth()`
|
||||||
|
|
||||||
|
A convenience hook that reads the current user from the session. Returns `null` if not authenticated:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useAuth } from '$lib/auth'; // project-defined hook
|
||||||
|
|
||||||
|
const user = await useAuth();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// user: { id: string, email: string, role: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
The `useAuth` hook is not provided by Velox itself — you define it in your project using `useSession` and your own user model. The [Authentication guide](guide-auth.md) shows a complete implementation.
|
||||||
|
|
||||||
|
## `useCache()`
|
||||||
|
|
||||||
|
Interact with Velox's built-in server-side cache:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCache } from 'velox/server';
|
||||||
|
|
||||||
|
const cache = useCache();
|
||||||
|
|
||||||
|
// Get from cache
|
||||||
|
const cached = await cache.get<User[]>('users:all');
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// Set with TTL (seconds)
|
||||||
|
const users = await db.users.findMany();
|
||||||
|
await cache.set('users:all', users, { ttl: 300 });
|
||||||
|
|
||||||
|
// Invalidate
|
||||||
|
await cache.delete('users:all');
|
||||||
|
await cache.deletePattern('users:*');
|
||||||
|
```
|
||||||
227
velox-docs/pages/api-router.md
Normal file
227
velox-docs/pages/api-router.md
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
---
|
||||||
|
title: Router API
|
||||||
|
sort: 100
|
||||||
|
section-id: api-reference
|
||||||
|
keywords: router, useRouter, navigate, Link, createRouter, programmatic navigation
|
||||||
|
description: Complete API reference for the Velox router — createRouter, useRouter, navigate, and Link
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Router API
|
||||||
|
|
||||||
|
The Velox router provides both declarative and programmatic navigation. This page documents the full public API surface of `@velox/router`.
|
||||||
|
|
||||||
|
## `useRouter()`
|
||||||
|
|
||||||
|
The `useRouter` hook returns the current router instance. It is available inside any client-side component:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useRouter } from 'velox/client';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Router Instance Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `pathname` | `string` | Current URL pathname, e.g. `/blog/my-post` |
|
||||||
|
| `search` | `string` | Current query string including `?`, e.g. `?page=2` |
|
||||||
|
| `hash` | `string` | Current hash fragment including `#` |
|
||||||
|
| `params` | `Record<string, string>` | Dynamic route parameters |
|
||||||
|
| `query` | `URLSearchParams` | Parsed query parameters |
|
||||||
|
| `state` | `unknown` | History state object (if provided to `navigate`) |
|
||||||
|
|
||||||
|
### Router Instance Methods
|
||||||
|
|
||||||
|
#### `navigate(href, options?)`
|
||||||
|
|
||||||
|
Performs client-side navigation to the given href:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
router.navigate('/dashboard');
|
||||||
|
|
||||||
|
// With options:
|
||||||
|
router.navigate('/profile/edit', {
|
||||||
|
replace: true, // replace current history entry
|
||||||
|
state: { from: '/dashboard' }, // pass arbitrary state
|
||||||
|
scroll: false, // don't scroll to top after navigation
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `back()` / `forward()`
|
||||||
|
|
||||||
|
Navigate through the browser history stack:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
router.back();
|
||||||
|
router.forward();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `prefetch(href)`
|
||||||
|
|
||||||
|
Manually prefetch a route (downloads the JS bundle and optionally data):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
router.prefetch('/heavy-page');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `refresh()`
|
||||||
|
|
||||||
|
Re-run the current route's server block and update the page without a full navigation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
router.refresh();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `<Link>` Component
|
||||||
|
|
||||||
|
The `<Link>` component renders an accessible `<a>` element with client-side navigation and built-in prefetching:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Link } from 'velox/client';
|
||||||
|
|
||||||
|
<Link href="/about">About</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `href` | `string` | required | Destination URL |
|
||||||
|
| `prefetch` | `'hover' \| 'viewport' \| false` | `'hover'` | Prefetch strategy |
|
||||||
|
| `replace` | `boolean` | `false` | Replace history entry instead of pushing |
|
||||||
|
| `scroll` | `boolean` | `true` | Scroll to top after navigation |
|
||||||
|
| `state` | `unknown` | `undefined` | History state object |
|
||||||
|
| `activeClass` | `string` | `'active'` | CSS class applied when href matches current pathname |
|
||||||
|
| `exactActiveClass` | `string` | `'exact-active'` | CSS class applied only on exact pathname match |
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Link
|
||||||
|
href="/blog"
|
||||||
|
prefetch="viewport"
|
||||||
|
activeClass="nav-link--active"
|
||||||
|
exactActiveClass="nav-link--exact"
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `createRouter()`
|
||||||
|
|
||||||
|
For testing or server-side use, create a router instance manually:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRouter } from 'velox/router';
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
base: '/app', // base path prefix
|
||||||
|
history: 'hash', // 'browser' (default) | 'hash' | 'memory'
|
||||||
|
scrollRestoration: 'auto', // 'auto' | 'manual'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useParams()`
|
||||||
|
|
||||||
|
Access current route parameters in any component:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useParams } from 'velox/client';
|
||||||
|
|
||||||
|
const { slug, category } = useParams<{ slug: string; category: string }>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useSearchParams()`
|
||||||
|
|
||||||
|
Read and update the URL search parameters reactively:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useSearchParams } from 'velox/client';
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Read
|
||||||
|
const page = searchParams.get('page') ?? '1';
|
||||||
|
|
||||||
|
// Update (triggers navigation without full page reload)
|
||||||
|
setSearchParams({ page: String(currentPage + 1) });
|
||||||
|
|
||||||
|
// Merge with existing params
|
||||||
|
setSearchParams(prev => {
|
||||||
|
prev.set('sort', 'date');
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `usePathname()`
|
||||||
|
|
||||||
|
Reactively access the current pathname:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { usePathname } from 'velox/client';
|
||||||
|
|
||||||
|
const pathname = usePathname();
|
||||||
|
// pathname is a Signal<string>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useNavigate()`
|
||||||
|
|
||||||
|
A lightweight hook that returns only the `navigate` function, without the full router object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useNavigate } from 'velox/client';
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
navigate('/login');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation Events
|
||||||
|
|
||||||
|
Subscribe to router lifecycle events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useRouter } from 'velox/client';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const unsubscribe = router.on('beforeNavigate', ({ to, from, cancel }) => {
|
||||||
|
if (hasUnsavedChanges && !confirm('Leave without saving?')) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
onCleanup(unsubscribe);
|
||||||
|
```
|
||||||
|
|
||||||
|
Available events: `beforeNavigate`, `afterNavigate`, `navigationError`.
|
||||||
|
|
||||||
|
## `redirect()` (Server)
|
||||||
|
|
||||||
|
Used inside server blocks and API route handlers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { redirect } from 'velox/server';
|
||||||
|
|
||||||
|
// Temporary redirect (302)
|
||||||
|
throw redirect('/login');
|
||||||
|
|
||||||
|
// Permanent redirect (301)
|
||||||
|
throw redirect('/new-url', 301);
|
||||||
|
|
||||||
|
// With custom headers
|
||||||
|
throw redirect('/dashboard', 302, {
|
||||||
|
'Set-Cookie': 'session=...; Path=/',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `notFound()` (Server)
|
||||||
|
|
||||||
|
Trigger the nearest `_error.velox` with a 404 status:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { notFound } from 'velox/server';
|
||||||
|
|
||||||
|
const post = await db.posts.find(id);
|
||||||
|
if (!post) throw notFound();
|
||||||
|
```
|
||||||
243
velox-docs/pages/api-server.md
Normal file
243
velox-docs/pages/api-server.md
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
---
|
||||||
|
title: Server API
|
||||||
|
sort: 120
|
||||||
|
section-id: api-reference
|
||||||
|
keywords: server API, createServer, defineHandler, Response helpers, server-side
|
||||||
|
description: Reference for Velox server-side APIs — createServer, defineHandler, and Response helpers
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Server API
|
||||||
|
|
||||||
|
The Velox server API provides utilities for API route handlers, middleware, and server-side logic. All exports documented here come from `velox/server`.
|
||||||
|
|
||||||
|
## `defineHandler(fn)`
|
||||||
|
|
||||||
|
Wraps an API route handler with type inference and error handling:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (request) => {
|
||||||
|
return Response.json({ status: 'ok' });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The handler receives a `VeloxRequest` object (an extension of the standard `Request`) and must return a `Response` or `Promise<Response>`.
|
||||||
|
|
||||||
|
### `VeloxRequest`
|
||||||
|
|
||||||
|
The enhanced request object passed to handlers:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `url` | `string` | Full request URL |
|
||||||
|
| `method` | `string` | HTTP method (uppercase) |
|
||||||
|
| `headers` | `Headers` | Request headers |
|
||||||
|
| `params` | `Record<string, string>` | URL route parameters |
|
||||||
|
| `query` | `URLSearchParams` | Parsed query string |
|
||||||
|
| `cookies` | `RequestCookies` | Parsed cookies |
|
||||||
|
| `context` | `Map<string, unknown>` | Values set by middleware |
|
||||||
|
| `json()` | `Promise<unknown>` | Parse body as JSON |
|
||||||
|
| `text()` | `Promise<string>` | Parse body as text |
|
||||||
|
| `formData()` | `Promise<FormData>` | Parse body as form data |
|
||||||
|
| `arrayBuffer()` | `Promise<ArrayBuffer>` | Parse body as binary |
|
||||||
|
|
||||||
|
## Response Helpers
|
||||||
|
|
||||||
|
Velox exports a set of `Response` factory helpers for common responses:
|
||||||
|
|
||||||
|
### `json(data, init?)`
|
||||||
|
|
||||||
|
Return a JSON response with appropriate `Content-Type` header:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { json } from 'velox/server';
|
||||||
|
|
||||||
|
return json({ users }, { status: 200 });
|
||||||
|
return json({ error: 'Not found' }, { status: 404 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `text(body, init?)`
|
||||||
|
|
||||||
|
Return a plain text response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { text } from 'velox/server';
|
||||||
|
|
||||||
|
return text('Hello, World!');
|
||||||
|
return text('Not Found', { status: 404 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `html(body, init?)`
|
||||||
|
|
||||||
|
Return an HTML response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { html } from 'velox/server';
|
||||||
|
|
||||||
|
return html('<h1>Hello</h1>');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `redirect(url, status?, headers?)`
|
||||||
|
|
||||||
|
Return a redirect response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { redirect } from 'velox/server';
|
||||||
|
|
||||||
|
return redirect('/login', 302);
|
||||||
|
return redirect('/new-location', 301);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `notFound(message?)`
|
||||||
|
|
||||||
|
Return a 404 response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { notFound } from 'velox/server';
|
||||||
|
|
||||||
|
return notFound('User not found');
|
||||||
|
// Returns: Response with status 404 and JSON body
|
||||||
|
```
|
||||||
|
|
||||||
|
### `unauthorized(message?)`
|
||||||
|
|
||||||
|
Return a 401 response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { unauthorized } from 'velox/server';
|
||||||
|
|
||||||
|
return unauthorized('Please log in');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `forbidden(message?)`
|
||||||
|
|
||||||
|
Return a 403 response:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { forbidden } from 'velox/server';
|
||||||
|
|
||||||
|
return forbidden('You do not have access to this resource');
|
||||||
|
```
|
||||||
|
|
||||||
|
### `badRequest(message?, details?)`
|
||||||
|
|
||||||
|
Return a 400 response with optional validation details:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { badRequest } from 'velox/server';
|
||||||
|
|
||||||
|
return badRequest('Validation failed', { field: 'email', message: 'Invalid format' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `stream(generator, init?)`
|
||||||
|
|
||||||
|
Stream a response body using an async generator:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { stream } from 'velox/server';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async () => {
|
||||||
|
return stream(async function* () {
|
||||||
|
for await (const chunk of getLargeDataset()) {
|
||||||
|
yield JSON.stringify(chunk) + '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `createServer(options)`
|
||||||
|
|
||||||
|
Bootstrap a standalone Velox server programmatically (useful for testing):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createServer } from 'velox/server';
|
||||||
|
|
||||||
|
const server = await createServer({
|
||||||
|
root: '/path/to/project',
|
||||||
|
port: 3700,
|
||||||
|
mode: 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
console.log(`Server running on port ${server.port}`);
|
||||||
|
|
||||||
|
// Later:
|
||||||
|
await server.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `VeloxServer` Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `start()` | Start the HTTP server |
|
||||||
|
| `stop()` | Gracefully shut down the server |
|
||||||
|
| `getUrl()` | Returns the server base URL as a string |
|
||||||
|
| `inject(request)` | Inject a synthetic request for testing without a network |
|
||||||
|
|
||||||
|
## Cookies
|
||||||
|
|
||||||
|
### Reading Cookies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const session = request.cookies.get('session');
|
||||||
|
const userId = request.cookies.get('userId')?.value;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Cookies
|
||||||
|
|
||||||
|
Use the `ResponseCookies` API on the response object, or the `setCookie` helper:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { setCookie, deleteCookie } from 'velox/server';
|
||||||
|
|
||||||
|
const response = json({ ok: true });
|
||||||
|
setCookie(response, 'session', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting Cookies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = redirect('/login');
|
||||||
|
deleteCookie(response, 'session');
|
||||||
|
return response;
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useEnv(key, defaultValue?)`
|
||||||
|
|
||||||
|
Type-safe environment variable access with optional validation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useEnv } from 'velox/server';
|
||||||
|
|
||||||
|
const dbUrl = useEnv('DATABASE_URL'); // throws if missing
|
||||||
|
const port = useEnv('PORT', '3700'); // returns defaultValue if missing
|
||||||
|
const apiKey = useEnv.required('SECRET_KEY'); // alias for useEnv(key)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Velox catches thrown errors in route handlers and middleware. Define a custom error handler in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
errorHandler: async (error, request) => {
|
||||||
|
if (error.status === 404) {
|
||||||
|
return json({ error: 'Not Found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
253
velox-docs/pages/components.md
Normal file
253
velox-docs/pages/components.md
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
---
|
||||||
|
title: Components
|
||||||
|
sort: 110
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: components, props, slots, lifecycle, tsx, velox components, islands
|
||||||
|
description: The Velox component model, including props, slots, lifecycle hooks, and the islands architecture
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Components
|
||||||
|
|
||||||
|
Velox components are TypeScript/TSX files that describe a piece of UI. They are the fundamental building blocks of a Velox application — reusable, composable, and by default rendered entirely on the server.
|
||||||
|
|
||||||
|
## Defining a Component
|
||||||
|
|
||||||
|
A basic Velox component is a TypeScript function that returns JSX:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/Greeting.tsx
|
||||||
|
interface GreetingProps {
|
||||||
|
name: string;
|
||||||
|
formal?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Greeting({ name, formal = false }: GreetingProps) {
|
||||||
|
const salutation = formal ? 'Good day' : 'Hello';
|
||||||
|
return <p>{salutation}, {name}!</p>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it in a route or another component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import Greeting from '../components/Greeting.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Greeting name="Alice" />
|
||||||
|
<Greeting name="Bob" formal />
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
Props are typed using TypeScript interfaces or types. All props are validated at compile time — no runtime prop checking overhead.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface CardProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
href?: string;
|
||||||
|
variant?: 'default' | 'featured' | 'compact';
|
||||||
|
children?: VeloxNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Card({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
href,
|
||||||
|
variant = 'default',
|
||||||
|
children,
|
||||||
|
}: CardProps) {
|
||||||
|
return (
|
||||||
|
<div class={`card card--${variant}`}>
|
||||||
|
<h3>{href ? <a href={href}>{title}</a> : title}</h3>
|
||||||
|
<p>{description}</p>
|
||||||
|
{children && <div class="card__body">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default Props
|
||||||
|
|
||||||
|
Set default values directly in the destructuring parameter, as shown above. Velox does not use a separate `defaultProps` mechanism.
|
||||||
|
|
||||||
|
### Required vs Optional Props
|
||||||
|
|
||||||
|
By TypeScript convention, optional props are marked with `?`. Omitting a required prop is a compile-time error.
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
The `children` prop is the default slot — content placed between the opening and closing tags of a component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Card title="Welcome" description="Get started">
|
||||||
|
<p>This is the card body.</p>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Named Slots
|
||||||
|
|
||||||
|
For more complex component APIs, use named slot props:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface ModalProps {
|
||||||
|
title: VeloxNode;
|
||||||
|
footer?: VeloxNode;
|
||||||
|
children: VeloxNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({ title, footer, children }: ModalProps) {
|
||||||
|
return (
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal__header">{title}</div>
|
||||||
|
<div class="modal__body">{children}</div>
|
||||||
|
{footer && <div class="modal__footer">{footer}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Modal
|
||||||
|
title={<h2>Confirm Delete</h2>}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<button onClick={cancel}>Cancel</button>
|
||||||
|
<button onClick={confirm}>Delete</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>Are you sure you want to delete this item?</p>
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server vs Client Components
|
||||||
|
|
||||||
|
By default, all Velox components run on the server. They have no JavaScript bundle size on the client and cannot use browser APIs or client-side reactivity.
|
||||||
|
|
||||||
|
To make a component interactive on the client, use the `client:*` hydration directive when you include it in a route:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import Counter from '../components/Counter.tsx';
|
||||||
|
import LazyChart from '../components/LazyChart.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Hydrate immediately when page loads -->
|
||||||
|
<Counter client:load />
|
||||||
|
|
||||||
|
<!-- Hydrate when browser is idle -->
|
||||||
|
<Counter client:idle />
|
||||||
|
|
||||||
|
<!-- Hydrate when the component enters the viewport -->
|
||||||
|
<LazyChart client:visible />
|
||||||
|
|
||||||
|
<!-- Hydrate only when a media query matches -->
|
||||||
|
<Sidebar client:media="(max-width: 768px)" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Client Components
|
||||||
|
|
||||||
|
Client components can use signals, effects, and browser APIs:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { signal, effect, onMount, onCleanup } from 'velox/client';
|
||||||
|
|
||||||
|
export default function LiveClock() {
|
||||||
|
const time = signal(new Date().toLocaleTimeString());
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
time.value = new Date().toLocaleTimeString();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
onCleanup(() => clearInterval(interval));
|
||||||
|
});
|
||||||
|
|
||||||
|
return <p>Current time: {time}</p>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Hooks
|
||||||
|
|
||||||
|
Client-side components can use the following lifecycle hooks:
|
||||||
|
|
||||||
|
| Hook | When it runs |
|
||||||
|
|------|-------------|
|
||||||
|
| `onMount(fn)` | After the component is first rendered and inserted into the DOM |
|
||||||
|
| `onUpdate(fn)` | After every re-render (reactive signal change) |
|
||||||
|
| `onCleanup(fn)` | Before the component unmounts or before the next `onUpdate` call |
|
||||||
|
| `onDestroy(fn)` | When the component is permanently unmounted |
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { signal, onMount, onUpdate, onCleanup } from 'velox/client';
|
||||||
|
|
||||||
|
export default function DataComponent() {
|
||||||
|
const data = signal<any[]>([]);
|
||||||
|
const loading = signal(true);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const response = await fetch('/api/data');
|
||||||
|
data.value = await response.json();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
// abort pending requests, clear timers, etc.
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading.value ? <p>Loading...</p> : <ul>{data.value.map(d => <li>{d.name}</li>)}</ul>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Composition Patterns
|
||||||
|
|
||||||
|
### Higher-Order Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function withAuth<T extends {}>(Component: VeloxComponent<T>) {
|
||||||
|
return function AuthGated(props: T) {
|
||||||
|
const user = useUser();
|
||||||
|
if (!user) return <Redirect to="/login" />;
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Render Props / Function-as-Children
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface FetcherProps<T> {
|
||||||
|
url: string;
|
||||||
|
render: (data: T) => VeloxNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Fetcher<T>({ url, render }: FetcherProps<T>) {
|
||||||
|
// ... fetch logic
|
||||||
|
return render(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Library
|
||||||
|
|
||||||
|
Velox ships an optional official component library `@velox/ui` with accessible, unstyled base components. Install it separately:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button, Input, Dialog } from '@velox/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Component API](api-components.md) reference for the full `defineComponent` API and advanced component patterns.
|
||||||
249
velox-docs/pages/configuration.md
Normal file
249
velox-docs/pages/configuration.md
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
---
|
||||||
|
title: Configuration
|
||||||
|
sort: 140
|
||||||
|
section-id: getting-started
|
||||||
|
keywords: configuration, velox.config.ts, settings, options, defineConfig
|
||||||
|
description: Complete reference for velox.config.ts and all available configuration options
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Velox is configured through a single `velox.config.ts` file at the root of your project. This file is evaluated at build time (and at dev-server startup) to determine how Velox should compile and serve your application.
|
||||||
|
|
||||||
|
## Creating the Config File
|
||||||
|
|
||||||
|
If you used `create-velox` to scaffold your project, a `velox.config.ts` is generated for you. To create one manually:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// options go here
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`defineConfig` is a helper that provides full TypeScript type inference over the configuration object — use it rather than exporting a plain object.
|
||||||
|
|
||||||
|
## Top-Level Options
|
||||||
|
|
||||||
|
### `app`
|
||||||
|
|
||||||
|
General application metadata.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app: {
|
||||||
|
name: string; // used in HTML <title> and meta tags
|
||||||
|
baseUrl: string; // canonical base URL (e.g. https://example.com)
|
||||||
|
defaultLocale?: string; // default locale for i18n (default: 'en')
|
||||||
|
trailingSlash?: boolean; // append trailing slash to all routes (default: false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
app: {
|
||||||
|
name: 'My Velox App',
|
||||||
|
baseUrl: process.env.PUBLIC_BASE_URL ?? 'http://localhost:3700',
|
||||||
|
defaultLocale: 'en',
|
||||||
|
trailingSlash: false,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### `server`
|
||||||
|
|
||||||
|
Development and production server settings.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
server: {
|
||||||
|
port?: number; // dev server port (default: 3700)
|
||||||
|
host?: string; // bind address (default: 'localhost')
|
||||||
|
https?: { // enable HTTPS for the dev server
|
||||||
|
cert: string; // path to certificate file
|
||||||
|
key: string; // path to key file
|
||||||
|
};
|
||||||
|
cors?: {
|
||||||
|
origin: string | string[] | '*';
|
||||||
|
credentials?: boolean;
|
||||||
|
methods?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `build`
|
||||||
|
|
||||||
|
Build system options.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
build: {
|
||||||
|
target: 'node' | 'edge' | 'static'; // deployment target
|
||||||
|
outDir?: string; // output directory (default: '.velox/output')
|
||||||
|
sourcemap?: boolean | 'external'; // generate source maps
|
||||||
|
minify?: boolean; // minify output (default: true in production)
|
||||||
|
splitting?: boolean; // enable code splitting (default: true)
|
||||||
|
analyze?: boolean; // emit a bundle analysis report
|
||||||
|
prerender?: string[]; // explicit list of routes to pre-render
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `routes`
|
||||||
|
|
||||||
|
Fine-grained routing options.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
routes: {
|
||||||
|
dir?: string; // routes directory (default: 'routes')
|
||||||
|
extensions?: string[]; // file extensions treated as routes
|
||||||
|
// default: ['.velox', '.tsx', '.ts']
|
||||||
|
exclude?: string[]; // glob patterns to exclude
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `assets`
|
||||||
|
|
||||||
|
Asset handling and optimisation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
assets: {
|
||||||
|
publicDir?: string; // static assets directory (default: 'public')
|
||||||
|
imageOptimisation?: {
|
||||||
|
enabled: boolean;
|
||||||
|
formats?: ('webp' | 'avif')[];
|
||||||
|
quality?: number; // 1–100, default 80
|
||||||
|
};
|
||||||
|
fonts?: {
|
||||||
|
preload?: boolean;
|
||||||
|
subsets?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `css`
|
||||||
|
|
||||||
|
Stylesheet handling.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
css: {
|
||||||
|
modules?: {
|
||||||
|
localsConvention?: 'camelCase' | 'camelCaseOnly' | 'dashes';
|
||||||
|
generateScopedName?: string;
|
||||||
|
};
|
||||||
|
preprocessors?: {
|
||||||
|
sass?: boolean; // enable Sass (requires @velox/sass)
|
||||||
|
less?: boolean;
|
||||||
|
stylus?: boolean;
|
||||||
|
};
|
||||||
|
postcss?: {
|
||||||
|
plugins?: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `plugins`
|
||||||
|
|
||||||
|
The plugin array lets you extend Velox with first-party and community plugins.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
import { veloxMDX } from '@velox/mdx';
|
||||||
|
import { veloxSass } from '@velox/sass';
|
||||||
|
import { veloxPWA } from '@velox/pwa';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
veloxMDX(),
|
||||||
|
veloxSass(),
|
||||||
|
veloxPWA({
|
||||||
|
name: 'My App',
|
||||||
|
themeColor: '#3a7bd5',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `experimental`
|
||||||
|
|
||||||
|
Opt-in to experimental features that are not yet stable.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
experimental: {
|
||||||
|
viewTransitions?: boolean; // CSS View Transitions API support
|
||||||
|
partialHydration?: boolean; // fine-grained component hydration
|
||||||
|
reactCompat?: boolean; // React component compatibility shim
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment-Specific Configuration
|
||||||
|
|
||||||
|
You can provide environment-specific overrides:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
app: {
|
||||||
|
name: 'My App',
|
||||||
|
baseUrl: process.env.PUBLIC_BASE_URL!,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: parseInt(process.env.PORT ?? '3700'),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: process.env.VELOX_TARGET as 'node' | 'edge' ?? 'node',
|
||||||
|
sourcemap: process.env.NODE_ENV !== 'production',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Per-Route Rendering Configuration
|
||||||
|
|
||||||
|
You can override the rendering mode for individual routes from within the route file:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/dashboard.velox server block
|
||||||
|
export const config = {
|
||||||
|
render: 'ssr', // 'ssr' | 'ssg' | 'isr' | 'csr'
|
||||||
|
revalidate: 60, // ISR: revalidate every 60 seconds
|
||||||
|
edge: true, // run on edge runtime
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Example
|
||||||
|
|
||||||
|
A production-ready `velox.config.ts` for a large application:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
import { veloxMDX } from '@velox/mdx';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
app: {
|
||||||
|
name: 'Acme Corp',
|
||||||
|
baseUrl: process.env.PUBLIC_BASE_URL!,
|
||||||
|
defaultLocale: 'en',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3700,
|
||||||
|
cors: {
|
||||||
|
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'edge',
|
||||||
|
sourcemap: 'external',
|
||||||
|
analyze: process.env.ANALYZE === '1',
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
imageOptimisation: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['webp', 'avif'],
|
||||||
|
quality: 85,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [veloxMDX()],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
For the complete type definitions, see the [Config API](api-config.md) reference.
|
||||||
267
velox-docs/pages/data-fetching.md
Normal file
267
velox-docs/pages/data-fetching.md
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
---
|
||||||
|
title: Data Fetching
|
||||||
|
sort: 130
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: data fetching, SSR, SSG, ISR, server-side rendering, static generation, fetch, async
|
||||||
|
description: How to fetch data in Velox using SSR, SSG, ISR, and client-side fetching patterns
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Data Fetching
|
||||||
|
|
||||||
|
Velox provides multiple data-fetching patterns depending on when and how often your data changes. You can mix strategies freely across routes in the same project.
|
||||||
|
|
||||||
|
## Server-Side Rendering (SSR)
|
||||||
|
|
||||||
|
In SSR mode, data is fetched fresh on every request. Use `await` directly in the server block of a `.velox` route:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
// routes/blog/[slug].velox
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { NotFound } from 'velox/server';
|
||||||
|
|
||||||
|
const { slug } = params;
|
||||||
|
const post = await db.posts.findBySlug(slug);
|
||||||
|
|
||||||
|
if (!post) throw new NotFound();
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
};
|
||||||
|
export const config = { render: 'ssr' };
|
||||||
|
---
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
<p class="byline">By {post.author} on {post.publishedAt}</p>
|
||||||
|
<div innerHTML={post.htmlContent} />
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `render: 'ssr'` export is optional — SSR is the default for routes that contain `await` expressions.
|
||||||
|
|
||||||
|
### Request Context in SSR
|
||||||
|
|
||||||
|
In SSR mode, the `request` object is available in the server block:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const authHeader = request.headers.get('Authorization');
|
||||||
|
const userAgent = request.headers.get('User-Agent');
|
||||||
|
const cookieHeader = request.headers.get('Cookie');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Site Generation (SSG)
|
||||||
|
|
||||||
|
SSG routes are rendered once at build time. They are ideal for content that rarely changes.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const config = { render: 'ssg' };
|
||||||
|
|
||||||
|
const features = await fetch('https://api.example.com/features').then(r => r.json());
|
||||||
|
---
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Features</h1>
|
||||||
|
<ul>
|
||||||
|
{features.map(f => <li>{f.name}: {f.description}</li>)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic SSG Routes
|
||||||
|
|
||||||
|
For SSG routes with dynamic segments, export a `paths` function to tell Velox which parameter values to pre-render:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
export const config = { render: 'ssg' };
|
||||||
|
|
||||||
|
// Called at build time to enumerate all slugs
|
||||||
|
export async function paths() {
|
||||||
|
const slugs = await db.posts.findManySlugs();
|
||||||
|
return slugs.map(slug => ({ slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slug } = params;
|
||||||
|
const post = await db.posts.findBySlug(slug);
|
||||||
|
export const meta = { title: post.title };
|
||||||
|
---
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
<div innerHTML={post.htmlContent} />
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Incremental Static Regeneration (ISR)
|
||||||
|
|
||||||
|
ISR generates pages statically but revalidates them in the background after a configurable interval:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const config = {
|
||||||
|
render: 'isr',
|
||||||
|
revalidate: 3600, // regenerate at most once per hour
|
||||||
|
};
|
||||||
|
|
||||||
|
const products = await db.products.findMany({ orderBy: 'created_at' });
|
||||||
|
---
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{products.map(p => <ProductCard product={p} />)}
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
On a cache miss (first request, or after `revalidate` seconds), Velox serves the stale page immediately while regenerating the fresh version in the background. The next request gets the fresh version.
|
||||||
|
|
||||||
|
### Manual Revalidation
|
||||||
|
|
||||||
|
Trigger revalidation programmatically (e.g., from a webhook):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/revalidate+server.ts
|
||||||
|
import { revalidatePath, revalidateTag } from 'velox/server';
|
||||||
|
|
||||||
|
export const POST = defineHandler(async (req) => {
|
||||||
|
const { path, secret } = await req.json();
|
||||||
|
|
||||||
|
if (secret !== process.env.REVALIDATE_SECRET) {
|
||||||
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await revalidatePath(path);
|
||||||
|
return Response.json({ revalidated: true });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Tag-based revalidation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// When fetching, attach cache tags:
|
||||||
|
const data = await fetch('/api/products', {
|
||||||
|
next: { tags: ['products'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Later, invalidate all pages that used the 'products' tag:
|
||||||
|
await revalidateTag('products');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-Side Fetching
|
||||||
|
|
||||||
|
For data that must be fresh on the client (user-specific dashboards, real-time data), use client-side fetching in an interactive component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { signal, onMount } from 'velox/client';
|
||||||
|
|
||||||
|
export default function UserDashboard() {
|
||||||
|
const stats = signal<DashboardStats | null>(null);
|
||||||
|
const error = signal<string | null>(null);
|
||||||
|
const loading = signal(true);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/dashboard/stats', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch stats');
|
||||||
|
stats.value = await res.json();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e.message;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading.value && <Spinner />}
|
||||||
|
{error.value && <ErrorMessage message={error.value} />}
|
||||||
|
{stats.value && <StatsGrid stats={stats.value} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using `@velox/query`
|
||||||
|
|
||||||
|
For production client-side data fetching, `@velox/query` handles caching, deduplication, background refetch, and stale-while-revalidate:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useQuery } from '@velox/query';
|
||||||
|
|
||||||
|
export default function ProductList() {
|
||||||
|
const { data, status, error, refetch } = useQuery({
|
||||||
|
key: ['products'],
|
||||||
|
fetcher: () => fetch('/api/products').then(r => r.json()),
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 'loading') return <Spinner />;
|
||||||
|
if (status === 'error') return <p>Error: {error.message}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{data.map(p => <li key={p.id}>{p.name}</li>)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Data Fetching
|
||||||
|
|
||||||
|
Fetch multiple data sources in parallel using `Promise.all`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
const [user, posts, categories] = await Promise.all([
|
||||||
|
db.users.findById(userId),
|
||||||
|
db.posts.findByUser(userId),
|
||||||
|
db.categories.findAll(),
|
||||||
|
]);
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## Streaming
|
||||||
|
|
||||||
|
For pages with slow data, use streaming to send the page shell immediately and stream in the slow parts:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import { Suspense } from 'velox';
|
||||||
|
|
||||||
|
const fastData = await db.pageConfig.find();
|
||||||
|
|
||||||
|
// Slow query wrapped in Suspense — the page shell renders immediately
|
||||||
|
const slowPostsPromise = db.posts.findMany({ include: { comments: true } });
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>{fastData.title}</h1>
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<PostList postsPromise={slowPostsPromise} />
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<Suspense>` boundary renders its fallback immediately while the async data resolves, then streams the final HTML to the client.
|
||||||
|
|
||||||
|
## Fetch Utilities
|
||||||
|
|
||||||
|
Velox wraps the global `fetch` with several quality-of-life additions in server contexts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fetch } from 'velox/server';
|
||||||
|
|
||||||
|
// Automatic base URL resolution for relative paths
|
||||||
|
const data = await fetch('/api/internal-endpoint');
|
||||||
|
|
||||||
|
// Request deduplication — concurrent identical fetches share one in-flight request
|
||||||
|
const [a, b] = await Promise.all([fetch('/api/same'), fetch('/api/same')]);
|
||||||
|
// ^ only one HTTP request is made
|
||||||
|
```
|
||||||
213
velox-docs/pages/deploy-cloudflare.md
Normal file
213
velox-docs/pages/deploy-cloudflare.md
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
---
|
||||||
|
title: Deploy to Cloudflare
|
||||||
|
sort: 110
|
||||||
|
section-id: deployment
|
||||||
|
keywords: Cloudflare, Pages, Workers, deployment, edge, CDN, Wrangler
|
||||||
|
description: Deploying a Velox application to Cloudflare Pages and Workers
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deploy to Cloudflare
|
||||||
|
|
||||||
|
Velox runs natively on Cloudflare Workers. By combining Cloudflare Pages for static assets and Cloudflare Workers for server-side rendering, you get a globally distributed application with sub-millisecond cold starts.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Velox on Cloudflare uses:
|
||||||
|
- **Cloudflare Pages** — serves static assets from the CDN edge
|
||||||
|
- **Cloudflare Workers** — handles SSR requests at the edge
|
||||||
|
- **KV** — used for ISR page caching
|
||||||
|
- **D1** — SQLite-at-the-edge database (optional)
|
||||||
|
- **R2** — object storage for user uploads (optional)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Install the Cloudflare adapter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/cloudflare
|
||||||
|
npm install -D wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the adapter:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// velox.config.ts
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
import { cloudflare } from '@velox/cloudflare';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
adapter: cloudflare({
|
||||||
|
kvNamespace: 'VELOX_CACHE', // KV namespace for ISR cache
|
||||||
|
routes: {
|
||||||
|
exclude: ['/admin/*'], // serve these from Pages CDN only
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
build: {
|
||||||
|
target: 'edge',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrangler Configuration
|
||||||
|
|
||||||
|
Create `wrangler.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
name = "my-velox-app"
|
||||||
|
compatibility_date = "2026-01-01"
|
||||||
|
compatibility_flags = ["nodejs_compat"]
|
||||||
|
pages_build_output_dir = ".velox/output"
|
||||||
|
|
||||||
|
[[kv_namespaces]]
|
||||||
|
binding = "VELOX_CACHE"
|
||||||
|
id = "your-kv-namespace-id"
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "my-database"
|
||||||
|
database_id = "your-database-id"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
NODE_ENV = "production"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Steps
|
||||||
|
|
||||||
|
### Option 1: Cloudflare Pages Dashboard
|
||||||
|
|
||||||
|
1. Go to [dash.cloudflare.com](https://dash.cloudflare.com) → **Pages** → **Create a project**
|
||||||
|
2. Connect your Git provider and select your repository
|
||||||
|
3. Set build settings:
|
||||||
|
- **Framework preset:** Velox
|
||||||
|
- **Build command:** `npm run build`
|
||||||
|
- **Build output directory:** `.velox/output`
|
||||||
|
4. Add environment variables
|
||||||
|
5. Click **Save and Deploy**
|
||||||
|
|
||||||
|
### Option 2: Wrangler CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Log in
|
||||||
|
npx wrangler login
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Deploy
|
||||||
|
npx wrangler pages deploy .velox/output --project-name my-velox-app
|
||||||
|
|
||||||
|
# Or deploy as a Worker
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Cloudflare D1 (SQLite at the Edge)
|
||||||
|
|
||||||
|
Cloudflare D1 is a serverless SQLite database that runs at the edge alongside your Workers.
|
||||||
|
|
||||||
|
### Create a Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler d1 create my-database
|
||||||
|
npx wrangler d1 execute my-database --file=./migrations/001_init.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query D1 in Velox
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/posts+server.ts
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
// env.DB is automatically injected by the Cloudflare adapter
|
||||||
|
const { DB } = process.env as any;
|
||||||
|
const { results } = await DB.prepare('SELECT * FROM posts WHERE published = 1').all();
|
||||||
|
return Response.json(results);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Drizzle's D1 driver:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { drizzle } from 'drizzle-orm/d1';
|
||||||
|
import * as schema from '$lib/schema';
|
||||||
|
|
||||||
|
export function getDB(env: Env) {
|
||||||
|
return drizzle(env.DB, { schema });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Cloudflare KV
|
||||||
|
|
||||||
|
For key-value storage (feature flags, cached responses):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const value = await env.MY_KV.get('my-key');
|
||||||
|
await env.MY_KV.put('my-key', JSON.stringify(data), { expirationTtl: 3600 });
|
||||||
|
await env.MY_KV.delete('my-key');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Cloudflare R2
|
||||||
|
|
||||||
|
For file uploads and object storage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/upload+server.ts
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
|
||||||
|
export const POST = defineHandler(async (req) => {
|
||||||
|
const formData = await req.formData();
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
|
||||||
|
const key = `uploads/${Date.now()}-${file.name}`;
|
||||||
|
await env.R2_BUCKET.put(key, file.stream(), {
|
||||||
|
httpMetadata: { contentType: file.type },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json({ url: `https://assets.example.com/${key}` });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Domains
|
||||||
|
|
||||||
|
1. Add your domain in Cloudflare's DNS settings (it should already be proxied through Cloudflare)
|
||||||
|
2. Go to Pages → Your project → **Custom domains**
|
||||||
|
3. Click **Set up a custom domain** and enter your domain
|
||||||
|
4. Cloudflare provisions SSL automatically
|
||||||
|
|
||||||
|
For Workers, add a route in `wrangler.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[routes]]
|
||||||
|
pattern = "example.com/*"
|
||||||
|
zone_name = "example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Set secrets securely:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler secret put DATABASE_URL
|
||||||
|
npx wrangler secret put SESSION_SECRET
|
||||||
|
npx wrangler secret put JWT_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-secret variables go in `wrangler.toml` under `[vars]` or in the Pages dashboard.
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
- Use **Cache Rules** in the Cloudflare dashboard to cache SSR pages at the CDN layer
|
||||||
|
- Enable **Argo Smart Routing** for improved global routing
|
||||||
|
- Use **Tiered Cache** to reduce origin requests
|
||||||
|
- Set `Cache-Control: public, max-age=0, s-maxage=3600` on ISR pages to let Cloudflare cache them
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
| Feature | Limit |
|
||||||
|
|---------|-------|
|
||||||
|
| Worker request timeout | 30s (CPU time) |
|
||||||
|
| Worker memory | 128 MB |
|
||||||
|
| KV value size | 25 MB |
|
||||||
|
| D1 database size | 2 GB |
|
||||||
|
| R2 object size | 5 GB |
|
||||||
301
velox-docs/pages/deploy-docker.md
Normal file
301
velox-docs/pages/deploy-docker.md
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
---
|
||||||
|
title: Docker
|
||||||
|
sort: 120
|
||||||
|
section-id: deployment
|
||||||
|
keywords: Docker, Dockerfile, docker-compose, container, containerisation, deployment
|
||||||
|
description: Containerising a Velox application with Docker and docker-compose
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
|
||||||
|
Containerising your Velox application with Docker makes it portable across any infrastructure — from a simple VPS to a Kubernetes cluster. This guide covers building optimised Docker images and running multi-service stacks with docker-compose.
|
||||||
|
|
||||||
|
## Dockerfile
|
||||||
|
|
||||||
|
A production-grade multi-stage Dockerfile:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Stage 1: Install dependencies
|
||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package manifests only — enables Docker layer caching
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --frozen-lockfile
|
||||||
|
|
||||||
|
# Stage 2: Build the application
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build args — pass at build time
|
||||||
|
ARG PUBLIC_BASE_URL
|
||||||
|
ARG NODE_ENV=production
|
||||||
|
ENV PUBLIC_BASE_URL=$PUBLIC_BASE_URL
|
||||||
|
ENV NODE_ENV=$NODE_ENV
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Production runtime
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3700
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup --system --gid 1001 veloxgroup && \
|
||||||
|
adduser --system --uid 1001 veloxuser
|
||||||
|
|
||||||
|
# Copy only production artefacts
|
||||||
|
COPY --from=builder --chown=veloxuser:veloxgroup /app/.velox/output ./
|
||||||
|
COPY --from=builder --chown=veloxuser:veloxgroup /app/package.json ./
|
||||||
|
|
||||||
|
# Install production dependencies only
|
||||||
|
RUN npm ci --omit=dev --frozen-lockfile
|
||||||
|
|
||||||
|
USER veloxuser
|
||||||
|
|
||||||
|
EXPOSE 3700
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build \
|
||||||
|
--build-arg PUBLIC_BASE_URL=https://example.com \
|
||||||
|
-t my-velox-app:latest .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run \
|
||||||
|
--env-file .env.production \
|
||||||
|
-p 3700:3700 \
|
||||||
|
--name velox-app \
|
||||||
|
my-velox-app:latest
|
||||||
|
|
||||||
|
# Run in background
|
||||||
|
docker run -d \
|
||||||
|
--env-file .env.production \
|
||||||
|
-p 3700:3700 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--name velox-app \
|
||||||
|
my-velox-app:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.dockerignore`
|
||||||
|
|
||||||
|
Exclude files from the build context to speed up builds:
|
||||||
|
|
||||||
|
```
|
||||||
|
node_modules
|
||||||
|
.velox
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
tests
|
||||||
|
*.test.*
|
||||||
|
*.spec.*
|
||||||
|
coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## docker-compose
|
||||||
|
|
||||||
|
A complete stack with the app, PostgreSQL, and Redis:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:3700}
|
||||||
|
image: my-velox-app:latest
|
||||||
|
ports:
|
||||||
|
- "3700:3700"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DATABASE_URL: postgresql://velox:${DB_PASSWORD}@db:5432/velox
|
||||||
|
REDIS_URL: redis://redis:6379
|
||||||
|
SESSION_SECRET: ${SESSION_SECRET}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3700/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: velox
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: velox
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U velox"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
- nginx_cache:/var/cache/nginx
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
nginx_cache:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nginx Reverse Proxy
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# nginx/nginx.conf
|
||||||
|
events { worker_connections 1024; }
|
||||||
|
|
||||||
|
http {
|
||||||
|
upstream velox_app {
|
||||||
|
server app:3700;
|
||||||
|
keepalive 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP → HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
# Static assets with long cache
|
||||||
|
location /assets/ {
|
||||||
|
proxy_pass http://velox_app;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Application
|
||||||
|
location / {
|
||||||
|
proxy_pass http://velox_app;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Check Endpoint
|
||||||
|
|
||||||
|
Add a health check to your application for Docker and load balancers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/health+server.ts
|
||||||
|
import { defineHandler, json } from 'velox/server';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async () => {
|
||||||
|
try {
|
||||||
|
await db.$queryRaw`SELECT 1`;
|
||||||
|
return json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
|
} catch (error) {
|
||||||
|
return json({ status: 'error', error: String(error) }, { status: 503 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Database Migrations
|
||||||
|
|
||||||
|
Run migrations as a separate one-shot container before starting the app:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml — add this service
|
||||||
|
migrate:
|
||||||
|
image: my-velox-app:latest
|
||||||
|
command: npx prisma migrate deploy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://velox:${DB_PASSWORD}@db:5432/velox
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: "no"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container Best Practices
|
||||||
|
|
||||||
|
| Practice | Why |
|
||||||
|
|----------|-----|
|
||||||
|
| Multi-stage builds | Reduces final image size by 60–80% |
|
||||||
|
| Non-root user | Limits damage if the container is compromised |
|
||||||
|
| Read-only filesystem | Mount only what needs to be writable |
|
||||||
|
| `--restart unless-stopped` | Survives host reboots |
|
||||||
|
| Resource limits | Prevents a runaway container from affecting neighbours |
|
||||||
|
| Health checks | Enables zero-downtime rolling updates |
|
||||||
|
|
||||||
|
Set resource limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 128M
|
||||||
|
```
|
||||||
275
velox-docs/pages/deploy-self-hosted.md
Normal file
275
velox-docs/pages/deploy-self-hosted.md
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
---
|
||||||
|
title: Self-Hosted
|
||||||
|
sort: 130
|
||||||
|
section-id: deployment
|
||||||
|
keywords: self-hosted, VPS, nginx, PM2, systemd, Linux, server deployment
|
||||||
|
description: Running Velox on a VPS with nginx as a reverse proxy, managed by PM2 or systemd
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Self-Hosted
|
||||||
|
|
||||||
|
Deploying Velox to your own server (a VPS, dedicated server, or on-premises machine) gives you full control over your infrastructure. This guide covers setting up a Linux server with nginx as a reverse proxy, and managing the Node.js process with either PM2 or systemd.
|
||||||
|
|
||||||
|
## Server Preparation
|
||||||
|
|
||||||
|
This guide assumes Ubuntu 24.04 LTS. Adjust package manager commands for other distributions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update the system
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
# Install Node.js (LTS) via nvm
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
|
||||||
|
source ~/.bashrc
|
||||||
|
nvm install 22
|
||||||
|
nvm use 22
|
||||||
|
nvm alias default 22
|
||||||
|
|
||||||
|
# Install nginx
|
||||||
|
sudo apt install -y nginx
|
||||||
|
|
||||||
|
# Install PostgreSQL (if needed)
|
||||||
|
sudo apt install -y postgresql postgresql-contrib
|
||||||
|
|
||||||
|
# Install Redis (if needed)
|
||||||
|
sudo apt install -y redis-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying the Application
|
||||||
|
|
||||||
|
### Option A: Direct File Copy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On your local machine — build the app
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Copy build output to the server
|
||||||
|
rsync -avz --delete .velox/output/ user@your-server.com:/var/www/my-velox-app/
|
||||||
|
rsync -avz package.json package-lock.json user@your-server.com:/var/www/my-velox-app/
|
||||||
|
|
||||||
|
# On the server — install production dependencies
|
||||||
|
cd /var/www/my-velox-app
|
||||||
|
npm ci --omit=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Git + CI/CD
|
||||||
|
|
||||||
|
Create a deploy script on the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# /home/deploy/deploy.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_DIR=/var/www/my-velox-app
|
||||||
|
REPO=https://github.com/your-org/your-repo.git
|
||||||
|
|
||||||
|
cd $APP_DIR
|
||||||
|
|
||||||
|
# Pull latest code
|
||||||
|
git fetch --all
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm ci --frozen-lockfile --omit=dev
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Restart application
|
||||||
|
pm2 reload my-velox-app --update-env
|
||||||
|
|
||||||
|
echo "Deployment complete."
|
||||||
|
```
|
||||||
|
|
||||||
|
Call this from your GitHub Actions workflow:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy.yml
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
username: deploy
|
||||||
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
|
script: /home/deploy/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Store production secrets in `/etc/environment` or a `.env.production` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /etc/environment (system-wide)
|
||||||
|
NODE_ENV=production
|
||||||
|
DATABASE_URL=postgresql://velox:secretpass@localhost:5432/velox
|
||||||
|
SESSION_SECRET=your-long-random-secret-here
|
||||||
|
JWT_SECRET=another-long-random-secret
|
||||||
|
|
||||||
|
# Or in a project-specific .env.production
|
||||||
|
sudo nano /var/www/my-velox-app/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the env file in your start command (covered below).
|
||||||
|
|
||||||
|
## Process Management with PM2
|
||||||
|
|
||||||
|
PM2 is a battle-tested process manager for Node.js applications.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a PM2 ecosystem file:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// /var/www/my-velox-app/ecosystem.config.js
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: 'my-velox-app',
|
||||||
|
script: 'server.js',
|
||||||
|
cwd: '/var/www/my-velox-app',
|
||||||
|
instances: 'max', // one process per CPU core
|
||||||
|
exec_mode: 'cluster', // enable cluster mode for zero-downtime restarts
|
||||||
|
env_file: '/var/www/my-velox-app/.env.production',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
PORT: 3700,
|
||||||
|
},
|
||||||
|
error_file: '/var/log/pm2/my-velox-app-error.log',
|
||||||
|
out_file: '/var/log/pm2/my-velox-app-out.log',
|
||||||
|
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
max_memory_restart: '500M', // restart if memory exceeds 500 MB
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Start and persist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
pm2 save # persist process list across reboots
|
||||||
|
pm2 startup # install systemd startup script
|
||||||
|
|
||||||
|
# Useful commands
|
||||||
|
pm2 status
|
||||||
|
pm2 logs my-velox-app
|
||||||
|
pm2 reload my-velox-app # zero-downtime reload
|
||||||
|
pm2 restart my-velox-app # full restart
|
||||||
|
pm2 monit # real-time monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
## systemd Service (Alternative to PM2)
|
||||||
|
|
||||||
|
For simpler setups, a systemd unit file:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/my-velox-app.service
|
||||||
|
[Unit]
|
||||||
|
Description=My Velox Application
|
||||||
|
After=network.target postgresql.service redis.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/var/www/my-velox-app
|
||||||
|
EnvironmentFile=/var/www/my-velox-app/.env.production
|
||||||
|
ExecStart=/usr/bin/node server.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=my-velox-app
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=65536
|
||||||
|
MemoryMax=512M
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable my-velox-app
|
||||||
|
sudo systemctl start my-velox-app
|
||||||
|
sudo journalctl -u my-velox-app -f # follow logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/my-velox-app
|
||||||
|
upstream velox {
|
||||||
|
server 127.0.0.1:3700;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name example.com www.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain application/json application/javascript text/css;
|
||||||
|
|
||||||
|
# Long-lived cache for hashed static assets
|
||||||
|
location ~ ^/assets/.*\.[0-9a-f]{8}\. {
|
||||||
|
proxy_pass http://velox;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://velox;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /etc/nginx/sites-available/my-velox-app /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSL with Let's Encrypt
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y certbot python3-certbot-nginx
|
||||||
|
sudo certbot --nginx -d example.com -d www.example.com
|
||||||
|
# Certbot automatically renews certificates via a cron job
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw allow OpenSSH
|
||||||
|
sudo ufw allow 'Nginx Full'
|
||||||
|
sudo ufw enable
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
183
velox-docs/pages/deploy-vercel.md
Normal file
183
velox-docs/pages/deploy-vercel.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
---
|
||||||
|
title: Deploy to Vercel
|
||||||
|
sort: 100
|
||||||
|
section-id: deployment
|
||||||
|
keywords: Vercel, deployment, serverless, edge, CI/CD, deploy
|
||||||
|
description: Step-by-step guide to deploying a Velox application to Vercel
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deploy to Vercel
|
||||||
|
|
||||||
|
Vercel is the recommended hosting platform for Velox applications. The Velox Vercel adapter handles all configuration automatically — no manual `vercel.json` setup is required for most projects.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Vercel account ([vercel.com](https://vercel.com))
|
||||||
|
- The Vercel CLI: `npm install -g vercel`
|
||||||
|
- A Velox project committed to a Git repository (GitHub, GitLab, or Bitbucket)
|
||||||
|
|
||||||
|
## Automatic Deployment via Git Integration
|
||||||
|
|
||||||
|
### Step 1 — Push Your Project to Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
git remote add origin https://github.com/your-username/your-project.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Import the Project on Vercel
|
||||||
|
|
||||||
|
1. Go to [vercel.com/new](https://vercel.com/new)
|
||||||
|
2. Click **Add New Project** → **Import Git Repository**
|
||||||
|
3. Select your repository
|
||||||
|
4. Vercel automatically detects Velox and sets the correct build settings:
|
||||||
|
- **Framework Preset:** Velox
|
||||||
|
- **Build Command:** `velox build`
|
||||||
|
- **Output Directory:** `.velox/output`
|
||||||
|
- **Install Command:** `npm ci`
|
||||||
|
|
||||||
|
### Step 3 — Configure Environment Variables
|
||||||
|
|
||||||
|
In the Vercel project dashboard:
|
||||||
|
1. Go to **Settings** → **Environment Variables**
|
||||||
|
2. Add each variable from your `.env.production` file
|
||||||
|
|
||||||
|
Do **not** add `PUBLIC_BASE_URL` manually — Vercel sets `VERCEL_URL` automatically and the Velox Vercel adapter uses it.
|
||||||
|
|
||||||
|
### Step 4 — Deploy
|
||||||
|
|
||||||
|
Click **Deploy**. Vercel will:
|
||||||
|
1. Clone your repository
|
||||||
|
2. Install dependencies
|
||||||
|
3. Run `velox build`
|
||||||
|
4. Deploy to its global edge network
|
||||||
|
|
||||||
|
Your site is live at `https://your-project.vercel.app`.
|
||||||
|
|
||||||
|
## Vercel CLI Deployment
|
||||||
|
|
||||||
|
For manual or scripted deployments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install adapter
|
||||||
|
npm install @velox/vercel
|
||||||
|
|
||||||
|
# Deploy to preview (equivalent to a branch deploy)
|
||||||
|
vercel
|
||||||
|
|
||||||
|
# Deploy to production
|
||||||
|
vercel --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring the Vercel Adapter
|
||||||
|
|
||||||
|
Install and configure `@velox/vercel`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// velox.config.ts
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
import { vercel } from '@velox/vercel';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
adapter: vercel({
|
||||||
|
// Route-level edge function configuration
|
||||||
|
edgeRoutes: ['/api/stream', '/api/realtime'],
|
||||||
|
|
||||||
|
// Override ISR revalidation for specific routes
|
||||||
|
isr: {
|
||||||
|
expiration: 60, // default revalidation (seconds)
|
||||||
|
bypassToken: process.env.VERCEL_ISR_BYPASS_TOKEN,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable Vercel Image Optimisation (uses Vercel's CDN)
|
||||||
|
images: {
|
||||||
|
sizes: [640, 1080, 1920],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edge Functions
|
||||||
|
|
||||||
|
Mark individual routes to run on Vercel Edge instead of Node.js serverless:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/stream+server.ts
|
||||||
|
export const config = { edge: true };
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
// Runs on Vercel Edge — uses Web APIs only
|
||||||
|
return new Response('Hello from edge!');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge functions are deployed globally in ~70 Vercel regions and have ~0ms cold start. Use them for latency-sensitive, stateless handlers.
|
||||||
|
|
||||||
|
## Custom Domains
|
||||||
|
|
||||||
|
1. Go to your project on Vercel → **Settings** → **Domains**
|
||||||
|
2. Click **Add Domain**
|
||||||
|
3. Enter your domain (e.g., `example.com`)
|
||||||
|
4. Follow the DNS configuration instructions:
|
||||||
|
- Add a **CNAME** record: `www` → `cname.vercel-dns.com`
|
||||||
|
- Add an **A** record: `@` → `76.76.21.21`
|
||||||
|
5. SSL certificates are provisioned automatically via Let's Encrypt
|
||||||
|
|
||||||
|
Set the canonical base URL environment variable in Vercel:
|
||||||
|
|
||||||
|
```
|
||||||
|
PUBLIC_BASE_URL = https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preview Deployments
|
||||||
|
|
||||||
|
Every pull request gets an automatic preview deployment at a unique URL (`https://your-project-git-branch-name.vercel.app`). This is configured by default and requires no additional setup.
|
||||||
|
|
||||||
|
To share preview deployments with your team, enable **Password Protection** under **Settings** → **Deployment Protection**.
|
||||||
|
|
||||||
|
## Build Cache
|
||||||
|
|
||||||
|
Velox's Velocitor build cache is automatically persisted across deployments by the Vercel adapter. Subsequent deployments typically build 3–5× faster than the first.
|
||||||
|
|
||||||
|
## `vercel.json` Reference
|
||||||
|
|
||||||
|
For advanced configuration, create a `vercel.json` at your project root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"regions": ["iad1", "fra1"],
|
||||||
|
"cleanUrls": true,
|
||||||
|
"trailingSlash": false,
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "/assets/(.*)",
|
||||||
|
"headers": [
|
||||||
|
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"redirects": [
|
||||||
|
{ "source": "/old-path", "destination": "/new-path", "permanent": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring and Analytics
|
||||||
|
|
||||||
|
Enable Vercel Analytics and Speed Insights in your project dashboard. The Velox adapter hooks into these automatically — no code changes needed.
|
||||||
|
|
||||||
|
For custom event tracking:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { track } from '@vercel/analytics';
|
||||||
|
|
||||||
|
track('button_clicked', { component: 'Hero', variant: 'primary' });
|
||||||
|
```
|
||||||
288
velox-docs/pages/guide-auth.md
Normal file
288
velox-docs/pages/guide-auth.md
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
---
|
||||||
|
title: Authentication
|
||||||
|
sort: 100
|
||||||
|
section-id: guides
|
||||||
|
keywords: authentication, JWT, OAuth2, session, login, auth, security
|
||||||
|
description: Implementing authentication in Velox using JWT, OAuth2, and session-based strategies
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
This guide covers the three most common authentication patterns for Velox applications: session-based auth, JWT-based auth, and OAuth2 with third-party providers.
|
||||||
|
|
||||||
|
## Session-Based Authentication
|
||||||
|
|
||||||
|
Session auth stores user state server-side. The client holds only a signed session cookie. This is the simplest and most secure approach for most web applications.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Install the session plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/session @velox/session-redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RedisSessionStore } from '@velox/session-redis';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
session: {
|
||||||
|
secret: process.env.SESSION_SECRET!,
|
||||||
|
cookieName: 'app.session',
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
store: new RedisSessionStore({ url: process.env.REDIS_URL! }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/auth/login+server.ts
|
||||||
|
import { defineHandler, json, redirect } from 'velox/server';
|
||||||
|
import { useSession } from 'velox/server';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { verifyPassword } from '$lib/crypto';
|
||||||
|
|
||||||
|
export const POST = defineHandler(async (req) => {
|
||||||
|
const { email, password } = await req.json();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return json({ error: 'Email and password are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.users.findByEmail(email);
|
||||||
|
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||||
|
return json({ error: 'Invalid credentials' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await useSession();
|
||||||
|
await session.regenerate(); // prevent session fixation
|
||||||
|
await session.update({ userId: user.id, role: user.role });
|
||||||
|
|
||||||
|
return json({ ok: true, redirectTo: '/dashboard' });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/auth.ts
|
||||||
|
import { defineMiddleware, redirect } from 'velox/server';
|
||||||
|
import { useSession } from 'velox/server';
|
||||||
|
|
||||||
|
const PUBLIC_PATHS = ['/login', '/register', '/api/auth'];
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const pathname = new URL(request.url).pathname;
|
||||||
|
|
||||||
|
if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await useSession<{ userId: string }>();
|
||||||
|
|
||||||
|
if (!session.data.userId) {
|
||||||
|
return redirect(`/login?next=${encodeURIComponent(pathname)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.context.set('userId', session.data.userId);
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Authentication
|
||||||
|
|
||||||
|
JWT is stateless and ideal for API-first applications or mobile/SPA backends.
|
||||||
|
|
||||||
|
### Generating Tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/jwt.ts
|
||||||
|
import { SignJWT, jwtVerify } from 'jose';
|
||||||
|
|
||||||
|
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
|
||||||
|
|
||||||
|
export interface JWTPayload {
|
||||||
|
sub: string; // user ID
|
||||||
|
role: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signToken(userId: string, role: string): Promise<string> {
|
||||||
|
return new SignJWT({ sub: userId, role })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('15m')
|
||||||
|
.sign(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signRefreshToken(userId: string): Promise<string> {
|
||||||
|
return new SignJWT({ sub: userId, type: 'refresh' })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('30d')
|
||||||
|
.sign(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyToken(token: string): Promise<JWTPayload> {
|
||||||
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
return payload as JWTPayload;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login + Refresh Endpoints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/auth/token+server.ts
|
||||||
|
import { defineHandler, json, badRequest, unauthorized } from 'velox/server';
|
||||||
|
import { signToken, signRefreshToken, verifyToken } from '$lib/jwt';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { verifyPassword } from '$lib/crypto';
|
||||||
|
|
||||||
|
export const POST = defineHandler(async (req) => {
|
||||||
|
const { grant_type, email, password, refresh_token } = await req.json();
|
||||||
|
|
||||||
|
if (grant_type === 'password') {
|
||||||
|
const user = await db.users.findByEmail(email);
|
||||||
|
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||||
|
return unauthorized('Invalid credentials');
|
||||||
|
}
|
||||||
|
return json({
|
||||||
|
access_token: await signToken(user.id, user.role),
|
||||||
|
refresh_token: await signRefreshToken(user.id),
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expires_in: 900,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grant_type === 'refresh_token') {
|
||||||
|
const payload = await verifyToken(refresh_token).catch(() => null);
|
||||||
|
if (!payload || payload.type !== 'refresh') {
|
||||||
|
return unauthorized('Invalid refresh token');
|
||||||
|
}
|
||||||
|
const user = await db.users.findById(payload.sub);
|
||||||
|
return json({
|
||||||
|
access_token: await signToken(user.id, user.role),
|
||||||
|
token_type: 'Bearer',
|
||||||
|
expires_in: 900,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return badRequest('Unsupported grant_type');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## OAuth2 with Third-Party Providers
|
||||||
|
|
||||||
|
### Using `@velox/auth`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/auth
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure providers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/auth.ts
|
||||||
|
import { createAuth } from '@velox/auth';
|
||||||
|
|
||||||
|
export const auth = createAuth({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
id: 'github',
|
||||||
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||||
|
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
||||||
|
tokenUrl: 'https://github.com/login/oauth/access_token',
|
||||||
|
userInfoUrl: 'https://api.github.com/user',
|
||||||
|
scopes: ['user:email'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'google',
|
||||||
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
// Google uses OpenID Connect — most fields are auto-discovered
|
||||||
|
},
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
async onUserInfo(provider, userInfo) {
|
||||||
|
const user = await db.users.upsert({
|
||||||
|
where: { email: userInfo.email },
|
||||||
|
create: { email: userInfo.email, name: userInfo.name, provider },
|
||||||
|
update: { name: userInfo.name },
|
||||||
|
});
|
||||||
|
return { id: user.id, role: user.role };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
OAuth callback route:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/auth/[provider]/callback+server.ts
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
return auth.handleCallback(req);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Hashing
|
||||||
|
|
||||||
|
Always use a slow hashing algorithm. Velox recommends Argon2:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/crypto.ts
|
||||||
|
import { hash, verify } from '@node-rs/argon2';
|
||||||
|
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
return hash(password, {
|
||||||
|
memoryCost: 19456,
|
||||||
|
timeCost: 2,
|
||||||
|
outputLen: 32,
|
||||||
|
parallelism: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return verify(hash, password);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Role-Based Access Control
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/rbac.ts
|
||||||
|
import { defineMiddleware, forbidden } from 'velox/server';
|
||||||
|
|
||||||
|
const ROUTE_ROLES: Record<string, string[]> = {
|
||||||
|
'/admin': ['admin'],
|
||||||
|
'/api/admin': ['admin'],
|
||||||
|
'/api/reports': ['admin', 'analyst'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const pathname = new URL(request.url).pathname;
|
||||||
|
const requiredRoles = Object.entries(ROUTE_ROLES)
|
||||||
|
.find(([path]) => pathname.startsWith(path))?.[1];
|
||||||
|
|
||||||
|
if (!requiredRoles) return next();
|
||||||
|
|
||||||
|
const user = request.context.get('user') as { role: string } | undefined;
|
||||||
|
if (!user || !requiredRoles.includes(user.role)) {
|
||||||
|
return forbidden('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
```
|
||||||
285
velox-docs/pages/guide-database.md
Normal file
285
velox-docs/pages/guide-database.md
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
---
|
||||||
|
title: Database Integration
|
||||||
|
sort: 110
|
||||||
|
section-id: guides
|
||||||
|
keywords: database, Prisma, DrizzleORM, SQL, ORM, PostgreSQL, database integration
|
||||||
|
description: How to integrate databases in Velox using Prisma, DrizzleORM, or raw SQL
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Database Integration
|
||||||
|
|
||||||
|
Velox is database-agnostic. This guide covers the three most popular approaches: Prisma (full-featured ORM), DrizzleORM (lightweight TypeScript-first ORM), and raw SQL with a typed query builder.
|
||||||
|
|
||||||
|
## Prisma
|
||||||
|
|
||||||
|
Prisma is the most popular ORM in the Node.js ecosystem. It provides a schema-first approach, auto-generated type-safe client, and powerful migrations.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install prisma @prisma/client
|
||||||
|
npx prisma init
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a `prisma/schema.prisma` file. Example schema:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
// prisma/schema.prisma
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
role Role @default(USER)
|
||||||
|
posts Post[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String
|
||||||
|
slug String @unique
|
||||||
|
content String
|
||||||
|
published Boolean @default(false)
|
||||||
|
author User @relation(fields: [authorId], references: [id])
|
||||||
|
authorId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
USER
|
||||||
|
ADMIN
|
||||||
|
ANALYST
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate and run the initial migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name init
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Client Singleton
|
||||||
|
|
||||||
|
In a server environment, always reuse a single Prisma client instance:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/db.ts
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||||
|
|
||||||
|
export const db = globalForPrisma.prisma ?? new PrismaClient({
|
||||||
|
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage in Routes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/blog/[slug].velox server block
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
const post = await db.post.findUnique({
|
||||||
|
where: { slug: params.slug, published: true },
|
||||||
|
include: { author: { select: { name: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!post) throw notFound();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await db.$transaction(async (tx) => {
|
||||||
|
const user = await tx.user.create({ data: { email, name } });
|
||||||
|
const profile = await tx.profile.create({ data: { userId: user.id } });
|
||||||
|
return { user, profile };
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## DrizzleORM
|
||||||
|
|
||||||
|
DrizzleORM is a TypeScript-first ORM with a SQL-like query API and zero overhead.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install drizzle-orm postgres
|
||||||
|
npm install -D drizzle-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
Define your schema in TypeScript:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/schema.ts
|
||||||
|
import { pgTable, text, boolean, timestamp, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export const roleEnum = pgEnum('role', ['user', 'admin', 'analyst']);
|
||||||
|
|
||||||
|
export const users = pgTable('users', {
|
||||||
|
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
email: text('email').notNull().unique(),
|
||||||
|
name: text('name'),
|
||||||
|
role: roleEnum('role').default('user').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const posts = pgTable('posts', {
|
||||||
|
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
slug: text('slug').notNull().unique(),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
published: boolean('published').default(false).notNull(),
|
||||||
|
authorId: text('author_id').notNull().references(() => users.id),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/db.ts
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
import * as schema from './schema';
|
||||||
|
|
||||||
|
const client = postgres(process.env.DATABASE_URL!);
|
||||||
|
export const db = drizzle(client, { schema });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { posts, users } from '$lib/schema';
|
||||||
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// Find one
|
||||||
|
const post = await db.query.posts.findFirst({
|
||||||
|
where: and(eq(posts.slug, slug), eq(posts.published, true)),
|
||||||
|
with: { author: { columns: { name: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find many with ordering
|
||||||
|
const recentPosts = await db
|
||||||
|
.select()
|
||||||
|
.from(posts)
|
||||||
|
.where(eq(posts.published, true))
|
||||||
|
.orderBy(desc(posts.createdAt))
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
const [newPost] = await db
|
||||||
|
.insert(posts)
|
||||||
|
.values({ title, slug, content, authorId })
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await db
|
||||||
|
.update(posts)
|
||||||
|
.set({ published: true, updatedAt: new Date() })
|
||||||
|
.where(eq(posts.id, postId));
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await db.delete(posts).where(eq(posts.id, postId));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrations
|
||||||
|
|
||||||
|
Configure Drizzle Kit:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// drizzle.config.ts
|
||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './lib/schema.ts',
|
||||||
|
out: './drizzle',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: { url: process.env.DATABASE_URL! },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx drizzle-kit generate
|
||||||
|
npx drizzle-kit migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Raw SQL with `postgres.js`
|
||||||
|
|
||||||
|
For maximum control and performance with complex queries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/db.ts
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
export const sql = postgres(process.env.DATABASE_URL!, {
|
||||||
|
max: 20, // connection pool size
|
||||||
|
idle_timeout: 20, // seconds before idle connection closes
|
||||||
|
connect_timeout: 10, // connection timeout
|
||||||
|
types: {
|
||||||
|
bigint: postgres.BigInt, // return BigInt instead of string
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { sql } from '$lib/db';
|
||||||
|
|
||||||
|
// Parameterised query (safe from SQL injection)
|
||||||
|
const users = await sql<User[]>`
|
||||||
|
SELECT id, email, name, role
|
||||||
|
FROM users
|
||||||
|
WHERE role = ${role}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Transaction
|
||||||
|
const result = await sql.begin(async (sql) => {
|
||||||
|
const [user] = await sql`
|
||||||
|
INSERT INTO users (email, name)
|
||||||
|
VALUES (${email}, ${name})
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
await sql`
|
||||||
|
INSERT INTO audit_log (user_id, action)
|
||||||
|
VALUES (${user.id}, 'register')
|
||||||
|
`;
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Pooling in Production
|
||||||
|
|
||||||
|
For serverless or edge deployments, use a connection pooler:
|
||||||
|
|
||||||
|
- **PgBouncer** — a lightweight PostgreSQL connection pooler for VPS deployments
|
||||||
|
- **Supabase Supavisor** — serverless-aware pooler built for transactional workloads
|
||||||
|
- **Neon / PlanetScale** — managed databases with built-in HTTP-based connection pooling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// For serverless (e.g. Vercel, Cloudflare) — use HTTP-based driver
|
||||||
|
import { neon } from '@neondatabase/serverless';
|
||||||
|
|
||||||
|
const sql = neon(process.env.DATABASE_URL!);
|
||||||
|
const users = await sql`SELECT * FROM users LIMIT 10`;
|
||||||
|
```
|
||||||
261
velox-docs/pages/guide-i18n.md
Normal file
261
velox-docs/pages/guide-i18n.md
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
---
|
||||||
|
title: Internationalisation
|
||||||
|
sort: 130
|
||||||
|
section-id: guides
|
||||||
|
keywords: i18n, internationalisation, localisation, translation, locale routing, multilingual
|
||||||
|
description: Setting up internationalisation in Velox — translation files, locale routing, and pluralisation
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Internationalisation
|
||||||
|
|
||||||
|
Velox has built-in internationalisation (i18n) support through `@velox/i18n`. It handles locale detection, URL-based locale routing, typed translation files, and pluralisation.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @velox/i18n
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
i18n: {
|
||||||
|
locales: ['en', 'fr', 'de', 'ja'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
routing: 'prefix-except-default',
|
||||||
|
// 'prefix': all locales get a prefix (/en, /fr, /de, /ja)
|
||||||
|
// 'prefix-except-default': default locale has no prefix
|
||||||
|
// 'domain': different domains per locale
|
||||||
|
messagesDir: 'messages',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translation Files
|
||||||
|
|
||||||
|
Create a `messages/` directory at your project root:
|
||||||
|
|
||||||
|
```
|
||||||
|
messages/
|
||||||
|
├── en.json
|
||||||
|
├── fr.json
|
||||||
|
├── de.json
|
||||||
|
└── ja.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation files use a flat or nested key structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// messages/en.json
|
||||||
|
{
|
||||||
|
"nav.home": "Home",
|
||||||
|
"nav.blog": "Blog",
|
||||||
|
"nav.about": "About",
|
||||||
|
"home.hero.title": "Build faster with Velox",
|
||||||
|
"home.hero.subtitle": "The TypeScript framework for the modern web",
|
||||||
|
"home.cta": "Get started",
|
||||||
|
"post.readMore": "Read more",
|
||||||
|
"post.publishedOn": "Published on {date}",
|
||||||
|
"post.comments": "{count, plural, =0{No comments} =1{1 comment} other{# comments}}",
|
||||||
|
"auth.loginButton": "Log in",
|
||||||
|
"auth.logoutButton": "Log out",
|
||||||
|
"errors.notFound": "Page not found",
|
||||||
|
"errors.serverError": "Something went wrong"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// messages/fr.json
|
||||||
|
{
|
||||||
|
"nav.home": "Accueil",
|
||||||
|
"nav.blog": "Blog",
|
||||||
|
"nav.about": "À propos",
|
||||||
|
"home.hero.title": "Construisez plus vite avec Velox",
|
||||||
|
"home.hero.subtitle": "Le framework TypeScript pour le web moderne",
|
||||||
|
"home.cta": "Commencer",
|
||||||
|
"post.readMore": "Lire la suite",
|
||||||
|
"post.publishedOn": "Publié le {date}",
|
||||||
|
"post.comments": "{count, plural, =0{Aucun commentaire} =1{1 commentaire} other{# commentaires}}",
|
||||||
|
"auth.loginButton": "Se connecter",
|
||||||
|
"auth.logoutButton": "Se déconnecter",
|
||||||
|
"errors.notFound": "Page introuvable",
|
||||||
|
"errors.serverError": "Une erreur est survenue"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Translations
|
||||||
|
|
||||||
|
### In Server Blocks
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import { useTranslations } from 'velox/i18n';
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
const locale = useLocale().locale;
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>{t('home.hero.title')}</h1>
|
||||||
|
<p>{t('home.hero.subtitle')}</p>
|
||||||
|
<a href="/docs">{t('home.cta')}</a>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Parameters
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
const t = useTranslations();
|
||||||
|
const publishedDate = new Intl.DateTimeFormat(locale).format(post.createdAt);
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>{t('post.publishedOn', { date: publishedDate })}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pluralisation
|
||||||
|
|
||||||
|
Velox uses the ICU message format for pluralisation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
t('post.comments', { count: 0 }); // "No comments"
|
||||||
|
t('post.comments', { count: 1 }); // "1 comment"
|
||||||
|
t('post.comments', { count: 42 }); // "42 comments"
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Client Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useTranslations } from 'velox/i18n/client';
|
||||||
|
|
||||||
|
export default function LikeButton({ postId }: { postId: string }) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const liked = signal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={() => liked.value = !liked.value}>
|
||||||
|
{liked.value ? t('post.liked') : t('post.like')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Locale Routing
|
||||||
|
|
||||||
|
With `routing: 'prefix-except-default'` and `defaultLocale: 'en'`:
|
||||||
|
|
||||||
|
| URL | Locale |
|
||||||
|
|-----|--------|
|
||||||
|
| `/` | `en` |
|
||||||
|
| `/about` | `en` |
|
||||||
|
| `/fr` | `fr` |
|
||||||
|
| `/fr/about` | `fr` |
|
||||||
|
| `/de/blog/my-post` | `de` |
|
||||||
|
|
||||||
|
Velox automatically generates alternate hreflang links for SEO.
|
||||||
|
|
||||||
|
## Locale Switcher Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useLocale, usePathname } from 'velox/i18n/client';
|
||||||
|
|
||||||
|
export default function LocaleSwitcher() {
|
||||||
|
const { locale, locales } = useLocale();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="locale-switcher">
|
||||||
|
{locales.map(loc => (
|
||||||
|
<a
|
||||||
|
key={loc}
|
||||||
|
href={getLocalizedPath(pathname.value, loc)}
|
||||||
|
class={loc === locale ? 'active' : ''}
|
||||||
|
hreflang={loc}
|
||||||
|
>
|
||||||
|
{getLocaleLabel(loc)}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocaleLabel(locale: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
en: 'English',
|
||||||
|
fr: 'Français',
|
||||||
|
de: 'Deutsch',
|
||||||
|
ja: '日本語',
|
||||||
|
};
|
||||||
|
return labels[locale] ?? locale;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain-Based Routing
|
||||||
|
|
||||||
|
For country-specific top-level domains:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default defineConfig({
|
||||||
|
i18n: {
|
||||||
|
locales: ['en', 'fr', 'de'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
routing: 'domain',
|
||||||
|
domains: {
|
||||||
|
en: 'example.com',
|
||||||
|
fr: 'example.fr',
|
||||||
|
de: 'example.de',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type-Safe Translation Keys
|
||||||
|
|
||||||
|
Generate a TypeScript type for your translation keys:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx velox i18n:generate-types
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates `.velox/types/i18n.d.ts` so that `t('invalid.key')` is a compile-time error.
|
||||||
|
|
||||||
|
## RTL Languages
|
||||||
|
|
||||||
|
For right-to-left languages (Arabic, Hebrew, etc.), add the `dir` attribute dynamically:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// layouts/default.velox
|
||||||
|
---
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const isRTL = ['ar', 'he', 'fa'].includes(locale);
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang={locale} dir={isRTL ? 'rtl' : 'ltr'}>
|
||||||
|
...
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date, Number, and Currency Formatting
|
||||||
|
|
||||||
|
Use the `Intl` APIs with the current locale:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useLocale } from 'velox/i18n';
|
||||||
|
|
||||||
|
const { locale } = useLocale();
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat(locale, { dateStyle: 'long' });
|
||||||
|
const formatted = dateFormatter.format(new Date(post.createdAt));
|
||||||
|
|
||||||
|
// Currency
|
||||||
|
const priceFormatter = new Intl.NumberFormat(locale, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
});
|
||||||
|
const price = priceFormatter.format(product.price);
|
||||||
|
```
|
||||||
283
velox-docs/pages/guide-performance.md
Normal file
283
velox-docs/pages/guide-performance.md
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
---
|
||||||
|
title: Performance
|
||||||
|
sort: 140
|
||||||
|
section-id: guides
|
||||||
|
keywords: performance, code splitting, lazy loading, caching, optimisation, Core Web Vitals
|
||||||
|
description: Performance optimisation strategies for Velox apps — code splitting, lazy loading, and caching
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Performance
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Velox is designed to be fast by default. The Rust-based compiler handles tree-shaking, code splitting, and asset optimisation automatically. This guide covers the additional strategies you can apply to push your application to peak performance.
|
||||||
|
|
||||||
|
## Core Web Vitals Targets
|
||||||
|
|
||||||
|
Before optimising, establish baselines. Aim for:
|
||||||
|
|
||||||
|
| Metric | Good | Needs Work |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| LCP (Largest Contentful Paint) | ≤ 2.5s | > 4.0s |
|
||||||
|
| FID / INP (Interaction to Next Paint) | ≤ 200ms | > 500ms |
|
||||||
|
| CLS (Cumulative Layout Shift) | ≤ 0.1 | > 0.25 |
|
||||||
|
| TTFB (Time to First Byte) | ≤ 800ms | > 1800ms |
|
||||||
|
|
||||||
|
Use Velox's built-in analytics to monitor these in production:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// velox.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
analytics: {
|
||||||
|
webVitals: true,
|
||||||
|
endpoint: '/api/analytics/vitals',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Splitting
|
||||||
|
|
||||||
|
Velox automatically splits your JavaScript bundle by route. Every route gets its own chunk, and shared code is extracted into a common chunk. You do not need to configure this.
|
||||||
|
|
||||||
|
For further control, use dynamic imports:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Only loads when the user clicks "Open"
|
||||||
|
const HeavyEditor = lazy(() => import('./HeavyEditor'));
|
||||||
|
|
||||||
|
function PostPage() {
|
||||||
|
const isEditing = signal(false);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
{isEditing.value && (
|
||||||
|
<Suspense fallback={<Skeleton />}>
|
||||||
|
<HeavyEditor post={post} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
<button onClick={() => isEditing.value = true}>Edit</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lazy Hydration
|
||||||
|
|
||||||
|
Delay hydration of interactive components until they are needed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<!-- Hydrate only when the component scrolls into view -->
|
||||||
|
<CommentSection client:visible postId={post.id} />
|
||||||
|
|
||||||
|
<!-- Hydrate only when the browser is idle -->
|
||||||
|
<AnalyticsWidget client:idle />
|
||||||
|
|
||||||
|
<!-- Hydrate only when a CSS media query matches -->
|
||||||
|
<MobileNav client:media="(max-width: 768px)" />
|
||||||
|
|
||||||
|
<!-- Never hydrate — purely server-rendered, no JS -->
|
||||||
|
<StaticSidebar />
|
||||||
|
```
|
||||||
|
|
||||||
|
The less JavaScript you ship to the client, the better the INP score.
|
||||||
|
|
||||||
|
## Image Optimisation
|
||||||
|
|
||||||
|
Enable image optimisation in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
assets: {
|
||||||
|
imageOptimisation: {
|
||||||
|
enabled: true,
|
||||||
|
formats: ['webp', 'avif'],
|
||||||
|
quality: 85,
|
||||||
|
maxWidth: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `<Image>` component for automatic width/height and format negotiation:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Image } from 'velox';
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src="/assets/images/hero.jpg"
|
||||||
|
alt="Hero image"
|
||||||
|
width={1200}
|
||||||
|
height={400}
|
||||||
|
priority // preload this image (use for above-the-fold images)
|
||||||
|
sizes="(max-width: 768px) 100vw, 1200px"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `priority` prop adds a `<link rel="preload">` tag to the document head and marks the image as `fetchpriority="high"`, which is the single biggest LCP improvement for image-heavy pages.
|
||||||
|
|
||||||
|
## HTTP Caching
|
||||||
|
|
||||||
|
Set appropriate `Cache-Control` headers for your routes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In a route server block
|
||||||
|
response.headers.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
|
||||||
|
|
||||||
|
// Or via config for specific paths
|
||||||
|
export default defineConfig({
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
source: '/assets/*',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/',
|
||||||
|
headers: [
|
||||||
|
{ key: 'Cache-Control', value: 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server-Side Caching
|
||||||
|
|
||||||
|
Cache expensive computations and database queries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useCache } from 'velox/server';
|
||||||
|
|
||||||
|
const cache = useCache();
|
||||||
|
|
||||||
|
async function getPopularPosts() {
|
||||||
|
const cached = await cache.get<Post[]>('posts:popular');
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const posts = await db.posts.findMany({
|
||||||
|
where: { published: true },
|
||||||
|
orderBy: { viewCount: 'desc' },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cache.set('posts:popular', posts, { ttl: 300 }); // 5 minutes
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Query Optimisation
|
||||||
|
|
||||||
|
Common patterns that prevent N+1 queries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad: N+1 query
|
||||||
|
const posts = await db.post.findMany();
|
||||||
|
const postsWithAuthors = await Promise.all(
|
||||||
|
posts.map(p => db.user.findById(p.authorId)) // N extra queries!
|
||||||
|
);
|
||||||
|
|
||||||
|
// Good: single query with include
|
||||||
|
const postsWithAuthors = await db.post.findMany({
|
||||||
|
include: { author: { select: { id: true, name: true } } },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add indexes for commonly filtered columns:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- In a migration
|
||||||
|
CREATE INDEX CONCURRENTLY idx_posts_published_created
|
||||||
|
ON posts (published, created_at DESC)
|
||||||
|
WHERE published = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edge Deployment
|
||||||
|
|
||||||
|
Deploy to edge locations close to your users:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// velox.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'edge',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge-compatible routes must use Web APIs only:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Edge-compatible
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
const data = await fetch('https://api.example.com/data');
|
||||||
|
return Response.json(await data.json());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ Not edge-compatible (uses Node.js built-ins)
|
||||||
|
import fs from 'node:fs';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bundle Analysis
|
||||||
|
|
||||||
|
Generate and review a bundle analysis report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANALYZE=1 npm run build
|
||||||
|
# Opens .velox/output/analyze.html in your browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
- Unexpectedly large dependencies (replace with smaller alternatives)
|
||||||
|
- Duplicate dependencies at different versions
|
||||||
|
- Client-side imports of server-only packages
|
||||||
|
|
||||||
|
## Prefetching
|
||||||
|
|
||||||
|
Configure link prefetching globally:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default defineConfig({
|
||||||
|
prefetch: {
|
||||||
|
defaultStrategy: 'hover', // 'hover' | 'viewport' | false
|
||||||
|
concurrency: 2, // max concurrent prefetch requests
|
||||||
|
ignore: ['/admin/*', '/api/*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Font Loading
|
||||||
|
|
||||||
|
Avoid layout shift from font loading:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* styles/global.css */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('/fonts/inter-var.woff2') format('woff2');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap; /* show fallback text immediately */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// layouts/default.velox
|
||||||
|
<Head>
|
||||||
|
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossOrigin="" />
|
||||||
|
</Head>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Monitoring
|
||||||
|
|
||||||
|
Integrate with monitoring services:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/monitoring.ts
|
||||||
|
export function reportWebVitals({ name, value, id }: Metric) {
|
||||||
|
fetch('/api/analytics/vitals', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name, value, id, page: window.location.pathname }),
|
||||||
|
keepalive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
327
velox-docs/pages/guide-testing.md
Normal file
327
velox-docs/pages/guide-testing.md
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
---
|
||||||
|
title: Testing
|
||||||
|
sort: 120
|
||||||
|
section-id: guides
|
||||||
|
keywords: testing, unit tests, integration tests, E2E, Playwright, Vitest, testing strategy
|
||||||
|
description: Testing Velox applications with unit tests, integration tests, and end-to-end tests using Playwright
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
Velox integrates with Vitest for unit and integration tests, and Playwright for end-to-end tests. The `@velox/test` package provides additional test utilities tailored for Velox's server blocks and API routes.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -D vitest @velox/test @playwright/test
|
||||||
|
npx playwright install
|
||||||
|
```
|
||||||
|
|
||||||
|
Add test scripts to `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:coverage": "vitest --coverage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure Vitest:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vitest.config.ts
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import { veloxTestPlugin } from '@velox/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [veloxTestPlugin()],
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ['./tests/setup.ts'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'lcov'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit Tests
|
||||||
|
|
||||||
|
### Testing Utility Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/formatters.ts
|
||||||
|
export function formatCurrency(amount: number, currency = 'USD'): string {
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/unit/formatters.test.ts
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { formatCurrency } from '$lib/formatters';
|
||||||
|
|
||||||
|
describe('formatCurrency', () => {
|
||||||
|
it('formats USD amounts', () => {
|
||||||
|
expect(formatCurrency(1000)).toBe('$1,000.00');
|
||||||
|
expect(formatCurrency(9.99)).toBe('$9.99');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats other currencies', () => {
|
||||||
|
expect(formatCurrency(1000, 'EUR')).toBe('€1,000.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles zero', () => {
|
||||||
|
expect(formatCurrency(0)).toBe('$0.00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// tests/unit/Counter.test.tsx
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { render, fireEvent } from '@velox/test';
|
||||||
|
import Counter from '$components/Counter';
|
||||||
|
|
||||||
|
describe('Counter', () => {
|
||||||
|
it('renders with initial value', () => {
|
||||||
|
const { getByText } = render(<Counter initialValue={5} />);
|
||||||
|
expect(getByText('5')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments on button click', async () => {
|
||||||
|
const { getByText, getByRole } = render(<Counter initialValue={0} />);
|
||||||
|
await fireEvent.click(getByRole('button', { name: /increment/i }));
|
||||||
|
expect(getByText('1')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrements below initial value', async () => {
|
||||||
|
const { getByText, getByRole } = render(<Counter initialValue={3} />);
|
||||||
|
await fireEvent.click(getByRole('button', { name: /decrement/i }));
|
||||||
|
expect(getByText('2')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Testing API Routes
|
||||||
|
|
||||||
|
The `createTestServer` utility starts a real Velox server for integration testing:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/integration/api/users.test.ts
|
||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import { createTestServer } from '@velox/test';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
let server: Awaited<ReturnType<typeof createTestServer>>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = await createTestServer({ seed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await server.stop();
|
||||||
|
await db.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/users', () => {
|
||||||
|
it('returns all users', async () => {
|
||||||
|
const res = await server.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/users',
|
||||||
|
headers: { Authorization: `Bearer ${server.getAdminToken()}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 401 without auth', async () => {
|
||||||
|
const res = await server.inject({ method: 'GET', url: '/api/users' });
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/users', () => {
|
||||||
|
it('creates a new user', async () => {
|
||||||
|
const res = await server.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/users',
|
||||||
|
body: { email: 'new@example.com', name: 'New User' },
|
||||||
|
headers: { Authorization: `Bearer ${server.getAdminToken()}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(201);
|
||||||
|
const user = await res.json();
|
||||||
|
expect(user.email).toBe('new@example.com');
|
||||||
|
expect(user.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects duplicate email', async () => {
|
||||||
|
await server.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/users',
|
||||||
|
body: { email: 'dup@example.com', name: 'First' },
|
||||||
|
headers: { Authorization: `Bearer ${server.getAdminToken()}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/users',
|
||||||
|
body: { email: 'dup@example.com', name: 'Second' },
|
||||||
|
headers: { Authorization: `Bearer ${server.getAdminToken()}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(409);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Database Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/integration/db/posts.test.ts
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { createTestUser, createTestPost } from '../factories';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.$executeRaw`TRUNCATE posts, users RESTART IDENTITY CASCADE`;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Post queries', () => {
|
||||||
|
it('finds published posts only', async () => {
|
||||||
|
const user = await createTestUser();
|
||||||
|
await createTestPost({ authorId: user.id, published: true });
|
||||||
|
await createTestPost({ authorId: user.id, published: false });
|
||||||
|
|
||||||
|
const posts = await db.post.findMany({ where: { published: true } });
|
||||||
|
expect(posts).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## End-to-End Tests with Playwright
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3700',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:3700',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/e2e/auth.spec.ts
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Authentication', () => {
|
||||||
|
test('user can log in', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.getByLabel('Email').fill('test@example.com');
|
||||||
|
await page.getByLabel('Password').fill('correct-password');
|
||||||
|
await page.getByRole('button', { name: 'Log in' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL('/dashboard');
|
||||||
|
await expect(page.getByText('Welcome back')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows error for invalid credentials', async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.getByLabel('Email').fill('test@example.com');
|
||||||
|
await page.getByLabel('Password').fill('wrong-password');
|
||||||
|
await page.getByRole('button', { name: 'Log in' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('alert')).toContainText('Invalid credentials');
|
||||||
|
await expect(page).toHaveURL('/login');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Factories
|
||||||
|
|
||||||
|
Use factories to generate test data consistently:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/factories.ts
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
import { hashPassword } from '$lib/crypto';
|
||||||
|
|
||||||
|
export async function createTestUser(overrides = {}) {
|
||||||
|
return db.user.create({
|
||||||
|
data: {
|
||||||
|
email: `user-${Date.now()}@example.com`,
|
||||||
|
name: 'Test User',
|
||||||
|
passwordHash: await hashPassword('test-password'),
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestPost(overrides = {}) {
|
||||||
|
return db.post.create({
|
||||||
|
data: {
|
||||||
|
title: 'Test Post',
|
||||||
|
slug: `test-post-${Date.now()}`,
|
||||||
|
content: 'Test content',
|
||||||
|
published: true,
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/test.yml
|
||||||
|
name: Tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: test
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with: { node-version: '22' }
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx prisma migrate deploy
|
||||||
|
env:
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@localhost/test
|
||||||
|
- run: npm test
|
||||||
|
- run: npx playwright install --with-deps
|
||||||
|
- run: npm run test:e2e
|
||||||
|
```
|
||||||
93
velox-docs/pages/index.md
Normal file
93
velox-docs/pages/index.md
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
sort: 100
|
||||||
|
section-id: getting-started
|
||||||
|
keywords: velox, framework, typescript, javascript, full-stack, introduction
|
||||||
|
description: An introduction to Velox, the high-performance TypeScript web framework
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introduction to Velox
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Velox is a high-performance, full-stack TypeScript and JavaScript web framework built for developers who refuse to compromise between developer experience and production performance. Combining a file-based routing model inspired by the best parts of Next.js, an island-based rendering architecture inspired by Astro, and a Rust-powered build toolchain that makes cold starts and hot reloads feel instantaneous, Velox occupies a unique position in the modern web ecosystem.
|
||||||
|
|
||||||
|
## Why Velox?
|
||||||
|
|
||||||
|
The JavaScript ecosystem is rich, yet fragmented. Some frameworks offer outstanding developer experience but struggle with raw performance at scale. Others are extremely fast but require significant configuration overhead before you can write your first component. Velox was created to close that gap.
|
||||||
|
|
||||||
|
The core philosophy of Velox can be summarised in three words: **fast by default**. Every architectural decision — from the Rust-based bundler (Velocitor) to the zero-runtime component model — is made to ensure that your production application runs as efficiently as possible without requiring manual intervention from the developer.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Rust-Powered Toolchain (Velocitor)
|
||||||
|
|
||||||
|
The Velox build system, internally called Velocitor, is written in Rust. It handles TypeScript transpilation, module bundling, tree-shaking, code splitting, and asset optimisation in a single pass. On modern hardware, a full production build of a medium-sized application typically completes in under three seconds. Incremental rebuilds during development are sub-50ms for most file changes.
|
||||||
|
|
||||||
|
### File-Based Routing
|
||||||
|
|
||||||
|
Velox uses a file-based routing system. Any `.velox` or `.tsx` file placed under the `routes/` directory automatically becomes a route. Dynamic segments use `[param]` notation, optional segments use `[[param]]`, and catch-all routes use `[...rest]`. No manual route registration is ever required.
|
||||||
|
|
||||||
|
### Islands Architecture
|
||||||
|
|
||||||
|
By default, Velox renders pages entirely on the server. Interactive components are "islands" — explicitly opt-in to client-side hydration using the `client:*` directive. This means your pages ship zero JavaScript by default; you add interactivity exactly where it is needed.
|
||||||
|
|
||||||
|
### Hybrid Rendering Modes
|
||||||
|
|
||||||
|
Velox supports four rendering strategies per route:
|
||||||
|
- **SSR (Server-Side Rendering)** — rendered fresh on every request
|
||||||
|
- **SSG (Static Site Generation)** — pre-rendered at build time
|
||||||
|
- **ISR (Incremental Static Regeneration)** — statically generated but revalidated on a schedule
|
||||||
|
- **CSR (Client-Side Rendering)** — fully client-rendered for dashboard-style pages
|
||||||
|
|
||||||
|
You can mix rendering modes across routes within a single project.
|
||||||
|
|
||||||
|
### TypeScript First
|
||||||
|
|
||||||
|
Velox is written in TypeScript and treats TypeScript as a first-class citizen. Configuration files, route handlers, middleware, and components all benefit from full type inference. There is no separate type-generation step required.
|
||||||
|
|
||||||
|
### Edge-Ready
|
||||||
|
|
||||||
|
Velox applications are deployable to Cloudflare Workers, Vercel Edge, and similar runtimes out of the box. The framework's core HTTP runtime has no Node.js-specific dependencies and operates on standard Web APIs (`Request`, `Response`, `fetch`, `crypto`), making edge deployment trivially simple.
|
||||||
|
|
||||||
|
### Built-In Middleware System
|
||||||
|
|
||||||
|
The middleware system allows you to intercept and transform requests and responses at multiple points in the pipeline. Auth, rate limiting, logging, CORS, and header manipulation are all achievable through composable middleware functions.
|
||||||
|
|
||||||
|
## How Velox Compares
|
||||||
|
|
||||||
|
| Feature | Velox | Next.js | Astro | Remix |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Build system | Rust (Velocitor) | Webpack/Turbopack | Vite | Vite |
|
||||||
|
| Default JS shipped | Zero | Varies | Zero | Varies |
|
||||||
|
| Rendering modes | SSR/SSG/ISR/CSR | SSR/SSG/ISR | SSG/SSR | SSR |
|
||||||
|
| Full-stack API routes | Yes | Yes | Yes | Yes |
|
||||||
|
| File-based routing | Yes | Yes | Yes | No |
|
||||||
|
| TypeScript support | First-class | First-class | First-class | First-class |
|
||||||
|
| Edge runtime | Native | Adapter | Adapter | Adapter |
|
||||||
|
|
||||||
|
## Who Is Velox For?
|
||||||
|
|
||||||
|
Velox is well suited for:
|
||||||
|
|
||||||
|
- Teams building content-heavy marketing sites that need fast initial load times with selective interactivity
|
||||||
|
- Full-stack application teams who want a unified framework for frontend and backend
|
||||||
|
- Engineers at high-scale companies who need ISR and edge caching to serve global audiences
|
||||||
|
- Developers migrating from Next.js who want faster build times without giving up the Next.js mental model
|
||||||
|
|
||||||
|
Velox is perhaps not the best choice if you are building a highly stateful single-page application that is mostly client-rendered — in that case a pure SPA framework may be simpler.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The quickest way to get started with Velox is to install the CLI and scaffold a new project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create velox@latest my-app
|
||||||
|
cd my-app
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your new project will be running at `http://localhost:3700` in under ten seconds.
|
||||||
|
|
||||||
|
Read on to the [Installation](installation.md) guide for full setup instructions, or jump directly to the [Quick Start](quick-start.md) if you prefer to learn by doing.
|
||||||
194
velox-docs/pages/installation.md
Normal file
194
velox-docs/pages/installation.md
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
sort: 110
|
||||||
|
section-id: getting-started
|
||||||
|
keywords: install, setup, npm, yarn, pnpm, bun, node, requirements
|
||||||
|
description: How to install Velox and set up your development environment
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
This page covers everything you need to install Velox and get your development environment ready to build production applications.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
Before installing Velox, ensure your system meets the following requirements:
|
||||||
|
|
||||||
|
| Requirement | Minimum Version | Recommended |
|
||||||
|
|---|---|---|
|
||||||
|
| Node.js | 20.0.0 | 22.x LTS |
|
||||||
|
| npm | 9.0.0 | 10.x |
|
||||||
|
| Operating system | macOS 12, Ubuntu 22.04, Windows 10 | Latest stable |
|
||||||
|
| RAM | 2 GB | 8 GB+ |
|
||||||
|
| Disk space | 500 MB | 2 GB (for caches) |
|
||||||
|
|
||||||
|
Velox's Rust-based build system (Velocitor) ships as a pre-compiled binary for macOS (arm64 + x86_64), Linux (x86_64 + aarch64), and Windows (x86_64). You do **not** need a Rust toolchain installed on your machine.
|
||||||
|
|
||||||
|
## Installing the Velox CLI
|
||||||
|
|
||||||
|
The Velox CLI (`velox`) is the primary tool for scaffolding projects, running the development server, and triggering builds. Install it globally with your preferred package manager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install -g velox-cli
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn global add velox-cli
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm add -g velox-cli
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun add -g velox-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, verify the CLI is available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
velox --version
|
||||||
|
# velox-cli v1.4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a New Project
|
||||||
|
|
||||||
|
### Using `create-velox` (Recommended)
|
||||||
|
|
||||||
|
The easiest way to start a new Velox project is through the `create-velox` initialiser. It interactively walks you through project setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create velox@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
You will be asked:
|
||||||
|
1. **Project name** — used as the directory name and initial package name
|
||||||
|
2. **Template** — choose from `minimal`, `blog`, `docs`, `dashboard`, or `e-commerce`
|
||||||
|
3. **TypeScript or JavaScript** — TypeScript is strongly recommended
|
||||||
|
4. **Package manager** — the scaffolder will use this for the initial install
|
||||||
|
|
||||||
|
For non-interactive use, pass flags directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create velox@latest my-app -- --template minimal --ts --pm pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
If you prefer to configure everything from scratch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir my-app && cd my-app
|
||||||
|
npm init -y
|
||||||
|
npm install velox
|
||||||
|
npm install -D velox-cli typescript @types/node
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a minimal `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
app: {
|
||||||
|
name: 'my-app',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installing Dependencies in an Existing Project
|
||||||
|
|
||||||
|
If you are adding Velox to an existing TypeScript project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install velox
|
||||||
|
npm install -D velox-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the following scripts to your `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "velox dev",
|
||||||
|
"build": "velox build",
|
||||||
|
"start": "velox start",
|
||||||
|
"preview": "velox preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Version Management
|
||||||
|
|
||||||
|
We recommend using a Node version manager to ensure you always have the correct version. Popular options:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# nvm (macOS/Linux)
|
||||||
|
nvm install 22
|
||||||
|
nvm use 22
|
||||||
|
|
||||||
|
# fnm (fast, cross-platform)
|
||||||
|
fnm install 22
|
||||||
|
fnm use 22
|
||||||
|
|
||||||
|
# volta (pins versions per project)
|
||||||
|
volta install node@22
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pin the Node version for your project by adding a `.nvmrc` or `.node-version` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
22
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Velox reads environment variables from `.env` files at the project root. The following files are loaded in order (later files override earlier ones):
|
||||||
|
|
||||||
|
| File | Loaded in | Committed to git? |
|
||||||
|
|---|---|---|
|
||||||
|
| `.env` | All environments | Usually yes (no secrets) |
|
||||||
|
| `.env.local` | All environments | No (gitignored) |
|
||||||
|
| `.env.development` | `velox dev` only | Usually yes |
|
||||||
|
| `.env.production` | `velox build` / `velox start` | Usually yes |
|
||||||
|
| `.env.test` | Test runs | Usually yes |
|
||||||
|
|
||||||
|
Variables prefixed with `PUBLIC_` are exposed to the browser. All other variables remain server-side only.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
PUBLIC_APP_NAME=My Velox App
|
||||||
|
DATABASE_URL=postgres://localhost:5432/mydb
|
||||||
|
SECRET_API_KEY=do-not-expose-this
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating Velox
|
||||||
|
|
||||||
|
To update Velox to the latest version within your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install velox@latest
|
||||||
|
npm install -D velox-cli@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
To check whether updates are available without applying them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
velox upgrade --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting Installation
|
||||||
|
|
||||||
|
**`velox` command not found after global install**
|
||||||
|
Ensure your package manager's global bin directory is in your `PATH`. For npm, run `npm config get prefix` and add the `bin` subdirectory to your shell profile.
|
||||||
|
|
||||||
|
**Velocitor binary fails to run on Linux**
|
||||||
|
Some minimal Linux environments (e.g., Alpine Linux) may be missing the `glibc` version Velocitor requires. Install `glibc` compatibility libraries or use the musl build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install velox@latest --velox-binary-variant=musl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows Defender flags the Velocitor binary**
|
||||||
|
This is a false positive. Add an exclusion for your project's `node_modules/.velox-bin/` directory.
|
||||||
|
|
||||||
|
Next, follow the [Quick Start](quick-start.md) guide to build your first Velox application.
|
||||||
242
velox-docs/pages/layouts.md
Normal file
242
velox-docs/pages/layouts.md
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
---
|
||||||
|
title: Layouts
|
||||||
|
sort: 150
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: layouts, nested layouts, shared layouts, layout groups, _layout, slots
|
||||||
|
description: How to use nested layouts, shared layouts, and layout groups in Velox
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Layouts
|
||||||
|
|
||||||
|
Layouts define the structural shell around your page content — navigation headers, sidebars, footers, and anything else that persists across multiple pages. Velox provides a flexible, composable layout system built directly into the file-based router.
|
||||||
|
|
||||||
|
## Default Layout
|
||||||
|
|
||||||
|
The file `layouts/default.velox` is the root layout applied to all routes unless overridden:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// layouts/default.velox
|
||||||
|
---
|
||||||
|
import Header from '../components/Header.tsx';
|
||||||
|
import Footer from '../components/Footer.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{meta.title} — My App</title>
|
||||||
|
<meta name="description" content={meta.description} />
|
||||||
|
<link rel="stylesheet" href="/styles/global.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<main id="main-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<slot />` element is where the current page's content is injected. Layout files receive `meta` (the exports from the route's server block) and `params` automatically.
|
||||||
|
|
||||||
|
## Route-Level Layout Override
|
||||||
|
|
||||||
|
A route can specify a different layout using the `layout` export in its server block:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const layout = 'minimal'; // uses layouts/minimal.velox
|
||||||
|
export const meta = { title: 'Login' };
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `layout` to `false` to render the page with no layout at all:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const layout = false; // bare HTML response
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>...</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `_layout.velox` — Directory Scoped Layouts
|
||||||
|
|
||||||
|
Place a `_layout.velox` file inside any `routes/` subdirectory to apply a layout to all routes in that directory and its subdirectories:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
├── _layout.velox ← root layout (all routes)
|
||||||
|
├── index.velox
|
||||||
|
├── about.velox
|
||||||
|
└── admin/
|
||||||
|
├── _layout.velox ← admin layout (only /admin/* routes)
|
||||||
|
├── index.velox
|
||||||
|
└── users/
|
||||||
|
├── _layout.velox ← users layout (only /admin/users/* routes)
|
||||||
|
└── index.velox
|
||||||
|
```
|
||||||
|
|
||||||
|
Layouts nest — the admin layout wraps its content inside the root layout's `<slot />`, and the users layout wraps inside the admin layout's `<slot />`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Root Layout
|
||||||
|
└── Admin Layout
|
||||||
|
└── Users Layout
|
||||||
|
└── Page Content
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Layout Example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// routes/admin/_layout.velox
|
||||||
|
---
|
||||||
|
import AdminNav from '../../components/AdminNav.tsx';
|
||||||
|
|
||||||
|
const user = request.context.get('user');
|
||||||
|
export const meta = { title: 'Admin' };
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="admin-shell">
|
||||||
|
<AdminNav user={user} />
|
||||||
|
<div class="admin-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout Groups
|
||||||
|
|
||||||
|
Wrap a directory in parentheses to create a **route group** that applies a shared layout without affecting the URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
├── (public)/
|
||||||
|
│ ├── _layout.velox ← marketing/public layout
|
||||||
|
│ ├── index.velox → /
|
||||||
|
│ └── pricing.velox → /pricing
|
||||||
|
└── (app)/
|
||||||
|
├── _layout.velox ← application layout (requires auth)
|
||||||
|
├── dashboard.velox → /dashboard
|
||||||
|
└── settings.velox → /settings
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `/` and `/pricing` use the public layout. Both `/dashboard` and `/settings` use the app layout. The group name `(public)` and `(app)` never appear in the URL.
|
||||||
|
|
||||||
|
## Shared Layout Components
|
||||||
|
|
||||||
|
For UI elements shared between multiple layouts (a navigation bar used by both the public and admin layouts, for example), extract them as regular components:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// components/TopNav.tsx
|
||||||
|
interface TopNavProps {
|
||||||
|
links: { label: string; href: string }[];
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TopNav({ links, user }: TopNavProps) {
|
||||||
|
return (
|
||||||
|
<nav class="top-nav">
|
||||||
|
<ul>
|
||||||
|
{links.map(link => (
|
||||||
|
<li><a href={link.href}>{link.label}</a></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{user ? <UserMenu user={user} /> : <a href="/login">Log in</a>}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it in both layouts:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// layouts/default.velox
|
||||||
|
<TopNav links={publicLinks} user={null} />
|
||||||
|
|
||||||
|
// routes/admin/_layout.velox
|
||||||
|
<TopNav links={adminLinks} user={user} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data in Layouts
|
||||||
|
|
||||||
|
`_layout.velox` files have their own server block and can fetch data independently:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// routes/dashboard/_layout.velox
|
||||||
|
---
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
const user = request.context.get('user');
|
||||||
|
const notifications = await db.notifications.findUnread(user.id);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="dashboard-shell">
|
||||||
|
<DashboardNav notifications={notifications} />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Layout data is fetched in parallel with page data — there is no waterfall.
|
||||||
|
|
||||||
|
## Parallel Slots
|
||||||
|
|
||||||
|
For complex layouts with multiple content regions, use named parallel slots:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// A layout that accepts both main content and a sidebar
|
||||||
|
---
|
||||||
|
export const slots = ['default', 'sidebar'];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="two-column">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<slot name="sidebar" />
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill the named slot from the page:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const layout = 'two-column';
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Default slot content -->
|
||||||
|
<article>Main content here</article>
|
||||||
|
|
||||||
|
<!-- Named slot -->
|
||||||
|
<velox:slot name="sidebar">
|
||||||
|
<TableOfContents />
|
||||||
|
</velox:slot>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `<Head>` Management
|
||||||
|
|
||||||
|
Layouts and pages can both add content to the HTML `<head>` using the `<Head>` component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Head } from 'velox';
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
<meta property="og:title" content={meta.title} />
|
||||||
|
<meta property="og:image" content={meta.ogImage} />
|
||||||
|
<script type="application/ld+json">{JSON.stringify(structuredData)}</script>
|
||||||
|
</Head>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<Head>` insertions from nested layouts and pages are all collected and deduplicated before the final HTML is emitted.
|
||||||
253
velox-docs/pages/middleware.md
Normal file
253
velox-docs/pages/middleware.md
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
---
|
||||||
|
title: Middleware
|
||||||
|
sort: 140
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: middleware, request pipeline, auth middleware, rate limiting, CORS, headers
|
||||||
|
description: How to use and write Velox middleware for request/response transformation
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Middleware
|
||||||
|
|
||||||
|
Middleware in Velox is a function that intercepts HTTP requests before they reach a route handler and can transform both the request and the response. Middleware is composable, typed, and supports async operations.
|
||||||
|
|
||||||
|
## How Middleware Works
|
||||||
|
|
||||||
|
The Velox request pipeline processes a request in the following order:
|
||||||
|
|
||||||
|
1. **Global middleware** — applied to every request
|
||||||
|
2. **Path-scoped middleware** — applied based on route matching
|
||||||
|
3. **Route handler** — the actual page or API route
|
||||||
|
4. **Response middleware** — runs on the way out (in reverse order)
|
||||||
|
|
||||||
|
```
|
||||||
|
Request
|
||||||
|
↓
|
||||||
|
[Global Middleware 1]
|
||||||
|
↓
|
||||||
|
[Global Middleware 2]
|
||||||
|
↓
|
||||||
|
[Path Middleware]
|
||||||
|
↓
|
||||||
|
[Route Handler]
|
||||||
|
↓
|
||||||
|
[Response (in reverse)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Middleware
|
||||||
|
|
||||||
|
Create a middleware file in the `middleware/` directory. A middleware function receives the `request` and a `next` function. Call `next()` to pass control to the next middleware or the route handler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/logger.ts
|
||||||
|
import { defineMiddleware } from 'velox/server';
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const response = await next();
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
console.log(`${request.method} ${request.url} — ${response.status} (${duration}ms)`);
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring Middleware
|
||||||
|
|
||||||
|
Register middleware in `velox.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
middleware: [
|
||||||
|
{ path: '*', handler: './middleware/logger' },
|
||||||
|
{ path: '/admin/*', handler: './middleware/auth' },
|
||||||
|
{ path: '/api/*', handler: './middleware/rateLimit' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, use a `middleware.ts` file at the project root for global middleware:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware.ts (project root — applies to all routes automatically)
|
||||||
|
import { defineMiddleware } from 'velox/server';
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
// runs on every request
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/auth.ts
|
||||||
|
import { defineMiddleware, redirect } from 'velox/server';
|
||||||
|
import { verifyJWT } from '$lib/auth';
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const token = request.cookies.get('session')?.value
|
||||||
|
?? request.headers.get('Authorization')?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return redirect('/login?next=' + encodeURIComponent(request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await verifyJWT(token);
|
||||||
|
} catch {
|
||||||
|
return redirect('/login?error=invalid_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach user to request context for downstream handlers
|
||||||
|
request.context.set('user', user);
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the context in your route:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/dashboard.velox server block
|
||||||
|
const user = request.context.get('user');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/rateLimit.ts
|
||||||
|
import { defineMiddleware } from 'velox/server';
|
||||||
|
|
||||||
|
const counters = new Map<string, { count: number; resetAt: number }>();
|
||||||
|
const WINDOW_MS = 60_000; // 1 minute
|
||||||
|
const MAX_REQUESTS = 100;
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const ip = request.headers.get('CF-Connecting-IP')
|
||||||
|
?? request.headers.get('X-Forwarded-For')
|
||||||
|
?? 'unknown';
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const counter = counters.get(ip) ?? { count: 0, resetAt: now + WINDOW_MS };
|
||||||
|
|
||||||
|
if (now > counter.resetAt) {
|
||||||
|
counter.count = 0;
|
||||||
|
counter.resetAt = now + WINDOW_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.count++;
|
||||||
|
counters.set(ip, counter);
|
||||||
|
|
||||||
|
if (counter.count > MAX_REQUESTS) {
|
||||||
|
return new Response('Too Many Requests', {
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
'Retry-After': String(Math.ceil((counter.resetAt - now) / 1000)),
|
||||||
|
'X-RateLimit-Limit': String(MAX_REQUESTS),
|
||||||
|
'X-RateLimit-Remaining': '0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await next();
|
||||||
|
response.headers.set('X-RateLimit-Limit', String(MAX_REQUESTS));
|
||||||
|
response.headers.set('X-RateLimit-Remaining', String(MAX_REQUESTS - counter.count));
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/cors.ts
|
||||||
|
import { defineMiddleware } from 'velox/server';
|
||||||
|
|
||||||
|
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') ?? ['*'];
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ request, next }) => {
|
||||||
|
const origin = request.headers.get('Origin') ?? '';
|
||||||
|
const isAllowed = ALLOWED_ORIGINS.includes('*') || ALLOWED_ORIGINS.includes(origin);
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
// Preflight response
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: corsHeaders(origin, isAllowed),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await next();
|
||||||
|
|
||||||
|
if (isAllowed) {
|
||||||
|
for (const [key, value] of Object.entries(corsHeaders(origin, true))) {
|
||||||
|
response.headers.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
function corsHeaders(origin: string, allowed: boolean) {
|
||||||
|
if (!allowed) return {};
|
||||||
|
return {
|
||||||
|
'Access-Control-Allow-Origin': origin,
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Allow-Credentials': 'true',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Headers Middleware
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// middleware/securityHeaders.ts
|
||||||
|
import { defineMiddleware } from 'velox/server';
|
||||||
|
|
||||||
|
export default defineMiddleware(async ({ next }) => {
|
||||||
|
const response = await next();
|
||||||
|
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY');
|
||||||
|
response.headers.set('X-XSS-Protection', '1; mode=block');
|
||||||
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
response.headers.set(
|
||||||
|
'Permissions-Policy',
|
||||||
|
'camera=(), microphone=(), geolocation=()'
|
||||||
|
);
|
||||||
|
response.headers.set(
|
||||||
|
'Content-Security-Policy',
|
||||||
|
"default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'unsafe-inline'"
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composing Middleware
|
||||||
|
|
||||||
|
Combine multiple middleware into a single handler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { composeMiddleware } from 'velox/server';
|
||||||
|
import logger from './logger';
|
||||||
|
import auth from './auth';
|
||||||
|
import rateLimit from './rateLimit';
|
||||||
|
|
||||||
|
export default composeMiddleware(logger, rateLimit, auth);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware Execution Order
|
||||||
|
|
||||||
|
In `velox.config.ts`, middleware is applied in the order it is declared for the request path, and in reverse order for the response path. This mirrors the middleware stack pattern found in Express, Koa, and similar frameworks.
|
||||||
|
|
||||||
|
| Middleware | On request | On response |
|
||||||
|
|------------|-----------|------------|
|
||||||
|
| logger | 1st | 4th (last) |
|
||||||
|
| rateLimit | 2nd | 3rd |
|
||||||
|
| auth | 3rd | 2nd |
|
||||||
|
| route | 4th | 1st |
|
||||||
217
velox-docs/pages/project-structure.md
Normal file
217
velox-docs/pages/project-structure.md
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
---
|
||||||
|
title: Project Structure
|
||||||
|
sort: 130
|
||||||
|
section-id: getting-started
|
||||||
|
keywords: project structure, directory layout, files, folders, architecture
|
||||||
|
description: A detailed explanation of every file and folder in a Velox project
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Understanding the Velox project structure is key to working effectively with the framework. This page explains the purpose of every directory and file you will encounter in a standard Velox project.
|
||||||
|
|
||||||
|
## Top-Level Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
my-velox-app/
|
||||||
|
├── routes/ ← all pages and API routes
|
||||||
|
├── layouts/ ← shared page layouts
|
||||||
|
├── components/ ← reusable UI components
|
||||||
|
├── lib/ ← shared server-side utilities
|
||||||
|
├── middleware/ ← request/response interceptors
|
||||||
|
├── public/ ← static assets (copied verbatim)
|
||||||
|
├── styles/ ← global CSS and design tokens
|
||||||
|
├── tests/ ← test files
|
||||||
|
├── .velox/ ← build cache and output (gitignored)
|
||||||
|
├── velox.config.ts ← framework configuration
|
||||||
|
├── tsconfig.json ← TypeScript configuration
|
||||||
|
├── package.json
|
||||||
|
└── .env ← environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## `routes/`
|
||||||
|
|
||||||
|
This is the most important directory. Every file in `routes/` maps to a URL path unless prefixed with `_` (underscore files are ignored by the router).
|
||||||
|
|
||||||
|
### Page Routes
|
||||||
|
|
||||||
|
Files ending in `.velox` or `.tsx` become page routes:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
├── index.velox → /
|
||||||
|
├── about.velox → /about
|
||||||
|
├── blog/
|
||||||
|
│ ├── index.velox → /blog
|
||||||
|
│ └── [slug].velox → /blog/:slug
|
||||||
|
├── docs/
|
||||||
|
│ └── [...path].velox → /docs/* (catch-all)
|
||||||
|
└── _components/ ← underscore prefix: ignored by router
|
||||||
|
└── Header.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
|
||||||
|
Files suffixed with `+server.ts` become API routes. They export HTTP method handlers:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── api/
|
||||||
|
├── users+server.ts → /api/users
|
||||||
|
└── users/[id]+server.ts → /api/users/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `_error.velox` | Custom error page (404, 500) in any directory |
|
||||||
|
| `_loading.velox` | Loading state shown during route transitions |
|
||||||
|
| `_layout.velox` | Layout that wraps all sibling routes |
|
||||||
|
|
||||||
|
## `layouts/`
|
||||||
|
|
||||||
|
Layout files define the shell that wraps your page content. A layout receives a `<slot />` where the page content is injected.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// layouts/default.velox
|
||||||
|
---
|
||||||
|
import Header from '../components/Header.tsx';
|
||||||
|
import Footer from '../components/Footer.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{meta.title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<slot />
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
The default layout (`layouts/default.velox`) wraps all routes unless a route specifies a different layout via the `layout` frontmatter property.
|
||||||
|
|
||||||
|
## `components/`
|
||||||
|
|
||||||
|
Reusable UI components that can be used in routes and layouts. Components do not have server blocks — they are purely presentational (though they can accept server-fetched data as props).
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── Header.tsx
|
||||||
|
├── Footer.tsx
|
||||||
|
├── Button.tsx
|
||||||
|
├── ui/
|
||||||
|
│ ├── Card.tsx
|
||||||
|
│ └── Modal.tsx
|
||||||
|
└── forms/
|
||||||
|
├── Input.tsx
|
||||||
|
└── Select.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## `lib/`
|
||||||
|
|
||||||
|
Server-side utility modules — database clients, external API helpers, auth utilities. Code in `lib/` is never bundled for the client unless explicitly imported from a client-side component.
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── db.ts ← database connection
|
||||||
|
├── auth.ts ← authentication helpers
|
||||||
|
├── email.ts ← email sending
|
||||||
|
└── cache.ts ← caching utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## `middleware/`
|
||||||
|
|
||||||
|
Middleware functions execute on every matching request before the route handler runs. Middleware files are auto-loaded based on path:
|
||||||
|
|
||||||
|
```
|
||||||
|
middleware/
|
||||||
|
├── auth.ts ← applies to all routes (no path suffix)
|
||||||
|
└── admin.ts ← must be explicitly referenced in velox.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## `public/`
|
||||||
|
|
||||||
|
Static files in `public/` are served at the root path without transformation. A file at `public/robots.txt` is served at `/robots.txt`. Use this for favicons, `manifest.json`, `sitemap.xml`, and static images that do not need optimisation.
|
||||||
|
|
||||||
|
## `styles/`
|
||||||
|
|
||||||
|
Global stylesheets. Velox supports CSS Modules (`.module.css`), plain CSS, and Sass (`.scss`) if the `@velox/sass` plugin is installed. The file `styles/global.css` is automatically injected into every page.
|
||||||
|
|
||||||
|
```
|
||||||
|
styles/
|
||||||
|
├── global.css ← injected automatically
|
||||||
|
├── tokens.css ← CSS custom properties / design tokens
|
||||||
|
└── reset.css ← CSS reset or normalise
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.velox/`
|
||||||
|
|
||||||
|
Generated directory — **never commit this to git**. Contains:
|
||||||
|
|
||||||
|
- `.velox/cache/` — Velocitor's incremental build cache (keyed by content hash)
|
||||||
|
- `.velox/output/` — production build output
|
||||||
|
- `.velox/tmp/` — temporary files used during development
|
||||||
|
|
||||||
|
Add `.velox/` to your `.gitignore`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# .gitignore
|
||||||
|
.velox/
|
||||||
|
.env.local
|
||||||
|
node_modules/
|
||||||
|
```
|
||||||
|
|
||||||
|
## `velox.config.ts`
|
||||||
|
|
||||||
|
The central configuration file for the framework. See the full [Configuration](configuration.md) reference for every available option. A minimal example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'velox';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
app: {
|
||||||
|
name: 'my-velox-app',
|
||||||
|
baseUrl: process.env.PUBLIC_BASE_URL ?? 'http://localhost:3700',
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tsconfig.json`
|
||||||
|
|
||||||
|
Velox requires certain TypeScript compiler options to function correctly. The scaffolder generates an appropriate `tsconfig.json` automatically. Key options:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "velox",
|
||||||
|
"strict": true,
|
||||||
|
"paths": {
|
||||||
|
"$lib/*": ["./lib/*"],
|
||||||
|
"$components/*": ["./components/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `$lib` and `$components` path aliases are available throughout your project without needing to calculate relative paths.
|
||||||
|
|
||||||
|
## Generated Files
|
||||||
|
|
||||||
|
Velox generates a small number of type definition files in `.velox/types/` that provide type-safe access to environment variables, route params, and other framework internals. These are referenced automatically by the `tsconfig.json` generated by the scaffolder.
|
||||||
|
|
||||||
|
Understanding this structure will help you navigate any Velox project confidently. Next, explore the [Configuration](configuration.md) reference to learn all available options.
|
||||||
208
velox-docs/pages/quick-start.md
Normal file
208
velox-docs/pages/quick-start.md
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
---
|
||||||
|
title: Quick Start
|
||||||
|
sort: 120
|
||||||
|
section-id: getting-started
|
||||||
|
keywords: quick start, first app, hello world, dev server, file structure
|
||||||
|
description: Build your first Velox application from scratch in minutes
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This guide walks you through creating a working Velox application from zero. By the end you will have a running dev server, a couple of pages with navigation between them, and an API route returning JSON.
|
||||||
|
|
||||||
|
## Step 1 — Scaffold the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create velox@latest my-first-app -- --template minimal --ts --pm npm
|
||||||
|
cd my-first-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The scaffolder creates the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-first-app/
|
||||||
|
├── routes/
|
||||||
|
│ ├── index.velox ← homepage route
|
||||||
|
│ └── about.velox ← about page route
|
||||||
|
├── layouts/
|
||||||
|
│ └── default.velox ← wraps all routes by default
|
||||||
|
├── public/
|
||||||
|
│ └── favicon.svg
|
||||||
|
├── velox.config.ts
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2 — Start the Development Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Velox starts the development server on port 3700 by default:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚡ Velox v1.4.0 — development server
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ Local: http://localhost:3700 │
|
||||||
|
│ Network: http://192.168.1.5:3700 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
Ready in 312ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3700` in your browser. You should see the default Velox welcome page.
|
||||||
|
|
||||||
|
## Step 3 — Understand the Default Page
|
||||||
|
|
||||||
|
Open `routes/index.velox`. A `.velox` file is a superset of TSX with some Velox-specific features:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
// Server-side frontmatter block — runs only on the server
|
||||||
|
export const meta = {
|
||||||
|
title: 'Home',
|
||||||
|
description: 'My first Velox app',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Welcome to Velox!</h1>
|
||||||
|
<p>Edit <code>routes/index.velox</code> to get started.</p>
|
||||||
|
<a href="/about">About</a>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `---` fenced block at the top is the **server block** — it runs exclusively during server-side rendering or static generation. Exports from the server block (like `meta`) are available as template variables in the HTML section below.
|
||||||
|
|
||||||
|
## Step 4 — Add a New Route
|
||||||
|
|
||||||
|
Create a new file at `routes/contact.velox`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
export const meta = {
|
||||||
|
title: 'Contact',
|
||||||
|
description: 'Get in touch with us',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Contact Us</h1>
|
||||||
|
<p>Send us a message at <a href="mailto:hello@example.com">hello@example.com</a></p>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the file. Velox automatically picks up the new route — navigate to `http://localhost:3700/contact` and you will see your new page without restarting the server.
|
||||||
|
|
||||||
|
## Step 5 — Create an API Route
|
||||||
|
|
||||||
|
API routes live in the `routes/` directory alongside page routes, but are named with a `+` prefix to distinguish them.
|
||||||
|
|
||||||
|
Create `routes/api/hello+server.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
const name = new URL(req.url).searchParams.get('name') ?? 'World';
|
||||||
|
return Response.json({ message: `Hello, ${name}!` });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it at `http://localhost:3700/api/hello?name=Velox`. You should receive:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "message": "Hello, Velox!" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6 — Fetch Data on the Server
|
||||||
|
|
||||||
|
Update `routes/index.velox` to fetch data server-side:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import { fetch } from 'velox/server';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
title: 'Home',
|
||||||
|
};
|
||||||
|
|
||||||
|
// This runs on the server only
|
||||||
|
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
|
||||||
|
const todo = await res.json();
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Welcome to Velox!</h1>
|
||||||
|
<p>Todo from API: {todo.title}</p>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7 — Add Interactivity (Islands)
|
||||||
|
|
||||||
|
By default, Velox ships zero client-side JavaScript. To add a client-side interactive component, create a component and opt into hydration:
|
||||||
|
|
||||||
|
Create `components/Counter.tsx`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { signal } from 'velox/client';
|
||||||
|
|
||||||
|
export default function Counter() {
|
||||||
|
const count = signal(0);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Count: {count.value}</p>
|
||||||
|
<button onClick={() => count.value++}>Increment</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it in your page with the `client:load` directive:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
import Counter from '../components/Counter.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>Home</h1>
|
||||||
|
<Counter client:load />
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `client:load` directive tells Velox to hydrate this component immediately when the page loads. Other directives include `client:idle` (hydrate when browser is idle) and `client:visible` (hydrate when the component enters the viewport).
|
||||||
|
|
||||||
|
## Step 8 — Build for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Velocitor compiles a production-ready build into the `.velox/output/` directory. Review the build output:
|
||||||
|
|
||||||
|
```
|
||||||
|
⚡ Velox — production build
|
||||||
|
Compiled 4 routes in 1.8s
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ / → SSR (0 KB JS) │
|
||||||
|
│ /about → SSG (0 KB JS) │
|
||||||
|
│ /contact → SSG (0 KB JS) │
|
||||||
|
│ /api/hello → Edge handler │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
Total JS shipped to client: 2.1 KB (Counter island only)
|
||||||
|
```
|
||||||
|
|
||||||
|
To preview the production build locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read about [Project Structure](project-structure.md) to understand every directory and file
|
||||||
|
- Explore [Routing](routing.md) for dynamic routes and catch-all patterns
|
||||||
|
- Learn about [Data Fetching](data-fetching.md) for SSR, SSG, and ISR patterns
|
||||||
|
- Check the [Configuration](configuration.md) reference for `velox.config.ts` options
|
||||||
237
velox-docs/pages/routing.md
Normal file
237
velox-docs/pages/routing.md
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
---
|
||||||
|
title: Routing
|
||||||
|
sort: 100
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: routing, file-based routing, dynamic routes, catch-all, nested routes, navigation
|
||||||
|
description: How Velox's file-based routing works, including dynamic segments, catch-all routes, and nested layouts
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# Routing
|
||||||
|
|
||||||
|
Velox uses a file-based routing system. The file tree under the `routes/` directory maps one-to-one to the URL structure of your application. There is no router configuration file — the filesystem is the configuration.
|
||||||
|
|
||||||
|
## Basic Routes
|
||||||
|
|
||||||
|
Any `.velox` or `.tsx` file placed inside `routes/` becomes a page route:
|
||||||
|
|
||||||
|
| File | URL |
|
||||||
|
|------|-----|
|
||||||
|
| `routes/index.velox` | `/` |
|
||||||
|
| `routes/about.velox` | `/about` |
|
||||||
|
| `routes/blog/index.velox` | `/blog` |
|
||||||
|
| `routes/blog/my-post.velox` | `/blog/my-post` |
|
||||||
|
| `routes/shop/shoes/index.velox` | `/shop/shoes` |
|
||||||
|
|
||||||
|
## Dynamic Segments
|
||||||
|
|
||||||
|
Wrap a path segment in square brackets to make it dynamic:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── blog/
|
||||||
|
└── [slug].velox → /blog/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the route, access the parameter via the `params` object in the server block:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
---
|
||||||
|
const { slug } = params;
|
||||||
|
const post = await db.posts.findBySlug(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
throw new NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const meta = { title: post.title };
|
||||||
|
---
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h1>{post.title}</h1>
|
||||||
|
<div innerHTML={post.htmlContent} />
|
||||||
|
</article>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Dynamic Segments
|
||||||
|
|
||||||
|
You can nest multiple dynamic segments:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── [category]/
|
||||||
|
└── [slug].velox → /:category/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Dynamic Segments
|
||||||
|
|
||||||
|
Wrap a segment in double square brackets to make it optional:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── blog/
|
||||||
|
└── [[page]].velox → /blog and /blog/:page
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the route, `params.page` will be `undefined` when the segment is not present.
|
||||||
|
|
||||||
|
## Catch-All Routes
|
||||||
|
|
||||||
|
Use the spread syntax `[...rest]` to match any number of path segments:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── docs/
|
||||||
|
└── [...path].velox → /docs/ and /docs/a/b/c/d
|
||||||
|
```
|
||||||
|
|
||||||
|
`params.path` is an array of path segments: `['a', 'b', 'c', 'd']`.
|
||||||
|
|
||||||
|
### Optional Catch-All
|
||||||
|
|
||||||
|
`[[...rest]]` matches zero or more segments (i.e., the base path also matches):
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── [[...slug]].velox → / and /anything/at/all
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Routes
|
||||||
|
|
||||||
|
Files with a `+server.ts` or `+server.js` suffix define API endpoints. They export HTTP method handlers named `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, or `OPTIONS`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// routes/api/users+server.ts
|
||||||
|
import { defineHandler } from 'velox/server';
|
||||||
|
import { db } from '$lib/db';
|
||||||
|
|
||||||
|
export const GET = defineHandler(async (req) => {
|
||||||
|
const users = await db.users.findMany();
|
||||||
|
return Response.json(users);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = defineHandler(async (req) => {
|
||||||
|
const body = await req.json();
|
||||||
|
const user = await db.users.create({ data: body });
|
||||||
|
return Response.json(user, { status: 201 });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Groups
|
||||||
|
|
||||||
|
Wrap a directory name in parentheses to create a route group. Groups do not affect the URL — they are purely an organisational tool:
|
||||||
|
|
||||||
|
```
|
||||||
|
routes/
|
||||||
|
└── (marketing)/
|
||||||
|
├── index.velox → /
|
||||||
|
├── about.velox → /about
|
||||||
|
└── pricing.velox → /pricing
|
||||||
|
└── _layout.velox ← layout applied only to marketing routes
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful when you want different layouts for different sections of your site without adding URL segments.
|
||||||
|
|
||||||
|
## Special Files
|
||||||
|
|
||||||
|
### `_layout.velox`
|
||||||
|
|
||||||
|
A `_layout.velox` file wraps all sibling routes and nested route directories. Layouts receive a `<slot />` where child content is injected:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// routes/blog/_layout.velox
|
||||||
|
---
|
||||||
|
import Sidebar from '../../components/Sidebar.tsx';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="blog-layout">
|
||||||
|
<Sidebar />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Layouts nest automatically — a deeply nested `_layout.velox` wraps its subtree, and the tree up to the root layout wraps the whole thing.
|
||||||
|
|
||||||
|
### `_error.velox`
|
||||||
|
|
||||||
|
Renders when an unhandled error occurs or when a `NotFound` is thrown. Receives `error` and `status` as props.
|
||||||
|
|
||||||
|
### `_loading.velox`
|
||||||
|
|
||||||
|
Shown as an immediate placeholder during route transitions (client-side navigation) while the next route is loading.
|
||||||
|
|
||||||
|
## Programmatic Navigation
|
||||||
|
|
||||||
|
Use the `useRouter` hook for client-side navigation in interactive components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useRouter } from 'velox/client';
|
||||||
|
|
||||||
|
export default function LoginButton() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
await performLogin();
|
||||||
|
router.navigate('/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={handleLogin}>Log in</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `<Link>` Component
|
||||||
|
|
||||||
|
For declarative navigation, use the `<Link>` component. It renders an `<a>` tag with client-side navigation and prefetching:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Link } from 'velox/client';
|
||||||
|
|
||||||
|
<Link href="/about">About Us</Link>
|
||||||
|
<Link href="/blog/my-post" prefetch="hover">My Post</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `prefetch` prop accepts `"hover"` (prefetch on hover), `"viewport"` (prefetch when link enters viewport), or `false` (no prefetch).
|
||||||
|
|
||||||
|
## Redirects
|
||||||
|
|
||||||
|
Return a redirect from a server block or API handler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { redirect } from 'velox/server';
|
||||||
|
|
||||||
|
// In a server block
|
||||||
|
if (!user) {
|
||||||
|
throw redirect('/login', 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In an API route
|
||||||
|
export const GET = defineHandler(async () => {
|
||||||
|
return redirect('/new-location', 301);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Metadata
|
||||||
|
|
||||||
|
Export `meta` from a route's server block to set page metadata:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const meta = {
|
||||||
|
title: 'Page Title',
|
||||||
|
description: 'Page description for SEO',
|
||||||
|
openGraph: {
|
||||||
|
image: 'https://example.com/og.png',
|
||||||
|
},
|
||||||
|
robots: 'index, follow',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
For dynamic metadata, use a function:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const meta = () => ({
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
});
|
||||||
|
```
|
||||||
258
velox-docs/pages/state-management.md
Normal file
258
velox-docs/pages/state-management.md
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
---
|
||||||
|
title: State Management
|
||||||
|
sort: 120
|
||||||
|
section-id: core-concepts
|
||||||
|
keywords: state, signals, store, reactive, computed, effect, state management
|
||||||
|
description: Velox's reactive state management system — signals, stores, and reactive primitives
|
||||||
|
language: en
|
||||||
|
---
|
||||||
|
|
||||||
|
# State Management
|
||||||
|
|
||||||
|
Velox uses a signals-based reactivity system for client-side state. Signals are fine-grained reactive primitives — when a signal's value changes, only the DOM nodes that actually read that signal update. There is no virtual DOM diffing, no component tree re-render, and no unnecessary reconciliation.
|
||||||
|
|
||||||
|
## Signals
|
||||||
|
|
||||||
|
A signal is a reactive value container:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal } from 'velox/client';
|
||||||
|
|
||||||
|
const count = signal(0);
|
||||||
|
|
||||||
|
// Read the current value
|
||||||
|
console.log(count.value); // 0
|
||||||
|
|
||||||
|
// Update the value
|
||||||
|
count.value = 1;
|
||||||
|
console.log(count.value); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
In JSX, signals are automatically unwrapped — you do not need `.value` in templates:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const count = signal(0);
|
||||||
|
|
||||||
|
// Both are equivalent:
|
||||||
|
<p>{count.value}</p>
|
||||||
|
<p>{count}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The second form (`{count}` without `.value`) subscribes the DOM node directly to the signal. This is more efficient because Velox skips the component function entirely and updates only the text node when the signal changes.
|
||||||
|
|
||||||
|
## Computed Signals
|
||||||
|
|
||||||
|
A computed signal derives its value from one or more other signals. It re-evaluates automatically when its dependencies change:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, computed } from 'velox/client';
|
||||||
|
|
||||||
|
const firstName = signal('Jane');
|
||||||
|
const lastName = signal('Doe');
|
||||||
|
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
||||||
|
|
||||||
|
console.log(fullName.value); // "Jane Doe"
|
||||||
|
firstName.value = 'John';
|
||||||
|
console.log(fullName.value); // "John Doe"
|
||||||
|
```
|
||||||
|
|
||||||
|
Computed signals are lazy and memoised — the computation only runs when the value is read, and only re-runs if a dependency has changed since the last read.
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
An effect runs a side effect whenever its reactive dependencies change:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, effect } from 'velox/client';
|
||||||
|
|
||||||
|
const query = signal('');
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
console.log('Search query changed:', query.value);
|
||||||
|
// this re-runs every time query.value changes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Effects clean themselves up automatically when the component unmounts. To clean up manually within an effect (e.g., cancel a previous request):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
effect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
fetch(`/api/search?q=${query.value}`, { signal: controller.signal })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(results => resultsSignal.value = results);
|
||||||
|
|
||||||
|
return () => controller.abort(); // cleanup function
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stores
|
||||||
|
|
||||||
|
For shared state across components, define a store. A store is a module that encapsulates signals and exposes a clean interface:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/stores/cart.ts
|
||||||
|
import { signal, computed } from 'velox/client';
|
||||||
|
|
||||||
|
export interface CartItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = signal<CartItem[]>([]);
|
||||||
|
|
||||||
|
export const cartStore = {
|
||||||
|
items,
|
||||||
|
total: computed(() =>
|
||||||
|
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
||||||
|
),
|
||||||
|
itemCount: computed(() =>
|
||||||
|
items.value.reduce((sum, item) => sum + item.quantity, 0)
|
||||||
|
),
|
||||||
|
|
||||||
|
addItem(item: Omit<CartItem, 'quantity'>) {
|
||||||
|
const existing = items.value.find(i => i.id === item.id);
|
||||||
|
if (existing) {
|
||||||
|
items.value = items.value.map(i =>
|
||||||
|
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items.value = [...items.value, { ...item, quantity: 1 }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(id: string) {
|
||||||
|
items.value = items.value.filter(i => i.id !== id);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
items.value = [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the store in any component — signals are module-level singletons:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cartStore } from '$lib/stores/cart';
|
||||||
|
|
||||||
|
export default function CartIcon() {
|
||||||
|
return (
|
||||||
|
<button>
|
||||||
|
Cart ({cartStore.itemCount})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Updates
|
||||||
|
|
||||||
|
Multiple signal updates within a `batch()` call are processed as a single reactive update, preventing intermediate renders:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, batch } from 'velox/client';
|
||||||
|
|
||||||
|
const x = signal(0);
|
||||||
|
const y = signal(0);
|
||||||
|
const z = signal(0);
|
||||||
|
|
||||||
|
// Without batch: triggers 3 re-renders
|
||||||
|
x.value = 1;
|
||||||
|
y.value = 2;
|
||||||
|
z.value = 3;
|
||||||
|
|
||||||
|
// With batch: triggers 1 re-render
|
||||||
|
batch(() => {
|
||||||
|
x.value = 1;
|
||||||
|
y.value = 2;
|
||||||
|
z.value = 3;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async Signals
|
||||||
|
|
||||||
|
For async data that needs to integrate with the reactivity system, use `asyncSignal`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { signal, asyncSignal } from 'velox/client';
|
||||||
|
|
||||||
|
const userId = signal<string | null>(null);
|
||||||
|
|
||||||
|
const userProfile = asyncSignal(async () => {
|
||||||
|
if (!userId.value) return null;
|
||||||
|
const res = await fetch(`/api/users/${userId.value}`);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
// userProfile.value is: { status: 'idle' | 'loading' | 'success' | 'error', data, error }
|
||||||
|
```
|
||||||
|
|
||||||
|
In JSX:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div>
|
||||||
|
{userProfile.value.status === 'loading' && <Spinner />}
|
||||||
|
{userProfile.value.status === 'success' && (
|
||||||
|
<p>Hello, {userProfile.value.data.name}</p>
|
||||||
|
)}
|
||||||
|
{userProfile.value.status === 'error' && (
|
||||||
|
<p>Error: {userProfile.value.error.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server State with `@velox/query`
|
||||||
|
|
||||||
|
For server state (data fetched from APIs), the `@velox/query` package provides a higher-level solution with caching, revalidation, and optimistic updates:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createQuery, createMutation } from '@velox/query';
|
||||||
|
|
||||||
|
const postsQuery = createQuery({
|
||||||
|
key: ['posts'],
|
||||||
|
fetcher: () => fetch('/api/posts').then(r => r.json()),
|
||||||
|
staleTime: 60_000, // data is fresh for 60 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPostMutation = createMutation({
|
||||||
|
mutator: (data) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
|
onSuccess: () => postsQuery.invalidate(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persisting State
|
||||||
|
|
||||||
|
To persist signal state to `localStorage` or `sessionStorage`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { persistedSignal } from 'velox/client';
|
||||||
|
|
||||||
|
const theme = persistedSignal('theme', 'light', {
|
||||||
|
storage: 'localStorage',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The signal is initialised from storage on mount and written back whenever it changes.
|
||||||
|
|
||||||
|
## Reactive Context (Dependency Injection)
|
||||||
|
|
||||||
|
For providing state to deeply nested component trees without prop drilling, use the context API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createContext, useContext } from 'velox/client';
|
||||||
|
|
||||||
|
const ThemeContext = createContext<'light' | 'dark'>('light');
|
||||||
|
|
||||||
|
// Provider (wraps a subtree)
|
||||||
|
<ThemeContext.Provider value={currentTheme}>
|
||||||
|
<App />
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
|
||||||
|
// Consumer (anywhere in the tree below)
|
||||||
|
function ThemedButton() {
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
return <button class={`btn btn--${theme}`}>Click me</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
302
velox-docs/search.json
Normal file
302
velox-docs/search.json
Normal file
File diff suppressed because one or more lines are too long
66
velox-docs/theme.yml
Normal file
66
velox-docs/theme.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# mdcms theme — default
|
||||||
|
# Edit colours, fonts, and layout here. See docs for full reference.
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# Colours
|
||||||
|
# ──────────────────────────────────
|
||||||
|
light:
|
||||||
|
accent: "#2563EB"
|
||||||
|
background: "#FFFFFF"
|
||||||
|
nav-background: "#F8FAFC"
|
||||||
|
text: "#1E293B"
|
||||||
|
text-muted: "#64748B"
|
||||||
|
|
||||||
|
dark:
|
||||||
|
accent: "#60A5FA"
|
||||||
|
background: "#0F172A"
|
||||||
|
nav-background: "#1E293B"
|
||||||
|
text: "#F1F5F9"
|
||||||
|
text-muted: "#94A3B8"
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# Semantic colours
|
||||||
|
# Used by callout tags (info, warning, success, error).
|
||||||
|
# Choose values that work on both light and dark backgrounds.
|
||||||
|
# ──────────────────────────────────
|
||||||
|
colours-semantic:
|
||||||
|
info: "#2563EB"
|
||||||
|
warning: "#D97706"
|
||||||
|
success: "#16A34A"
|
||||||
|
error: "#DC2626"
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# Callout defaults
|
||||||
|
# ──────────────────────────────────
|
||||||
|
callouts:
|
||||||
|
info:
|
||||||
|
icon: info
|
||||||
|
primary-colour: "#2563EB"
|
||||||
|
background-colour: "#2563EB"
|
||||||
|
warning:
|
||||||
|
icon: warning
|
||||||
|
primary-colour: "#D97706"
|
||||||
|
background-colour: "#D97706"
|
||||||
|
success:
|
||||||
|
icon: success
|
||||||
|
primary-colour: "#16A34A"
|
||||||
|
background-colour: "#16A34A"
|
||||||
|
error:
|
||||||
|
icon: error
|
||||||
|
primary-colour: "#DC2626"
|
||||||
|
background-colour: "#DC2626"
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# Typography
|
||||||
|
# Format: "provider:Font Name:weight" (provider: bunny | google)
|
||||||
|
# ──────────────────────────────────
|
||||||
|
font-body: "bunny:Noto Sans:400"
|
||||||
|
font-heading: "bunny:Noto Sans:700"
|
||||||
|
font-size: 1.0 # unitless multiplier (1.0 = 16px base)
|
||||||
|
line-height: 1.7 # unitless multiplier
|
||||||
|
|
||||||
|
# ──────────────────────────────────
|
||||||
|
# Layout
|
||||||
|
# ──────────────────────────────────
|
||||||
|
main-width: 80em
|
||||||
|
nav-width: 20em
|
||||||
Loading…
Reference in a new issue