Skip to content

fix(article): render article deep links server-side, not 404#23

Merged
Koopa0 merged 1 commit into
mainfrom
fix/article-deep-link-ssr
Jul 3, 2026
Merged

fix(article): render article deep links server-side, not 404#23
Koopa0 merged 1 commit into
mainfrom
fix/article-deep-link-ssr

Conversation

@Koopa0

@Koopa0 Koopa0 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Direct GET /articles/:slug returned an Express Cannot GET 404 for every published article — deep links were broken and article content was invisible to crawlers. In-app client-side navigation worked, so it only showed up on direct hits / receipts / SEO.

Root cause (SSR bundle, not the route table). articles/:slug was correctly registered as RenderMode.Server. The failure was inside SSR rendering:

  • markdown.service.ts imported isomorphic-dompurify, which pulls jsdom into the server bundle.
  • jsdom reads its default stylesheet at module-eval time via readFileSync(path.resolve(__dirname, "../../browser/default-stylesheet.css")). __dirname is undefined in the ESM server bundleReferenceError while evaluating the lazy article-detail chunk.
  • The router reports a navigation error → AngularNodeAppEngine.handle() returns null → Express falls through to its finalhandler 404.

Article-specific because only article-detail imports MarkdownService; topic pages rendered fine, and a missing slug still 302'd (its resolver redirect ran before the throw).

Fix. parse() already skips DOMPurify on the server (the browser re-sanitizes on hydration), so jsdom was never used server-side — dead weight that also can't be esbuild-bundled. Import the browser DOMPurify build (dompurify) instead; isomorphic-dompurify already delegates to it in the browser, so client sanitization is byte-identical. Drops the now-unused isomorphic-dompurify and redundant @types/dompurify (dompurify v3 ships its own types). jsdom stays as the unit-test DOM environment.

Result: article deep links now true SSR (200 with full body prose, JSON-LD, OG meta, <title>, canonical) — crawler/SEO-visible, not a shell fallback. No route-table or other-route changes.

Test Plan

Reproduced against the production bundle locally (Node fetch rewrite backend:8080 → live API) — before/after:

  • GET /articles/bounded-autonomy-personal-agent-os: 404 → 200, body carries 9 <h2> + 39 <p> of rendered prose, JSON-LD BlogPosting, og:title, SSR <title> + canonical.
  • Three CJK/URL-encoded slugs (…重新理解-go…, …你的變數住在-stack-還是-heap, 每一行程式碼…): 404 → 200 with content.
  • Control routes unchanged: /, /articles, /topics/:slug → 200; missing slug → 302 → /not-found.

Verification gate (all green):

  • npx tsc --noEmit — pass
  • npx ng lint — all files pass
  • npx ng test617/617 pass (incl. markdown.service.spec.ts XSS sanitization, 24 tests, exercising dompurify)
  • npx ng build --configuration production — pass; jsdom bundling warning gone

Direct GET /articles/:slug returned an Express "Cannot GET" 404 for every
published article (client-side navigation from within the app worked),
breaking deep links and hiding article content from crawlers.

Root cause was in the SSR bundle, not the route table. markdown.service.ts
imported isomorphic-dompurify, which pulls jsdom into the server bundle.
jsdom loads its default stylesheet at module-eval time via
readFileSync(path.resolve(__dirname, "../../browser/default-stylesheet.css")),
and __dirname is undefined in the ESM server bundle -> ReferenceError while
evaluating the lazy article-detail chunk -> the router reports a navigation
error -> AngularNodeAppEngine.handle() returns null -> Express falls through
to its finalhandler 404. It was article-specific because only article-detail
imports MarkdownService; topic pages (no MarkdownService) rendered fine, and
a missing slug still 302'd because its resolver redirect ran before the throw.

parse() already skips DOMPurify on the server (browser re-sanitizes on
hydration), so jsdom was never used server-side -- it was pure dead weight
that also cannot be esbuild-bundled. Import the browser DOMPurify build
(dompurify) instead; isomorphic-dompurify already delegates to it in the
browser, so client sanitization is unchanged. Drops the now-unused
isomorphic-dompurify and the redundant @types/dompurify (dompurify v3 ships
its own types); jsdom stays as the unit-test DOM environment.
@Koopa0 Koopa0 merged commit 6f32b6f into main Jul 3, 2026
5 checks passed
@augmentcode

augmentcode Bot commented Jul 3, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Fixes broken deep-links for published articles by ensuring the article detail route can be server-rendered instead of falling through to an Express 404.

Changes:

  • Replaces isomorphic-dompurify with the browser dompurify build to avoid pulling jsdom into the SSR bundle
  • Updates MarkdownService to import dompurify directly
  • Removes redundant @types/dompurify (DOMPurify v3 ships types)
Technical Notes: Prevents SSR chunk evaluation failures caused by jsdom module-eval behavior in the ESM server bundle, restoring 200 + full HTML for /articles/:slug.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

import { marked } from 'marked';
import hljs from 'highlight.js';
import DOMPurify from 'isomorphic-dompurify';
import DOMPurify from 'dompurify';

@augmentcode augmentcode Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MarkdownService.parse() still skips DOMPurify during SSR, but this PR makes article pages successfully SSR again—worth double-checking the trust boundary for markdown inputs so the initial server-rendered HTML can’t contain untrusted/scriptable content before client hydration re-sanitizes.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant