Skip to content

Commit 628deb8

Browse files
authored
Deploy to void (#13)
* Migrate blog to Void (Vite + Cloudflare Workers + Vue 3) Replace legacy Node.js build (build.js + mdit) with Void framework. Blog markdown files become first-class page routes via @void-x/md. Static assets moved to public/ for Void's static file serving. - Add vite.config.ts with voidPlugin, voidVue, voidMarkdown - Create pages/layout.vue (shared) and pages/blog/layout.vue (blog chrome + Disqus) - Create pages/index.vue homepage with all curated content - Move 111 blog .md files to pages/blog/ - Add middleware/html-compat.ts for .html → clean URL redirects - Copy static assets (css, js, images, ppt, collections, etc.) to public/ - Remove scaffolded artifacts (migrations, routes/api, src/demo.ts) - Update package.json deps, tsconfig.json, deploy.yml branch * Replace GitHub ribbon with Deploy on V___ ribbon - Update layout.vue: diagonal ribbon style replacing fork-me image - Remove original blog/ directory (content moved to pages/blog/ and public/blog/) - Remove root index.html (replaced by pages/index.vue) - Remove shadow HTML files from public/blog/ that have .md page equivalents * Fix deploy: move legacy files, fix SSR CSS and Disqus scripts - Move legacy root-level directories to legacy.bak/ to reduce deploy size and prevent Vite from bundling them into SSR - Fix CSS causing worker deploy failure by moving @import to <link> tags and using <component is="style"> for custom styles - Fix Disqus comments not loading by using <component is="script"> instead of onMounted (no client hydration in SSR pages) - Change image src to dynamic :src bindings to avoid Vite asset processing - Restore hangjs-family-photo2.jpg resized to 2000px (179KB vs 5.6MB) * Fix ppt deck.js paths to use relative URLs Replace absolute https://fengmk2.com:9443/ references with relative paths so slides work on any domain. * Return 404 for unknown paths and add sitemap generator Remove middleware/html-compat.ts which unconditionally stripped .html from any request path and issued a 301 without checking that the resource existed. Crawlers hitting garbage concatenated paths like /ppt/.../README.html were getting 301s that search engines then cached. Without the middleware, real .html files in public/ still serve 200 via env.ASSETS, and non-existent paths fall through to 404. Add public/sitemap.xml + public/robots.txt so crawlers can discover the canonical URL set instead of guessing. scripts/generate-sitemap.mjs walks pages/blog/**/*.md and regenerates the sitemap; wired into package.json as prebuild so every vite build refreshes it. * Switch void packages to @void-sdk GitHub Packages aliases Replace local file: paths with the official npm aliases documented in the void-sdk/void README: - void@npm:@void-sdk/void@^0.5.0 - @void/vue@npm:@void-sdk/vue@^0.4.0 - @void/md@npm:@void-sdk/md@^0.5.0 Rename imports in vite.config.ts from @void-x/* to @void/* and drop the resolve.alias shim that mapped between the two scopes. Switch packageManager to pnpm@10.33.0 per the README's recommended tooling and add pnpm-lock.yaml. * update deps * Add PR staging deploy workflow, switch deploy to pnpm Add .github/workflows/ci.yml adapted from voidzero-dev/setup.viteplus.dev: - test job on every push to master and every PR (pnpm install + build) - staging-deploy job on PRs only, deploys to VOID_PROJECT=fengmk2-staging and posts/updates a sticky comment with the preview URL Update .github/workflows/deploy.yml to use pnpm instead of npm ci, matching the packageManager switch to pnpm@10.33.0. Production deploy continues to read the projectId from .void/project.json. Both workflows use the latest action versions: actions/checkout@v6, actions/setup-node@v6, actions/github-script@v9, pnpm/action-setup@v5. Installation auths against npm.pkg.github.com via NODE_AUTH_TOKEN so @void-sdk/* packages resolve. * ignore .void * Switch CI/deploy workflows to voidzero-dev/setup-vp Replace the pnpm/action-setup + actions/setup-node pair with the official setup-vp action, matching the voidzero-dev/setup.viteplus.dev reference. setup-vp handles Node install via 'vp env use', auto-detects pnpm from the lockfile, and caches the pnpm store. Replace 'pnpm build' with 'vp run build' and 'pnpm exec void deploy' with 'vpx void deploy' so the workflow is package-manager-agnostic and relies on Vite+'s own script runner. Install auth against npm.pkg.github.com still flows through NODE_AUTH_TOKEN; setup-vp's default run-install uses that env to pull @void-sdk/* packages. Production and staging projects resolved exactly as before. * Migrate project to Vite+ (vp) Run 'vp migrate --no-interactive' to adopt Vite+ as the unified toolchain (runtime, package manager, dev/build, lint, format). - vite.config.ts now imports from 'vite-plus'; drops the Vite-only shape for Vite+'s defineConfig with 'staged', 'fmt', 'lint' blocks - package.json scripts use 'vp dev / vp build / vp preview'; add 'prepare: vp config' for the pre-commit hook setup - pnpm-workspace.yaml added with catalog routing 'vite' to '@voidzero-dev/vite-plus-core' and 'vitest' to 'vite-plus-test'; peerDependencyRules relax matching so void plugins keep resolving - env.d.ts adds the '*.vue' TypeScript shim - AGENTS.md captures the Vite+ workflow for coding agents - .vite-hooks/pre-commit wires staged-file checks via 'vp staged' Follow-up tweaks on top of the auto-migration: - Legacy content at the repo root (blog.bak, legacy.bak, MOVE, movement.js, max_new_space_size, libuv, scattered *.md/*.js/*.html) is added to 'fmt.ignorePatterns' and 'lint.ignorePatterns' in vite.config.ts so 'vp check' only looks at the active source tree. - Format-only touch-ups on CLAUDE.md, env.d.ts, tsconfig.json, package.json, scripts/generate-sitemap.mjs, pages/*.vue, pnpm-workspace.yaml, vite.config.ts, and .void/entry.ts produced by 'vp fmt --write'. - CI: add 'vp check' as a pipeline gate before 'vp run build'. 'vp build' skips npm lifecycle hooks, so both CI and deploy workflows now run 'vp run sitemap' before 'vpx void deploy' to keep public/sitemap.xml fresh on every deploy. Verification: - vp install: ok - vp check: ok (18 files formatted, 5 files lint/typecheck clean) - vp build: ok (162 modules, ~750ms) - vp test: fails at config resolution because '@cloudflare/vite-plugin' rejects the SSR environment's 'resolve.external' set by voidPlugin() when vitest loads the config. No tests exist, so this is non-blocking; revisit when tests are added (likely needs a vitest-scoped config variant). * fmt * Pin setup-vp to voidzero-dev/setup-vp#52 Use the commit SHA at the head of PR #52 ("fix: surface vp install errors outside collapsed log group") so install failures on CI/deploy show up outside the collapsed setup-vp log group. Revert to @v1 once the PR merges. * Bump setup-vp PR #52 pin to latest head 21f6c6e -> 697e70a (new commit pushed to fix/surface-vp-install-errors). * Refactor setup-vp usage to PR #54 latest behavior Bump setup-vp pin from e79fc32 to af4ffd9 (new head of PR #54) and drop the repo .npmrc auth-line dance. The updated PR now auto- generates _authToken entries at \$RUNNER_TEMP/.npmrc for every registry declared in the repo .npmrc when NODE_AUTH_TOKEN is set, so the committed .npmrc can stay as just the registry mapping. Also update CLAUDE.md guidance to match the new pattern. * Bump setup-vp PR #54 pin to latest head (af4ffd9 -> 42d342a) * Pin setup-vp to the merged commit of PR #54 42d342a (PR branch head) -> 4f5aa3e (squash-merge on main). PR #54 is now merged into voidzero-dev/setup-vp main with title "feat: respect project .npmrc and auto-generate _authToken". * Use voidzero-dev/setup-vp@v1 floating tag * Fixup * Serve a real 404 page for unknown URLs Void's default notFound handler renders the index component with status 404, so unknown URLs looked like the homepage. Add a catch-all pages/[...slug] that renders a dedicated Not Found page, and a global middleware that rewrites the response status to 404 because Void's renderedResponse builds a fresh Response without honoring c.status() set inside a loader. * Give every page a <title> Void only auto-derives a head title from YAML frontmatter (not from the first H1), and Vue pages need an explicit head() export. Add titles for the home page and the 404 page, and inject title: frontmatter into all markdown blog posts that lacked it (derived from each post's first H1). The new scripts/inject-md-titles.mjs is the one-off generator. * Log user-agent and client IP on 404 hits Helps spot scanners and broken inbound links from runtime logs. IP prefers cf-connecting-ip on Cloudflare and falls back to forwarded headers in other environments.
1 parent 219266e commit 628deb8

1,903 files changed

Lines changed: 74466 additions & 12 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"void": {
4+
"command": "npx",
5+
"args": ["void", "mcp"]
6+
}
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../voidzero-dev/void/packages/void/skills/migrate-vite-cloudflare-to-void

.claude/skills/void

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../voidzero-dev/void/packages/void/skills/void

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: CI
2+
on:
3+
push:
4+
branches: [master]
5+
pull_request:
6+
branches: [master]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v6
17+
- uses: voidzero-dev/setup-vp@v1
18+
with:
19+
cache: true
20+
env:
21+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
22+
- run: vp check
23+
- run: vp run build
24+
25+
staging-deploy:
26+
needs: test
27+
if: github.event_name == 'pull_request'
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v6
31+
- uses: voidzero-dev/setup-vp@v1
32+
with:
33+
cache: true
34+
env:
35+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
36+
- run: vp run sitemap
37+
- run: vpx void deploy
38+
env:
39+
VOID_TOKEN: ${{ secrets.VOID_TOKEN }}
40+
VOID_PROJECT: fengmk2-staging
41+
- name: Comment on PR
42+
uses: actions/github-script@v9
43+
with:
44+
script: |
45+
const marker = '<!-- staging-deploy -->';
46+
const body = `${marker}\n✅ Staging deployment successful!\n\nPreview: https://fengmk2-staging.void.app/\nCommit: ${context.sha}`;
47+
const comments = await github.paginate(github.rest.issues.listComments, {
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
issue_number: context.issue.number,
51+
});
52+
const existing = comments.find(c => c.body.includes(marker));
53+
if (existing) {
54+
await github.rest.issues.updateComment({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
comment_id: existing.id,
58+
body,
59+
});
60+
} else {
61+
await github.rest.issues.createComment({
62+
owner: context.repo.owner,
63+
repo: context.repo.repo,
64+
issue_number: context.issue.number,
65+
body,
66+
});
67+
}

.github/workflows/deploy.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Deploy
2+
on:
3+
push:
4+
branches: [master]
5+
workflow_dispatch:
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v6
12+
- uses: voidzero-dev/setup-vp@v1
13+
with:
14+
cache: true
15+
env:
16+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
17+
- run: vp run sitemap
18+
- run: vpx void deploy
19+
env:
20+
VOID_TOKEN: ${{ secrets.VOID_TOKEN }}

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ adc2013_chart.zip
1515
npm-debug.log
1616
nohup.out
1717
package-lock.json
18+
dist/
19+
.wrangler/
20+
.void/
21+
node_modules
22+
dist
23+
.void
24+
.wrangler
25+
*.tsbuildinfo

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24.14.0

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@void-sdk:registry=https://npm.pkg.github.com

.vite-hooks/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vp staged

.void/entry.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import app, { scheduled, __voidCollectPrerenderPaths, __voidHandleWebSocket, __withRawEnv } from 'virtual:void-routes';
2+
import { withRuntimeEnv } from "/Users/fengmk2/git/github.com/fengmk2/fengmk2.github.com/node_modules/.pnpm/@void-sdk+void@0.5.0_@void-sdk+md@0.5.0_@void-sdk+react@0.5.0_@void-sdk+solid@0.5.0_@vo_65db7940de9b53d51259765009dc4b8c/node_modules/@void-sdk/void/dist/runtime/env.mjs";
3+
4+
function __filterInternalBindings(env) {
5+
return new Proxy(env, {
6+
get(target, prop, receiver) {
7+
if (typeof prop === "string" && (prop.startsWith("__VOID_") || prop.startsWith("__PROJECT_"))) return undefined;
8+
return Reflect.get(target, prop, receiver);
9+
},
10+
has(target, prop) {
11+
if (typeof prop === "string" && (prop.startsWith("__VOID_") || prop.startsWith("__PROJECT_"))) return false;
12+
return Reflect.has(target, prop);
13+
},
14+
ownKeys(target) {
15+
return Reflect.ownKeys(target).filter(
16+
(key) => typeof key !== "string" || (!key.startsWith("__VOID_") && !key.startsWith("__PROJECT_"))
17+
);
18+
},
19+
getOwnPropertyDescriptor(target, prop) {
20+
if (typeof prop === "string" && (prop.startsWith("__VOID_") || prop.startsWith("__PROJECT_"))) return undefined;
21+
return Reflect.getOwnPropertyDescriptor(target, prop);
22+
},
23+
});
24+
}
25+
26+
async function serveWithAssets(request, env, response) {
27+
if (response.status !== 404 || !env.ASSETS || (request.method !== "GET" && request.method !== "HEAD")) return response;
28+
const asset = await env.ASSETS.fetch(new Request(request.url, { method: request.method, headers: request.headers }));
29+
if (asset.status === 404) return response;
30+
const res = new Response(asset.body, asset);
31+
if (new URL(request.url).pathname.startsWith("/assets/")) {
32+
res.headers.set("Cache-Control", "public, max-age=31536000, immutable");
33+
} else {
34+
res.headers.set("Cache-Control", "public, s-maxage=60, max-age=0, must-revalidate");
35+
}
36+
return res;
37+
}
38+
39+
async function __handleInternal(request, env, ctx) {
40+
const url = new URL(request.url);
41+
// Readiness probe — platform polls this to confirm the worker is live after WfP upload.
42+
// Handled here (before app.fetch) so user middleware cannot block it.
43+
if (request.method === "GET" && url.pathname === "/__void/ready") {
44+
return Response.json({ ready: true });
45+
}
46+
if (request.method !== "POST" || url.pathname !== "/__void/prerender-paths") return null;
47+
const __expected = env.__VOID_PROXY_TOKEN;
48+
if (__expected) {
49+
const __token = request.headers.get("x-void-internal");
50+
if (!__token || __token !== __expected) {
51+
return Response.json({ error: "unauthorized" }, { status: 401 });
52+
}
53+
}
54+
const paths = await __voidCollectPrerenderPaths(request, env, ctx);
55+
return Response.json({ paths });
56+
}
57+
async function handleScheduled(controller, env, ctx) {
58+
const userEnv = __filterInternalBindings(env);
59+
return withRuntimeEnv(env, () => scheduled(controller, userEnv, ctx));
60+
}
61+
62+
export default { fetch: async (request, env, ctx) => {
63+
const ws = await __voidHandleWebSocket(request, env);
64+
if (ws) return ws;
65+
return __withRawEnv(env, () => {
66+
const userEnv = __filterInternalBindings(env);
67+
return withRuntimeEnv(env, async () => {
68+
const internal = await __handleInternal(request, env, ctx);
69+
if (internal) return internal;
70+
return serveWithAssets(request, env, await app.fetch(request, userEnv, ctx));
71+
});
72+
});
73+
}, scheduled: handleScheduled };

0 commit comments

Comments
 (0)