├── example ├── svg.d.ts ├── .dockerignore ├── src │ ├── main.css │ ├── assets │ │ ├── SPA.svg │ │ ├── SPAwBR.svg │ │ └── SSR.svg │ ├── Example.svelte │ ├── Index.svelte │ ├── Landing.svelte │ └── Documentation.svelte ├── Dockerfile.dev ├── Caddyfile ├── Dockerfile ├── tsconfig.json ├── package.json └── static │ └── favicon.svg ├── .gitignore ├── default ├── index.ts └── index.html ├── src ├── index.ts ├── types.d.ts ├── strip.ts ├── cli.ts ├── rsbuild.config.ts ├── router.ts ├── Router.svelte └── ssr.ts ├── static ├── SPA.svg ├── SPAwBR.svg └── SSR.svg ├── tsconfig.json ├── LICENSE ├── package.json ├── docs ├── favicon.svg ├── example.html ├── index.html ├── css │ └── index.e62340e7.css ├── js │ └── index.22425872.js └── doc.html ├── release.sh └── README.md /example/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /example/.dockerignore: -------------------------------------------------------------------------------- 1 | # All dot files in root 2 | /.* 3 | 4 | dist 5 | docs 6 | node_modules 7 | 8 | Dockerfile* 9 | LICENSE 10 | README.md -------------------------------------------------------------------------------- /example/src/main.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | font-family: "Roboto", sans-serif; 4 | margin: 0; 5 | padding: 0; 6 | height: 100%; 7 | font-weight: 300; 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist 9 | 10 | # IDE 11 | .vscode/* 12 | !.vscode/extensions.json 13 | .idea 14 | -------------------------------------------------------------------------------- /default/index.ts: -------------------------------------------------------------------------------- 1 | import { hydrate } from "svelte"; 2 | import Index from "./Index.svelte"; 3 | 4 | hydrate(Index, { 5 | target: document.getElementById("root")!, 6 | props: {}, 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { navigate, link, setupStaticRoutes } from './router.js'; 2 | export type { Routes, Component } from './types.js'; 3 | export { default as Router } from './Router.svelte'; -------------------------------------------------------------------------------- /default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PrevelteKit - default index.html 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /example/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine AS base 2 | RUN apk add --no-cache pnpm 3 | WORKDIR /app 4 | COPY package.json tsconfig.json ./ 5 | RUN pnpm install 6 | ENTRYPOINT ["pnpm", "run", "dev"] 7 | 8 | # run with: 9 | # docker build -f Dockerfile.dev . -t preveltekit-dev 10 | # docker run -p3000:3000 -v./static:/app/static -v./src:/app/src -v./public:/app/public preveltekit-dev -------------------------------------------------------------------------------- /example/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | admin off 3 | auto_https off 4 | } 5 | 6 | #https://caddyserver.com/docs/caddyfile/patterns#single-page-apps-spas 7 | :3000 { 8 | root * /var/www/html 9 | try_files {path} {path}.html /index.html 10 | file_server { 11 | #https://caddyserver.com/docs/caddyfile/directives/file_server#precompressed 12 | precompressed br zstd gzip 13 | } 14 | } -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine AS base 2 | RUN apk add --no-cache brotli zopfli zstd pnpm 3 | WORKDIR /app 4 | COPY pnpm-lock.yaml package.json tsconfig.json ./ 5 | RUN pnpm install --frozen-lockfile 6 | COPY src ./src 7 | RUN pnpm build 8 | 9 | FROM caddy:2-alpine 10 | COPY Caddyfile /etc/caddy/Caddyfile 11 | COPY --from=base /app/dist/ /var/www/html 12 | 13 | # run with: 14 | # docker build . -t preveltekit 15 | # docker run -p3000:3000 preveltekit 16 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | // svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 7 | // to enforce using `import type` instead of `import` for Types. 8 | "verbatimModuleSyntax": true, 9 | "noEmit": true, 10 | "skipLibCheck": true, 11 | "types": ["svelte"] 12 | }, 13 | "include": ["./src/*", "svg.d.ts"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preveltekit-example", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "preveltekit dev", 7 | "build": "NODE_ENV=production preveltekit prod", 8 | "build-no-zip": "NODE_ENV=production preveltekit prod --no-zip", 9 | "stage": "NODE_ENV=production preveltekit stage" 10 | }, 11 | "dependencies": { 12 | "svelte": "^5.39.11" 13 | }, 14 | "devDependencies": { 15 | "preveltekit": "file:../", 16 | "typescript": "^5.9.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component as SvelteComponent } from 'svelte'; 2 | export type Component = SvelteComponent | null; 3 | 4 | export interface Routes { 5 | dynamicRoutes?: { path: string; component: Component }[]; 6 | staticRoutes?: { path: string; htmlFilename: string }[]; 7 | } 8 | 9 | // Extend HTMLScriptElement to include readyState for JSDOM compatibility 10 | declare global { 11 | // Extend Window interface for JSDOM and Svelte routes 12 | interface Window { 13 | __isBuildTime?: boolean; 14 | __svelteRoutes?: Routes; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static/SPA.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /example/src/assets/SPA.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2022"], 4 | "target": "ES2022", 5 | "skipLibCheck": true, 6 | // svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 7 | // to enforce using `import type` instead of `import` for Types. 8 | "verbatimModuleSyntax": true, 9 | "useDefineForClassFields": true, 10 | 11 | /* modules */ 12 | "module": "ESNext", 13 | "isolatedModules": true, 14 | "resolveJsonModule": true, 15 | "moduleResolution": "Bundler", 16 | 17 | /* type checking */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | 22 | "declaration": true, 23 | "outDir": "dist" 24 | }, 25 | "include": ["./src/*"], 26 | "exclude": ["node_modules", "./dist/**", "./default/**", "./src/strip.ts"], 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Thomas Bocek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preveltekit", 3 | "version": "1.2.23", 4 | "description": "PrevelteKit is a lightweight, high-performance web application framework built on Svelte 5, featuring Server-Side Pre Rendering (SSPR) using Rsbuild and jsdom", 5 | "repository": "https://github.com/tbocek/preveltekit", 6 | "homepage": "https://tbocek.github.io/preveltekit", 7 | "author": "Thomas Bocek", 8 | "license": "MIT", 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.js", 14 | "default": "./dist/index.js" 15 | } 16 | }, 17 | "bin": { 18 | "preveltekit": "dist/cli.js" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "rm -rf dist && mkdir -p dist/default && tsx src/strip.ts -i src/Router.svelte -o dist/Router.svelte && tsc && cp src/types.d.ts dist/ && cp -r default dist/", 25 | "prepublishOnly": "pnpm run build", 26 | "docs": "pnpm run build && rm -rf docs/* && cd example && pnpm install && pnpm run build-no-zip && cd dist && cp -r . ../../docs/", 27 | "release": "pnpm publish --access public" 28 | }, 29 | "peerDependencies": { 30 | "svelte": "^5.0.0" 31 | }, 32 | "dependencies": { 33 | "@rsbuild/core": "^1.6.7", 34 | "@rsbuild/plugin-svelte": "^1.0.11", 35 | "@rspack/core": "^1.6.4", 36 | "express": "^5.1.0", 37 | "jsdom": "^27.2.0" 38 | }, 39 | "devDependencies": { 40 | "@types/express": "^5.0.5", 41 | "@types/jsdom": "^27.0.0", 42 | "@types/node": "^24.10.1", 43 | "typescript": "^5.9.3", 44 | "svelte-preprocess": "^6.0.3", 45 | "tsx": "^4.20.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /static/SPAwBR.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /example/src/assets/SPAwBR.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /example/static/favicon.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /src/strip.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { preprocess } from 'svelte/compiler'; 3 | import { sveltePreprocess } from 'svelte-preprocess'; 4 | import { readFileSync, writeFileSync } from 'fs'; 5 | import { resolve } from 'path'; 6 | 7 | // Parse command line arguments 8 | const args: string[] = process.argv.slice(2); 9 | 10 | const getArg = (flag: string): string | null => { 11 | const index = args.indexOf(flag); 12 | return index !== -1 && args[index + 1] ? args[index + 1] : null; 13 | }; 14 | 15 | const inputFile: string | null = getArg('--input') || getArg('-i'); 16 | const outputFile: string | null = getArg('--output') || getArg('-o'); 17 | const showHelp: boolean = args.includes('--help') || args.includes('-h'); 18 | 19 | // Show help message 20 | if (showHelp || !inputFile || !outputFile) { 21 | console.log(` 22 | Usage: node compileRouter.js --input --output 23 | 24 | Options: 25 | -i, --input Input TypeScript Svelte file 26 | -o, --output Output JavaScript Svelte file 27 | -h, --help Show this help message 28 | 29 | Example: 30 | node compileRouter.js --input src/Router.svelte --output dist/Router.svelte 31 | node compileRouter.js -i src/Router.svelte -o dist/Router.svelte 32 | `); 33 | process.exit(showHelp ? 0 : 1); 34 | } 35 | 36 | try { 37 | // Read input file 38 | const inputPath: string = resolve(inputFile); 39 | console.log(`Reading: ${inputPath}`); 40 | const source: string = readFileSync(inputPath, 'utf8'); 41 | 42 | // Preprocess TypeScript to JavaScript 43 | console.log('Compiling TypeScript...'); 44 | const preprocessed = await preprocess( 45 | source, 46 | sveltePreprocess({ 47 | typescript: { 48 | tsconfigFile: './tsconfig.json' 49 | } 50 | }), 51 | { filename: inputFile } 52 | ); 53 | 54 | // Remove lang="ts" from script tag 55 | let output: string = preprocessed.code.replace(/ 5 | 6 |

Bitcoin Price Tracker (Server Pre-Rendered)

Loading...

7 | 8 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION_TYPE="patch" 5 | 6 | show_usage() { 7 | echo "Usage: $0 [OPTIONS]" 8 | echo "" 9 | echo "Options:" 10 | echo " --patch Increment patch version (default)" 11 | echo " --minor Increment minor version" 12 | echo " --major Increment major version" 13 | echo " -h, --help Show this help message" 14 | } 15 | 16 | while [[ $# -gt 0 ]]; do 17 | case $1 in 18 | --patch) VERSION_TYPE="patch"; shift ;; 19 | --minor) VERSION_TYPE="minor"; shift ;; 20 | --major) VERSION_TYPE="major"; shift ;; 21 | -h|--help) show_usage; exit 0 ;; 22 | *) echo "Unknown option: $1"; show_usage; exit 1 ;; 23 | esac 24 | done 25 | 26 | increment_version() { 27 | local version=${1#v} 28 | IFS='.' read -ra PARTS <<< "$version" 29 | local major=${PARTS[0]} 30 | local minor=${PARTS[1]:-0} 31 | local patch=${PARTS[2]:-0} 32 | 33 | case $2 in 34 | major) major=$((major + 1)); minor=0; patch=0 ;; 35 | minor) minor=$((minor + 1)); patch=0 ;; 36 | patch) patch=$((patch + 1)) ;; 37 | esac 38 | 39 | echo "$major.$minor.$patch" 40 | } 41 | 42 | # Check git status 43 | if ! git rev-parse --git-dir > /dev/null 2>&1; then 44 | echo "Error: Not in a git repository" 45 | exit 1 46 | fi 47 | 48 | if [[ -n $(git status --porcelain) ]]; then 49 | echo "Error: Uncommitted changes. Commit first." 50 | git status --porcelain 51 | exit 1 52 | fi 53 | 54 | # Get current version from package.json 55 | CURRENT_VERSION=$(node -p "require('./package.json').version") 56 | echo "Current version: $CURRENT_VERSION" 57 | 58 | # Calculate new version 59 | NEW_VERSION=$(increment_version "$CURRENT_VERSION" "$VERSION_TYPE") 60 | echo "New version: $NEW_VERSION" 61 | 62 | read -p "Proceed with version $NEW_VERSION? (y/N): " -n 1 -r 63 | echo 64 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 65 | echo "Cancelled." 66 | exit 0 67 | fi 68 | 69 | # Update package.json 70 | pnpm version "$NEW_VERSION" --no-git-tag-version 71 | git add package.json 72 | 73 | FILES=("README.md" "example/src/Landing.svelte" "example/src/Documentation.svelte") 74 | 75 | # Update version in all files 76 | for file in "${FILES[@]}"; do 77 | sed -i "s/\"preveltekit\": \"\\^[0-9]\+\.[0-9]\+\.[0-9]\+\"/\"preveltekit\": \"^$NEW_VERSION\"/g" "$file" 78 | git add "$file" 79 | done 80 | 81 | pnpm run docs 82 | git add docs/** 83 | git commit -m "release: v$NEW_VERSION" 84 | git tag "v$NEW_VERSION" 85 | git push && git push --tags 86 | 87 | # Run release 88 | pnpm run release 89 | 90 | echo "Released v$NEW_VERSION" -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { PrevelteSSR } from './ssr.js'; 3 | import { readFileSync } from 'node:fs'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { dirname, join } from 'node:path'; 6 | 7 | const args = process.argv.slice(2); 8 | const command = args[0]; 9 | const ssr = new PrevelteSSR(); 10 | 11 | // Get version from package.json 12 | function getVersion():string { 13 | try { 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = dirname(__filename); 16 | const packageJsonPath = join(__dirname, '..', 'package.json'); 17 | const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); 18 | return packageJson.version; 19 | } catch (error) { 20 | return '1.0.0'; // fallback 21 | } 22 | } 23 | 24 | const version = getVersion(); 25 | 26 | function getPort():number { 27 | const portIndex = args.indexOf('-p') !== -1 ? args.indexOf('-p') : args.indexOf('--port'); 28 | return portIndex !== -1 && args[portIndex + 1] ? parseInt(args[portIndex + 1]) : 3000; 29 | } 30 | 31 | function getNoZip(): boolean { 32 | const noZipIndex = args.indexOf('--no-zip'); 33 | return noZipIndex !== -1; 34 | } 35 | 36 | function showHelp() { 37 | console.log(` 38 | PrevelteKit SSR utilities v${version} 39 | 40 | Usage: 41 | preveltekit [options] 42 | 43 | Commands: 44 | prod Run production build and generate SSR HTML 45 | dev Run development server 46 | stage Run staging server 47 | 48 | Options: 49 | -p, --port Port number (default: 3000) 50 | --no-zip Do not compress zip/br/zstd the output files 51 | -h, --help Show help 52 | -v, --version Show version 53 | 54 | Examples: 55 | preveltekit prod 56 | preveltekit dev -p 3000 57 | preveltekit stage --port 8080 58 | `); 59 | } 60 | 61 | async function main() { 62 | try { 63 | if (!command || args.includes('-h') || args.includes('--help')) { 64 | showHelp(); 65 | process.exit(0); 66 | } 67 | 68 | if (args.includes('-v') || args.includes('--version')) { 69 | console.log(version); 70 | process.exit(0); 71 | } 72 | 73 | switch (command) { 74 | case 'prod': 75 | await ssr.generateSSRHtml(getNoZip()); 76 | break; 77 | 78 | case 'dev': 79 | const devPort = getPort(); 80 | const createDevServer = await ssr.createDevServer(); 81 | await createDevServer(devPort); 82 | break; 83 | 84 | case 'stage': 85 | const stagePort = getPort(); 86 | await ssr.generateSSRHtml(getNoZip()); 87 | const createStageServer = ssr.createStageServer(); 88 | createStageServer(stagePort); 89 | break; 90 | 91 | default: 92 | console.error(`Unknown command: ${command}`); 93 | showHelp(); 94 | process.exit(1); 95 | } 96 | } catch (error) { 97 | console.error(error); 98 | process.exit(1); 99 | } 100 | } 101 | 102 | main(); -------------------------------------------------------------------------------- /src/rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rsbuild/core"; 2 | import { pluginSvelte } from "@rsbuild/plugin-svelte"; 3 | import { existsSync, readFileSync } from "node:fs"; 4 | import { fileURLToPath } from "node:url"; 5 | import { dirname, join } from "node:path"; 6 | 7 | // When used as a library, with virtual modules, other users that are using this library can pretend to have 8 | // ./src/index.ts in the ./src directory without having it. If the file exists, the user provided file will 9 | // be used, otherwise this default will be used. Please note, this feature is marked experimental with rspack. 10 | function getVirtualModules() { 11 | const virtualModules: Record = {}; 12 | 13 | const currentFile = fileURLToPath(import.meta.url); 14 | const currentDir = dirname(currentFile); 15 | 16 | if (!existsSync("./src/index.ts")) { 17 | const libraryIndexPath = join(currentDir, "default", "index.ts"); 18 | const content = readFileSync(libraryIndexPath, "utf8"); 19 | virtualModules["./src/index.ts"] = content; 20 | } 21 | 22 | return virtualModules; 23 | } 24 | 25 | export const defaultConfig = defineConfig({ 26 | server: { 27 | publicDir: { 28 | name: "static", // Specify the directory for static assets 29 | copyOnBuild: "auto", // Automatically copy files during production build 30 | watch: process.env.NODE_ENV !== 'production', // Enable file watching during development 31 | }, 32 | }, 33 | environments: { 34 | web: { 35 | plugins: [pluginSvelte()], 36 | source: { 37 | entry: { 38 | index: "./src/index.ts", //default provided see above (virtual modules) 39 | }, 40 | }, 41 | output: { 42 | target: "web", 43 | }, 44 | }, 45 | }, 46 | dev: { hmr: false }, //I had issues with hmr in the past, easiest to disable it 47 | html: { template: "./src/index.html" }, //default provided, see ssr.ts 48 | output: { 49 | assetPrefix: "./", 50 | distPath: { 51 | root: 'dist', 52 | js: 'js', 53 | css: 'css', 54 | svg: 'svg', 55 | font: 'font', 56 | image: 'image', 57 | media: 'media', 58 | assets: 'assets', 59 | html: '.', 60 | }, 61 | }, //create relative paths, to run in subdirectories 62 | tools: { 63 | //tools only exist here due to virtual modules, needs restart when changed 64 | rspack: async (config) => { 65 | const virtualModules = getVirtualModules(); 66 | 67 | // Only create VirtualModulesPlugin if we have virtual modules to add 68 | if (Object.keys(virtualModules).length > 0) { 69 | const { rspack } = await import("@rspack/core"); 70 | const { VirtualModulesPlugin } = rspack.experiments; 71 | 72 | config.plugins = config.plugins || []; 73 | config.plugins.push(new VirtualModulesPlugin(virtualModules)); 74 | } 75 | 76 | return config; 77 | }, 78 | cssLoader: { 79 | url: { 80 | filter: (url) => { 81 | // Don't process absolute paths starting with / 82 | if (url.startsWith('/')) { 83 | return false; 84 | } 85 | return true; 86 | }, 87 | }, 88 | }, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /static/SSR.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /example/src/assets/SSR.svg: -------------------------------------------------------------------------------- 1 | https://github.com/tbocek/preveltekit -------------------------------------------------------------------------------- /example/src/Example.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 |

Bitcoin Price Tracker ({renderInfo})

44 | 45 |
46 | {#await pricePromise} 47 |

Loading...

48 | {:then priceData} 49 |
50 | {priceData.RAW.FROMSYMBOL} 51 | 52 |
53 |

{priceData.RAW.TOSYMBOL} {priceData.RAW.PRICE.toFixed(2)}

54 | 55 | Prices are volatile and for reference only. Not financial advice. 56 | 57 | {:catch error} 58 |

Error: {error.message}

59 | 60 | {/await} 61 |
62 |
63 | 64 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import type { Routes } from "./types"; 2 | 3 | export function setupStaticRoutes(routes: Routes): void { 4 | if (window.__isBuildTime) { 5 | window.__svelteRoutes = routes; 6 | } 7 | } 8 | 9 | export function navigate(path: string): void { 10 | history.pushState(null, "", path); 11 | window.dispatchEvent(new CustomEvent('svelteNavigate', { detail: { path } })); 12 | } 13 | 14 | export function link(node: HTMLAnchorElement): { destroy: () => void } { 15 | const handleClick = (event: MouseEvent) => { 16 | // Only handle if it's a left-click without modifier keys 17 | if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 18 | event.preventDefault(); 19 | 20 | // Get the href from the anchor tag 21 | const href = node.getAttribute('href') || ''; 22 | 23 | // Handle external links 24 | const isExternal = 25 | href.startsWith('http://') || 26 | href.startsWith('https://') || 27 | href.startsWith('//') || 28 | node.hasAttribute('external'); 29 | 30 | if (!isExternal) { 31 | // Determine the path based on whether it's absolute or relative 32 | let path; 33 | 34 | if (href.startsWith('/')) { 35 | // Absolute path - use as is 36 | path = href; 37 | } else if (href === '' || href === '#') { 38 | // Empty href or hash only - stay on current page 39 | path = window.location.pathname; 40 | } else { 41 | // Relative path - combine with current path 42 | const currentPath = window.location.pathname; 43 | 44 | // Ensure the current path ends with a slash if not the root 45 | const basePath = currentPath === '/' 46 | ? '/' 47 | : currentPath.endsWith('/') 48 | ? currentPath 49 | : currentPath + '/'; 50 | 51 | // Combine base path with relative href 52 | path = basePath + href; 53 | } 54 | 55 | // Clean up any double slashes (except after protocol) 56 | path = path.replace(/([^:]\/)\/+/g, '$1'); 57 | 58 | // Handle relative paths with ../ 59 | if (path.includes('../')) { 60 | const segments = path.split('/'); 61 | const cleanSegments = []; 62 | 63 | for (const segment of segments) { 64 | if (segment === '..') { 65 | // Go up one level by removing the last segment 66 | if (cleanSegments.length > 1) { // Ensure we don't go above root 67 | cleanSegments.pop(); 68 | } 69 | } else if (segment !== '' && segment !== '.') { 70 | // Add non-empty segments that aren't current directory 71 | cleanSegments.push(segment); 72 | } 73 | } 74 | 75 | // Reconstruct the path 76 | path = '/' + cleanSegments.join('/'); 77 | } 78 | 79 | // Navigate to the computed path 80 | navigate(path); 81 | } else { 82 | // For external links, just follow the href 83 | window.location.href = href; 84 | } 85 | } 86 | }; 87 | 88 | // Add event listener 89 | node.addEventListener('click', handleClick); 90 | 91 | // Return the destroy method 92 | return { 93 | destroy() { 94 | node.removeEventListener('click', handleClick); 95 | } 96 | }; 97 | } -------------------------------------------------------------------------------- /example/src/Index.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 117 | 118 |
119 | 138 | 139 |
140 |
141 | 142 |
143 |
144 | 145 |
146 | 149 |
150 |
151 | 152 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | PrevelteKit - default index.html 3 | 4 | 5 | 6 |

PrevelteKit (Pre-rendered)

This content was pre-rendered at build time and will hydrate on load

  • Static-first architecture - Deploy frontend anywhere as pure HTML/CSS/JS
  • Lightning fast builds - Hundreds of milliseconds with Rsbuild
  • No JavaScript runtime in production for frontend - Just static files on a server
(These buttons here are client-side routing • No page reloads)

Lightning Fast Builds

Rsbuild bundles in hundreds of milliseconds

🎯

Minimalistic

Less than 500 lines of code - just glue for Svelte, Rsbuild, 7 | and jsdom

🔄

HTML for Initial Page

Single Page App with Built-time Pre-Rendering - best of both 8 | worlds

🚀

Deploy Anywhere

GitHub Pages, S3, any web server - just static files

🛡️

Clear Separation

Frontend as static assets, backend as dedicated APIs

📦

Zero Config

Works out of the box with sensible defaults

Quick Start

mkdir -p preveltekit/src && cd preveltekit
 9 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
10 | npm install
11 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
12 | npm run dev
13 | # And open a browser with localhost:3000
14 | 15 | -------------------------------------------------------------------------------- /docs/css/index.e62340e7.css: -------------------------------------------------------------------------------- 1 | body,html{height:100%;margin:0;padding:0;font-family:Roboto,sans-serif;font-weight:300}.hero.svelte-1nllfry{max-width:1200px;margin:0 auto;padding:2rem 1rem}.hero-content.svelte-1nllfry{text-align:center;margin-bottom:4rem}h1.svelte-1nllfry{color:#2d3748;margin-bottom:1rem;font-size:3.5rem;font-weight:700}.subtitle.svelte-1nllfry{color:#4a5568;max-width:600px;margin-bottom:2rem;margin-left:auto;margin-right:auto;font-size:1.25rem}.key-benefits.svelte-1nllfry{text-align:left;max-width:600px;margin:2rem auto;padding-left:1.5rem;list-style:outside;display:inline-block}.benefit.svelte-1nllfry{color:#4a5568;margin-bottom:.75rem;font-size:1.1rem}.benefit.svelte-1nllfry strong:where(.svelte-1nllfry){color:#2d3748}.cta-buttons.svelte-1nllfry{flex-wrap:wrap;justify-content:center;gap:1rem;margin:2rem 0;display:flex}.cta-button.svelte-1nllfry{border-radius:8px;padding:.875rem 2rem;font-size:1.1rem;font-weight:600;text-decoration:none;transition:all .2s;display:inline-block}.cta-button.svelte-1nllfry:hover{transform:translateY(-2px);box-shadow:0 4px 12px #00000026}.primary.svelte-1nllfry{color:#fff;background:#4299e1}.secondary.svelte-1nllfry{color:#4299e1;background:#fff;border:2px solid #4299e1}.routing-note.svelte-1nllfry{color:#718096;margin-top:1rem;font-size:.875rem}.features-section.svelte-1nllfry{margin:4rem 0}.feature-grid.svelte-1nllfry{grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:2rem;display:grid}.feature.svelte-1nllfry{text-align:center;background:#fff;border-radius:12px;padding:2rem;transition:transform .2s;box-shadow:0 2px 8px #0000001a}.feature.svelte-1nllfry:hover{transform:translateY(-4px)}.feature-icon.svelte-1nllfry{margin-bottom:1rem;font-size:3rem}.feature.svelte-1nllfry h3:where(.svelte-1nllfry){color:#2d3748;margin-bottom:1rem;font-size:1.25rem}.feature.svelte-1nllfry p:where(.svelte-1nllfry){color:#4a5568;line-height:1.6}.quick-start.svelte-1nllfry{text-align:center;margin:4rem 0}.quick-start.svelte-1nllfry h2:where(.svelte-1nllfry){color:#2d3748;margin-bottom:2rem;font-size:2rem}.code-block.svelte-1nllfry{text-align:left;color:#e2e8f0;background:#1a202c;border-radius:8px;margin:1rem 0;padding:1rem;font-family:monospace;font-size:.8rem;overflow-x:auto}@media (max-width:768px){h1.svelte-1nllfry{font-size:2.5rem}.subtitle.svelte-1nllfry{font-size:1.1rem}.cta-buttons.svelte-1nllfry{flex-direction:column;align-items:center}.cta-button.svelte-1nllfry{width:200px}}.docs.svelte-an6hzx{max-width:800px;margin:0 auto;padding:2rem 1rem}header.svelte-an6hzx{text-align:center;margin-bottom:3rem}h1.svelte-an6hzx{color:#2d3748;margin-bottom:.5rem;font-size:2.5rem}.render-info.svelte-an6hzx{color:#718096;font-size:.875rem}section.svelte-an6hzx{margin-bottom:3rem}h2.svelte-an6hzx{color:#2d3748;border-bottom:2px solid #e2e8f0;margin-bottom:1.5rem;padding-bottom:.5rem;font-size:1.8rem}h3.svelte-an6hzx{color:#2d3748;margin:2rem 0 1rem;font-size:1.3rem}p.svelte-an6hzx{color:#4a5568;margin-bottom:1rem;line-height:1.6}ul.svelte-an6hzx{color:#4a5568;margin:1rem 0;padding-left:1.5rem;list-style-type:disc}li.svelte-an6hzx{margin-bottom:.5rem;line-height:1.6}strong.svelte-an6hzx{color:#2d3748;font-weight:600}code.svelte-an6hzx{background:#edf2f7;border-radius:4px;padding:.2rem .4rem;font-family:monospace;font-size:.8rem}.code-block.svelte-an6hzx{color:#e2e8f0;background:#1a202c;border-radius:8px;margin:1rem 0;padding:1rem;font-family:monospace;font-size:.8rem;overflow-x:auto}.code-block.svelte-an6hzx pre:where(.svelte-an6hzx){margin:0;font-family:monospace;font-size:.8rem;color:#e2e8f0!important}.code-block.svelte-an6hzx code:where(.svelte-an6hzx){padding:0;font-family:monospace;font-size:.8rem;line-height:1.5;color:#e2e8f0!important;background:0 0!important}.comparison-table.svelte-an6hzx{margin:1rem 0;overflow-x:auto}table.svelte-an6hzx{border-collapse:collapse;background:#fff;border-radius:8px;width:100%;overflow:hidden;box-shadow:0 2px 10px #0000001a}th.svelte-an6hzx,td.svelte-an6hzx{text-align:left;border-bottom:1px solid #e2e8f0;padding:1rem}th.svelte-an6hzx{color:#2d3748;background:#f7fafc;font-weight:600}td.svelte-an6hzx{color:#4a5568;vertical-align:top}small.svelte-an6hzx{color:#718096;margin-top:.25rem;display:block}.comparison-img.svelte-an6hzx{border-radius:4px;width:120px;height:auto;margin-bottom:.5rem}.container.svelte-18byggp{max-width:600px;margin:2rem auto;padding:1rem}h2.svelte-18byggp{text-align:center;margin-bottom:1.5rem}small.svelte-18byggp{color:#666;font-size:.875rem}.card.svelte-18byggp{text-align:center;background:#fff;border-radius:8px;padding:2rem;box-shadow:0 2px 4px #0000001a}.price-info.svelte-18byggp{justify-content:space-between;margin-bottom:1rem;display:flex}.price.svelte-18byggp{margin:1rem 0;font-size:2.5rem;font-weight:700}.disclaimer.svelte-18byggp{color:#666;border-top:1px solid #eee;margin-top:1rem;padding-top:1rem;display:block}.error.svelte-18byggp{color:#e53e3e}button.svelte-18byggp{color:#fff;cursor:pointer;background:#e53e3e;border:none;border-radius:4px;margin-top:1rem;padding:.5rem 1rem}.app.svelte-15izthb{background-color:#f7fafc;min-height:100vh}nav.svelte-15izthb{background-color:#fff;box-shadow:0 1px 3px #0000001a}.nav-content.svelte-15izthb{max-width:80rem;margin:0 auto;padding:0 1rem}.nav-items.svelte-15izthb{align-items:center;gap:3rem;height:4rem;display:flex}.logo.svelte-15izthb{color:#4299e1;font-size:1.5rem;font-weight:700}.nav-links.svelte-15izthb{gap:3rem;display:flex}.nav-links.svelte-15izthb a:where(.svelte-15izthb){color:#4299e1;padding:.5rem 0;font-size:1.1rem;font-weight:500;text-decoration:none;transition:color .2s}.nav-links.svelte-15izthb a:where(.svelte-15izthb):hover{color:#2b6cb0}.server-side-indicator.svelte-15izthb{color:#93c5fd;align-items:center;margin-left:.5rem;font-size:.875rem;display:flex}main.svelte-15izthb{max-width:80rem;margin:0 auto;padding:1.5rem 1rem}.content.svelte-15izthb{padding:1rem 0}footer.svelte-15izthb{background-color:#fff;margin-top:3rem}.footer-content.svelte-15izthb{text-align:center;color:#718096;max-width:80rem;margin:0 auto;padding:1.5rem 1rem;font-size:.875rem} -------------------------------------------------------------------------------- /example/src/Landing.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |

{message}

16 |

{subtitle}

17 | 18 |
    19 |
  • 20 | Static-first architecture - Deploy frontend anywhere as pure HTML/CSS/JS 21 |
  • 22 |
  • 23 | Lightning fast builds - Hundreds of milliseconds with Rsbuild 24 |
  • 25 |
  • 26 | No JavaScript runtime in production for frontend - Just static files on a server 27 |
  • 28 |
29 | 30 |
31 | Documentation 32 | Bitcoin Demo 35 |
36 | 37 |
38 | (These buttons here are client-side routing • No page reloads) 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |

Lightning Fast Builds

47 |

Rsbuild bundles in hundreds of milliseconds

48 |
49 | 50 |
51 |
🎯
52 |

Minimalistic

53 |

54 | Less than 500 lines of code - just glue for Svelte, Rsbuild, 55 | and jsdom 56 |

57 |
58 | 59 |
60 |
🔄
61 |

HTML for Initial Page

62 |

63 | Single Page App with Built-time Pre-Rendering - best of both 64 | worlds 65 |

66 |
67 | 68 |
69 |
🚀
70 |

Deploy Anywhere

71 |

GitHub Pages, S3, any web server - just static files

72 |
73 | 74 |
75 |
🛡️
76 |

Clear Separation

77 |

Frontend as static assets, backend as dedicated APIs

78 |
79 | 80 |
81 |
📦
82 |

Zero Config

83 |

Works out of the box with sensible defaults

84 |
85 |
86 |
87 | 88 |
89 |

Quick Start

90 |
91 | {@html `
mkdir -p preveltekit/src && cd preveltekit
 92 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
 93 | npm install
 94 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
 95 | npm run dev
 96 | # And open a browser with localhost:3000
`} 97 |
98 |
99 |
100 | 101 | 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PrevelteKit 2 | PrevelteKit is a minimalistic (>500 LoC) web application framework built on [Svelte 5](https://svelte.dev/), featuring single page applications with built-time pre-rendering using [Rsbuild](https://rsbuild.dev/) as the build/bundler tool and [jsdom](https://github.com/jsdom/jsdom) as the DOM environment for pre-rendering components during build. 3 | 4 | The inspiration for this project comes from the Vue SSR example in the [Rspack examples repository](https://github.com/rspack-contrib/rspack-examples/blob/main/rsbuild/ssr-express/prod-server.mjs). This project adapts those concepts for Svelte, providing a minimal setup. 5 | 6 | ## Why PrevelteKit? 7 | While there is a go-to solution for SSR for Svelte (SvelteKit), I was missing a minimalistic solution just for build-time pre-rendering. There is the prerender option in SvelteKit, but it's part of SvelteKit that comes with many additional features that might not be necessary for every project. 8 | 9 | From an architectural point of view, I prefer the clear separation between view code and server code, where the frontend requests data from the backend via dedicated /api endpoints. This approach treats the frontend as purely static assets (HTML/CSS/JS) that can be served from any CDN or simple web server. 10 | 11 | Meta-frameworks such as Next.js, Nuxt.js, SvelteKit blur this separation by requiring a JavaScript runtime (Node.js, Deno, or Bun) to handle server-side rendering, API routes, and build-time generation. While platforms like Vercel and Netlify can help with handling this complex setup (they are great services that I used in the past), serving just static content is much simpler: deploy anywhere (GitHub Pages, S3, any web serve) with predictable performance. You avoid the "full-stack JavaScript" complexity for your deployed frontend - it's just files on a server, nothing more. 12 | 13 | ## Why Not SvelteKit + adapter-static? 14 | While SvelteKit with adapter-static can achieve similar static site generation, PrevelteKit offers a minimalistic alternative using Svelte + jsdom + Rsbuild. At less than 500 lines of code, it's essentially glue code between these libraries rather than a full framework. This provides a lightweight solution for those who want static pre-rendering without SvelteKit's additional complexity and features. 15 | 16 | ## Why Rsbuild and not Vite? 17 | While [benchmarks](https://github.com/rspack-contrib/build-tools-performance) show that Rsbuild and Vite (Rolldown + Oxc) have comparable overall performance in many cases (not for the 10k component case - which I do not have in my projects), Rsbuild has a small advantage in producing the smallest compressed bundle size, while Vite (Rolldown + Oxc) have a small advantage in build time performance. 18 | 19 | In my experience, Rsbuild "just works" after many updates out of the box with minimal configuration, which reduced friction and setup time. However, I am watching Vite (Rolldown + Oxc) closely, as they are progressing fast. 20 | 21 | ## Links 22 | * [npm](https://www.npmjs.com/package/preveltekit) 23 | * [github](https://github.com/tbocek/preveltekit) 24 | * [github.io](https://tbocek.github.io/preveltekit) 25 | 26 | ## Key Features 27 | * ⚡️ Lightning Fast: Rsbuild bundles in the range of a couple hundred milliseconds 28 | * 🎯 Simple Routing: Built-in routing system 29 | * 🔄 Layout and staic content pre-rendered: With Svelte and hydration 30 | * 📦 Zero Config: Works out of the box with sensible defaults 31 | * 🛠️ Developer Friendly: Hot reload in development, production-ready in minutes 32 | * 🛡️ Security: Docker-based development environments to protect against supply chain attacks 33 | 34 | ## Automatic Fetch Handling 35 | 36 | PrevelteKit automatically manages fetch requests during build-time pre-rendering: 37 | - Components render with loading states in the pre-rendered HTML 38 | - No need to wrap fetch calls in `window.__isBuildTime` checks 39 | - Use Svelte's `{#await}` blocks for clean loading/error/success states 40 | - If anything went missing, in the worst case, fetch calls timeout after 5 seconds during pre-rendering 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Rendering Type
Initial LoadAfter Script Execution
SSR (classic SSR / Next.js / Nuxt)

User sees fully rendered content instantly

Content remains the same, scripts add interactivity
SPA (React App / pure Svelte)

User sees blank page or loading spinner

User sees full interactive content
SPA + Build-time Pre-Rendering (this approach)

User sees pre-rendered static content

Content becomes fully interactive
72 | 73 | ## Prerequisites 74 | Make sure you have the following installed: 75 | - Node.js (Latest LTS version recommended) 76 | - npm/pnpm or similar 77 | 78 | ## Quick Start 79 | 80 | ### Install 81 | ```bash 82 | # Create test directory and go into this directory 83 | mkdir -p preveltekit/src && cd preveltekit 84 | # Declare dependency and the dev script 85 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json 86 | # Download dependencies 87 | npm install 88 | # A very simple svelte file 89 | echo '

Count: {count}

' > src/Index.svelte 90 | # And open a browser with localhost:3000 91 | npm run dev 92 | ``` 93 | 94 | ## Slow Start 95 | 96 | One example is within this project in the example folder, and another example is the [notary example](https://github.com/tbocek/notary-example). The cli supports the following: dev/stage/prod. 97 | 98 | ### Start the development server 99 | ```bash 100 | npm run dev 101 | ``` 102 | This starts an Express development server on http://localhost:3000, with: 103 | - Live reloading 104 | - No optimization for faster builds 105 | - Ideal for rapid development 106 | 107 | ### Build for production 108 | ```bash 109 | npm run build 110 | ``` 111 | The production build: 112 | - Generates pre-compressed static files for optimal serving with best compression: 113 | - Brotli (`.br` files) 114 | - Zstandard (`.zst` files) 115 | - Zopfli (`.gz` files) 116 | - Optimizes assets for production 117 | 118 | ### Staging Environment 119 | ```bash 120 | npm stage 121 | ``` 122 | The development server prioritizes fast rebuilds and developer experience, while the production build focuses on optimization and performance. Always test your application with a stage and production build before deploying. 123 | 124 | ## 🐳 Docker Support 125 | To build with docker in production mode, use: 126 | ```bash 127 | docker build . -t preveltekit 128 | docker run -p3000:3000 preveltekit 129 | ``` 130 | 131 | To run in development mode with live reloading, run: 132 | ```bash 133 | docker build -f Dockerfile.dev . -t preveltekit-dev 134 | docker run -p3000:3000 -v./src:/app/src preveltekit-dev 135 | ``` 136 | 137 | ## Configuration 138 | PrevelteKit uses rsbuild.config.ts for configuration with sensible defaults. To customize settings, create an rsbuild.config.ts file in your project - it will merge with the default configuration. 139 | 140 | The framework provides fallback files (index.html and index.ts) from the default folder when you don't supply your own. Once you add your own index.html or index.ts files, PrevelteKit uses those instead, ignoring the defaults. 141 | 142 | This approach follows a "convention over configuration" pattern where you only need to specify what differs from the defaults. 143 | -------------------------------------------------------------------------------- /src/Router.svelte: -------------------------------------------------------------------------------- 1 | 201 | 202 | {#if ActiveComponent} 203 | 204 | {:else} 205 |

404 - Page Not Found for [{currentRoute}]

206 | {/if} 207 | -------------------------------------------------------------------------------- /src/ssr.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { JSDOM, ResourceLoader, VirtualConsole } from 'jsdom'; 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import { fileURLToPath } from 'node:url'; 6 | import express from 'express'; 7 | import { createRsbuild, loadConfig } from '@rsbuild/core'; 8 | import type { Routes } from './types.js'; 9 | import { mergeRsbuildConfig } from '@rsbuild/core'; 10 | import type { DistPathConfig } from '@rsbuild/core'; 11 | import { defaultConfig } from './rsbuild.config.js'; 12 | 13 | class LocalResourceLoader extends ResourceLoader { 14 | constructor(private resourceFolder?: string) { 15 | super(); 16 | } 17 | 18 | fetch(url: string, options: any = {}) { 19 | if (!this.resourceFolder) { 20 | return super.fetch(url, options); 21 | } 22 | 23 | const urlPath = new URL(url).pathname; 24 | const localPath = path.join(process.cwd(), this.resourceFolder, urlPath.replace(/^\//, '')); 25 | 26 | const promise = fs.promises.access(localPath) 27 | .then(() => fs.promises.readFile(localPath)) 28 | .then(content => Buffer.from(content)) 29 | .catch(() => super.fetch(url, options)); 30 | 31 | // Add abort method to match AbortablePromise interface 32 | (promise as any).abort = () => {}; 33 | 34 | return promise as any; 35 | } 36 | } 37 | 38 | class FetchWrapper { 39 | async fetch(_url: string, _init?: RequestInit): Promise { 40 | return new Promise(() => { 41 | // Promise stays open forever 42 | }); 43 | } 44 | } 45 | 46 | class RequestWrapper { 47 | constructor(input: RequestInfo | URL, init?: RequestInit) { 48 | // Resolve relative URLs to absolute URLs using the document's base URL 49 | if (typeof input === 'string' && !input.startsWith('http://') && !input.startsWith('https://')) { 50 | const baseURL = globalThis.location?.href || 'http://localhost/'; 51 | input = new URL(input, baseURL).href; 52 | } 53 | 54 | // If there's a signal from JSDOM, remove it before creating Node's Request 55 | if (init?.signal) { 56 | const { signal, ...restInit } = init; 57 | return new Request(input, restInit); 58 | } 59 | return new Request(input, init); 60 | } 61 | } 62 | 63 | async function fakeBrowser(ssrUrl: string, html: string, resourceFolder?: string, timeout = 5000): Promise { 64 | const virtualConsole = new VirtualConsole(); 65 | // ** for debugging ** 66 | virtualConsole.forwardTo(console); 67 | virtualConsole.on("jsdomError", (e:any) => { 68 | if (e.type === "not-implemented" && e.message.includes("navigation")) { 69 | } else { 70 | console.error("jsdomError", e); 71 | } 72 | }); 73 | 74 | const dom = new JSDOM(html, { 75 | url: ssrUrl, 76 | pretendToBeVisual: true, 77 | runScripts: 'dangerously', 78 | resources: new LocalResourceLoader(resourceFolder), 79 | virtualConsole, 80 | }); 81 | 82 | // Inject fetch and TextEncoder into the JSDOM window 83 | const fetchWrapper = new FetchWrapper(); 84 | dom.window.fetch = fetchWrapper.fetch.bind(fetchWrapper); 85 | dom.window.Request = RequestWrapper; 86 | dom.window.Response = Response; 87 | dom.window.Headers = Headers; 88 | dom.window.FormData = FormData; 89 | dom.window.Blob = Blob; 90 | dom.window.TextEncoder = TextEncoder; 91 | dom.window.TextDecoder = TextDecoder; 92 | dom.window.Element.prototype.animate = (() => { 93 | return new Proxy({}, { 94 | get: (_, prop) => { 95 | if (prop === 'finished' || prop === 'ready') return Promise.resolve(); 96 | return () => {}; 97 | } 98 | }); 99 | }) as any; 100 | dom.window.WebSocket = class FakeWebSocket { 101 | constructor() {} 102 | send() {} 103 | close() {} 104 | addEventListener() {} 105 | removeEventListener() {} 106 | } as any; 107 | dom.window.__isBuildTime = true; 108 | 109 | return new Promise((resolve, reject) => { 110 | let isResolved = false; 111 | const timeoutHandle = setTimeout(() => { 112 | if (!isResolved) { 113 | isResolved = true; 114 | reject(new Error('Timeout waiting for resources to load')); 115 | } 116 | }, timeout); 117 | 118 | try { 119 | const allScripts = Array.from(dom.window.document.querySelectorAll('script')) as HTMLScriptElement[]; 120 | let loadedScripts = 0; 121 | 122 | function cleanup() { 123 | clearTimeout(timeoutHandle); 124 | } 125 | 126 | function handleLoadComplete() { 127 | if (loadedScripts === allScripts.length) { 128 | const marker = 'SCRIPTS_EXECUTED_' + Date.now(); 129 | const markComplete = dom.window.document.createElement('script'); 130 | markComplete.setAttribute('data-marker', 'true'); 131 | 132 | markComplete.textContent = ` 133 | Promise.resolve().then(() => { 134 | return new Promise(resolve => setTimeout(resolve, 0)); 135 | }).then(() => { 136 | window['${marker}'] = true; 137 | }); 138 | `; 139 | 140 | dom.window.document.body.appendChild(markComplete); 141 | 142 | let checkCount = 0; 143 | const maxChecks = 500; 144 | 145 | const checkExecution = () => { 146 | if (dom.window[marker]) { 147 | if (!isResolved) { 148 | isResolved = true; 149 | cleanup(); 150 | const markerScript = dom.window.document.querySelector('script[data-marker="true"]'); 151 | if (markerScript) { 152 | markerScript.remove(); 153 | } 154 | resolve(dom); 155 | } 156 | } else if (checkCount++ < maxChecks) { 157 | setTimeout(checkExecution, 10); 158 | } else { 159 | if (!isResolved) { 160 | isResolved = true; 161 | cleanup(); 162 | reject(new Error('Script execution check timed out')); 163 | } 164 | } 165 | }; 166 | 167 | checkExecution(); 168 | } 169 | } 170 | 171 | function handleLoad() { 172 | loadedScripts++; 173 | handleLoadComplete(); 174 | } 175 | 176 | function handleError(error: any) { 177 | if (!isResolved) { 178 | isResolved = true; 179 | cleanup(); 180 | reject(error); 181 | } 182 | } 183 | 184 | allScripts.forEach(script => { 185 | script.addEventListener('load', handleLoad); 186 | script.addEventListener('error', handleError); 187 | }); 188 | 189 | if (allScripts.length === 0) { 190 | dom.window.addEventListener('load', () => { 191 | if (!isResolved) { 192 | isResolved = true; 193 | cleanup(); 194 | resolve(dom); 195 | } 196 | }); 197 | } 198 | 199 | } catch (error) { 200 | if (!isResolved) { 201 | isResolved = true; 202 | clearTimeout(timeoutHandle); 203 | reject(error); 204 | } 205 | } 206 | }); 207 | } 208 | 209 | export class PrevelteSSR { 210 | private async createCustomRsbuild() { 211 | const { content } = await loadConfig(); 212 | const finalConfig = mergeRsbuildConfig(defaultConfig, content); 213 | const currentDir = path.dirname(fileURLToPath(import.meta.url)); 214 | const libraryDir = path.join(currentDir, 'default'); 215 | if (!fs.existsSync('./src/index.html')) { 216 | finalConfig.html!.template = path.join(libraryDir, 'index.html'); 217 | } 218 | const rsbuild = await createRsbuild({ rsbuildConfig: finalConfig }); 219 | return rsbuild; 220 | } 221 | 222 | private async compressFiles(distPath:string) { 223 | const { exec } = await import('child_process'); 224 | const { promisify } = await import('util'); 225 | const execAsync = promisify(exec); 226 | 227 | try { 228 | await execAsync( 229 | `find ${distPath} -regex '.*\\.\\(js\\|css\\|html\\|svg\\)$' -exec sh -c 'zopfli {} & brotli -f {} & zstd -19f {} > /dev/null 2>&1 & wait' \\;` 230 | ); 231 | console.log('Files compressed with brotli, zopfli, and zstd'); 232 | } catch (error) { 233 | console.warn('Compression failed:', error); 234 | } 235 | } 236 | 237 | private getDistRoot = (distPath: string | DistPathConfig | undefined): string => { 238 | if (!distPath) return 'dist'; 239 | if (typeof distPath === 'string') return distPath; 240 | return distPath.root || 'dist'; 241 | }; 242 | 243 | async generateSSRHtml(noZip: boolean) { 244 | const rsbuild = await this.createCustomRsbuild(); 245 | await rsbuild.build(); 246 | const config = rsbuild.getRsbuildConfig(); 247 | 248 | const indexFileName = `${this.getDistRoot(config?.output?.distPath)}/index.html`; 249 | const indexHtml = await fs.promises.readFile(path.join(process.cwd(), indexFileName), "utf-8"); 250 | const dom = await fakeBrowser('http://localhost/', indexHtml, this.getDistRoot(config?.output?.distPath)); 251 | 252 | const processedDoms = new Map(); 253 | processedDoms.set('index.html', dom); 254 | 255 | const svelteRoutes = dom.window.__svelteRoutes as Routes; 256 | if (svelteRoutes?.staticRoutes) { //we may not have svelteRoutes or staticRoutes 257 | const promises: Promise[] = []; 258 | 259 | for (const route of svelteRoutes.staticRoutes) { 260 | if (processedDoms.has(route.htmlFilename)) continue; 261 | 262 | const promise = (async () => { 263 | try { 264 | const dom = await fakeBrowser(`http://localhost${route.path}`, indexHtml, this.getDistRoot(config?.output?.distPath)); 265 | processedDoms.set(route.htmlFilename, dom); 266 | } catch (error) { 267 | console.error(`Error processing route ${route.path}:`, error); 268 | } 269 | })(); 270 | 271 | promises.push(promise); 272 | } 273 | 274 | await Promise.all(promises); 275 | } 276 | 277 | for (const [htmlFilename, dom] of processedDoms.entries()) { 278 | const fileName = `${this.getDistRoot(config?.output?.distPath)}/${htmlFilename}`; 279 | const finalHtml = dom.serialize(); 280 | await fs.promises.writeFile(fileName, finalHtml); 281 | console.log(`Generated ${fileName}`); 282 | dom.window.close(); 283 | } 284 | 285 | if (!noZip && process.env.NODE_ENV === 'production') { 286 | const distPath = this.getDistRoot(config?.output?.distPath) || 'dist'; 287 | await this.compressFiles(distPath as string); 288 | } 289 | } 290 | 291 | async createDevServer() { 292 | const rsbuild = await this.createCustomRsbuild(); 293 | const rsbuildServer = await rsbuild.createDevServer(); 294 | const template = await rsbuildServer.environments.web.getTransformedHtml("index"); 295 | return async (port = 3000) => { 296 | const app = express(); 297 | const staticDir = path.resolve(process.cwd(), "static"); 298 | 299 | // Get generated assets from stats 300 | const stats = await rsbuildServer.environments.web.getStats(); 301 | const { assets } = stats.toJson({ all: false, assets: true }); 302 | const rsbuildAssets = new Set(assets?.map(a => `/${a.name}`) || []); 303 | 304 | app.use(async (req, res, next) => { 305 | if (req.url.includes("rsbuild-hmr?token=")) { 306 | return next(); 307 | } 308 | 309 | const urlPath = req.url.split('?')[0]; // strip query params 310 | if (rsbuildAssets.has(urlPath)) { 311 | console.debug('DEV request (rsbuild asset):', req.url); 312 | return next(); 313 | } 314 | 315 | if (/\.(svg|png|jpg|jpeg)$/i.test(req.url)) { 316 | const requestedPath = req.url.replace(/^\//, ""); // strip leading slash 317 | const filePath = path.join(staticDir, requestedPath); 318 | 319 | // prevent path traversal 320 | if (!filePath.startsWith(staticDir)) { 321 | console.debug('DEV request (forbidden):', filePath); 322 | return res.status(403).send("Forbidden"); 323 | } 324 | 325 | try { 326 | await fs.promises.access(filePath, fs.constants.R_OK); 327 | return res.sendFile(filePath); 328 | } catch { 329 | console.debug('DEV request (file not found):', filePath); 330 | return res.status(404).send("Not Found"); 331 | } 332 | } 333 | 334 | try { 335 | const dom = await fakeBrowser(`${req.protocol}://${req.get('host')}${req.url}`, template); 336 | try { 337 | const svelteRoutes = dom.window.__svelteRoutes as Routes; 338 | if (svelteRoutes?.staticRoutes) { //we may not have svelteRoutes or staticRoutes 339 | for (const route of svelteRoutes.staticRoutes) { 340 | if (req.url.startsWith(route.path)) { 341 | console.debug('DEV request (hydration):', req.url); 342 | const html = dom.serialize(); 343 | res.writeHead(200, { 'Content-Type': 'text/html' }); 344 | res.end(html); 345 | return; // stop here, do not continue to next middleware 346 | } 347 | } 348 | } 349 | } finally { 350 | dom.window.close(); 351 | } 352 | } catch (err) { 353 | console.error(`SSR render error, downgrade to CSR for [${req.url}]`, err); 354 | } 355 | console.debug('DEV request (default rsbuild):', req.url); 356 | return next(); 357 | }) 358 | app.use(rsbuildServer.middlewares); 359 | 360 | const httpServer = app.listen(port, async () => { 361 | await rsbuildServer.afterListen(); 362 | rsbuildServer.connectWebSocket({ server: httpServer }); 363 | console.log(`Dev server running on port ${port}`); 364 | }); 365 | 366 | return { server: httpServer, rsbuildServer }; 367 | }; 368 | } 369 | 370 | private listHtmlFiles(folder: string): string[] { 371 | return fs.readdirSync(folder) 372 | .filter(file => file.endsWith('.html')) 373 | .map(file => path.basename(file, '.html')); 374 | } 375 | 376 | createStageServer() { 377 | return (port = 3000) => { 378 | const app = express(); 379 | 380 | app.get('/', (req, _, next) => { 381 | req.url = '/index.html'; 382 | return next(); 383 | }); 384 | 385 | const htmlFolder = path.join(process.cwd(), 'dist'); 386 | const htmlFiles = this.listHtmlFiles(htmlFolder); 387 | htmlFiles.forEach((filename) => { 388 | app.get(`/${filename}`, (req, _, next) => { 389 | req.url = `/${filename}.html`; 390 | return next(); 391 | }); 392 | }); 393 | 394 | app.use(express.static('dist')); 395 | return app.listen(port, () => { 396 | console.log(`Stage server running on port ${port}`); 397 | }); 398 | }; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /example/src/Documentation.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 |

PrevelteKit Documentation

15 |

({renderType})

16 |
17 | 18 |
19 |

Quick Start

20 | Make sure you have node/npm installed. Here is a minimalistic example: 21 |
22 | {@html `
mkdir -p preveltekit/src && cd preveltekit
 23 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
 24 | npm install
 25 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
 26 | npm run dev
 27 | # And open a browser with localhost:3000
`} 28 |
29 |
30 | 31 |
32 |

Core Features

33 | 34 |

35 | ⚡ Single Page Application with Built-time Pre-rendering (SPAwBR) 36 |

37 |

38 | PrevelteKit combines the best of SPA and build-time rendering with 39 | hydration approaches. Unlike traditional SSR that renders on each 40 | request, or pure SPA that shows blank pages initially, SPAwBR 41 | pre-renders your layout and static content at build time while 42 | maintaining full interactivity through hydration. This provides fast 43 | initial page loads with visible content, then progressive 44 | enhancement as JavaScript loads. 45 |

46 | 47 |

🎯 Simple Architecture

48 |

49 | Built on a clear separation between frontend and backend. Your 50 | frontend is purely static assets (HTML/CSS/JS) that can be served 51 | from any CDN or web server, while data comes from dedicated API 52 | endpoints. No JavaScript runtime required for serving. 53 |

54 | 55 |

⚡ Lightning Fast Builds

56 |

57 | Built on Rsbuild for builds in the range of hundreds of 58 | milliseconds. The system automatically handles: 59 |

60 |
    61 |
  • TypeScript compilation and type checking
  • 62 |
  • Asset optimization and bundling
  • 63 |
  • CSS processing and minification
  • 64 |
  • Pre-compression (Brotli, Zstandard, Gzip)
  • 65 |
66 | 67 |

🔧 Development Workflow

68 |

Three modes available to suit your needs:

69 |
    70 |
  • 71 | Development (npm run dev): Express server with 72 | fast rebuilds and live reloading 73 |
  • 74 |
  • 75 | Staging (npm run stage): Production build with 76 | local preview server 77 |
  • 78 |
  • 79 | Production (npm run build): Optimized build 80 | with pre-compression for deployment 81 |
  • 82 |
83 |
84 | 85 |
86 |

Rendering Comparison

87 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 111 | 120 | 121 | 122 | 123 | 132 | 141 | 142 | 143 | 147 | 155 | 164 | 165 | 166 |
Rendering TypeInitial LoadAfter Script
SSR (classic SSR / Next.js / Nuxt) 103 | SSR Initial 108 |
User sees content instantly 109 |
(rendered on each request) 110 |
SSR After
User sees content instantly
(no additional loading)
SPA (React App / pure Svelte)SPA Initial
User sees white page or spinner
(no content until JS loads)
SPA Loaded
User sees full content
(after script execution)
SPA + Build-time Pre-Rendering (this 145 | approach)SPAwBR Initial
User sees layout and static content
(pre-rendered at build time)
SPAwBR Hydrated
User sees interactive content
(hydrated with full functionality)
167 |
168 |
169 | 170 |
171 |

SPAwBR Development

172 | 173 |

🔍 Detecting Build-time Pre-rendering

174 |

175 | PrevelteKit uses window.__isBuildTime to indicate when code 176 | is running during build-time pre-rendering. This is crucial for handling 177 | client-side-only code like API calls and intervals. 178 |

179 | 180 |
181 | {@html `
// Basic detection
182 | let renderInfo = "Client Rendered";
183 | if (window?.__isBuildTime) {
184 |     renderInfo = "Server Pre-Rendered";
185 | }
`} 186 |
187 | 188 |

🔄 Handling Client-Side Operations

189 |

190 | PrevelteKit automatically handles fetch requests during build-time 191 | pre-rendering. Fetch calls made during pre-rendering will timeout 192 | after 5 seconds, allowing your components to render with loading 193 | states. You no longer need to wrap fetch calls in window.__isBuildTime checks. 196 |

197 | 198 |
199 | {@html `
// Fetch automatically handled during pre-rendering
200 | let pricePromise = $state(fetchBitcoinPrice());
201 | 
202 | // Use Svelte's await block for clean handling
203 | {#await pricePromise}
204 |     <p>Loading...</p>
205 | {:then data}
206 |     <p>{data}</p>
207 | {:catch error}
208 |     <p>Error: {error.message}</p>
209 | {/await}
`} 210 |
211 | 212 |

213 | When to still use build-time checks: 214 |

215 |
    216 |
  • Browser APIs (localStorage, sessionStorage, geolocation)
  • 217 |
  • DOM manipulation that shouldn't happen during pre-rendering
  • 218 |
  • Third-party scripts that expect a real browser environment
  • 219 |
220 |
221 | 222 |
223 |

Configuration

224 |

225 | PrevelteKit uses rsbuild.config.ts for configuration 226 | with sensible defaults. To customize settings, create an 227 | rsbuild.config.ts file in your project - it will merge with 228 | the default configuration. 229 |

230 | 231 |

232 | The framework provides fallback files (index.html and 233 | index.ts) from the default folder when you don't supply 234 | your own. Once you add your own files, PrevelteKit uses those 235 | instead, ignoring the defaults. 236 |

237 |
238 | 239 |
240 |

Client-Side Routing

241 |

242 | PrevelteKit includes a built-in routing system that handles 243 | navigation between different pages in your application. The router 244 | uses pattern matching to determine which component to render based 245 | on the current URL path. 246 |

247 | 248 |

🧭 Route Configuration

249 |

250 | Define your routes as an array of route objects, each specifying a 251 | path pattern, the component to render, and the static HTML file 252 | name: 253 |

254 |
255 |
const routes: Routes = {
257 |      dynamicRoutes: [
258 |          {
259 |              path: "*/doc",
260 |              component: Documentation
261 |          },
262 |          {
263 |              path: "*/example",
264 |              component: Example
265 |          },
266 |          {
267 |              path: "*/",
268 |              component: Landing
269 |          }
270 |      ],
271 |      staticRoutes: [
272 |          {
273 |              path: "/doc",
274 |              htmlFilename: "doc.html"
275 |          },
276 |          {
277 |              path: "/example",
278 |              htmlFilename: "example.html"
279 |          },
280 |          {
281 |              path: "/",
282 |              htmlFilename: "index.html"
283 |          }
284 |      ]
285 |  };
286 | 
287 |  <Router routes>
289 |
290 | 291 |

🔍 Path Patterns

292 |

PrevelteKit supports flexible path patterns for routing:

293 |
    294 |
  • 295 | Wildcard prefix (*/path): Matches 296 | any single segment before the path (e.g., */doc 297 | matches /doc and /any/doc) 298 |
  • 299 |
  • 300 | Root wildcard (*/): Matches the 301 | root path and single-segment paths 302 |
  • 303 |
  • 304 | Exact paths (/about): Matches the 305 | exact path only 306 |
  • 307 |
  • 308 | Parameters (/user/:id): Captures 309 | URL segments as parameters 310 |
  • 311 |
312 | 313 |

🔗 Navigation

314 |

315 | Use the route action for client-side navigation that updates 316 | the URL without page reloads: 317 |

318 |
319 |
import { route } from 'preveltekit';
321 | 
322 |     <a use:link href="doc">Documentation</a>
323 |     <a use:link href="example">Example</a>
325 |
326 | 327 |

📄 Static File Mapping & Hybrid Routing

328 |

329 | The staticRoutes array configuration serves a dual purpose 330 | in PrevelteKit's hybrid routing approach: 331 |

332 |
333 |
htmlFilename: "doc.html"  // Generates dist/doc.html at build time
336 |
337 | 338 |

339 | Static Generation: During the build process, 340 | PrevelteKit generates actual HTML files in your dist/ folder 341 | for each route: 342 |

343 |
    344 |
  • dist/index.html - Pre-rendered root route
  • 345 |
  • 346 | dist/doc.html - Pre-rendered documentation page 347 |
  • 348 |
  • dist/example.html - Pre-rendered example page
  • 349 |
350 | 351 |

352 | Dynamic Routing: Once the application loads, the same 353 | route configuration enables client-side navigation between pages without 354 | full page reloads. This provides: 355 |

356 |
    357 |
  • Fast initial page loads from pre-rendered static HTML
  • 358 |
  • Instant navigation between routes via client-side routing
  • 359 |
  • 360 | SEO benefits from static HTML while maintaining SPA 361 | functionality 362 |
  • 363 |
364 | 365 |

366 | This hybrid approach means users get static HTML files for direct 367 | access (bookmarks, search engines) and dynamic routing for seamless 368 | navigation within the application. 369 |

370 | 371 |

⚙️ Route Matching Priority

372 |

373 | Routes are matched based on specificity, with more specific patterns 374 | taking precedence: 375 |

376 |
    377 |
  1. Exact path matches (highest priority)
  2. 378 |
  3. Parameter-based routes
  4. 379 |
  5. Wildcard patterns (lowest priority)
  6. 380 |
381 |

382 | Always place more specific routes before general wildcard routes in 383 | your configuration to ensure proper matching behavior. 384 |

385 |
386 | 387 |
388 |

Docker Support

389 |

Development environment:

390 |
391 |
docker build -f Dockerfile.dev . -t preveltekit-dev
393 | docker run -p3000:3000 -v./src:/app/src preveltekit-dev
395 |
396 | 397 |

Production build:

398 |
399 |
docker build . -t preveltekit
401 | docker run -p3000:3000 preveltekit
403 |
404 |
405 | 406 |
407 |

Architecture Philosophy

408 |

409 | PrevelteKit emphasizes static-first architecture with 410 | clear separation between frontend and backend: 411 |

412 |
    413 |
  • 414 | Frontend: Pure static assets (HTML/CSS/JS) 415 | served from any web server or CDN 416 |
  • 417 |
  • 418 | Backend: Dedicated API endpoints for data, can 419 | be built with any technology 420 |
  • 421 |
  • 422 | Deployment: No JavaScript runtime required - 423 | just static files 424 |
  • 425 |
426 | 427 |

428 | This approach offers compelling simplicity compared to full-stack 429 | meta-frameworks: 430 |

431 |
    432 |
  • Deploy anywhere (GitHub Pages, S3, any web server)
  • 433 |
  • Predictable performance with no server processes to monitor
  • 434 |
  • Easier debugging with clear boundaries
  • 435 |
  • Freedom to choose your backend technology
  • 436 |
437 |
438 | 439 |
440 |

Deployment

441 |

442 | The production build generates static files with pre-compressed 443 | variants: 444 |

445 |
    446 |
  • Standard files (.js, .css, .html)
  • 447 |
  • Brotli compressed (.br)
  • 448 |
  • Gzip compressed (.gz)
  • 449 |
  • Zstandard compressed (.zst)
  • 450 |
451 |

452 | Deploy to any static hosting or web server. The pre-compressed files 453 | enable optimal performance when served with appropriate web server 454 | configuration. 455 |

456 |
457 | 458 |
459 |

Why PrevelteKit?

460 |

461 | While SvelteKit provides comprehensive capabilities, PrevelteKit 462 | focuses on a minimalistic solution for build-time pre-rendering. 463 | With less than 500 lines of code, it's essentially glue code for 464 | Svelte, Rsbuild, and jsdom - perfect for projects that need fast 465 | initial loads without the complexity of full JavaScript 466 | infrastructure for the frontend deployment. 467 |

468 | 469 |

470 | PrevelteKit serves as a starting point for projects that need 471 | pre-rendered content without the overhead of a full meta-framework, 472 | following a "convention over configuration" approach. 473 |

474 |
475 |
476 | 477 | 621 | -------------------------------------------------------------------------------- /docs/js/index.22425872.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e,i,s,l,t={516:function(e,i,s){var l=s(441);s(678),s(4);var t=s(741),a=s(987),n=t.vUu(`

  • Static-first architecture - Deploy frontend anywhere as pure HTML/CSS/JS
  • Lightning fast builds - Hundreds of milliseconds with Rsbuild
  • No JavaScript runtime in production for frontend - Just static files on a server
(These buttons here are client-side routing • No page reloads)

Lightning Fast Builds

Rsbuild bundles in hundreds of milliseconds

🎯

Minimalistic

Less than 500 lines of code - just glue for Svelte, Rsbuild, 2 | and jsdom

🔄

HTML for Initial Page

Single Page App with Built-time Pre-Rendering - best of both 3 | worlds

🚀

Deploy Anywhere

GitHub Pages, S3, any web server - just static files

🛡️

Clear Separation

Frontend as static assets, backend as dedicated APIs

📦

Zero Config

Works out of the box with sensible defaults

Quick Start

`);function c(e){let i=t.zgK("PrevelteKit"),s=t.zgK("Minimalistic Build-time Pre-rendering for Svelte");window.__isBuildTime&&(t.hZp(i,"PrevelteKit (Pre-rendered)"),t.hZp(s,"This content was pre-rendered at build time and will hydrate on load"));var l=n(),c=t.jfp(l),I=t.jfp(c),r=t.jfp(I,!0);t.cLc(I);var o=t.hg4(I,2),d=t.jfp(o,!0);t.cLc(o);var h=t.hg4(o,4),v=t.jfp(h);t.XId(v,e=>null===a.nf||void 0===a.nf?void 0:(0,a.nf)(e));var g=t.hg4(v,2);t.XId(g,e=>null===a.nf||void 0===a.nf?void 0:(0,a.nf)(e)),t.cLc(h),t.K2T(2),t.cLc(c);var p=t.hg4(c,4),m=t.hg4(t.jfp(p),2),u=t.jfp(m);t.qyt(u,()=>`
mkdir -p preveltekit/src && cd preveltekit
  4 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
  5 | npm install
  6 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
  7 | npm run dev
  8 | # And open a browser with localhost:3000
`),t.cLc(m),t.cLc(p),t.cLc(l),t.vNg(()=>{t.jax(r,t.JtY(i)),t.jax(d,t.JtY(s))}),t.BCw(e,l)}let I="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgMzAwIj48cmVjdCB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI1NSIgeD0iMjAiIHk9IjI1IiBmaWxsPSIjMjU2M2ViIiByeD0iOCIgc3R5bGU9InN0cm9rZTpub25lIi8+PHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyOCIgeD0iMjAiIHk9IjI2IiBmaWxsPSIjM2I4MmY2IiByeD0iOCIgc3R5bGU9InN0cm9rZS13aWR0aDoxIi8+PHBhdGggZmlsbD0iIzNiODJmNiIgZD0iTTIwIDQ2aDM2MHY4SDIweiIvPjxjaXJjbGUgY3g9IjQwIiBjeT0iNDAiIHI9IjYiIGZpbGw9IiNlZjQ0NDQiLz48Y2lyY2xlIGN4PSI2MCIgY3k9IjQwIiByPSI2IiBmaWxsPSIjZjU5ZTBiIi8+PGNpcmNsZSBjeD0iODAiIGN5PSI0MCIgcj0iNiIgZmlsbD0iIzEwYjk4MSIvPjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYiIHg9IjExMCIgeT0iMzIiIGZpbGw9IiNkYmVhZmUiIHN0cm9rZT0iIzkzYzVmZCIgcng9IjgiLz48dGV4dCB4PSIxMTciIHk9IjQzIiBmaWxsPSIjMWU0MGFmIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiPmh0dHBzOi8vZ2l0aHViLmNvbS90Ym9jZWsvcHJldmVsdGVraXQ8L3RleHQ+PHBhdGggZmlsbD0iI2Y4ZmFmYyIgc3Ryb2tlPSIjY2JkNWUxIiBkPSJNMjUgNTloMzQ4djIxNEgyNXoiIHN0eWxlPSJzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIzMzMiIGhlaWdodD0iNDUiIHg9IjMzIiB5PSI2NyIgZmlsbD0iI2UwZTdmZiIgcng9IjQiIHN0eWxlPSJmaWxsOiNjNmQzZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjEuMiIvPjxyZWN0IHdpZHRoPSI4MCIgaGVpZ2h0PSI4IiB4PSI0OSIgeT0iNzYiIGZpbGw9IiM0ZjQ2ZTUiIHJ4PSIyIi8+PHJlY3Qgd2lkdGg9IjEyMCIgaGVpZ2h0PSI2IiB4PSI0OSIgeT0iOTciIGZpbGw9IiM2MzY2ZjEiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjEyMCIgaGVpZ2h0PSI2IiB4PSI0OSIgeT0iODgiIGZpbGw9IiM2MzY2ZjEiIHJ4PSIxIi8+PGcgZmlsbD0iIzRmNDZlNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIgLTMpIj48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMjgwIiB5PSI4NSIgcng9IjEiLz48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMzAwIiB5PSI4NSIgcng9IjEiLz48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMzIwIiB5PSI4NSIgcng9IjEiLz48L2c+PGcgZmlsbD0iIzRmNDZlNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIgNCkiPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIyODAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMDAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMjAiIHk9Ijg1IiByeD0iMSIvPjwvZz48ZyBmaWxsPSIjNGY0NmU1IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMiAxMSkiPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIyODAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMDAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMjAiIHk9Ijg1IiByeD0iMSIvPjwvZz48cmVjdCB3aWR0aD0iMTY4IiBoZWlnaHQ9Ijc0IiB4PSIzMiIgeT0iMTE4IiBmaWxsPSIjZTBlN2ZmIiByeD0iNCIgc3R5bGU9ImZpbGw6I2M2ZDNmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIxMzAiIGhlaWdodD0iNiIgeD0iNDkiIHk9IjEyMiIgZmlsbD0iIzRmNDZlNSIgcng9IjEiLz48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjQiIHg9IjQ5IiB5PSIxMzMiIGZpbGw9IiM2MzY2ZjEiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjEyMCIgaGVpZ2h0PSI0IiB4PSI0OSIgeT0iMTQyIiBmaWxsPSIjNjM2NmYxIiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxMTEiIGhlaWdodD0iNCIgeD0iNDkiIHk9IjE1MSIgZmlsbD0iIzYzNjZmMSIgcng9IjEiIHN0eWxlPSJzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIxMjgiIGhlaWdodD0iNCIgeD0iNDkiIHk9IjE2MSIgZmlsbD0iIzYzNjZmMSIgcng9IjEiIHN0eWxlPSJzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSI2MCIgaGVpZ2h0PSIxMiIgeD0iNDkiIHk9IjE3MyIgZmlsbD0iIzNiODJmNiIgcng9IjIiLz48cmVjdCB3aWR0aD0iMTU1IiBoZWlnaHQ9Ijc0IiB4PSIyMTEiIHk9IjExOCIgZmlsbD0iI2UwZTdmZiIgcng9IjQiIHN0eWxlPSJmaWxsOiNjNmQzZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjEuMiIvPjxyZWN0IHdpZHRoPSIxMzAiIGhlaWdodD0iNiIgeD0iMjIwIiB5PSIxMjIiIGZpbGw9IiM0ZjQ2ZTUiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjExMCIgaGVpZ2h0PSI0IiB4PSIyMjAiIHk9IjEzMyIgZmlsbD0iIzYzNjZmMSIgcng9IjEiLz48cmVjdCB3aWR0aD0iOTAiIGhlaWdodD0iNCIgeD0iMjIwIiB5PSIxNDIiIGZpbGw9IiM2MzY2ZjEiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjQ3IiBoZWlnaHQ9IjQiIHg9IjIyMCIgeT0iMTUxIiBmaWxsPSIjNjM2NmYxIiByeD0iMSIgc3R5bGU9InN0cm9rZS13aWR0aDoxIi8+PHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjEyIiB4PSIyMTkiIHk9IjE3NSIgZmlsbD0iIzNiODJmNiIgcng9IjIiLz48cmVjdCB3aWR0aD0iMzM0IiBoZWlnaHQ9IjY0IiB4PSIzMSIgeT0iMjAwIiBmaWxsPSIjZTBlN2ZmIiByeD0iNCIgc3R5bGU9ImZpbGw6I2M2ZDNmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iNiIgeD0iNTAiIHk9IjIwOSIgZmlsbD0iIzRmNDZlNSIgcng9IjEiIHN0eWxlPSJzdHJva2Utd2lkdGg6MS4yIi8+PHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjQiIHg9IjUwIiB5PSIyMjEiIGZpbGw9IiM2MzY2ZjEiIHJ4PSIxIi8+PHJlY3Qgd2lkdGg9IjI2NCIgaGVpZ2h0PSI0IiB4PSI1MCIgeT0iMjMyIiBmaWxsPSIjNjM2NmYxIiByeD0iMiIgc3R5bGU9InN0cm9rZS13aWR0aDoxLjUiLz48cmVjdCB3aWR0aD0iMjAyIiBoZWlnaHQ9IjQiIHg9IjUwIiB5PSIyNDIiIGZpbGw9IiM2MzY2ZjEiIHJ4PSIyIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOjEuMiIvPjxyZWN0IHdpZHRoPSIyNDYiIGhlaWdodD0iNCIgeD0iNTAiIHk9IjI1MiIgZmlsbD0iIzYzNjZmMSIgcng9IjIiIHN0eWxlPSJzdHJva2Utd2lkdGg6MS41Ii8+PGNpcmNsZSBjeD0iMzQ5IiBjeT0iNzkiIHI9IjUiIGZpbGw9IiM5M2M1ZmQiIHN0eWxlPSJzdHJva2Utd2lkdGg6MS43Ii8+PHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyNTUiIHg9IjIwIiB5PSIyNSIgZmlsbD0iIzI1NjNlYiIgc3Ryb2tlPSIjMWU0MGFmIiBzdHJva2Utd2lkdGg9IjIiIHJ4PSI4IiBzdHlsZT0iZmlsbDpub25lIi8+PC9zdmc+";var r=t.vUu(`

PrevelteKit Documentation

Quick Start

Make sure you have node/npm installed. Here is a minimalistic example:

Core Features

⚡ Single Page Application with Built-time Pre-rendering (SPAwBR)

PrevelteKit combines the best of SPA and build-time rendering with 9 | hydration approaches. Unlike traditional SSR that renders on each 10 | request, or pure SPA that shows blank pages initially, SPAwBR 11 | pre-renders your layout and static content at build time while 12 | maintaining full interactivity through hydration. This provides fast 13 | initial page loads with visible content, then progressive 14 | enhancement as JavaScript loads.

🎯 Simple Architecture

Built on a clear separation between frontend and backend. Your 15 | frontend is purely static assets (HTML/CSS/JS) that can be served 16 | from any CDN or web server, while data comes from dedicated API 17 | endpoints. No JavaScript runtime required for serving.

⚡ Lightning Fast Builds

Built on Rsbuild for builds in the range of hundreds of 18 | milliseconds. The system automatically handles:

  • TypeScript compilation and type checking
  • Asset optimization and bundling
  • CSS processing and minification
  • Pre-compression (Brotli, Zstandard, Gzip)

🔧 Development Workflow

Three modes available to suit your needs:

  • Development (npm run dev): Express server with 19 | fast rebuilds and live reloading
  • Staging (npm run stage): Production build with 20 | local preview server
  • Production (npm run build): Optimized build 21 | with pre-compression for deployment

Rendering Comparison

Rendering TypeInitial LoadAfter Script
SSR (classic SSR / Next.js / Nuxt)SSR Initial
User sees content instantly
(rendered on each request)
SSR After
User sees content instantly
(no additional loading)
SPA (React App / pure Svelte)SPA Initial
User sees white page or spinner
(no content until JS loads)
SPA Loaded
User sees full content
(after script execution)
SPA + Build-time Pre-Rendering (this 22 | approach)SPAwBR Initial
User sees layout and static content
(pre-rendered at build time)
SPAwBR Hydrated
User sees interactive content
(hydrated with full functionality)

SPAwBR Development

🔍 Detecting Build-time Pre-rendering

PrevelteKit uses window.__isBuildTime to indicate when code 23 | is running during build-time pre-rendering. This is crucial for handling 24 | client-side-only code like API calls and intervals.

🔄 Handling Client-Side Operations

PrevelteKit automatically handles fetch requests during build-time 25 | pre-rendering. Fetch calls made during pre-rendering will timeout 26 | after 5 seconds, allowing your components to render with loading 27 | states. You no longer need to wrap fetch calls in window.__isBuildTime checks.

When to still use build-time checks:

  • Browser APIs (localStorage, sessionStorage, geolocation)
  • DOM manipulation that shouldn't happen during pre-rendering
  • Third-party scripts that expect a real browser environment

Configuration

PrevelteKit uses rsbuild.config.ts for configuration 28 | with sensible defaults. To customize settings, create an rsbuild.config.ts file in your project - it will merge with 29 | the default configuration.

The framework provides fallback files (index.html and index.ts) from the default folder when you don't supply 30 | your own. Once you add your own files, PrevelteKit uses those 31 | instead, ignoring the defaults.

Client-Side Routing

PrevelteKit includes a built-in routing system that handles 32 | navigation between different pages in your application. The router 33 | uses pattern matching to determine which component to render based 34 | on the current URL path.

🧭 Route Configuration

Define your routes as an array of route objects, each specifying a 35 | path pattern, the component to render, and the static HTML file 36 | name:

const routes: Routes = {
 37 |      dynamicRoutes: [
 38 |          {
 39 |              path: "*/doc",
 40 |              component: Documentation
 41 |          },
 42 |          {
 43 |              path: "*/example",
 44 |              component: Example
 45 |          },
 46 |          {
 47 |              path: "*/",
 48 |              component: Landing
 49 |          }
 50 |      ],
 51 |      staticRoutes: [
 52 |          {
 53 |              path: "/doc",
 54 |              htmlFilename: "doc.html"
 55 |          },
 56 |          {
 57 |              path: "/example",
 58 |              htmlFilename: "example.html"
 59 |          },
 60 |          {
 61 |              path: "/",
 62 |              htmlFilename: "index.html"
 63 |          }
 64 |      ]
 65 |  };
 66 | 
 67 |  <Router routes>

🔍 Path Patterns

PrevelteKit supports flexible path patterns for routing:

  • Wildcard prefix (*/path): Matches 68 | any single segment before the path (e.g., */doc matches /doc and /any/doc)
  • Root wildcard (*/): Matches the 69 | root path and single-segment paths
  • Exact paths (/about): Matches the 70 | exact path only
  • Parameters (/user/:id): Captures 71 | URL segments as parameters

🔗 Navigation

Use the route action for client-side navigation that updates 72 | the URL without page reloads:

import { route } from 'preveltekit';
 73 | 
 74 |     <a use:link href="doc">Documentation</a>
 75 |     <a use:link href="example">Example</a>

📄 Static File Mapping & Hybrid Routing

The staticRoutes array configuration serves a dual purpose 76 | in PrevelteKit's hybrid routing approach:

htmlFilename: "doc.html"  // Generates dist/doc.html at build time

Static Generation: During the build process, 77 | PrevelteKit generates actual HTML files in your dist/ folder 78 | for each route:

  • dist/index.html - Pre-rendered root route
  • dist/doc.html - Pre-rendered documentation page
  • dist/example.html - Pre-rendered example page

Dynamic Routing: Once the application loads, the same 79 | route configuration enables client-side navigation between pages without 80 | full page reloads. This provides:

  • Fast initial page loads from pre-rendered static HTML
  • Instant navigation between routes via client-side routing
  • SEO benefits from static HTML while maintaining SPA 81 | functionality

This hybrid approach means users get static HTML files for direct 82 | access (bookmarks, search engines) and dynamic routing for seamless 83 | navigation within the application.

⚙️ Route Matching Priority

Routes are matched based on specificity, with more specific patterns 84 | taking precedence:

  1. Exact path matches (highest priority)
  2. Parameter-based routes
  3. Wildcard patterns (lowest priority)

Always place more specific routes before general wildcard routes in 85 | your configuration to ensure proper matching behavior.

Docker Support

Development environment:

docker build -f Dockerfile.dev . -t preveltekit-dev
 86 | docker run -p3000:3000 -v./src:/app/src preveltekit-dev

Production build:

docker build . -t preveltekit
 87 | docker run -p3000:3000 preveltekit

Architecture Philosophy

PrevelteKit emphasizes static-first architecture with 88 | clear separation between frontend and backend:

  • Frontend: Pure static assets (HTML/CSS/JS) 89 | served from any web server or CDN
  • Backend: Dedicated API endpoints for data, can 90 | be built with any technology
  • Deployment: No JavaScript runtime required - 91 | just static files

This approach offers compelling simplicity compared to full-stack 92 | meta-frameworks:

  • Deploy anywhere (GitHub Pages, S3, any web server)
  • Predictable performance with no server processes to monitor
  • Easier debugging with clear boundaries
  • Freedom to choose your backend technology

Deployment

The production build generates static files with pre-compressed 93 | variants:

  • Standard files (.js, .css, .html)
  • Brotli compressed (.br)
  • Gzip compressed (.gz)
  • Zstandard compressed (.zst)

Deploy to any static hosting or web server. The pre-compressed files 94 | enable optimal performance when served with appropriate web server 95 | configuration.

Why PrevelteKit?

While SvelteKit provides comprehensive capabilities, PrevelteKit 96 | focuses on a minimalistic solution for build-time pre-rendering. 97 | With less than 500 lines of code, it's essentially glue code for 98 | Svelte, Rsbuild, and jsdom - perfect for projects that need fast 99 | initial loads without the complexity of full JavaScript 100 | infrastructure for the frontend deployment.

PrevelteKit serves as a starting point for projects that need 101 | pre-rendered content without the overhead of a full meta-framework, 102 | following a "convention over configuration" approach.

`);function o(e){let i=t.zgK("Client Rendered");(null==(s=window)?void 0:s.__isBuildTime)&&t.hZp(i,"Server Pre-Rendered");var s,l=r(),a=t.jfp(l),n=t.hg4(t.jfp(a),2),c=t.jfp(n);t.cLc(n),t.cLc(a);var o=t.hg4(a,2),d=t.hg4(t.jfp(o),2),h=t.jfp(d);t.qyt(h,()=>`
mkdir -p preveltekit/src && cd preveltekit
103 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
104 | npm install
105 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
106 | npm run dev
107 | # And open a browser with localhost:3000
`),t.cLc(d),t.cLc(o);var v=t.hg4(o,4),g=t.hg4(t.jfp(v),2),p=t.jfp(g),m=t.hg4(t.jfp(p)),u=t.jfp(m),x=t.hg4(t.jfp(u)),j=t.jfp(x);t.K2T(5),t.cLc(x);var z=t.hg4(x),S=t.jfp(z);t.K2T(4),t.cLc(z),t.cLc(u);var b=t.hg4(u),P=t.hg4(t.jfp(b)),Z=t.jfp(P);t.K2T(4),t.cLc(P);var y=t.hg4(P),M=t.jfp(y);t.K2T(4),t.cLc(y),t.cLc(b);var f=t.hg4(b),N=t.hg4(t.jfp(f)),H=t.jfp(N);t.K2T(4),t.cLc(N);var D=t.hg4(N),B=t.jfp(D);t.K2T(4),t.cLc(D),t.cLc(f),t.cLc(m),t.cLc(p),t.cLc(g),t.cLc(v);var w=t.hg4(v,2),G=t.hg4(t.jfp(w),6),W=t.jfp(G);t.qyt(W,()=>`
// Basic detection
108 | let renderInfo = "Client Rendered";
109 | if (window?.__isBuildTime) {
110 |     renderInfo = "Server Pre-Rendered";
111 | }
`),t.cLc(G);var k=t.hg4(G,6),T=t.jfp(k);t.qyt(T,()=>`
// Fetch automatically handled during pre-rendering
112 | let pricePromise = $state(fetchBitcoinPrice());
113 | 
114 | // Use Svelte's await block for clean handling
115 | {#await pricePromise}
116 |     <p>Loading...</p>
117 | {:then data}
118 |     <p>{data}</p>
119 | {:catch error}
120 |     <p>Error: {error.message}</p>
121 | {/await}
`),t.cLc(k),t.K2T(4),t.cLc(w),t.K2T(12),t.cLc(l),t.vNg(()=>{t.jax(c,`(${t.JtY(i)??""})`),t.aIK(j,"src",I),t.aIK(S,"src",I),t.aIK(Z,"src","data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgMzAwIj48cmVjdCB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI1NSIgeD0iMjAiIHk9IjI1IiBmaWxsPSIjMjU2M2ViIiByeD0iOCIgc3R5bGU9InN0cm9rZTpub25lIi8+PHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyOCIgeD0iMjAiIHk9IjI2IiBmaWxsPSIjM2I4MmY2IiByeD0iOCIgc3R5bGU9InN0cm9rZS13aWR0aDoxIi8+PHBhdGggZmlsbD0iIzNiODJmNiIgZD0iTTIwIDQ2aDM2MHY4SDIweiIvPjxjaXJjbGUgY3g9IjQwIiBjeT0iNDAiIHI9IjYiIGZpbGw9IiNlZjQ0NDQiLz48Y2lyY2xlIGN4PSI2MCIgY3k9IjQwIiByPSI2IiBmaWxsPSIjZjU5ZTBiIi8+PGNpcmNsZSBjeD0iODAiIGN5PSI0MCIgcj0iNiIgZmlsbD0iIzEwYjk4MSIvPjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYiIHg9IjExMCIgeT0iMzIiIGZpbGw9IiNkYmVhZmUiIHN0cm9rZT0iIzkzYzVmZCIgcng9IjgiLz48dGV4dCB4PSIxMTciIHk9IjQzIiBmaWxsPSIjMWU0MGFmIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiPmh0dHBzOi8vZ2l0aHViLmNvbS90Ym9jZWsvcHJldmVsdGVraXQ8L3RleHQ+PHBhdGggZmlsbD0iI2Y4ZmFmYyIgc3Ryb2tlPSIjY2JkNWUxIiBkPSJNMjUgNTloMzQ4djIxNEgyNXoiIHN0eWxlPSJzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIzNjAiIGhlaWdodD0iMjU1IiB4PSIyMCIgeT0iMjUiIGZpbGw9IiMyNTYzZWIiIHN0cm9rZT0iIzFlNDBhZiIgc3Ryb2tlLXdpZHRoPSIyIiByeD0iOCIgc3R5bGU9ImZpbGw6bm9uZSIvPjwvc3ZnPg=="),t.aIK(M,"src",I),t.aIK(H,"src","data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgMzAwIj48cmVjdCB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI1NSIgeD0iMjAiIHk9IjI1IiBmaWxsPSIjMjU2M2ViIiByeD0iOCIgc3R5bGU9InN0cm9rZTpub25lIi8+PHJlY3Qgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyOCIgeD0iMjAiIHk9IjI2IiBmaWxsPSIjM2I4MmY2IiByeD0iOCIgc3R5bGU9InN0cm9rZS13aWR0aDoxIi8+PHBhdGggZmlsbD0iIzNiODJmNiIgZD0iTTIwIDQ2aDM2MHY4SDIweiIvPjxjaXJjbGUgY3g9IjQwIiBjeT0iNDAiIHI9IjYiIGZpbGw9IiNlZjQ0NDQiLz48Y2lyY2xlIGN4PSI2MCIgY3k9IjQwIiByPSI2IiBmaWxsPSIjZjU5ZTBiIi8+PGNpcmNsZSBjeD0iODAiIGN5PSI0MCIgcj0iNiIgZmlsbD0iIzEwYjk4MSIvPjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYiIHg9IjExMCIgeT0iMzIiIGZpbGw9IiNkYmVhZmUiIHN0cm9rZT0iIzkzYzVmZCIgcng9IjgiLz48dGV4dCB4PSIxMTciIHk9IjQzIiBmaWxsPSIjMWU0MGFmIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiPmh0dHBzOi8vZ2l0aHViLmNvbS90Ym9jZWsvcHJldmVsdGVraXQ8L3RleHQ+PHBhdGggZmlsbD0iI2Y4ZmFmYyIgc3Ryb2tlPSIjY2JkNWUxIiBkPSJNMjUgNTloMzQ4djIxNEgyNXoiIHN0eWxlPSJzdHJva2Utd2lkdGg6MSIvPjxyZWN0IHdpZHRoPSIzMzMiIGhlaWdodD0iNDUiIHg9IjMzIiB5PSI2NyIgZmlsbD0iI2UwZTdmZiIgcng9IjQiIHN0eWxlPSJmaWxsOiNjNmQzZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjEuMiIvPjxyZWN0IHdpZHRoPSI4MCIgaGVpZ2h0PSI4IiB4PSI0OSIgeT0iNzYiIGZpbGw9IiM0ZjQ2ZTUiIHJ4PSIyIi8+PGcgZmlsbD0iIzRmNDZlNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIgLTMpIj48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMjgwIiB5PSI4NSIgcng9IjEiLz48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMzAwIiB5PSI4NSIgcng9IjEiLz48cmVjdCB3aWR0aD0iMTUiIGhlaWdodD0iNCIgeD0iMzIwIiB5PSI4NSIgcng9IjEiLz48L2c+PGcgZmlsbD0iIzRmNDZlNSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTIgNCkiPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIyODAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMDAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMjAiIHk9Ijg1IiByeD0iMSIvPjwvZz48ZyBmaWxsPSIjNGY0NmU1IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMiAxMSkiPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIyODAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMDAiIHk9Ijg1IiByeD0iMSIvPjxyZWN0IHdpZHRoPSIxNSIgaGVpZ2h0PSI0IiB4PSIzMjAiIHk9Ijg1IiByeD0iMSIvPjwvZz48cmVjdCB3aWR0aD0iMTY4IiBoZWlnaHQ9Ijc0IiB4PSIzMiIgeT0iMTE4IiBmaWxsPSIjZTBlN2ZmIiByeD0iNCIgc3R5bGU9ImZpbGw6I2M2ZDNmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS4yIi8+PHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjEyIiB4PSI0OSIgeT0iMTczIiBmaWxsPSIjM2I4MmY2IiByeD0iMiIvPjxyZWN0IHdpZHRoPSIxNTUiIGhlaWdodD0iNzQiIHg9IjIxMSIgeT0iMTE4IiBmaWxsPSIjZTBlN2ZmIiByeD0iNCIgc3R5bGU9ImZpbGw6I2M2ZDNmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS4yIi8+PHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjEyIiB4PSIyMTkiIHk9IjE3NSIgZmlsbD0iIzNiODJmNiIgcng9IjIiLz48cmVjdCB3aWR0aD0iMzM0IiBoZWlnaHQ9IjY0IiB4PSIzMSIgeT0iMjAwIiBmaWxsPSIjZTBlN2ZmIiByeD0iNCIgc3R5bGU9ImZpbGw6I2M2ZDNmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MSIvPjxjaXJjbGUgY3g9IjM0OSIgY3k9Ijc5IiByPSI1IiBmaWxsPSIjOTNjNWZkIiBzdHlsZT0ic3Ryb2tlLXdpZHRoOjEuNyIvPjxyZWN0IHdpZHRoPSIzNjAiIGhlaWdodD0iMjU1IiB4PSIyMCIgeT0iMjUiIGZpbGw9IiMyNTYzZWIiIHN0cm9rZT0iIzFlNDBhZiIgc3Ryb2tlLXdpZHRoPSIyIiByeD0iOCIgc3R5bGU9ImZpbGw6bm9uZSIvPjwvc3ZnPg=="),t.aIK(B,"src",I)}),t.BCw(e,l)}function d(e,i,s){t.hZp(i,s(),!0)}var h=t.vUu('

Prices are volatile and for reference only. Not financial advice.',1),v=t.vUu('

',1),g=t.vUu("

Loading...

"),p=t.vUu('

Bitcoin Price Tracker

');function m(e,i){t.VCO(i,!0);let s=t.wk1("Client Rendered");async function l(){let e=await fetch("https://min-api.cryptocompare.com/data/generateAvg?fsym=BTC&tsym=USD&e=coinbase");if(!e.ok)throw Error("Failed to fetch data");return e.json()}(null==(n=window)?void 0:n.__isBuildTime)&&t.hZp(s,"Server Pre-Rendered");let a=t.wk1(t.BXG(l()));t.MWq(()=>{let e=setInterval(()=>{t.hZp(a,l(),!0)},6e4);return()=>clearInterval(e)});var n,c=p(),I=t.jfp(c),r=t.hg4(t.jfp(I)),o=t.jfp(r);t.cLc(r),t.cLc(I);var m=t.hg4(I,2),u=t.jfp(m);t.Txz(u,()=>t.JtY(a),e=>{var i=g();t.BCw(e,i)},(e,i)=>{var s=h(),l=t.esp(s),a=t.jfp(l),n=t.jfp(a,!0);t.cLc(a);var c=t.hg4(a,2),I=t.jfp(c);t.cLc(c),t.cLc(l);var r=t.hg4(l,2),o=t.jfp(r);t.cLc(r),t.K2T(2),t.vNg((e,s)=>{t.jax(n,t.JtY(i).RAW.FROMSYMBOL),t.jax(I,`Updated: ${e??""}`),t.jax(o,`${t.JtY(i).RAW.TOSYMBOL??""} ${s??""}`)},[()=>new Date(1e3*t.JtY(i).RAW.LASTUPDATE).toLocaleTimeString(),()=>t.JtY(i).RAW.PRICE.toFixed(2)]),t.BCw(e,s)},(e,i)=>{var s=v(),n=t.esp(s),c=t.jfp(n);t.cLc(n),t.hg4(n,2).__click=[d,a,l],t.vNg(()=>t.jax(c,`Error: ${t.JtY(i).message??""}`)),t.BCw(e,s)}),t.cLc(m),t.cLc(c),t.vNg(()=>t.jax(o,`(${t.JtY(s)??""})`)),t.BCw(e,c),t.uYY()}t.MmH(["click"]);var u=t.vUu(`
`);(0,l.Qv)(function(e){let i={dynamicRoutes:[{path:"*/doc",component:o},{path:"*/example",component:m},{path:"*/",component:c}],staticRoutes:[{path:"/doc",htmlFilename:"doc.html"},{path:"/example",htmlFilename:"example.html"},{path:"/",htmlFilename:"index.html"}]};var s=u(),l=t.hg4(t.jfp(s),2),n=t.jfp(l),I=t.jfp(n);(0,a.Ix)(I,{get routes(){return i}}),t.cLc(n),t.cLc(l),t.K2T(2),t.cLc(s),t.BCw(e,s)},{target:document.getElementById("root"),props:{}})}},a={};function n(e){var i=a[e];if(void 0!==i)return i.exports;var s=a[e]={exports:{}};return t[e](s,s.exports,n),s.exports}n.m=t,n.d=(e,i)=>{for(var s in i)n.o(i,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:i[s]})},n.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),e=[],n.O=(i,s,l,t)=>{if(s){t=t||0;for(var a=e.length;a>0&&e[a-1][2]>t;a--)e[a]=e[a-1];e[a]=[s,l,t];return}for(var c=1/0,a=0;a=t)&&Object.keys(n.O).every(e=>n.O[e](s[r]))?s.splice(r--,1):(I=!1,t0===i[e],s=(e,s)=>{var l,t,[a,c,I]=s,r=0;if(a.some(e=>0!==i[e])){for(l in c)n.o(c,l)&&(n.m[l]=c[l]);if(I)var o=I(n)}for(e&&e(s);rn(516));c=n.O(c)})(); -------------------------------------------------------------------------------- /docs/doc.html: -------------------------------------------------------------------------------- 1 | 2 | PrevelteKit - default index.html 3 | 4 | 5 | 6 |

PrevelteKit Documentation

(Server Pre-Rendered)

Quick Start

Make sure you have node/npm installed. Here is a minimalistic example:
mkdir -p preveltekit/src && cd preveltekit
  7 | echo '{"devDependencies": {"preveltekit": "^1.2.23"},"dependencies": {"svelte": "^5.39.11"},"scripts": {"dev": "preveltekit dev"}}' > package.json
  8 | npm install
  9 | echo '<script>let count = $state(0);</script><h1>Count: {count}</h1><button onclick={() => count++}>Click me</button>' > src/Index.svelte
 10 | npm run dev
 11 | # And open a browser with localhost:3000

Core Features

⚡ Single Page Application with Built-time Pre-rendering (SPAwBR)

PrevelteKit combines the best of SPA and build-time rendering with 12 | hydration approaches. Unlike traditional SSR that renders on each 13 | request, or pure SPA that shows blank pages initially, SPAwBR 14 | pre-renders your layout and static content at build time while 15 | maintaining full interactivity through hydration. This provides fast 16 | initial page loads with visible content, then progressive 17 | enhancement as JavaScript loads.

🎯 Simple Architecture

Built on a clear separation between frontend and backend. Your 18 | frontend is purely static assets (HTML/CSS/JS) that can be served 19 | from any CDN or web server, while data comes from dedicated API 20 | endpoints. No JavaScript runtime required for serving.

⚡ Lightning Fast Builds

Built on Rsbuild for builds in the range of hundreds of 21 | milliseconds. The system automatically handles:

  • TypeScript compilation and type checking
  • Asset optimization and bundling
  • CSS processing and minification
  • Pre-compression (Brotli, Zstandard, Gzip)

🔧 Development Workflow

Three modes available to suit your needs:

  • Development (npm run dev): Express server with 22 | fast rebuilds and live reloading
  • Staging (npm run stage): Production build with 23 | local preview server
  • Production (npm run build): Optimized build 24 | with pre-compression for deployment

Rendering Comparison

Rendering TypeInitial LoadAfter Script
SSR (classic SSR / Next.js / Nuxt)SSR Initial
User sees content instantly
(rendered on each request)
SSR After
User sees content instantly
(no additional loading)
SPA (React App / pure Svelte)SPA Initial
User sees white page or spinner
(no content until JS loads)
SPA Loaded
User sees full content
(after script execution)
SPA + Build-time Pre-Rendering (this 25 | approach)SPAwBR Initial
User sees layout and static content
(pre-rendered at build time)
SPAwBR Hydrated
User sees interactive content
(hydrated with full functionality)

SPAwBR Development

🔍 Detecting Build-time Pre-rendering

PrevelteKit uses window.__isBuildTime to indicate when code 26 | is running during build-time pre-rendering. This is crucial for handling 27 | client-side-only code like API calls and intervals.

// Basic detection
 28 | let renderInfo = "Client Rendered";
 29 | if (window?.__isBuildTime) {
 30 |     renderInfo = "Server Pre-Rendered";
 31 | }

🔄 Handling Client-Side Operations

PrevelteKit automatically handles fetch requests during build-time 32 | pre-rendering. Fetch calls made during pre-rendering will timeout 33 | after 5 seconds, allowing your components to render with loading 34 | states. You no longer need to wrap fetch calls in window.__isBuildTime checks.

// Fetch automatically handled during pre-rendering
 35 | let pricePromise = $state(fetchBitcoinPrice());
 36 | 
 37 | // Use Svelte's await block for clean handling
 38 | {#await pricePromise}
 39 |     <p>Loading...</p>
 40 | {:then data}
 41 |     <p>{data}</p>
 42 | {:catch error}
 43 |     <p>Error: {error.message}</p>
 44 | {/await}

When to still use build-time checks:

  • Browser APIs (localStorage, sessionStorage, geolocation)
  • DOM manipulation that shouldn't happen during pre-rendering
  • Third-party scripts that expect a real browser environment

Configuration

PrevelteKit uses rsbuild.config.ts for configuration 45 | with sensible defaults. To customize settings, create an rsbuild.config.ts file in your project - it will merge with 46 | the default configuration.

The framework provides fallback files (index.html and index.ts) from the default folder when you don't supply 47 | your own. Once you add your own files, PrevelteKit uses those 48 | instead, ignoring the defaults.

Client-Side Routing

PrevelteKit includes a built-in routing system that handles 49 | navigation between different pages in your application. The router 50 | uses pattern matching to determine which component to render based 51 | on the current URL path.

🧭 Route Configuration

Define your routes as an array of route objects, each specifying a 52 | path pattern, the component to render, and the static HTML file 53 | name:

const routes: Routes = {
 54 |      dynamicRoutes: [
 55 |          {
 56 |              path: "*/doc",
 57 |              component: Documentation
 58 |          },
 59 |          {
 60 |              path: "*/example",
 61 |              component: Example
 62 |          },
 63 |          {
 64 |              path: "*/",
 65 |              component: Landing
 66 |          }
 67 |      ],
 68 |      staticRoutes: [
 69 |          {
 70 |              path: "/doc",
 71 |              htmlFilename: "doc.html"
 72 |          },
 73 |          {
 74 |              path: "/example",
 75 |              htmlFilename: "example.html"
 76 |          },
 77 |          {
 78 |              path: "/",
 79 |              htmlFilename: "index.html"
 80 |          }
 81 |      ]
 82 |  };
 83 | 
 84 |  <Router routes>

🔍 Path Patterns

PrevelteKit supports flexible path patterns for routing:

  • Wildcard prefix (*/path): Matches 85 | any single segment before the path (e.g., */doc matches /doc and /any/doc)
  • Root wildcard (*/): Matches the 86 | root path and single-segment paths
  • Exact paths (/about): Matches the 87 | exact path only
  • Parameters (/user/:id): Captures 88 | URL segments as parameters

🔗 Navigation

Use the route action for client-side navigation that updates 89 | the URL without page reloads:

import { route } from 'preveltekit';
 90 | 
 91 |     <a use:link href="doc">Documentation</a>
 92 |     <a use:link href="example">Example</a>

📄 Static File Mapping & Hybrid Routing

The staticRoutes array configuration serves a dual purpose 93 | in PrevelteKit's hybrid routing approach:

htmlFilename: "doc.html"  // Generates dist/doc.html at build time

Static Generation: During the build process, 94 | PrevelteKit generates actual HTML files in your dist/ folder 95 | for each route:

  • dist/index.html - Pre-rendered root route
  • dist/doc.html - Pre-rendered documentation page
  • dist/example.html - Pre-rendered example page

Dynamic Routing: Once the application loads, the same 96 | route configuration enables client-side navigation between pages without 97 | full page reloads. This provides:

  • Fast initial page loads from pre-rendered static HTML
  • Instant navigation between routes via client-side routing
  • SEO benefits from static HTML while maintaining SPA 98 | functionality

This hybrid approach means users get static HTML files for direct 99 | access (bookmarks, search engines) and dynamic routing for seamless 100 | navigation within the application.

⚙️ Route Matching Priority

Routes are matched based on specificity, with more specific patterns 101 | taking precedence:

  1. Exact path matches (highest priority)
  2. Parameter-based routes
  3. Wildcard patterns (lowest priority)

Always place more specific routes before general wildcard routes in 102 | your configuration to ensure proper matching behavior.

Docker Support

Development environment:

docker build -f Dockerfile.dev . -t preveltekit-dev
103 | docker run -p3000:3000 -v./src:/app/src preveltekit-dev

Production build:

docker build . -t preveltekit
104 | docker run -p3000:3000 preveltekit

Architecture Philosophy

PrevelteKit emphasizes static-first architecture with 105 | clear separation between frontend and backend:

  • Frontend: Pure static assets (HTML/CSS/JS) 106 | served from any web server or CDN
  • Backend: Dedicated API endpoints for data, can 107 | be built with any technology
  • Deployment: No JavaScript runtime required - 108 | just static files

This approach offers compelling simplicity compared to full-stack 109 | meta-frameworks:

  • Deploy anywhere (GitHub Pages, S3, any web server)
  • Predictable performance with no server processes to monitor
  • Easier debugging with clear boundaries
  • Freedom to choose your backend technology

Deployment

The production build generates static files with pre-compressed 110 | variants:

  • Standard files (.js, .css, .html)
  • Brotli compressed (.br)
  • Gzip compressed (.gz)
  • Zstandard compressed (.zst)

Deploy to any static hosting or web server. The pre-compressed files 111 | enable optimal performance when served with appropriate web server 112 | configuration.

Why PrevelteKit?

While SvelteKit provides comprehensive capabilities, PrevelteKit 113 | focuses on a minimalistic solution for build-time pre-rendering. 114 | With less than 500 lines of code, it's essentially glue code for 115 | Svelte, Rsbuild, and jsdom - perfect for projects that need fast 116 | initial loads without the complexity of full JavaScript 117 | infrastructure for the frontend deployment.

PrevelteKit serves as a starting point for projects that need 118 | pre-rendered content without the overhead of a full meta-framework, 119 | following a "convention over configuration" approach.

120 | 121 | --------------------------------------------------------------------------------