diff --git a/.github/actions/deploy-to-cloudflare-pages/action.yml b/.github/actions/deploy-to-cloudflare-pages/action.yml
new file mode 100644
index 000000000..4f7a4a177
--- /dev/null
+++ b/.github/actions/deploy-to-cloudflare-pages/action.yml
@@ -0,0 +1,103 @@
+name: "Deploy to Cloudflare Pages"
+description: "Deploys to Cloudflare Pages on either a temporary branch with preview comment, or on the production version if on the main branch."
+inputs:
+ project_name:
+ description: "CloudFlare Pages project name"
+ comment_body:
+ description: "The message to display when deployment is ready"
+ default: "Deployment is ready."
+ required: false
+ production_url:
+ description: "The URL to mention as the production URL."
+ required: true
+ deploy_dir:
+ description: "The directory from which to deploy."
+ required: true
+ cloudflare_api_token:
+ description: "The Cloudflare API token to use for deployment."
+ required: true
+ cloudflare_account_id:
+ description: "The Cloudflare account ID to use for deployment."
+ required: true
+ github_token:
+ description: "The GitHub token to use for posting PR comments."
+ required: true
+runs:
+ using: composite
+ steps:
+ # Install wrangler globally to avoid workspace issues
+ - name: Install Wrangler
+ shell: bash
+ run: npm install -g wrangler
+
+ # Deploy using Wrangler (use pre-installed wrangler)
+ - name: Deploy to Cloudflare Pages
+ id: deploy
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ inputs.cloudflare_api_token }}
+ accountId: ${{ inputs.cloudflare_account_id }}
+ command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=${{ github.ref_name }}
+ wranglerVersion: '' # Use pre-installed version
+
+ # Deploy preview for PRs
+ - name: Deploy Preview to Cloudflare Pages
+ id: preview-deployment
+ if: github.event_name == 'pull_request'
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ inputs.cloudflare_api_token }}
+ accountId: ${{ inputs.cloudflare_account_id }}
+ command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=pr-${{ github.event.pull_request.number }}
+ wranglerVersion: '' # Use pre-installed version
+
+ # Post deployment URL as PR comment
+ - name: Comment PR with Preview URL
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ env:
+ COMMENT_BODY: ${{ inputs.comment_body }}
+ PRODUCTION_URL: ${{ inputs.production_url }}
+ PROJECT_NAME: ${{ inputs.project_name }}
+ with:
+ github-token: ${{ inputs.github_token }}
+ script: |
+ const prNumber = context.issue.number;
+ // Construct preview URL based on Cloudflare Pages pattern
+ const projectName = process.env.PROJECT_NAME;
+ const previewUrl = `https://pr-${prNumber}.${projectName}.pages.dev`;
+
+ // Check if we already commented
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber
+ });
+
+ const customMessage = process.env.COMMENT_BODY;
+ const botComment = comments.data.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes(customMessage)
+ );
+
+ const mainUrl = process.env.PRODUCTION_URL;
+ const commentBody = `${customMessage}!\n\nš Preview URL: ${previewUrl}\nš Production URL: ${mainUrl}\n\nā
All checks passed\n\n_This preview will be updated automatically with new commits._`;
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: commentBody
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ issue_number: prNumber,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: commentBody
+ });
+ }
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index b80fadb69..6abd9af47 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -16,10 +16,10 @@ on:
- 'requirements-docs.txt'
- '.github/workflows/deploy-docs.yml'
- 'scripts/fix-mkdocs-structure.ts'
-
+
# Allow manual triggering from Actions tab
workflow_dispatch:
-
+
# Run on pull requests for preview deployments
pull_request:
branches:
@@ -38,55 +38,55 @@ jobs:
name: Build and Deploy MkDocs
runs-on: ubuntu-latest
timeout-minutes: 10
-
+
# Required permissions for deployment
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
id-token: write # For OIDC authentication (if needed)
-
+
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
-
+
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: 'requirements-docs.txt'
-
+
- name: Install MkDocs and Dependencies
run: |
pip install --upgrade pip
pip install -r requirements-docs.txt
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
-
+
# Setup pnpm before fixing docs structure
- name: Setup pnpm
uses: pnpm/action-setup@v4
-
+
# Setup Node.js with pnpm
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'pnpm'
-
+
# Install Node.js dependencies for the TypeScript script
- name: Install Dependencies
run: |
pnpm install --frozen-lockfile
-
+
- name: Fix Documentation Structure
run: |
# Fix duplicate navigation entries by moving overview pages to index.md
pnpm run chore:fix-mkdocs-structure
-
+
- name: Build MkDocs Site
run: |
# Build with strict mode but allow expected warnings
@@ -115,74 +115,14 @@ jobs:
test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
echo "ā
Site validation passed"
-
- # Install wrangler globally to avoid workspace issues
- - name: Install Wrangler
- run: |
- npm install -g wrangler
-
- # Deploy using Wrangler (use pre-installed wrangler)
- - name: Deploy to Cloudflare Pages
- id: deploy
- if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
- uses: cloudflare/wrangler-action@v3
+
+ - name: Deploy
+ uses: ./.github/actions/deploy-to-cloudflare-pages
with:
- apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- command: pages deploy site --project-name=trilium-docs --branch=${{ github.ref_name }}
- wranglerVersion: '' # Use pre-installed version
-
- # Deploy preview for PRs
- - name: Deploy Preview to Cloudflare Pages
- id: preview-deployment
- if: github.event_name == 'pull_request'
- uses: cloudflare/wrangler-action@v3
- with:
- apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- command: pages deploy site --project-name=trilium-docs --branch=pr-${{ github.event.pull_request.number }}
- wranglerVersion: '' # Use pre-installed version
-
- # Post deployment URL as PR comment
- - name: Comment PR with Preview URL
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const prNumber = context.issue.number;
- // Construct preview URL based on Cloudflare Pages pattern
- const previewUrl = `https://pr-${prNumber}.trilium-docs.pages.dev`;
- const mainUrl = 'https://docs.triliumnotes.org';
-
- // Check if we already commented
- const comments = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: prNumber
- });
-
- const botComment = comments.data.find(comment =>
- comment.user.type === 'Bot' &&
- comment.body.includes('Documentation preview is ready')
- );
-
- const commentBody = `š Documentation preview is ready!\n\nš Preview URL: ${previewUrl}\nš Production URL: ${mainUrl}\n\nā
All checks passed\n\n_This preview will be updated automatically with new commits._`;
-
- if (botComment) {
- // Update existing comment
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: botComment.id,
- body: commentBody
- });
- } else {
- // Create new comment
- await github.rest.issues.createComment({
- issue_number: prNumber,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: commentBody
- });
- }
+ project_name: "trilium-docs"
+ comment_body: "š Documentation preview is ready"
+ production_url: "https://docs.triliumnotes.org"
+ deploy_dir: "site"
+ cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 07ad94fd7..d9da7c329 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- main
+ paths-ignore:
+ - "apps/website/**"
pull_request:
permissions:
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
new file mode 100644
index 000000000..377c7544e
--- /dev/null
+++ b/.github/workflows/website.yml
@@ -0,0 +1,48 @@
+name: Deploy website
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - "apps/website/**"
+
+ pull_request:
+ paths:
+ - "apps/website/**"
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ name: Build & deploy website
+
+ permissions:
+ contents: read
+ deployments: write
+ pull-requests: write # For PR preview comments
+
+ steps:
+ - uses: actions/checkout@v5
+ - uses: pnpm/action-setup@v4
+ - name: Set up node & dependencies
+ uses: actions/setup-node@v5
+ with:
+ node-version: 22
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --filter website --frozen-lockfile
+
+ - name: Build the website
+ run: pnpm website:build
+
+ - name: Deploy
+ uses: ./.github/actions/deploy-to-cloudflare-pages
+ with:
+ project_name: "trilium-homepage"
+ comment_body: "š Website preview is ready"
+ production_url: "https://triliumnotes.org"
+ deploy_dir: "apps/website/dist"
+ cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json
index 5f1b2ca98..95e7236ba 100644
--- a/apps/desktop/tsconfig.json
+++ b/apps/desktop/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
- "include": [],
+ "include": [],
"references": [
{
"path": "../server"
diff --git a/apps/website/.gitignore b/apps/website/.gitignore
index 5a3bb8506..23f5d6425 100644
--- a/apps/website/.gitignore
+++ b/apps/website/.gitignore
@@ -1,28 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
node_modules
+dist
+dist-ssr
+*.local
-# Output
-.output
-.vercel
-.netlify
-.wrangler
-/.svelte-kit
-/build
-
-# OS
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
.DS_Store
-Thumbs.db
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
-# Env
-.env
-.env.*
-!.env.example
-!.env.test
-
-# Vite
-vite.config.js.timestamp-*
-vite.config.ts.timestamp-*
-
-# Paraglide
-src/lib/paraglide
-
-project.inlang/cache
\ No newline at end of file
+*.d.ts
+!types-assets.d.ts
+*.map
\ No newline at end of file
diff --git a/apps/website/.npmrc b/apps/website/.npmrc
deleted file mode 100644
index b6f27f135..000000000
--- a/apps/website/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-engine-strict=true
diff --git a/apps/website/README.md b/apps/website/README.md
index 88ad0a1c6..1ebc2149e 100644
--- a/apps/website/README.md
+++ b/apps/website/README.md
@@ -1,11 +1,15 @@
-# apps/website
+# `create-preact`
-Landing page for Trilium Notes powered by [Svelte](https://github.com/sveltejs/cli) and [Tailwind CSS](https://tailwindcss.com/).
+
+
+
-## Developing
+Get started using Preact and Vite!
-To run a dev server that will hot-reload changes: `pnpm dev`
+## Getting Started
-## Building
+- `npm run dev` - Starts a dev server at http://localhost:5173/
-To create a production build: `pnpm build`
+- `npm run build` - Builds for production, emitting to `dist/`. Prerenders all found routes in app to static HTML
+
+- `npm run preview` - Starts a server at http://localhost:4173/ to test production build locally
diff --git a/apps/website/eslint.config.js b/apps/website/eslint.config.js
deleted file mode 100644
index 165237185..000000000
--- a/apps/website/eslint.config.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import js from '@eslint/js';
-import { includeIgnoreFile } from '@eslint/compat';
-import svelte from 'eslint-plugin-svelte';
-import globals from 'globals';
-import { fileURLToPath } from 'node:url';
-import ts from 'typescript-eslint';
-import svelteConfig from './svelte.config.js';
-
-const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
-
-export default ts.config(
- includeIgnoreFile(gitignorePath),
- js.configs.recommended,
- ...ts.configs.recommended,
- ...svelte.configs.recommended,
- {
- languageOptions: {
- globals: { ...globals.browser, ...globals.node }
- },
- rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
- // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
- "no-undef": 'off' }
- },
- {
- files: [
- '**/*.svelte',
- '**/*.svelte.ts',
- '**/*.svelte.js'
- ],
- languageOptions: {
- parserOptions: {
- projectService: true,
- extraFileExtensions: ['.svelte'],
- parser: ts.parser,
- svelteConfig
- }
- }
- }
-);
diff --git a/apps/website/index.html b/apps/website/index.html
new file mode 100644
index 000000000..59b5a1e3c
--- /dev/null
+++ b/apps/website/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Trilium Notes
+
+
+
+
+
+
diff --git a/apps/website/messages/en.json b/apps/website/messages/en.json
deleted file mode 100644
index 37a989440..000000000
--- a/apps/website/messages/en.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "$schema": "https://inlang.com/schema/inlang-message-format",
- "hello_world": "Hello, {name} from en!"
-}
diff --git a/apps/website/package.json b/apps/website/package.json
index aa8f73e17..6258f3301 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -1,37 +1,25 @@
{
- "name": "website",
"private": true,
- "version": "0.0.1",
+ "name": "@triliumnext/website",
"type": "module",
"scripts": {
- "dev": "vite dev",
+ "dev": "vite",
"build": "vite build",
- "preview": "vite preview",
- "prepare": "svelte-kit sync || echo ''",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "lint": "eslint ."
- },
- "devDependencies": {
- "@eslint/compat": "^1.2.5",
- "@eslint/js": "^9.18.0",
- "@sveltejs/adapter-auto": "^6.0.0",
- "@sveltejs/kit": "^2.16.0",
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
- "@tailwindcss/typography": "^0.5.15",
- "@tailwindcss/vite": "^4.0.0",
- "eslint": "^9.18.0",
- "eslint-plugin-svelte": "^3.0.0",
- "globals": "^16.0.0",
- "mdsvex": "^0.12.3",
- "svelte": "^5.0.0",
- "svelte-check": "^4.0.0",
- "tailwindcss": "^4.0.0",
- "typescript": "^5.0.0",
- "typescript-eslint": "^8.20.0",
- "vite": "^7.0.0"
+ "preview": "vite preview"
},
"dependencies": {
- "@inlang/paraglide-js": "^2.0.0"
+ "preact": "^10.26.9",
+ "preact-iso": "^2.10.0",
+ "preact-render-to-string": "^6.6.1"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "eslint": "^9.36.0",
+ "eslint-config-preact": "^2.0.0",
+ "typescript": "^5.9.2",
+ "vite": "^7.0.4"
+ },
+ "eslintConfig": {
+ "extends": "preact"
}
}
diff --git a/apps/website/project.inlang/project_id b/apps/website/project.inlang/project_id
deleted file mode 100644
index 2002b3bce..000000000
--- a/apps/website/project.inlang/project_id
+++ /dev/null
@@ -1 +0,0 @@
-dv1iXGpHP2mMvuQQo4
\ No newline at end of file
diff --git a/apps/website/project.inlang/settings.json b/apps/website/project.inlang/settings.json
deleted file mode 100644
index acfd0d3d4..000000000
--- a/apps/website/project.inlang/settings.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "$schema": "https://inlang.com/schema/project-settings",
- "modules": [
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
- "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
- ],
- "plugin.inlang.messageFormat": {
- "pathPattern": "./messages/{locale}.json"
- },
- "baseLocale": "en",
- "locales": [
- "en"
- ]
-}
diff --git a/apps/website/public/collection_board.webp b/apps/website/public/collection_board.webp
new file mode 100644
index 000000000..6ed49dd44
Binary files /dev/null and b/apps/website/public/collection_board.webp differ
diff --git a/apps/website/public/collection_calendar.webp b/apps/website/public/collection_calendar.webp
new file mode 100644
index 000000000..34b7961e0
Binary files /dev/null and b/apps/website/public/collection_calendar.webp differ
diff --git a/apps/website/public/collection_geomap.webp b/apps/website/public/collection_geomap.webp
new file mode 100644
index 000000000..e564fbfc6
Binary files /dev/null and b/apps/website/public/collection_geomap.webp differ
diff --git a/apps/website/public/collection_table.webp b/apps/website/public/collection_table.webp
new file mode 100644
index 000000000..2113caf65
Binary files /dev/null and b/apps/website/public/collection_table.webp differ
diff --git a/apps/website/public/screenshot_desktop_mac_dark.webp b/apps/website/public/screenshot_desktop_mac_dark.webp
new file mode 100644
index 000000000..347838682
Binary files /dev/null and b/apps/website/public/screenshot_desktop_mac_dark.webp differ
diff --git a/apps/website/public/screenshot_desktop_mac_light.webp b/apps/website/public/screenshot_desktop_mac_light.webp
new file mode 100644
index 000000000..2415bbd3f
Binary files /dev/null and b/apps/website/public/screenshot_desktop_mac_light.webp differ
diff --git a/apps/website/public/screenshot_desktop_win_dark.webp b/apps/website/public/screenshot_desktop_win_dark.webp
new file mode 100644
index 000000000..051d8f1c2
Binary files /dev/null and b/apps/website/public/screenshot_desktop_win_dark.webp differ
diff --git a/apps/website/public/screenshot_desktop_win_light.webp b/apps/website/public/screenshot_desktop_win_light.webp
new file mode 100644
index 000000000..a44801ea3
Binary files /dev/null and b/apps/website/public/screenshot_desktop_win_light.webp differ
diff --git a/apps/website/public/type_canvas.webp b/apps/website/public/type_canvas.webp
new file mode 100644
index 000000000..839818606
Binary files /dev/null and b/apps/website/public/type_canvas.webp differ
diff --git a/apps/website/public/type_code.webp b/apps/website/public/type_code.webp
new file mode 100644
index 000000000..91816541a
Binary files /dev/null and b/apps/website/public/type_code.webp differ
diff --git a/apps/website/public/type_file.webp b/apps/website/public/type_file.webp
new file mode 100644
index 000000000..6423d4fc4
Binary files /dev/null and b/apps/website/public/type_file.webp differ
diff --git a/apps/website/public/type_mermaid.webp b/apps/website/public/type_mermaid.webp
new file mode 100644
index 000000000..c7c7eb3c6
Binary files /dev/null and b/apps/website/public/type_mermaid.webp differ
diff --git a/apps/website/public/type_mindmap.webp b/apps/website/public/type_mindmap.webp
new file mode 100644
index 000000000..23addc065
Binary files /dev/null and b/apps/website/public/type_mindmap.webp differ
diff --git a/apps/website/public/type_text.webp b/apps/website/public/type_text.webp
new file mode 100644
index 000000000..dd897dbca
Binary files /dev/null and b/apps/website/public/type_text.webp differ
diff --git a/apps/website/src/app.css b/apps/website/src/app.css
deleted file mode 100644
index e6f251501..000000000
--- a/apps/website/src/app.css
+++ /dev/null
@@ -1,10 +0,0 @@
-@import 'tailwindcss';
-@plugin '@tailwindcss/typography';
-
-main a {
- text-decoration: revert;
-}
-
-a.rounded-full, a.rounded-xl {
- text-decoration: none;
-}
diff --git a/apps/website/src/app.d.ts b/apps/website/src/app.d.ts
deleted file mode 100644
index da08e6da5..000000000
--- a/apps/website/src/app.d.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// See https://svelte.dev/docs/kit/types#app.d.ts
-// for information about these interfaces
-declare global {
- namespace App {
- // interface Error {}
- // interface Locals {}
- // interface PageData {}
- // interface PageState {}
- // interface Platform {}
- }
-}
-
-export {};
diff --git a/apps/website/src/app.html b/apps/website/src/app.html
deleted file mode 100644
index fee9ee80f..000000000
--- a/apps/website/src/app.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- %sveltekit.head%
-
-
- %sveltekit.body%
-
-
diff --git a/apps/website/src/assets/boxicons/bx-arrow-in-down-square-half.svg b/apps/website/src/assets/boxicons/bx-arrow-in-down-square-half.svg
new file mode 100644
index 000000000..6f4f20ac7
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-arrow-in-down-square-half.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-buy-me-a-coffee.svg b/apps/website/src/assets/boxicons/bx-buy-me-a-coffee.svg
new file mode 100644
index 000000000..5e105e2f7
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-buy-me-a-coffee.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-calendar.svg b/apps/website/src/assets/boxicons/bx-calendar.svg
new file mode 100644
index 000000000..a9e283996
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-calendar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-chevrons-up.svg b/apps/website/src/assets/boxicons/bx-chevrons-up.svg
new file mode 100644
index 000000000..ffb1feeef
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-chevrons-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-code.svg b/apps/website/src/assets/boxicons/bx-code.svg
new file mode 100644
index 000000000..56dfdf85b
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-code.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-columns-3.svg b/apps/website/src/assets/boxicons/bx-columns-3.svg
new file mode 100644
index 000000000..646faa6da
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-columns-3.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-discussion.svg b/apps/website/src/assets/boxicons/bx-discussion.svg
new file mode 100644
index 000000000..221197340
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-discussion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-docker.svg b/apps/website/src/assets/boxicons/bx-docker.svg
new file mode 100644
index 000000000..b6d925377
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-docker.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-extension.svg b/apps/website/src/assets/boxicons/bx-extension.svg
new file mode 100644
index 000000000..92fc51472
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-extension.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-file.svg b/apps/website/src/assets/boxicons/bx-file.svg
new file mode 100644
index 000000000..99f70f539
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-folder.svg b/apps/website/src/assets/boxicons/bx-folder.svg
new file mode 100644
index 000000000..af88fa32b
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-folder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-github.svg b/apps/website/src/assets/boxicons/bx-github.svg
new file mode 100644
index 000000000..d027cc0db
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-globe.svg b/apps/website/src/assets/boxicons/bx-globe.svg
new file mode 100644
index 000000000..fbd0bb711
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-help-circle.svg b/apps/website/src/assets/boxicons/bx-help-circle.svg
new file mode 100644
index 000000000..73335bc11
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-help-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-history.svg b/apps/website/src/assets/boxicons/bx-history.svg
new file mode 100644
index 000000000..a87acdea8
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-map.svg b/apps/website/src/assets/boxicons/bx-map.svg
new file mode 100644
index 000000000..3a27e2fa6
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-menu.svg b/apps/website/src/assets/boxicons/bx-menu.svg
new file mode 100644
index 000000000..1c1eebcb5
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-message-dots.svg b/apps/website/src/assets/boxicons/bx-message-dots.svg
new file mode 100644
index 000000000..521797856
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-message-dots.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-network-chart.svg b/apps/website/src/assets/boxicons/bx-network-chart.svg
new file mode 100644
index 000000000..2693f0bac
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-network-chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-note.svg b/apps/website/src/assets/boxicons/bx-note.svg
new file mode 100644
index 000000000..7cf848727
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-note.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-paperclip.svg b/apps/website/src/assets/boxicons/bx-paperclip.svg
new file mode 100644
index 000000000..7d392cf8c
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-paperclip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-paypal.svg b/apps/website/src/assets/boxicons/bx-paypal.svg
new file mode 100644
index 000000000..e4f3db18c
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-paypal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-pen.svg b/apps/website/src/assets/boxicons/bx-pen.svg
new file mode 100644
index 000000000..e26e07d9a
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-pen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-reddit.svg b/apps/website/src/assets/boxicons/bx-reddit.svg
new file mode 100644
index 000000000..e6090b397
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-reddit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-refresh-cw.svg b/apps/website/src/assets/boxicons/bx-refresh-cw.svg
new file mode 100644
index 000000000..c25ae9c9d
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-refresh-cw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-search.svg b/apps/website/src/assets/boxicons/bx-search.svg
new file mode 100644
index 000000000..5f648745d
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-send-alt.svg b/apps/website/src/assets/boxicons/bx-send-alt.svg
new file mode 100644
index 000000000..68ed3e1e2
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-send-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-shield.svg b/apps/website/src/assets/boxicons/bx-shield.svg
new file mode 100644
index 000000000..babfd1ec1
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-shield.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-swap-horizontal.svg b/apps/website/src/assets/boxicons/bx-swap-horizontal.svg
new file mode 100644
index 000000000..dc45d78e3
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-swap-horizontal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-table.svg b/apps/website/src/assets/boxicons/bx-table.svg
new file mode 100644
index 000000000..7a3cd410c
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-tag.svg b/apps/website/src/assets/boxicons/bx-tag.svg
new file mode 100644
index 000000000..6e698bd71
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-tag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/boxicons/bx-vector-square.svg b/apps/website/src/assets/boxicons/bx-vector-square.svg
new file mode 100644
index 000000000..57eefdeb9
--- /dev/null
+++ b/apps/website/src/assets/boxicons/bx-vector-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/website/src/assets/favicon.ico b/apps/website/src/assets/favicon.ico
new file mode 100644
index 000000000..398e3854e
Binary files /dev/null and b/apps/website/src/assets/favicon.ico differ
diff --git a/apps/website/static/icon-color.svg b/apps/website/src/assets/icon-color.svg
similarity index 100%
rename from apps/website/static/icon-color.svg
rename to apps/website/src/assets/icon-color.svg
diff --git a/apps/website/src/components/Button.css b/apps/website/src/components/Button.css
new file mode 100644
index 000000000..4eabe906a
--- /dev/null
+++ b/apps/website/src/components/Button.css
@@ -0,0 +1,20 @@
+a.button {
+ display: block;
+ background-color: var(--brand-1);
+ padding: 0.5em 1em;
+ border-radius: 6px;
+ color: var(--brand-foreground-color);
+ text-decoration: none;
+ text-align: center;
+ box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.15);
+
+ &.outline {
+ border: 1px solid var(--brand-1);
+ color: var(--brand-1);
+ background-color: transparent;
+ }
+
+ .text {
+ vertical-align: middle;
+ }
+}
\ No newline at end of file
diff --git a/apps/website/src/components/Button.tsx b/apps/website/src/components/Button.tsx
new file mode 100644
index 000000000..729346e58
--- /dev/null
+++ b/apps/website/src/components/Button.tsx
@@ -0,0 +1,44 @@
+import { ComponentChildren } from "preact";
+import Icon from "./Icon.js";
+import "./Button.css";
+
+interface LinkProps {
+ className?: string;
+ href?: string;
+ openExternally?: boolean;
+ children: ComponentChildren;
+ title?: string;
+ onClick?: (e: MouseEvent) => void;
+}
+
+interface ButtonProps extends Omit {
+ href?: string;
+ iconSvg?: string;
+ text: ComponentChildren;
+ openExternally?: boolean;
+ outline?: boolean;
+}
+
+export default function Button({ iconSvg, text, className, outline, ...restProps }: ButtonProps) {
+ return (
+
+ {iconSvg && <>{" "}>}
+ {text}
+
+ )
+}
+
+export function Link({ openExternally, children, ...restProps }: LinkProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/website/src/components/Card.tsx b/apps/website/src/components/Card.tsx
new file mode 100644
index 000000000..49c357132
--- /dev/null
+++ b/apps/website/src/components/Card.tsx
@@ -0,0 +1,37 @@
+import { ComponentChildren, HTMLAttributes } from "preact";
+import { Link } from "./Button.js";
+import Icon from "./Icon.js";
+
+interface CardProps extends Omit, "title"> {
+ title: ComponentChildren;
+ imageUrl?: string;
+ iconSvg?: string;
+ className?: string;
+ moreInfoUrl?: string;
+ children: ComponentChildren;
+}
+
+export default function Card({ title, children, imageUrl, iconSvg, className, moreInfoUrl, ...restProps }: CardProps) {
+ return (
+
+ {imageUrl &&

}
+
+
+
+ {iconSvg && }{" "}
+ {title}
+
+
+
+ {children}
+
+
+ {moreInfoUrl && (
+
+ Learn more...
+
+ )}
+
+
+ )
+}
diff --git a/apps/website/src/components/DownloadButton.css b/apps/website/src/components/DownloadButton.css
new file mode 100644
index 000000000..d7ed4cf8f
--- /dev/null
+++ b/apps/website/src/components/DownloadButton.css
@@ -0,0 +1,27 @@
+a.download-button {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ gap: 1em;
+
+ @media (min-width: 720px) {
+ display: flex !important;
+ }
+
+ .platform {
+ font-size: 0.75em;
+ opacity: 0.75;
+ }
+
+ &.big {
+ padding: 0.5em 3.5em;
+ margin: 1em 0;
+ gap: m;
+ text-align: left;
+
+ .platform {
+ display: block;
+ }
+ }
+}
+
diff --git a/apps/website/src/components/DownloadButton.tsx b/apps/website/src/components/DownloadButton.tsx
new file mode 100644
index 000000000..8ed8cf18a
--- /dev/null
+++ b/apps/website/src/components/DownloadButton.tsx
@@ -0,0 +1,30 @@
+import { getRecommendedDownload, RecommendedDownload } from "../download-helper.js";
+import "./DownloadButton.css";
+import Button from "./Button.js";
+import downloadIcon from "../assets/boxicons/bx-arrow-in-down-square-half.svg?raw";
+import packageJson from "../../../../package.json" with { type: "json" };
+import { useEffect, useState } from "preact/hooks";
+
+interface DownloadButtonProps {
+ big?: boolean;
+}
+
+export default function DownloadButton({ big }: DownloadButtonProps) {
+ const [ recommendedDownload, setRecommendedDownload ] = useState();
+ useEffect(() => setRecommendedDownload(getRecommendedDownload()), []);
+
+ return (recommendedDownload &&
+