Skip to content

docs: add remote markdown and mermaid rendering#145

Open
Kzoeps wants to merge 1 commit into
mainfrom
epds-docs-binding
Open

docs: add remote markdown and mermaid rendering#145
Kzoeps wants to merge 1 commit into
mainfrom
epds-docs-binding

Conversation

@Kzoeps

@Kzoeps Kzoeps commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What changed

  • Added a Markdoc remote-doc tag for runtime GitHub Markdown rendering with /raw cache fallback.
  • Switched the ePDS page to use the canonical ePDS tutorial Markdown.
  • Added Mermaid rendering for fenced mermaid diagrams.
  • Updated raw and search generation to use rawUrl canonical sources.
  • Included h4 headings in the right-side table of contents.

Validation

  • npm run build
  • Verified ePDS page renders canonical content, Mermaid diagrams, raw link, and h4 TOC entries locally.

Summary by CodeRabbit

  • New Features

    • Added support for rendering Mermaid diagrams in documentation.
    • Enabled loading canonical documentation content from remote GitHub repositories with fallback support.
    • Table of Contents now includes H4 headings for improved document navigation.
  • Style

    • Added styling for H4 heading links and remote document status messages.
    • Added styling for Mermaid diagram rendering and error states.
  • Documentation

    • Added guide for using remote documentation includes.
  • Chores

    • Added Mermaid library dependency.

@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hypercerts-v0.2-documentation Ready Ready Preview, Comment Jun 11, 2026 9:09am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds remote markdown loading at browser runtime, Mermaid diagram rendering from code fences, and build-time integration to fetch and cache remote content. Pages can now embed canonical documentation from GitHub via the {% remote-doc %} tag, with fallback to build-time cached versions. Mermaid diagrams render automatically in code blocks marked with the mermaid language. Custom raw content URLs are specified via page frontmatter rawUrl.

Changes

Remote Markdown & Mermaid Rendering System

Layer / File(s) Summary
Mermaid Diagram Component & Rendering
package.json, components/MermaidDiagram.js, markdoc/nodes/fence.markdoc.js, styles/globals.css, pages/_app.js
New MermaidDiagram component lazily loads mermaid library and renders chart source to SVG with dark/neutral theme support. Mermaid code fences are detected via fence.transform() and routed to render as MermaidDiagram tags. Mermaid package added as dependency; component registered in app's Markdoc mappings.
Remote Markdown Content Loading Component
components/RemoteMarkdown.js, styles/globals.css
New RemoteMarkdown component fetches validated Hypercerts GitHub sources at runtime, strips YAML frontmatter, parses via Markdoc, and renders with link rewriting (converts relative hrefs to GitHub blob/tree). Fetches live GitHub raw content first; falls back to build-time /raw cache on failure. Dispatches remote-docs:loaded event after successful render.
Markdoc Tag Registration & Component Mapping
markdoc/tags/remote-doc.markdoc.js, markdoc/tags/index.js, pages/_app.js
Markdoc tag remote-doc defined with required source attribute. Tag and RemoteMarkdown component registered in global Markdoc mappings; RemoteDoc alias added for RemoteMarkdown.
Raw URL Frontmatter Override Support
components/CopyRawButton.js, components/Layout.js
CopyRawButton accepts frontmatter prop and uses frontmatter.rawUrl when present, otherwise generates local /raw URL. Layout passes frontmatter to CopyRawButton to enable override.
Build-Time Raw & Search Integration
lib/generate-raw-pages.js, lib/generate-search-index.js
Scripts now parse frontmatter to extract rawUrl: generate-raw-pages.js fetches remote markdown from rawUrl and caches to /raw (or copies local if absent). generate-search-index.js extracts rawUrl and fetches remote body content for indexing when specified.
Table of Contents H4 & Remote Content Support
components/TableOfContents.js, styles/globals.css
TableOfContents now collects H4 headings alongside H2/H3, generates missing id attributes, and re-collects when remote-docs:loaded event fires. Dedicated .toc-link-h4 class applied to fourth-level heading links.
Documentation & Example Usage
docs/remote-markdown.md, pages/architecture/epds.md
New documentation page describes {% remote-doc %} tag usage, runtime fetch + fallback behavior, Markdoc/Mermaid rendering within remote content, and rawUrl frontmatter role. ePDS page demonstrates usage by loading tutorial from remote GitHub source.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • hypercerts-org/documentation#44: Modifies components/TableOfContents.js and TOC styling; this PR extends TOC to support H4 headings and remote-docs:loaded event sync.
  • hypercerts-org/documentation#110: Modifies the "Copy raw / View raw" feature in components/CopyRawButton.js and raw-page generation; this PR extends both to support frontmatter.rawUrl overrides.
  • hypercerts-org/documentation#102: Modifies lib/generate-search-index.js search-index generation logic; this PR extends it to fetch and index remote content via rawUrl.

🐰 A rabbit's delight, these docs so bright,
Remote sources fetched at runtime's light,
Mermaid diagrams dance in the frame,
TOC headings, all H4, stake their claim!
Build-time caches when GitHub's away—
Documentation lives to sync and play! 🎨📚

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding remote markdown loading and mermaid diagram rendering capabilities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch epds-docs-binding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
components/TableOfContents.js (1)

20-49: ⚖️ Poor tradeoff

Heading id generation from text may create duplicates.

Lines 24-27 generate a heading id by slugifying textContent when the element lacks an existing id. If two headings share identical text (e.g., "Overview" appears twice), both will receive the same id, which violates HTML uniqueness and breaks TOC link targeting.

For typical documentation with distinct heading text this is unlikely, but consider appending a numeric suffix if a collision is detected.

♻️ Optional: De-duplicate heading IDs
 const collectHeadings = () => {
   const elements = article.querySelectorAll("h2, h3, h4");
+  const usedIds = new Set();
   const items = Array.from(elements).map((el) => {
     if (!el.id) {
-      el.id = el.textContent
+      let baseId = el.textContent
         .toLowerCase()
         .replace(/[^a-z0-9]+/g, "-")
         .replace(/(^-|-$)/g, "");
+      let id = baseId;
+      let counter = 1;
+      while (usedIds.has(id)) {
+        id = `${baseId}-${counter}`;
+        counter++;
+      }
+      el.id = id;
     }
+    usedIds.add(el.id);
     return {
       id: el.id,
       text: el.textContent,
       level: Number(el.tagName.slice(1)),
     };
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/TableOfContents.js` around lines 20 - 49, collectHeadings
currently slugifies el.textContent and assigns el.id directly which can produce
duplicate ids; change the id generation in collectHeadings to ensure uniqueness
by checking existing ids (e.g., document.getElementById or a Set of collected
ids) and appending a numeric suffix like "-1", "-2", etc., until an unused id is
found before assigning el.id, then continue returning the item for setHeadings
and keeping setActiveId("") behavior unchanged.
components/RemoteMarkdown.js (1)

147-150: ⚡ Quick win

Consider caching the transformed AST to avoid redundant parsing.

transformMarkdown is called three times per successful fetch: once at Line 188 (GitHub validation), once at Line 198 (cache validation), and once at Line 219 (final render memoization). The first two calls discard the result and only validate that parsing succeeds, but Markdoc parsing and transformation are not free.

♻️ Refactor to parse once

Store the transformed content from the fetch effect and skip re-transformation in the render memo:

 export function RemoteMarkdown({ source, children }) {
   ...
-  const [markdown, setMarkdown] = useState(null);
+  const [transformedContent, setTransformedContent] = useState(null);
   ...

   useEffect(() => {
     ...
     async function loadRemoteMarkdown() {
       ...
       try {
         const nextMarkdown = await fetchMarkdown(sourceInfo.rawUrl, controller.signal, 'GitHub');
-        transformMarkdown(nextMarkdown);
-        setMarkdown(nextMarkdown);
+        const content = transformMarkdown(nextMarkdown);
+        setTransformedContent(content);
         return;
       } catch (err) { ... }

       try {
         const cachedMarkdown = await fetchMarkdown(rawCacheUrl, controller.signal, 'Build-time raw cache');
-        transformMarkdown(cachedMarkdown);
-        setMarkdown(cachedMarkdown);
+        const content = transformMarkdown(cachedMarkdown);
+        setTransformedContent(content);
       } catch (err) { ... }
     }
     ...
   }, [rawCacheUrl, sourceInfo]);

   const renderedState = useMemo(() => {
-    if (!markdown || !sourceInfo) return { renderedContent: null, renderError: null };
+    if (!transformedContent || !sourceInfo) return { renderedContent: null, renderError: null };

     try {
-      const content = transformMarkdown(markdown);
       return {
-        renderedContent: Markdoc.renderers.react(content, React, {
+        renderedContent: Markdoc.renderers.react(transformedContent, React, {
           components: remoteMarkdocComponents,
         }),
         renderError: null,
       };
     } catch (err) {
       return { renderedContent: null, renderError: err };
     }
-  }, [markdown, sourceInfo]);
+  }, [transformedContent, sourceInfo]);

Also applies to: 183-206, 215-229

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/RemoteMarkdown.js` around lines 147 - 150, transformMarkdown is
being invoked multiple times per fetch; compute and store the transformed result
once in the fetch effect and reuse it instead of re-parsing. In RemoteMarkdown's
fetch handler, call Markdoc.parse/Markdoc.transform (via transformMarkdown)
once, save the transformed AST/output into a state or ref (e.g., parsedDoc or
transformedMarkdown), use that stored value for the GitHub validation and cache
validation checks, and pass it into the memoized render instead of calling
transformMarkdown again; keep stripFrontmatter/markdocConfig usage the same and
wrap the single transform in try/catch to preserve error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/generate-raw-pages.js`:
- Around line 50-55: Add a timeout to the build-time fetch by passing an
AbortSignal to the fetch call (e.g., signal: AbortSignal.timeout(10000)) so
slow/unresponsive remote servers cancel after ~10s; update the fetch in
generate-raw-pages.js where rawUrl is fetched (affecting the
response/response.ok logic and returned response.text()) to include this signal
and handle the abort error as needed, and make the identical change in
generate-search-index.js for its fetch calls to keep behavior consistent across
both scripts.
- Around line 36-42: getFrontmatterRawUrl currently uses a frontmatter regex
that requires a trailing newline after the closing '---' and extracts rawUrl
with an inline quoted-value regex, causing inconsistency with
generate-search-index.js; update getFrontmatterRawUrl to use the same
frontmatter pattern as generate-search-index.js (do not require a trailing
newline after the closing '---') and replace the inline rawUrl extraction with
the shared stripYamlValue helper (import or extract stripYamlValue into a common
module) so quote normalization and parsing behavior are identical across both
scripts.

---

Nitpick comments:
In `@components/RemoteMarkdown.js`:
- Around line 147-150: transformMarkdown is being invoked multiple times per
fetch; compute and store the transformed result once in the fetch effect and
reuse it instead of re-parsing. In RemoteMarkdown's fetch handler, call
Markdoc.parse/Markdoc.transform (via transformMarkdown) once, save the
transformed AST/output into a state or ref (e.g., parsedDoc or
transformedMarkdown), use that stored value for the GitHub validation and cache
validation checks, and pass it into the memoized render instead of calling
transformMarkdown again; keep stripFrontmatter/markdocConfig usage the same and
wrap the single transform in try/catch to preserve error handling.

In `@components/TableOfContents.js`:
- Around line 20-49: collectHeadings currently slugifies el.textContent and
assigns el.id directly which can produce duplicate ids; change the id generation
in collectHeadings to ensure uniqueness by checking existing ids (e.g.,
document.getElementById or a Set of collected ids) and appending a numeric
suffix like "-1", "-2", etc., until an unused id is found before assigning
el.id, then continue returning the item for setHeadings and keeping
setActiveId("") behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f54b1a5b-feff-4292-bd43-e77377b1bd54

📥 Commits

Reviewing files that changed from the base of the PR and between a8499ca and 73aa137.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • components/CopyRawButton.js
  • components/Layout.js
  • components/MermaidDiagram.js
  • components/RemoteMarkdown.js
  • components/TableOfContents.js
  • docs/remote-markdown.md
  • lib/generate-raw-pages.js
  • lib/generate-search-index.js
  • markdoc/nodes/fence.markdoc.js
  • markdoc/tags/index.js
  • markdoc/tags/remote-doc.markdoc.js
  • package.json
  • pages/_app.js
  • pages/architecture/epds.md
  • styles/globals.css

Comment thread lib/generate-raw-pages.js
Comment on lines +36 to +42
function getFrontmatterRawUrl(markdown) {
const match = markdown.match(/^---\n([\s\S]*?)\n---\n/);
if (!match) return null;

const files = walkDir(PAGES_DIR);
const rawUrlMatch = match[1].match(/^rawUrl:\s*["']?([^"'\n]+)["']?\s*$/m);
return rawUrlMatch?.[1] || null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Inconsistent frontmatter and rawUrl extraction compared to generate-search-index.js.

The frontmatter regex on line 37 requires a trailing newline after the closing ---, but generate-search-index.js line 26 does not. Additionally, this file extracts rawUrl with an inline regex that handles quotes, while generate-search-index.js uses a cleaner stripYamlValue(...) utility for quote normalization.

Consider extracting a shared stripYamlValue helper (or importing from a common module) and using consistent frontmatter regex patterns across both scripts to reduce maintenance burden and edge-case divergence.

♻️ Proposed refactor for consistency
+function stripYamlValue(value) {
+  return value.trim().replace(/^['"]|['"]$/g, '');
+}
+
 function getFrontmatterRawUrl(markdown) {
-  const match = markdown.match(/^---\n([\s\S]*?)\n---\n/);
+  const match = markdown.match(/^---\n([\s\S]*?)\n---/);
   if (!match) return null;
 
-  const rawUrlMatch = match[1].match(/^rawUrl:\s*["']?([^"'\n]+)["']?\s*$/m);
-  return rawUrlMatch?.[1] || null;
+  const rawUrlMatch = match[1].match(/^rawUrl:\s*(.+)$/m);
+  return rawUrlMatch ? stripYamlValue(rawUrlMatch[1]) : null;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generate-raw-pages.js` around lines 36 - 42, getFrontmatterRawUrl
currently uses a frontmatter regex that requires a trailing newline after the
closing '---' and extracts rawUrl with an inline quoted-value regex, causing
inconsistency with generate-search-index.js; update getFrontmatterRawUrl to use
the same frontmatter pattern as generate-search-index.js (do not require a
trailing newline after the closing '---') and replace the inline rawUrl
extraction with the shared stripYamlValue helper (import or extract
stripYamlValue into a common module) so quote normalization and parsing behavior
are identical across both scripts.

Comment thread lib/generate-raw-pages.js
Comment on lines +50 to +55
const response = await fetch(rawUrl, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Failed to fetch rawUrl for ${relative(PAGES_DIR, file)}: ${rawUrl} returned ${response.status} ${response.statusText || ''}`.trim());
}

return response.text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeout protection to all build-time fetch operations.

Both generate-raw-pages.js and generate-search-index.js perform fetch calls without timeout. If any remote server is slow or unresponsive, the build will hang indefinitely. Node.js fetch supports signal: AbortSignal.timeout(ms) to automatically abort after a deadline. Recommend adding a consistent timeout (e.g., 10 seconds) to both scripts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generate-raw-pages.js` around lines 50 - 55, Add a timeout to the
build-time fetch by passing an AbortSignal to the fetch call (e.g., signal:
AbortSignal.timeout(10000)) so slow/unresponsive remote servers cancel after
~10s; update the fetch in generate-raw-pages.js where rawUrl is fetched
(affecting the response/response.ok logic and returned response.text()) to
include this signal and handle the abort error as needed, and make the identical
change in generate-search-index.js for its fetch calls to keep behavior
consistent across both scripts.

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