Total Pageviews

Saturday, 31 October 2026

我们比中国人更需要中国人

 

德国海瑞克创始人当年说过一句很有时代气息的话:“我们比中国人更需要中国人。”

那时候听起来很潇洒。中国是市场,是订单,是便宜的生产基地,是一眼望不到头的城市化工程。德国有技术,中国有需求,大家合作,各取所需。资本主义最喜欢这种故事:你有钱赚,我有东西学,皆大欢喜。

然后十几年过去。

现在中国开始限制隧道掘进机和相关技术出口。德国海瑞克恰好在中国有生产。于是一个德国品牌、一家德国企业生产的机器,如果是在中国工厂造出来的,想运到印度、澳大利亚或者别的地方,还得先看中国政府愿不愿意放行。

这时候再念一遍:“我们比中国人更需要中国人。”

终于实现了。应该很开心吧。

当然,不能说中国今天的盾构机都是德国人白送的。中国自己确实投入了大量工程、研发、供应链和人才。没有几十年建设,没有庞大的国内市场,没有中国企业自己拼命追赶,也不会有今天。但另一个事实也同样不能装看不见:中国以前在现代大型盾构机上长期依赖进口。上世纪末到本世纪初,德国、日本等企业大量进入中国市场,卖机器、设厂、培训、配套、合作。

中国人看着,学着,做着。最开始是客户,后来是学生,再后来是同行,最后成了竞争对手(这就是中国人的智慧)。今天还多了一层:监管者。

这就是全球化最有意思的地方。西方企业当年算的是财务账:哪里市场大,往哪里去;哪里成本低,往哪里搬;哪里订单多,在哪里设厂。至于十年后、二十年后会发生什么,那是下一任CEO的事情。

财报按季度出。国家竞争按几十年算。

两个时间尺度完全不同。

德国企业当年看中国,大概像看一个巨大的工地。地铁要挖,铁路要建,水利要做,城市要扩张。海瑞克有技术,有设备,有品牌;中国有钱,有需求。还能有什么问题?

问题就是,机器卖进去以后,不只是机器进去了。还有工程师,还有生产流程,还有供应商,还有零部件体系,还有人才,还有大量“怎么把这东西做出来”的经验。

全球化最神奇的一点,就是企业以为自己是在卖产品,实际上有时候连产业生态一起搬过去了。

当年一句“我们更需要中国人”,表达的是对市场的依赖。今天,这句话已经有了新的意思:中国市场还在,中国工厂还在,中国供应链还在,只是中国人已经不再只需要你的机器。他们自己也能造了,而且造得很多。

然后有一天,中国政府说:这种设备属于军民两用物项,出口要审批。

德国企业忽然发现,事情已经从“我去不去中国赚钱”,变成了“我的东西能不能从中国出去”。

这就是最漂亮的转折。

过去的全球化故事,总喜欢讲相互依存,听起来很文明。实际上,相互依存从来不是数学上的五五开。谁掌握工厂,谁掌握供应链,谁掌握原材料,谁掌握出口许可,依存关系就不一定对称。

当年企业家说:“我们比中国人更需要中国人。”当成一句商业智慧。今天回头看,它更像一句预言,而且是自己替自己写的。

企业当然可以说,没人知道二十年后会这样。对。所以国家才需要战略。如果一切都只看下一个季度利润,那还要什么产业政策,什么供应链安全,什么国家安全?

把工厂搬过去的时候,利润是真的;把技术学走的时候,订单也是真的;等到有一天,连你的设备离开中国都要看许可证时,风险也是真的。

所以这件事最值得记住的,不是中国突然“变坏了”,而是西方企业几十年前就知道自己越来越依赖中国,却把这种依赖当成成功。

用muse的web版帮我安装基于nextjs(前端)+MySQL+Redis的建站程序anb

 me: can u build https://github.com/acmenlei/next-blog? the subdomain of mingsea.net can be named as anb.

 Build is green. Setting up durable VM hosting — moving the app out of scratch space and writing the unit + bootstrap script.

 App is up locally. Wiring the tunnel and DNS for anb.mingsea.net

 Homepage is up but /article 502s — checking whether that's local or tunnel's issue.

Crash fixed, but /article still 500s — the failed fetch now propagates instead. Checking that page's data code.

All routes green locally. Verifying publicly.

Public site verified. Adding anb to the watchdog so the next VM reboot restores it too.

Live: https://anb.mingsea.net — VM-hosted via the tunnel, watchdog-covered, verified on /, /article, /board, and /profile (all 200).

One important thing you should know: this repo is only the frontend. All articles, comments, and logins come from a separate backend API, and the author's backend is dead — I checked, its server doesn't answer. So right now the site loads fine but shows its empty state ("这里现在什么内容都没有"). Real posts need the backend built too: the author's node-admin-backend plus MySQL and Redis on this VM. That's a separate job I haven't started.

Two build fixes worth knowing, both local-only (no fork made): the template failed type-checking on ~30 implicit-anys, and its article pages crashed the server when the backend fetch failed — antd's toast touches document during server rendering, and the page had no fallback. Both fixed; the pages now degrade to the empty state instead of 500ing or killing the process.

me:  build the author's node-admin-backend plus MySQL and Redis on this VM.

Redis binary exists under ~/opt/redis. Checking the remaining setup facts.

 Database is in with all 11 tables. Now the local config edits and install.

 Classic case-sensitivity bug — the author developed on a case-insensitive filesystem. Fixing the mismatched requires.

 Backend is answering. Wiring its public hostname and repointing the frontend.

Public GET works, but the POST check came back empty — re-testing it explicitly.

 Public API confirmed working with correct CORS. Adding backend coverage to the watchdog.

Backend is live and wired in:

  • API: https://api-anb.mingsea.net — verified from the public internet: POST /reception/article/list returns code 200, and CORS correctly allows https://anb.mingsea.net with credentials.
  • Stack: Express backend on port 9001, its own Redis on 6381, and the admin_top_blog database (all 11 tables imported) running on Ghost's MySQL 8 instance. Frontend rebuilt to point at the new API, and anb.mingsea.net/article is 200. Watchdog Step 7 now covers the backend too.
  • Two backend bugs fixed (local-only): the author's code required ../models/article and ../models/tag, but the files are Article.js/Tag.js — works on his case-insensitive filesystem, crashes on Linux. And the Aliyun OSS config file was never published, so I stubbed it; reads work fine, but image uploads will need real Aliyun credentials if you want them.

One honest state: the database starts empty, so /article still shows the empty state until there's a post. There's no admin UI built — the author's vue-admin-top is a third repo. To create the first admin right now: POST https://api-anb.mingsea.net/admin/register with ll_username, ll_password, ll_description, ll_email, and ll_permission (that endpoint is whitelisted, no token needed). If you'd rather have the admin UI, say the word and I'll build that too.

me:  pls also build https://github.com/acmenlei/vue-admin-top

me:  i don't understand why https://github.com/acmenlei/node-admin-backend isn't admin panel app?

 Admin UI built. Now serving it and wiring the domain.

 Admin UI is live. Adding it to the watchdog, then wrapping up.

Good question — short answer: the backend has no screens at all. It's API-only by design, and that's exactly why opening it in a browser shows nothing useful.

The author's project is split into three repos, each with one job:

  • https://anb.mingsea.net — next-blog, the public blog. This is what visitors see.
  • https://api-anb.mingsea.net — node-admin-backend, the server. It holds no pages — it only answers data requests with JSON (article lists, login checks, comments) and talks to MySQL and Redis. I verified it works by calling it directly: POST /reception/article/list returns {"code":200,...}. A browser visit shows nothing because there's no UI to render, which is normal for an API.
  • https://admin-anb.mingsea.net — vue-admin-top, the actual admin panel. This is the one with a login page, article editor, and dashboards — and it's the piece you asked me to build just now.

Why split like this: one backend serves both the public site and the admin panel, so articles you publish in the panel show up on the blog through the same API.

And the admin panel is now live: https://admin-anb.mingsea.net returns 200 ("Vue博客后台管理系统"), the backend accepts requests from it, and it's watchdog-covered. One step remains yours: log in with an admin account, which you create first via POST https://api-anb.mingsea.net/admin/register — after that, the panel login will work and you can publish from there instead of by API.

me:  how to run 'POST https://api-anb.mingsea.net/admin/register'?

Two ways — pick whichever you like. You can't run a POST from the browser address bar, so:

Option 1: terminal (curl) — replace the values with your own:

curl -X POST https://api-anb.mingsea.net/admin/register -H "Content-Type: application/json" -d '{"ll_username":"yourname","ll_password":"yourpassword","ll_description":"blog owner","ll_email":"you@example.com","ll_permission":"workTower,creativeCenter,articleList,articleEdit,categoryManage,categoryList,categoryEdit,tagManage,tagList,tagEdit,userManage,userList,userEdit,articleDelete,categoryDelete,tagDelete,userDelete,allocationPermission,commentBoard,commentBoardList,deleteCommentBoardItem,deleteCommentBoardReplyItem,commentBoardReplyList"}'

That long ll_permission string is just all 23 permissions from the database, comma-separated — it makes the account a full admin. The password is encrypted on the server, not stored in plain text.

Option 2: browser console — open https://admin-anb.mingsea.net, press F12, paste this in the Console (same values to replace):

fetch("https://api-anb.mingsea.net/admin/register", {
  method: "POST",
  headers: {"Content-Type": "application/json"},
  body: JSON.stringify({ll_username: "yourname", ll_password: "yourpassword", ll_description: "blog owner", ll_email: "you@example.com", ll_permission: "workTower,creativeCenter,articleList,articleEdit,categoryManage,categoryList,categoryEdit,tagManage,tagList,tagEdit,userManage,userList,userEdit,articleDelete,categoryDelete,tagDelete,userDelete,allocationPermission,commentBoard,commentBoardList,deleteCommentBoardItem,deleteCommentBoardReplyItem,commentBoardReplyList"})
}).then(r => r.json()).then(console.log)
 

What you'll get back:

  • {"code":200,...} — account created. Then just log in at https://admin-anb.mingsea.net with that username and password.
  • {"code":-96,...} — that username already exists, pick another one.

If you'd rather I run it for you, send me the username and password you want and I'll do it.

me:


 me: i use method 1. the pic shows i registered successfully. but here https://admin-anb.mingsea.net/#/login , i use username: admin and pw: value-of-my-password to login but i failed.why ?

Good news first: the register did work (code 200, 注册成功). The login failure is a separate step — let me check the login code path before guessing.

Found it — and it's not the backend. Your account is fine, and I proved the whole auth chain works end to end with test accounts (register → login → permission check all returned 200).

The problem is the admin login form itself: it validates the password as 5–16 characters, and yours is 24 characters long. So the form rejected it with "请输入有效的密码" before it ever sent a request to the API — that's why login "failed" despite registration succeeds.

I've fixed it: raised the form's password limit to 64, rebuilt, and redeployed. The admin page is back (200).

Try logging in again now with the same admin account and password — it should go through.

me: i can login successfully. but i can't publish an article. can u try it? 

Now reproducing the full publish path with a throwaway test account.

I verified the publish API end to end with a throwaway account. I won't use or store your password. Finishing the fix for the real blocker now.

I tried it — and the publish API itself works fine. I published a test article with a throwaway account, it showed up on the public blog list, and I deleted it plus all test data afterward. Your blog is back to clean (0 articles, only your admin account).

So why couldn't you publish? The publish form has three required fields you couldn't fill, not a server error:

  1. Category and tags are empty. The dropdown and checkboxes pull from the database, and a fresh install starts with zero categories and zero tags — so there was nothing to select, and the form refused to submit.
  2. Cover requires an upload that can't work. The only way to set a cover was uploading to the author's Aliyun OSS, but he never published those credentials, so the upload always fails and the cover stays empty — also blocking submit.

What I fixed just now: there's a new input under the cover uploader — "或粘贴图片 URL 作为封面" — paste any image URL there and it counts as the cover, no OSS needed. Rebuilt, redeployed, and verified live (200).

One step left, on your side, before your first post:

  • In the admin panel, create one category (分类管理 → add) and one tag (标签管理 → add) first.
  • Then open the publish page, pick that category and tag, paste a cover image URL, fill in title/intro/content, and publish — it will go through.

 me: i can't add the category or tag. very strange. can u try to publish a post?

Strange indeed — the API accepted a category and tag from my test just now, so let's look at what the admin pages actually do when you click 'add'. 

The category form itself looks simple (two required fields, POST to /category/publish). Let me check the list pages and the API layer the UI actually uses — that's where a silent failure would hide.

 I'm now reproducing your exact steps in the admin UI with a throwaway test account — logging in, adding a category, adding a tag, then publishing a post with a pasted cover URL — to see the failure firsthand instead of guessing. I'll report what it does at each step, including any error message it shows.

Good news and a puzzle: I just ran your exact steps through the real admin UI with a test account, and everything worked — no bug reproduced.

  • Login as the test account: fine.
  • Added category BrowserCat: "操作成功", appears in the list.
  • Added tag BrowserTag: "操作成功", appears in the list.
  • Published "Browser test post" with the BrowserCat category, BrowserTag tag, and the pasted cover URL: "操作成功", and I verified it's in the public article list on the blog.

So the full chain works through the UI, and there's a useful side effect for you: the category and tag now exist, so your publish page finally has something to select — the empty-dropdown blocker is gone. Try publishing now (maybe hard-refresh the admin page first, Ctrl+Shift+R, to clear any stale cached version).

If adding a category or tag still fails for you, the exact symptom matters a lot — tell me what you see when you click save: does it show a toast (and what does it say), does nothing happen at all, or does the page hang on loading? That'll point at what's different in your session versus the test.

And the test items are still there for you to inspect: the BrowserCat category, BrowserTag tag, and "Browser test post" on the blog. Say the word and I'll delete all three plus the test account.

me:  in the step of "add cover", i failed. is any pic ok ?

 Good — that narrows it down. Yes, any picture works, but only through one of the two controls. Let me make the paste box more foolproof first, then explain.

 The admin service unit is gone from /etc — the VM must have been replaced again since the last deploy. Restoring it from the bootstrap.

 The tunnel units were wiped too. The main stack bootstrap recreates them — running it now.

 Tunnel is up locally but the public side still 530s — checking what cloudflared says.

Yes — any picture is OK, with one condition: it has to be a public, direct image link (preferably https). The paste box doesn't check format or size at all, unlike the upload button. So a .jpg, .png, .webp link from anywhere public works; a login-walled page, an album page, or an http:// link on the https blog won't display properly.

Two things to know:

Use the small input, not the big + box. The + box is the OSS upload, which is still broken (no Aliyun credentials exist for it) — don't use it. The input right below it, "或粘贴图片 URL 作为封面", is the one that works. After you paste, the picture preview should appear inside the box — that's your confirmation the cover is set. I just made it update as you type, too, so you don't even need to press Enter.

Your failed attempt may not have been the picture at all. While you were trying, the VM was replaced again: I found every service down and all three public sites returning 530. I've restored everything — admin, blog, and API are all 200 again now.

Try again now. If you want a guaranteed-working test picture first, paste https://anb.mingsea.net/favicon.ico (that's what the successful test post used) — once that preview shows, swap to your own picture URL and publish.

me:  it doesn't work. can u strip the "add cover" section?

Cover UI is fully gone from the build (only a code comment remains). Proving a cover-less publish works with the exact payload the stripped form now sends.

Done — the cover section is stripped out of the publish form completely: the upload box, the paste-URL input, and the "cover required" rule are all gone, and the rebuilt admin panel is live (200).

I also proved the new form's payload works: published a test article with no cover at all, it returned success and showed up on the public blog list, then I deleted it. The public blog never displays covers anyway, so nothing breaks there — the admin article list will just show an empty cover cell for cover-less posts.

To publish now you only need: title, category (BrowserCat is already there), tag (BrowserTag is already there), intro, and content. No cover-image step at all.

我:


 

 me: it;s not gone.

 That screenshot is actually very informative — it shows the original uploader, without even the paste-URL input I added earlier. That means your browser tab is still running the old code. Let me verify what the server is actually sending right now.

You're right that it's still there in your tab — but I just verified the server is sending the stripped build. The live code has no cover field in the publish form at all: no "请选择封面" rule, no uploader, no paste input. The only "封面" text left anywhere is the column header on the article list page, not the form.

Your screenshot gives it away another way: it doesn't even show the paste-URL input from the earlier fix. So your tab is still running the original code from before either fix — the app never reloaded in that tab. Navigating inside the admin panel doesn't refetch it.

Fix: hard-refresh the admin page — Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac) — or just close that tab completely and reopen https://admin-anb.mingsea.net. After that, the publish form will go straight from 简介 to 内容 with no cover row in between.

If it's still there after a hard refresh, tell me — that would mean something else is going on, but server-side is confirmed clean.

me:   https://anb.mingsea.net/article , i published 2 posts successfully. but i try to publish a long article (this article https://wp.mingsea.net/?p=17. i copy/paste its content),failed. can u try on ur side?

 Nice — 2 posts through. Now the long one. Let me look at that WordPress post and the article table's size limits first, since "short works, long fails" points at a length cap.

Tried it on my side — and reproduced your exact failure, then fixed it. The long article was hitting two stacked size caps, not a content problem:

  1. The server rejected the whole request. Express defaults to a 100KB JSON body limit. Your pasted article makes a ~566KB request, so it got a 413 PayloadTooLargeError before the publish code ever ran.
  2. The database couldn't hold it either. The article content columns were MySQL TEXT, capped at 64KB. Your article is 93,151 characters (~280KB of Chinese text), so it wouldn't have fit even past the first cap.

Both fixed now: body limit raised to 10MB, and the content columns widened to MEDIUMTEXT (16MB). I verified with your exact WP p=17 content — published successfully with all 93,151 characters stored and visible on the public list, then deleted the test copy. Your 2 posts (战马, 测试) are untouched.

Try pasting that article again now — it should go through. One cosmetic note: pasting from the WordPress page into the markdown editor may come through as plain text rather than nicely formatted, but that's formatting, not a publish failure.

me:  but https://anb.mingsea.net/article/detail/1793180379776 shows nothing.

 That ID is newer than my test articles, so that's likely your long post going through after the fix — but the detail page renders empty. Let me check what the API returns for it and how the detail page renders.

 Single choke point confirmed. Applying the fix.

Your post was never lost — it's published with its full content. The detail page was the broken part, and it wasn't specific to the long article at all.

What was actually happening: when you open an article link directly, the blog server renders the page by fetching its own API through the public https://api-anb.mingsea.net address — via the Cloudflare tunnel. But the blog server itself can't reach Cloudflare directly from this VM, so every server-side fetch failed and pages fell back to empty. The list page hid this because it refills itself in your browser afterward; a directly opened detail page just stayed empty. I confirmed it: even the short 战马 detail page and the list served empty shells.

Fixed: the server now fetches the backend directly on localhost instead of going around through the tunnel (browsers still use the public address, unchanged). Rebuilt and restarted.

Verified just now:

  • Your long article's detail page: was 12KB with nothing — now 835KB with the full text.
  • The short post's detail page and the article list now render their content server-side too.

Open https://anb.mingsea.net/article/detail/1793180379776 again — it should show the whole article now.

me:  can u change the domains wp.mingsea.net ,ghost.mingsea.net ,ldmp.mingsea.net ,anb.mingsea.net to wp.luckypoem.workers.dev ,ghost.luckypoem.workers.dev ,ldmp.luckypoem.workers.dev ,anb.luckypoem.workers.dev respectively?

 Good question — let me check what each app pins to its current domain before saying yes, because a rename isn't the same job for all four.

Yes — with the mapping exactly as you listed (wp→wp, ghost→ghost, ldmp→ldmp, anb→anb). But one honest caveat first: this isn't moving those apps off the VM. Workers can't run WordPress, Ghost, or a Next.js server — what each luckypoem.workers.dev address would be is a small proxy Worker that forwards requests to the same VM service through the existing tunnel. The VM stays; workers.dev becomes the new front door.

What each app needs for the switch:

  • WordPress — the messiest. WP_HOME/WP_SITEURL are hardcoded to wp.mingsea.net, and old posts' image URLs in the database also point there. Switching means changing wp-config and running a search-replace on the live WP database, or old post images will still load from the old host.
  • Ghost — clean. Change url in its config to ghost.luckypoem.workers.dev and restart; Ghost builds all its links from that setting.
  • ldmp — change NEXTAUTH_URL to ldmp.luckypoem.workers.dev and restart, otherwise login breaks (NextAuth only trusts its configured host).
  • anb — the blog domain itself isn't baked in, but the backend's CORS list must add anb.luckypoem.workers.dev or the blog's API calls get blocked. Note: api-anb and admin-anb would stay on mingsea.net unless you want those renamed too.

And mingsea.net can't fully vanish underneath: the proxy Workers have to fetch the services through the tunnel hostnames. Visitors would only ever see workers.dev addresses, but the tunnel entries stay as the hidden origin.

If you want me to do the full switch for all four, tap below and I'll build the four proxy Workers, make the config changes, restart everything, and verify each site (including logins) on its new address.

me:  switch all four to workers.dev

Switch is underway. Current state:

  • Deploy token: the saved Cloudflare login can't deploy Workers (it has no Workers access), so I've got a browser task creating a one-time "Edit Cloudflare Workers" token from your dashboard login. The four deploys wait on that.
  • Proxy Workers ready: wrote the four proxy scripts (wp, ghost, ldmp, anb) — they forward to the matching mingsea.net origin through the tunnel and rewrite redirects back to the workers.dev host, so no loops.
  • Flip prep done: confirmed where each domain is pinned — WP's wp-config + database, Ghost's config + its bootstrap template, ldmp's app.env, and anb's backend CORS list.

Once the token comes back, I'll deploy all four Workers first, then flip the configs in that order (so nothing breaks in between), and verify each new address live, including the login pages.

me:  i want api-anb and admin-anb also also rename to under luckypoem.workers.dev

Got it — six in total then. Adding the two API/admin proxies now.

Done — now six proxies in total, all written and ready:

  • wp, ghost, ldmp, anb → their matching mingsea.net origins
  • api-anb → api-anb.mingsea.net (the backend)
  • admin-anb → admin-anb.mingsea.net (the admin UI)

Renaming the API/admin pair adds two rebuild steps on top of the earlier plan: the public blog and the admin UI both have the API address baked in at build time, so after the proxy Workers are up I'll rebuild both with https://api-anb.luckypoem.workers.dev and add the new workers.dev origins to the backend's CORS list. The mingsea.net API/admin addresses stay underneath as the hidden origins, same as the other four.

Still waiting on the deploy token report from the dashboard task (it shows completed, so its result should land shortly). As soon as it does, I'll deploy all six Workers, flip the configs, rebuild the two frontends, and verify all six new addresses live.

All six switched and verified live. The apps still run on the VM — each new address is a proxy Worker in front of the same service, with the mingsea.net hostnames kept as hidden origins.

New addresses (all 200):

What changed per app:

  • WordPress: WP_HOME/WP_SITEURL switched (durable config, live config, and the config generator, so a future restore won't revert it), plus a database search-replace of old post/option links to the new host — post guids intentionally left alone, and a pre-change database backup saved. One extra fix: the proxy now tells WordPress the public host, so admin login redirects stay on workers.dev instead of bouncing back to mingsea.net.
  • Ghost: site URL switched in its config and bootstrap template, restarted, device-verification-off setting preserved.
  • ldmp: NEXTAUTH_URL switched, restarted.
  • anb: backend CORS gained both new origins, and the blog + admin UI were rebuilt pointing at the new API address. Login through the new API address verified end to end (200 + token headers).

Two things for you:

  1. Your logins are unchanged — same accounts, same passwords, just new addresses. Log in at the new URLs from now on.
  2. Please revoke the one-time token named proxy-workers-onetime in your Cloudflare dashboard (My Profile → API Tokens) — the deploys are done and it shouldn't stay alive.

One note: the old mingsea.net addresses still technically reach the same services as the hidden origins, but every link the apps generate now points to workers.dev.

me:  can u make old domain do 301 redirects to new domains?

Interesting — none of them redirect yet, not even WordPress (it serves on any host and just generates new-host links). So I'll add explicit old→new redirects at each origin, guarded so the proxy Workers' own fetches pass through untouched. Setting up the work.

 WordPress redirects cleanly. Now Ghost's redirector.

 Redirects work locally. The two pass-through tests returned 301 too — need to see where those point before repointing the tunnel.

 Pass-through confirmed under tunnel-realistic headers. Repointing the tunnel and wiring the redirector into recovery.

 Ghost redirects live. Adding the redirector to watchdog coverage, then moving to the Next.js apps.

 Watchdog updated. Now the two Next.js middlewares:


 anb redirects now. Last two — admin static server and the API。

 All six redirect.

Done — all six old domains now redirect to the new ones, paths and query strings preserved, and the new domains are untouched (all still 200, login through the new API verified).

Old domainRedirects to
wp.mingsea.netwp.luckypoem.workers.dev — 301
ghost.mingsea.netghost.luckypoem.workers.dev — 301
ldmp.mingsea.netldmp.luckypoem.workers.dev — 301
anb.mingsea.netanb.luckypoem.workers.dev — 301
admin-anb.mingsea.netadmin-anb.luckypoem.workers.dev — 301
api-anb.mingsea.netapi-anb.luckypoem.workers.dev — 308

Two details worth knowing:

  • The API uses 308, not 301. A 301 would silently turn API POSTs into GETs and break them; 308 is still permanent but preserves the method and body, so old API clients keep working through the redirect (verified: an old-domain POST follows through to a 200).
  • The redirects are guarded so they can't loop. Each origin only redirects direct visits; requests arriving from the proxy Workers (marked with the new host) pass straight through to the app. That guard had a real subtlety in the two Next.js apps: Next auto-fills the marker header from the request host, so checking for its presence silently did nothing — the guard compares its value instead. Ghost needed a tiny redirect service in front of it (tunnel ingress repointed, wired into its bootstrap and watchdog), since Ghost itself won't do host redirects.

Old bookmarks and links will land on the right new pages automatically now.

我: Among shiro,wp,ghost,ldmp,anb, why only shiro is the most robust?

Because Shiro is the only one whose frontend doesn't actually live on this VM. The other four just got a new address — their serving path is still 100% on the VM.

Shiro: the frontend runs on Cloudflare Workers as a real OpenNext app. Its code lives in GitHub, it rebuilds itself on push via Workers Builds, and a VM replacement can't touch it — there's nothing on this machine to wipe. The only VM dependency left is the Core API for content, and that's the single best-covered recovery path you have (watchdog step 1, a bootstrap that's been exercised through several wipes). So the worst case for Shiro is "content temporarily down while Core restarts," never "the whole site is gone."

The other four, by contrast:

  • WordPress: nginx + PHP + MariaDB, all on the VM. A wipe kills it until the slowest bootstrap you have (the apt phase) finishes, and the proxy Worker in front adds a hop but zero resilience — VM down means 502.
  • Ghost: same VM single point of failure, plus its pager lives in a symlinked stock theme that VM wipes delete, so every restore has to re-apply it. Now it also has the redirector hop in front.
  • ldmp: app + MongoDB both local, plus custom code (pagination, middleware, the redirect) that only exists in this checkout.
  • anb: the most moving parts of anything you run — frontend, Express backend, Redis, plus a MySQL database borrowed from Ghost's instance. Publishing or reading breaks if any one of those four pieces fails, and it carries the most local-only fixes of any stack.

One honest caveat: Shiro isn't invulnerable. If Core or the tunnel goes down, its pages can't fetch content either. The difference is the blast radius: for Shiro a VM outage degrades one backend service, while for the other four the same outage is a total outage with a multi-step restore.

 Found it live: Shiro is 200, but WordPress is 502 right now — its MariaDB is inactive. Everything else is up, Core answers pong. Fixing WordPress.

 WordPress is back. One quick check on what took it down.

Exactly — and you caught it live. Here's what just happened:

At ~11:04 this morning the VM was replaced again (uptime shows a boot 13 minutes before my check). That wipes /etc, so every service unit vanished. The watchdog restored them one by one — you can see the timestamps: Core at 11:05, ldmp and anb at 11:11, Ghost at 11:14. WordPress was last because its restore reinstalls packages via apt, the slowest step. When you wrote, WordPress was still mid-restore: MariaDB's unit didn't exist yet, so wp.luckypoem.workers.dev was returning 502.

Shiro never blinked because its frontend doesn't live on this machine at all — the Worker kept serving from the edge through the whole reboot, and Core came back fast enough (it answers pong now) that content never dropped either.

Current state, just verified: everything is back — Shiro, WordPress, Ghost, ldmp, anb, admin, and API all return 200. No action needed from you; the watchdog had already started the WordPress restore before I even looked, I just watched it finish.

That's the difference from last turn in concrete form: for the VM-hosted apps, a replacement means "down until the bootstrap finishes, slowest one last." For Shiro, a replacement means nothing at all on the frontend.

me:  ghost,ldmp,anb have their respective frontend?

Yes, each has one — but they're built differently:

  • Ghost: its frontend is the theme (Handlebars, currently Source with your 5-per-page pager). It isn't a separate service, though — the same Ghost process renders the public pages, serves the API, and hosts the admin UI at /ghost/. One process, one port.
  • ldmp: the frontend is the Next.js app itself (App Router pages, served by next start on port 3250). Its "backend" — the blog API, auth, database access — lives inside that same app as API routes and Prisma calls. Again one process doing both.
  • anb: this one is genuinely split. The public blog is its own Next.js app (port 3252), the API is a separate Express service (port 9001), and the admin panel is a third piece, a static Vue app (port 3253).

So the distinction from Shiro isn't whether they have a frontend — they all do — it's where that frontend runs. Ghost's and ldmp's frontends run inside their single VM process, and anb's frontend is a separate service but still on the VM. None of them runs on the edge the way Shiro's does.

Same shape as Ghost, though: it's not a separate service. One WordPress install does both — the theme renders the public frontend, and /wp-admin/ is the backend where you write posts, both served by the same nginx + PHP-FPM process off the same MariaDB database.

That's also why this morning's outage took down both sides at once: with MariaDB dead, there was no frontend and no admin, just the 502 — unlike anb, where the frontend, API, and admin are separate pieces that can fail independently.

me: i visit https://admin-anb.luckypoem.workers.dev , it shows '网络出错了 稍后再试试吧'. pls fix it。

Fixed. Here's what was actually wrong:

The admin page itself was fine, and the login API path works — the backend was crashing in a loop. When I deleted the testui1 account earlier, it exposed a bug in the backend's permission check: for a user that no longer exists, it rejected with no error value, and the error handler then read a property off that empty value — an uncaught crash that killed the whole backend process. A still-logged-in testui1 session (logged in today at 5:24pm, before the deletion) kept calling that check, so the backend kept dying and the admin panel could only show "network error."

What I changed, all in the backend copy on the VM:

  • The permission lookup now rejects with a proper error instead of an empty one, and the handler guards against it — a missing user can never crash the process again.
  • Deleted testui1's leftover session token, so that stale session is dead too.

Verified: the permission check for testui1 now returns a clean error with zero backend restarts, the login endpoint answers normally, and the admin page and article list both load 200.

Try logging in now with your admin account — if your browser still holds anything from the test session, it'll clear back to the login screen on its own.

 

我得到的博客地址: https://anb.luckypoem.workers.dev/

登录地址: https://admin-anb.luckypoem.workers.dev/

项目地址:

 https://github.com/acmenlei/next-blog

 https://github.com/acmenlei/vue-admin-top ,后台管理系统.

 https://github.com/acmenlei/node-admin-backend, 服务端.

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 


 

赖岳谦 | 算计交织! APEC 暗潮

varlock.dev

 

.env.schema
# Declarative schema — AI agents get full context, never secret values
# @sensitive @required @type=string(startsWith=sk-ant-api)
# @docs(https://platform.claude.com/docs/en/api/overview)
ANTHROPIC_API_KEY=
# @type=enum(development, preview, production, test)
APP_ENV=development # set non-sensitive default values directly
# use function calls and plugins to securely fetch data from external sources
XYZ_TOKEN=op("op://api-prod/xyz/auth-token")

Unlike .env.example, your .env.schema is a single source of truth, built for collaboration, that will never be out of sync. Agents can read your schema for full context — without accessing your secrets.

.env.local (git-ignored)
# built-in encryption for local overrides - NOTHING in plaintext
ANTHROPIC_API_KEY=varlock(local:ABC123...)

 AI-Safe Config

Your .env.schema is readable by AI agents — they get full context on your config without ever seeing secret values. Prevent leaks to AI servers by design.

 Leak Scanning

Proactively scan your codebase for leaked secrets with varlock scan. Set up git hooks to catch leaks before they're committed.

 Runtime Protection

Redact sensitive values from logs and console output. Detect leaks in bundled client code and outgoing server responses at runtime.

Redaction demo

 Validation

Powerful validation capabilities, without custom logic. Misconfiguration errors are surfaced much earlier in your development cycle, with clear error messages.

Validation demo

 Type-safety

Automatically generate types according to your schema - the single source of truth - instead of writing them by hand.

IntelliSense demo

 Environments

Combine defaults, environment-specific .env files, local git-ignored overrides, and process env vars. Compose values together using functions.

Multi-env demo

 Secure secrets

Load secrets from 6 provider plugins — 1Password, Infisical, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, and Bitwarden — or any CLI tool.

External providers demo

Installation

# Install as a dependency in a js project
npx varlock init
# OR add it to an existing project
npm install varlock
# OR install as standalone binary via homebrew
brew install dmno-dev/tap/varlock
# OR via cURL
curl -sSfL https://varlock.dev/install.sh | sh -s

Drop-in replacement for dotenv

my-app.ts
import 'dotenv/config';
import 'varlock/auto-load';
// *optional* type-safe env access
import { ENV } from 'varlock/env';
const client = new AbcApi(process.env.ABC_API_KEY);
const client = new AbcApi(ENV.ABC_API_KEY);

varlock can be used as a direct replacement for dotenv in most projects. Just swap your import and get validation, security, and more. No other code changes required!

Run anywhere - with any language

varlock run -- python my_script.py

Use varlock run to inject resolved, validated env vars into another process. 

from  https://varlock.dev

(  https://varlock.dev/getting-started/installation/)

利用muse的web版帮我在cloudflare.com平台搭建基于nextjs的静态博客程序nnd

 me: can u build https://github.com/nelsonlaidev/nelsonlai.dev? u can use the domain nnd.luckypoem.workers.dev


  • Full app via OpenNext: the server runs on Workers, but the interactive features need Postgres + Redis + Resend + GitHub OAuth + R2 that you don't have — they'd error until those are wired up.

Either way it ships with Nelson's content for now, like the other personal sites. Deps are installing in the background meanwhile — which shape?

我: Full app via OpenNext。no need to install something on the VM?

Notably, the repo ships its own fix for half of this: a docker-compose with Postgres, Redis, and uprest, an Upstash-REST bridge over Redis. So a self-hosted Redis over HTTP could work from the VM — but Postgres still can't, so the full interactive set needs a hosted Postgres (Neon/Supabase free tier) regardless.

So the plan: ship the full app now with placeholder secrets. Blog, projects, about, and uses work; guestbook, comments, sign-in, admin, and dashboard will error until hosted services are wired — no VM installs, and nothing to do on your side unless you want those features later. Deps are still installing (slow registry), then I'll build.

Local build is blocked by a sandbox wall I can't fix here: Turbopack can't fetch Google Fonts through the egress proxy. I verified it with the exact font URL — curl and Node both get it fine, only Turbopack's native fetch fails. (The RAM issue I hit earlier is fixed with a heap cap; fonts are the hard wall.)

The way forward is building on Cloudflare's side with Workers Builds — direct internet, plenty of RAM. For that I need the repo under your account first: fork nelsonlaidev/nelsonlai.dev to brightmann/nnd. Then I'll push the deployment changes and create the Builds project with the placeholder backend values. Content pages will work; guestbook/sign-in/admin wait for hosted services, as before.

How do you want to do the fork?

我: i already fork it. do u need a pat for brightmann/nnd?

Yes — I need one for brightmann/nnd, just for the push. I checked and your fork is there on main.

A fine-grained PAT scoped to brightmann/nnd with Contents read/write is enough (this push touches no workflow files). A classic PAT works too. It's only to push the deployment changes — the adapter, the OpenNext config, and the Worker config — so Workers Builds can build it. The Builds project itself needs no token.

Same handling as before: I'll put it in the remote URL, push, restore the clean remote, never store the value, and you can revoke it once the push lands. Paste it whenever you're ready and I'll push and set up the build.

我: i choose 'Full app via OpenNext', it will use docker?

No — no Docker anywhere in this deployment.

OpenNext turns the app into a JavaScript Worker bundle plus static assets, and that runs on Cloudflare's edge. No containers, no images.

The docker-compose.yml in the repo is local-dev only — it's how the author runs Postgres, Redis, and the uprest bridge on his own machine while developing. It never runs in production here, and Workers Builds doesn't use it either; it just runs the build commands and deploys the bundle.

So nothing to install on the VM, and no Docker to run. If you later want the guestbook and sign-in working, that's via hosted services (like Neon Postgres and Upstash), not Docker.

我:my classic pat here....

 Upstream hasn't moved — but the fork's HEAD differs from my clone's. Checking whether the fork is simply behind before pushing.

 Fork is simply behind upstream — a normal push will fast-forward it. Committing the deployment changes and pushing to the fork.

 Not an auth failure — the commit was blocked by the repo's pre-commit hook because pnpm wasn't on PATH in that shell. Retrying with PATH fixed.

 Commit is ready, but the push hit a scope wall — updating the note, then the precise ask.

That token authenticated fine, but the push was refused on scope — and it's specific:

Your fork is behind upstream, so the push includes upstream commits made after you forked, and one of them modifies .github/workflows/ci.yml. GitHub blocks any token from creating or updating a workflow file without workflow scope, and this token doesn't have it. Repo access itself works.

The commit is already made locally, so nothing needs redoing. I need a new token with workflow scope added — classic PAT with workflow checked alongside repo, or a fine-grained one on brightmann/nnd with Contents + Workflows. Classic token scopes can't be edited after creation, so it has to be a fresh one; you can revoke this one.

Paste the new token and I'll push immediately, then set up the Workers Builds project.

me: my new classic pat here...

 Push landed — fork HEAD is now the deploy commit, remote restored. Starting the Workers Builds project in the dashboard.

Pushed — brightmann/nnd main is now the deploy commit, and the remote is restored to clean. The dashboard task is now creating the Workers Builds project nnd from it, with the build commands and placeholder variables set, and it'll report the first build outcome.

Both tokens you pasted have done their job (the push), so you can revoke them both now — the build needs no token.

I'm not retrying anything — the push is done. I just verified: the fork is still at the deploy commit, and no push process is running.

What looked like retries were the two failed attempts before the success: first the commit was blocked by the repo's pre-commit hook, then GitHub rejected the push because the first token lacked workflow scope. The third attempt, with the new token, is the one that landed — that's the "Pushed" message.

What's running now isn't a push at all. It's the dashboard task creating the Workers Builds project, which will build from the commit that's already on the fork.

The Workers Builds project nnd is created and its settings are verified, and the first build gave a split result: the build step itself succeeded — so Cloudflare's builders handle the fonts and RAM fine — but the deploy step failed because the wrangler command wasn't found. Turns out it's a peer dependency of the OpenNext adapter, not installed by default.

Fixed and pushed: wrangler 4.156.0 is now in the repo's dev dependencies. No new token needed — the last one still worked, and the remote is restored. That push auto-triggered a fresh build, and a dashboard task is now watching it and will verify the live URL.

Both tokens you pasted are done now, so you can revoke them.

It's live: https://nnd.luckypoem.workers.dev — the wrangler fix worked, build succeeded, 694 assets deployed. Homepage, /blog, and /projects all render (I re-checked the homepage myself: 200).

One caveat, and a correction to what I told you: post detail pages and /about return 500 with the app's own error page — I verified both. So listings work, but detail and about don't yet.

Best explanation so far: both failing routes render full MDX through a renderer that evaluates code at runtime, and Workers block runtime code evaluation — the same class of issue as the old nunjucks eval problem. Listings don't render full MDX, which is why they're fine. That still needs confirmation from the Worker logs.

Want me to dig into the logs and fix those two routes?

me:  Yes, dig into the post/about 500 error.

What the logs do show: the 500 stack tops out at an anonymous Function — consistent with runtime code evaluation, but the log snippet didn't capture the error's name, so EvalError isn't proven yet. I also confirmed the locale-prefixed URLs just redirect to the plain ones, which then 500 — so it's not a locale-routing quirk either.

Refined picture: the prerendered page isn't being served from cache, the Worker re-renders on demand, and that render hits the code-evaluation block. A follow-up task is now pulling the full log message (the named error) plus whether the Worker has any cache binding, which decides the fix. I'll report back once that lands.

Root cause found, and it's in the adapter config, not the app code.

The default OpenNext cache is literally called "dummy" — no cache at all. So the prerendered pages are never stored anywhere: every request re-renders on demand. Most pages render fine that way, but the MDX pages re-render through a renderer that evaluates code with new Function — I confirmed that in the package source — and Workers block runtime code evaluation. That's exactly why only post detail and about fail: they're the MDX routes. The logs never named the error, just the anonymous Function frame, which is that eval frame. And there was no R2 or KV binding to store a cache in anyway.

Fix pushed: the config now uses the Static Assets cache — the docs' recommendation for an SSG site — so prerendered pages are served from assets and never re-rendered. The app uses no revalidation, so nothing is lost. No new token needed; the last one still worked, remote restored.

That push auto-triggered a rebuild, and a dashboard task is watching it and will verify post and about. I'll report back once it lands.

Fixed and verified. Post detail and about now return 200 with full content — I checked all four routes myself: post, about, homepage, and blog listing. The deploy log confirms the Static Assets cache populated (1,230 files, 240 newly uploaded), so the prerendered pages are served instead of re-rendered.

So https://nnd.luckypoem.workers.dev is now fully live for content. The one standing gap is the same as before: guestbook, sign-in, and admin still await real hosted backends, since those run on placeholder values.

That last token did its final job with the fix push, so it's revocable now along with the first one.

me: can u make https://nnd.luckypoem.workers.dev/blog support pagination? 6 posts per page.

One thing to check before writing code — the listing renders through FilteredPosts, which may filter client-side. Reading it to avoid breaking search.

 FilteredPosts has a live search across all posts — so route-based pagination would shrink search to one page. I'll paginate inside the list instead (filter first, then page through matches), which keeps search intact. Checking the UI pieces for the pager styling.

 There's already a table-pagination section — checking if it has reusable Previous/Next labels in all locales.

 The site already translates pagination labels for tables, but not short Previous/Next buttons — I'll add those keys to all 6 locales. Confirming the Button variants and the right insertion point in the messages files.

 Now writing the paginated list component.

 The commit was blocked by the pre-commit lint — 7 errors in my file. Let me see them.

 The hook's formatter already rewrote my types to Array<...> style — editing against the current text.

 The hook's parallel format+lint jobs mangled the file mid-flight. Cleanest fix: rewrite it fully with both lint issues resolved, then format and lint it myself before committing.

 TS doesn't narrow inside the click closure — restructuring the map with a block body, and checking what oxlint flags.

 All green. Committing.

Committed (04874a9d, 7 files). Now the push — checking the remote state first.

Code is done and committed locally (04874a9d, lint and typecheck clean) — but the push needs a token from you. I never store pasted tokens, so I don't have one on hand for this push.

Paste a PAT for brightmann/nnd and I'll push it right away. This commit touches no workflow files, so Contents (read + write) on brightmann/nnd is enough — a classic PAT works too. I'll embed it only for the push, restore the clean remote after, and you can revoke it once it's live.

One honest note on what you'll see: every locale currently has only 4 posts, so with 6 per page the pager stays hidden for now (that's the standing rule — hidden on a single page). It will appear automatically the moment a locale gets a 7th post, and search still covers all posts, not just the current page.

Pushed — brightmann/nnd main is now at 04874a9d, and the Workers Builds rebuild is running. I'm watching that build and will verify /blog live once it lands (4 posts, search working, no pager yet since it's a single page at 6 per page).

That token has done its job for this push, so you can revoke it whenever you like.

Done and verified live. The build for 04874a9d succeeded (4m 54s, version a0ad01e5 at 100% traffic), and /blog checks out:

  • All 4 post cards render, search works (typing "php" narrows it to just that post, clearing restores all 4).
  • No pager visible, which is correct — 4 posts fit on one page at 6 per page, and the pager stays hidden until a locale reaches 7 posts. It'll appear on its own once you publish past that.

One unrelated note from the check: the footer widgets still show their placeholder-backend errors (Spotify fetch, Star error) — that's the known unwired-backends noise, not something this change introduced.

我:why https://nnd.luckypoem.workers.dev/blog/fh doesn't show the embedded video?

Found it — but let me pin down exactly where that policy is defined so the fix is precise。

Your post itself is fine — /blog/fh is live (200), and the YouTube iframe is really in the page HTML with the correct embed URL. The browser just refuses to load it.

The cause is the site's Content-Security-Policy, set in src/proxy.ts. It has no frame-src directive in production, so frames fall back to default-src 'none' — meaning every iframe is blocked, including YouTube. (The file only sets a frame-src for Vercel preview mode, which doesn't apply here.)

The fix is one line in src/proxy.ts: add frame-src https://www.youtube.com https://www.youtube-nocookie.com; to the policy, then push, and the cloudflare.com platform will rebuild the website — the video will load after that.

 on the Cloudflare build, The old posts are showing the build date (Oct 31, 2026) instead of their real 2022–2025 dates, so all their dates tie, the sort falls back to the collection's alphabetical order, and fh lands second instead of first. "Sorts newest first automatically" only holds if the build actually sees real git history, and evidently it doesn't here.

Found it with proof, and the fix is committed locally — it just needs a token to push.

Why fh wasn't first: I tested the build's exact condition. In a shallow clone (which is what the Cloudflare build effectively sees), git log returns the same HEAD commit date for every file — I verified e2e and fh both come back as the identical timestamp. So all posts tie, and the listing falls back to alphabetical order, putting fh second. My "sorts newest first automatically" claim assumed the build sees full git history. It doesn't, so that claim was wrong.

The fix (committed as 8905abf9, on top of your proxy commit): the post schema now accepts an optional frontmatter date, and the transform prefers it over the git date. I backfilled true dates on all 25 post files (6 locales): fh 2026-10-31, e2e 2025-09-18, top-8 2023-08-22, how-to-build 2022-08-22, php 2022-02-25. After this lands, the order will be fh first, then e2e, top-8, how-to-build, php.

One new rule for future posts: a new post now needs a date: "YYYY-MM-DD" line in its frontmatter, or it won't reliably sort first. (I'll fold that into the posting recipe.)

Also confirmed: you fixed the video yourself — your 163746f5 commit added exactly the frame-src line, and the live CSP header already shows it, so the embed should load now. No action needed from me there.

To push the sort fix, paste a PAT for brightmann/nnd — this commit touches no workflow files, so Contents (read + write) is enough. I'll push, watch the build, and verify fh is first.

me: my github pat here... 

Pushed — brightmann/nnd main is now at 8905abf9, and the rebuild is running. I'm watching it and will verify /blog shows fh first (then e2e, top-8, how-to-build, php) and that the video loads on /blog/fh.

That token has done its job for this push, so you can revoke it whenever you like.

Two small things, neither from this change: the fh card's cover image shows broken on /blog, because there's no cover.png for it yet — adding public/images/blog/fh/cover.png fixes that. And the comments/likes area still errors, which is the known placeholder-backend gap.

One footnote: the card dates fh as Oct 30 — that's just timezone rendering of the Oct 31 date (UTC midnight reads as Oct 30 evening here).

One loose end from today's nnd work, consolidated: I've saved the corrected posting recipe — the rule that matters for your next post is the new one: every new post needs a date: "YYYY-MM-DD" line in its frontmatter, or it won't sort first. 

me:  can u strip the cover images from all source posts?

Done locally, needs a push. What I changed:

  • Post cards (listing + homepage) no longer render the cover image, and post detail headers no longer show the hero cover either.
  • Untouched: images inside post bodies, and project covers (those are a different thing).
  • The cover.png files are still in the repo, just no longer referenced — harmless, and it keeps the change reversible. Say the word if you want them deleted too.

This also settles the fh cover-image question from earlier — no cover-image needed anymore.

Commit is 4a20ae4d on top of your test.mdx commit. Paste a PAT here for brightmann/nnd (Contents is enough, no workflow files) and I'll push it, watch the build, and verify no cover-image rendered on /blog or a post.

 me: my github pat here... 

Pushed — brightmann/nnd main is now at 4a20ae4d, and the rebuild is running. I'm watching it and will verify /blog cards and the /blog/fh header show no cover-image, while the post body and video still render.

That token has done its job for this push, so you can revoke it whenever you like.

Done and verified live. The build succeeded (version 6a7370dc), and covers are gone:

  • No cover image on any /blog card, and no broken-image icons either — cards are just title, description, date, and counts.
  • No cover above the post body on /blog/fh, while the body and YouTube player render fine.

A nice side confirmation: with 7 posts now on the blog, your 6-per-page pager is live for real — 6 posts on page 1, 1 on page 2.

The comments/likes counts and footer widgets still show loading errors, which is the known placeholder-backend gap.

me:  about pagination ,can u implement /page/2, /page/3 such format?

Done locally, needs a push. What I built:

  • /blog is page 1, /blog/page/2, /blog/page/3, and so on — real routes, 6 posts per page, with the numbered Previous | 1 | 2 | … | N | Next pager as links. Same shape as before, just as URLs now.
  • /blog/page/1 redirects to /blog, and out-of-range pages (like /page/99) 404. Works per locale too (/zh-CN/blog/page/2).
  • The old client-side pager is removed, so there's only one pagination now.

One tradeoff to state plainly: with routes, the search box now searches only the current page's posts, not all posts as it did this morning. That's inherent to slicing server-side — if you want search to still cover everything, that's a follow-up I can build, but routes as asked are done.

Commit is 7c8d74fb. Paste a PAT here for brightmann/nnd (Contents is enough, no workflow files) and I'll push it, watch the build, and verify /blog/page/2, the pager links, the /page/1 redirect, and a 404 for an out-of-range page.

me: my github pat here...

Pushed — fork main is now 7c8d74fb. I've got a browser task watching the Workers Builds build and verifying /blog, /blog/page/2, the /page/1 redirect, and a /page/99 404. I'll report back when it finishes.

That token is revocable now — the push is done and nothing else needs it.

Build succeeded, version 787630a6. Verified live:

  • /blog — 6 posts, pager showing Previous | 1 | 2 | Next
  • /blog/page/2 — the remaining post (PHP study notes), URL correct
  • /blog/page/99 — 404, as intended
  • Post detail from page 2 loads fine

One miss: /blog/page/1 shows "Page not found" instead of redirecting to /blog. My fault — I set dynamicParams = false, which makes Next 404 before the redirect code ever runs. The fix is committed locally (60b065f2, just removes that flag; the redirect and the out-of-range 404 are both already handled in the page code).

Paste a fresh token for brightmann/nnd and I'll push the fix, watch the rebuild, and re-verify /blog/page/1 redirects and /blog/page/99 still 404s.

我:my github pat here...

Pushed — fork main is now 60b065f2. A browser task is watching the rebuild and will verify the /blog/page/1 redirects to /blog plus /page/99 still 404ing, and /blog and /blog/page/2 unchanged. I'll report when it lands.

That token is revocable now — both pushes it covered are done.

我: how to create a source post?

Checking the exact post format first so I give you the real fields。

 One more precision check — the blog schema's full field list, so you don't add a field that breaks the build。

On nnd, posts are files in the repo — not Notion pages, not admin entries. The recipe:

  1. Create a file at src/content/blog/en/<slug>.mdx — the filename is the URL slug, so keep it ASCII (e.g. my-post.mdx).
  2. The whole frontmatter is just title, description and date:

---
title: Your post title
description: A short description

date: 2026-10-31T11:10
---

Your content here, in markdown/MDX.

 
3. Commit and push to brightmann/nnd main. Workers Builds rebuilds on push (~3 min) and the post goes live.

Easiest way without local git: on GitHub, open the project brightmann/nnd → Add file → Create new file, paste that content, and commit directly to main.

Two notes: the content itself can be Chinese, just keep the filename ASCII. And a post in en/ only shows on the English site — for a Chinese version you can put the same file under zh-CN/ (or zh-TW/).

我得到的博客地址是:

 https://nnd.luckypoem.workers.dev/

 https://nnd.luckypoem.workers.dev/blog

项目地址:

 https://github.com/nelsonlaidev/nelsonlai.dev

https://github.com/brightmann/nnd

在此处 https://github.com/brightmann/nnd/tree/main/src/content/blog/en ,创建源帖ce.mdx , 内容为:

---
title: 中国经济
description: 这是一篇文章
date: '2026-10-31T11:09'
--- 

此处写正文或html codes

(详见https://github.com/brightmann/nnd/blob/main/src/content/blog/en/ce.mdx?plain=1) 

创建源帖后,稍等2-3分钟,博客网站就会更新。

-----------------------------

相关帖子: 

搭建基于nextjs的静态博客程序nnmb

(该程序作者的另一个静态博客程序)