diff --git a/.github/workflows/site-build.yml b/.github/workflows/site-build.yml index 2d2207109d..ebd5c4ca68 100644 --- a/.github/workflows/site-build.yml +++ b/.github/workflows/site-build.yml @@ -24,7 +24,9 @@ jobs: with: node-version: "22" cache: npm - cache-dependency-path: package-lock.json + cache-dependency-path: | + package-lock.json + website/package-lock.json - name: Install JS dependencies run: npm ci @@ -32,6 +34,10 @@ jobs: - name: Build web SPAs run: npm run build + - name: Build documentation site + working-directory: website + run: npm ci && npm run build + - name: Prepare deploy bundle run: | set -euo pipefail @@ -43,7 +49,7 @@ jobs: mkdir -p _bundle/public/admin cp -a web/dist/admin/. _bundle/public/admin/ mkdir -p _bundle/public/docs - cp -a web/dist/docs/. _bundle/public/docs/ + cp -a website/dist/. _bundle/public/docs/ mkdir -p _bundle/worker cp -a cloudflare_site/worker/. _bundle/worker/ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..5762cdad4a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +--- +layout: page +title: Fullsend Docs +--- + + diff --git a/website/static/img/favicon.png b/docs/public/img/favicon.png similarity index 100% rename from website/static/img/favicon.png rename to docs/public/img/favicon.png diff --git a/website/static/img/logo.png b/docs/public/img/logo.png similarity index 100% rename from website/static/img/logo.png rename to docs/public/img/logo.png diff --git a/docs/site-deployment.md b/docs/site-deployment.md index ce26015f1d..e249d7707a 100644 --- a/docs/site-deployment.md +++ b/docs/site-deployment.md @@ -2,13 +2,13 @@ ## Overview -This repository publishes a static documentation site. The root landing page is [`web/public/index.html`](../web/public/index.html); the interactive document graph is [`web/public/graph.html`](../web/public/graph.html) (served at `/graph.html`). **Vite** builds the **admin** SPA under **`web/dist/admin/`** (see [`web/admin/README.md`](../web/admin/README.md)). The **docs site** is built by **Docusaurus** from the **`website/`** directory, reading markdown from **`docs/`** and producing static HTML in **`website/build/`**. CI copies **`assets/`**, **`admin/`**, and **`docs/`** into **`_bundle/public/`** so the Worker serves **`/admin/`** and **`/docs/`** from the same static asset tree. OAuth/CORS hardening for that Worker is summarized in [`docs/admin-oauth-worker.md`](admin-oauth-worker.md) (path-specific CORS for `/api/github/user`, no separate “OAuth enabled” env flag). +This repository publishes a static documentation site. The root landing page is [`web/public/index.html`](../web/public/index.html); the interactive document graph is [`web/public/graph.html`](../web/public/graph.html) (served at `/graph.html`). **Vite** builds the **admin** SPA under **`web/dist/admin/`** (see [`web/admin/README.md`](../web/admin/README.md)). The **docs site** is built by **VitePress** from the **`website/`** directory, reading markdown from **`docs/`** and producing static HTML in **`website/dist/`**. CI copies **`assets/`**, **`admin/`**, and **`docs/`** into **`_bundle/public/`** so the Worker serves **`/admin/`** and **`/docs/`** from the same static asset tree. OAuth/CORS hardening for that Worker is summarized in [`docs/admin-oauth-worker.md`](admin-oauth-worker.md) (path-specific CORS for `/api/github/user`, no separate “OAuth enabled” env flag). -**Build Site** runs **`npm ci`** and **`npm run build`** at the repository root, then packs **`public/`** (static files, including `assets/` and `admin/` from `web/dist/`, plus `docs/` from `website/build/`) and **`worker/`** (TypeScript Worker from the same checkout—PR head on PR builds) under **`_bundle/`** in one artifact. **Deploy Site** checks out **only the default branch** (trusted [`cloudflare_site/wrangler.toml`](../cloudflare_site/wrangler.toml); never PR-controlled config on the secret-bearing runner), downloads the artifact to **`_bundle/`**, then **copies only** **`_bundle/public/`** and **`_bundle/worker/`** into **`cloudflare_site/`** (so a malicious artifact cannot overwrite `wrangler.toml` or other repo files), then runs Wrangler. Deployment uses **Cloudflare Workers with [static assets](https://developers.cloudflare.com/workers/static-assets/)** (not the legacy **Pages direct-upload** / `wrangler pages deploy` flow). +**Build Site** runs **`npm ci`** and **`npm run build`** at the repository root, then packs **`public/`** (static files, including `assets/` and `admin/` from `web/dist/`, plus `docs/` from `website/dist/`) and **`worker/`** (TypeScript Worker from the same checkout—PR head on PR builds) under **`_bundle/`** in one artifact. **Deploy Site** checks out **only the default branch** (trusted [`cloudflare_site/wrangler.toml`](../cloudflare_site/wrangler.toml); never PR-controlled config on the secret-bearing runner), downloads the artifact to **`_bundle/`**, then **copies only** **`_bundle/public/`** and **`_bundle/worker/`** into **`cloudflare_site/`** (so a malicious artifact cannot overwrite `wrangler.toml` or other repo files), then runs Wrangler. Deployment uses **Cloudflare Workers with [static assets](https://developers.cloudflare.com/workers/static-assets/)** (not the legacy **Pages direct-upload** / `wrangler pages deploy` flow). Two GitHub Actions workflows: -- **Build Site** — on `pull_request` and `push` to `main`, checks out the PR head when relevant, installs Node dependencies, builds the admin SPA (Vite) and docs site (Docusaurus), assembles **`_bundle/public/`** and **`_bundle/worker/`**, uploads artifact **`site`** (`_bundle/` contents). +- **Build Site** — on `pull_request` and `push` to `main`, checks out the PR head when relevant, installs Node dependencies, builds the admin SPA (Vite) and docs site (VitePress), assembles **`_bundle/public/`** and **`_bundle/worker/`**, uploads artifact **`site`** (`_bundle/` contents). - **Deploy Site** — on successful **Build Site** via `workflow_run`, checks out the repo default ref (trusted Wrangler project files), downloads artifact **`site`** into **`_bundle/`**, copies **`public/`** and **`worker/`** into **`cloudflare_site/`**, then: - **push to `main`:** `wrangler deploy` → production Worker traffic. - **pull_request:** `wrangler versions upload --preview-alias pr-` → preview URL on `*.workers.dev` without changing production (alias falls back to `pr-` only when the same fork branch matches more than one open PR). @@ -65,7 +65,7 @@ Disable **GitHub Pages** under **Settings → Pages** if it was only used for th **Full stack (recommended for admin OAuth):** from the repository root, run **`npm run dev`** so Vite serves the SPA and Wrangler runs the site Worker with shared process env — see [`web/admin/README.md`](../web/admin/README.md). -**Static tree + Worker (closer to production asset layout):** install dependencies, build admin SPA and Docusaurus site, copy the same layout CI uses under `cloudflare_site/public/`, then run Wrangler: +**Static tree + Worker (closer to production asset layout):** install dependencies, build admin SPA and VitePress docs site, copy the same layout CI uses under `cloudflare_site/public/`, then run Wrangler: ```bash npm ci @@ -78,7 +78,7 @@ cp web/public/index.html cloudflare_site/public/index.html cp web/public/graph.html cloudflare_site/public/graph.html cp -a web/dist/assets/. cloudflare_site/public/assets/ cp -a web/dist/admin/. cloudflare_site/public/admin/ -cp -a website/build/. cloudflare_site/public/docs/ +cp -a website/dist/. cloudflare_site/public/docs/ cd cloudflare_site && npx wrangler@4 dev ``` diff --git a/package.json b/package.json index 619ead9aa0..e4e8761cc6 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,13 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run --config vite.config.ts && vitest run --config cloudflare_site/worker/vitest.config.mts", - "check": "svelte-check --tsconfig web/admin/tsconfig.json && svelte-check --tsconfig web/docs/tsconfig.json", - "lint": "eslint web/admin/src/ web/docs/src/", - "lint:fix": "eslint web/admin/src/ web/docs/src/ --fix", - "format": "prettier --write 'web/{admin,docs}/src/**/*.{svelte,ts,js,css}'", - "format:check": "prettier --check 'web/{admin,docs}/src/**/*.{svelte,ts,js,css}'", - "stylelint": "stylelint 'web/{admin,docs}/src/**/*.{svelte,css}'", - "stylelint:fix": "stylelint 'web/{admin,docs}/src/**/*.{svelte,css}' --fix" + "check": "svelte-check --tsconfig web/admin/tsconfig.json", + "lint": "eslint web/admin/src/", + "lint:fix": "eslint web/admin/src/ --fix", + "format": "prettier --write 'web/admin/src/**/*.{svelte,ts,js,css}'", + "format:check": "prettier --check 'web/admin/src/**/*.{svelte,ts,js,css}'", + "stylelint": "stylelint 'web/admin/src/**/*.{svelte,css}'", + "stylelint:fix": "stylelint 'web/admin/src/**/*.{svelte,css}' --fix" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.14.7", diff --git a/vite.config.ts b/vite.config.ts index 17c737669f..bebb651cb4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,6 @@ import { normalizePath } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import { defineConfig } from "vitest/config"; import type { Plugin } from "vite"; -import { fullsendDocsPlugin } from "./web/docs/build/vitePluginDocs"; const repoRoot = path.dirname(fileURLToPath(import.meta.url)); const webRoot = path.join(repoRoot, "web"); @@ -22,8 +21,6 @@ function spaFallbackPlugin(): Plugin { req.url = "/index.html"; } else if (url.startsWith("/admin/") && !path.extname(url)) { req.url = "/admin/index.html"; - } else if (url.startsWith("/docs/") && !path.extname(url)) { - req.url = "/docs/index.html"; } next(); }); @@ -94,11 +91,6 @@ export default defineConfig(({ command }) => ({ ), ], }), - svelte({ - configFile: path.join(webRoot, "docs/svelte.config.js"), - include: normalizePath(path.join(webRoot, "docs/**/*.svelte")), - }), - fullsendDocsPlugin(repoRoot), spaFallbackPlugin(), adminDevEnvLogPlugin(), adminRequestLogPlugin(), @@ -107,7 +99,6 @@ export default defineConfig(({ command }) => ({ rollupOptions: { input: { admin: path.join(webRoot, "admin/index.html"), - docs: path.join(webRoot, "docs/index.html"), }, }, }, @@ -118,11 +109,8 @@ export default defineConfig(({ command }) => ({ }, test: { environment: "jsdom", - environmentMatchGlobs: [["docs/build/**/*.test.ts", "node"]], include: [ "admin/src/**/*.test.ts", - "docs/build/**/*.test.ts", - "docs/src/**/*.test.ts", ], passWithNoTests: true, }, diff --git a/website/.gitignore b/website/.gitignore index 06cc35fcbe..0e1d48ce3e 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -1,4 +1,4 @@ node_modules/ -build/ -.docusaurus/ -.playwright-cli/ +dist/ +.vitepress/cache/ +.vitepress/.temp/ diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts new file mode 100644 index 0000000000..323c4d50c3 --- /dev/null +++ b/website/.vitepress/config.ts @@ -0,0 +1,315 @@ +import { defineConfig } from 'vitepress' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const docsDir = path.resolve(__dirname, '..', '..', 'docs') + +function getMarkdownFiles(dir: string, base: string): { text: string; link: string }[] { + const fullDir = path.resolve(docsDir, dir) + if (!fs.existsSync(fullDir)) return [] + const items: { text: string; link: string }[] = [] + for (const entry of fs.readdirSync(fullDir).sort()) { + const entryPath = path.resolve(fullDir, entry) + if (entry.endsWith('.md') && entry !== 'README.md') { + const slug = entry.replace(/\.md$/, '') + const content = fs.readFileSync(entryPath, 'utf-8') + const fmTitleMatch = content.match(/^title:\s*["']?(.+?)["']?\s*$/m) + const titleMatch = content.match(/^#\s+(.+)$/m) + items.push({ text: fmTitleMatch?.[1] || titleMatch?.[1] || slug, link: `/${base}/${slug}` }) + } else if (fs.statSync(entryPath).isDirectory()) { + const readme = path.resolve(entryPath, 'README.md') + if (fs.existsSync(readme)) { + const content = fs.readFileSync(readme, 'utf-8') + const titleMatch = content.match(/^#\s+(.+)$/m) + items.push({ text: titleMatch?.[1] || entry, link: `/${base}/${entry}/` }) + } + } + } + return items +} + +// Escape Vue-incompatible syntax ({ }, {{ }}, ) in markdown +// before markdown-it processes it. Code fence tracking uses backtick-count +// matching per CommonMark spec to correctly handle nested fences. +function escapeVueSyntax(src: string): string { + const lines = src.split('\n') + let fenceLen = 0 + let fenceChar = '' + return lines.map(line => { + const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/) + if (fenceMatch) { + const ch = fenceMatch[1][0] + const len = fenceMatch[1].length + if (fenceLen === 0) { + fenceLen = len + fenceChar = ch + return line + } + if (ch === fenceChar && len >= fenceLen && line.trim() === ch.repeat(len)) { + fenceLen = 0 + fenceChar = '' + return line + } + return line + } + if (fenceLen > 0) return line + return escapeLine(line) + }).join('\n') +} + +const KNOWN_TAGS = /^<\/?(?:a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h[1-6]|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|label|legend|li|link|main|map|mark|menu|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|search|section|select|slot|small|source|span|strong|style|sub|summary|sup|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr|svg|path|g|circle|rect|line|polyline|polygon|text|defs|use|symbol)[\s>\/!]/i + +function escapeLine(line: string): string { + let result = '' + let inInlineCode = false + for (let i = 0; i < line.length; i++) { + const ch = line[i] + if (ch === '`') { + inInlineCode = !inInlineCode + result += '`' + } else if (!inInlineCode && ch === '{') { + result += '{' + } else if (!inInlineCode && ch === '}') { + result += '}' + } else if (!inInlineCode && ch === '<') { + const rest = line.slice(i) + if (KNOWN_TAGS.test(rest) || /^