The site is prerendered by Angular and hosted through Cloudflare Workers Static Assets. Documentation is built and deployed alongside the blog, with the same shell, theme, origin, and URL conventions.
Build and inspect
pnpm build
pnpm previewThe build generates source-derived modules first, prerenders routes, then prepares deployment metadata. The Node preview serves dist/blog/browser at http://127.0.0.1:4173/ by default. It is useful for inspecting the production application but cannot reproduce every Cloudflare asset redirect.
Postbuild metadata preparation scans only each prerendered document's head.
Do not construct a full DOM merely to read canonicals, robots directives, dates,
or Markdown discovery links: static formula markup makes article bodies large,
and retaining their DOM representation can exhaust a constrained build heap.
For actual Worker/static-asset behavior after building:
pnpm preview:cloudflareThis starts local Wrangler at http://127.0.0.1:8787/. Use the Cloudflare Playwright project to check route handling and HTTP behavior. Do not infer successful publication from a local preview: production changes become public only through the repository's configured deployment process.
Hashed lazy chunks from an older deployment can disappear while a reader keeps that version of the application open. The client recognizes the browser's dynamic-import failure and performs one full navigation to the requested page so the new document loads the current asset graph. It rate-limits recovery per URL in session storage and retains the visible manual retry when reloading is unsafe or fails again.
Build outputs and ownership
| Output | Produced from |
|---|---|
| HTML routes | Angular client/server routes and generated content metadata |
404.html |
The prerendered /404 page |
_redirects |
configs/redirects.yaml |
sitemap.xml, robots.txt |
Prerender manifest, canonical URLs, and noindex metadata |
feed.xml, atom.xml |
Published article summaries and authored dates |
Markdown exports and llms.txt |
The same prose as HTML articles, CV, directories, and documentation |
dist/blog/markdown-routes.json, worker.mjs |
Prerendered Markdown alternate links and the shared request handler |
Never hand-edit generated files. The Worker entry and its map stay outside public static assets. A build validates that every advertised Markdown alternate exists.
Postbuild appends one-year immutable browser-cache rules to the copied _headers
for Angular's hashed root main-*.js, chunk-*.js, styles-*.css, and media/*
assets. These namespaces are reserved for build output; postbuild rejects
unversioned files within them. Keep authored downloads under /posts or /assets.
HTML, Markdown, and unversioned article images retain their existing cache policy.
The negotiated page Worker still owns Vary: Accept and private revalidation.
Verify cache headers through the Cloudflare preview, since the Node preview does
not apply _headers. A deployment is required for production headers to change.
URLs, redirects, and missing pages
Canonicals use slashless URLs such as /blog/notes/example and /docs/development. Cloudflare assets.html_handling is drop-trailing-slash, so these canonical URLs must return 200 directly. Check direct visits as well as Angular navigation when adding a route.
Prerender every article, ancestor category, archive page, and documentation page from the same metadata used by the router. Redirect pages need prerendering when the static host must handle direct visits. Use the shared 404 experience for unknown paths, with noindex metadata and an actual HTTP 404 from the production host.
Sitemap generation excludes redirect and noindex pages. Article lastmod comes from updated ?? date; a build timestamp is not a content modification date. Documentation has no invented publication/update timestamps, so it has no article dates or BlogPosting structured data.
Private article access
private: true removes an article from public discovery but still prerenders its
direct HTML route. Before publishing one, create a Cloudflare Access self-hosted
application for pufanyi.com/blog/<slug>* with an Allow policy for the exact
owner email. The trailing wildcard covers the canonical route, slash and HTML
aliases, and Markdown URL. Verify anonymous requests to each return an Access
login redirect, then confirm the owner can read the article after login. Protect
post-local assets separately when a private article has any. Keep the source
repository private if the authored prose itself must remain private.
Markdown negotiation and discovery
Every handbook page advertises its Markdown alternative. /docs uses /docs/index.md; other pages use /docs/<slug>.md. Article exports use /blog/<slug>.md; archive pages share /blog/index.md; homepage and CV share /profile.md.
curl -H 'Accept: text/markdown' http://127.0.0.1:8787/docs/developmentOnly an explicit preferred text/markdown accepts Markdown. Ordinary browser requests receive HTML. Negotiated responses use Vary: Accept and avoid shared caching keyed only by URL; the underlying HTML and Markdown assets retain separate caches. One-to-one article and documentation exports identify their HTML canonical, including direct GET/HEAD and conditional 304 responses. Shared archive and profile exports are not canonicalized to partial pages.
Keep wrangler.jsonc worker-first coverage and public/_headers MIME rules aligned for /blog/* and /docs/*. Missing .md URLs must return an HTML 404 with the correct MIME type; Cloudflare can otherwise apply the requested URL's Markdown header to its HTML fallback. PDF viewer .mjs and .wasm files need JavaScript and WebAssembly MIME types in preview too.
Release checks
Run check and noninteractive unit tests after the final change, then build and run the relevant production browser tests. Verify direct URLs, canonicals, internal documentation links, Markdown responses, feeds, and the custom 404. Review the actual source diff before pushing. Credentials and production deployment access belong to the existing repository environment; never add them to public documentation or static assets.
The production Worker disables its default workers.dev hostname. Keep traffic on
the configured custom domain so path-scoped Cloudflare Access applications cannot
be bypassed through the otherwise public Worker subdomain.