Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
73aa137
docs: add remote markdown and mermaid rendering
Kzoeps Jun 11, 2026
6f84154
docs-refresh: add registry-based external docs deploy check
Kzoeps Jun 11, 2026
44e7e8c
docs-refresh: make deployed fingerprint URL configurable
Kzoeps Jun 12, 2026
4e2a996
docs-refresh: default fingerprint URL to fork deployment
Kzoeps Jun 12, 2026
122fccf
docs-refresh: add hyperindex test source
Kzoeps Jun 12, 2026
07e15fe
docs-refresh: point hyperindex test source at skills update
Kzoeps Jun 12, 2026
d29eda2
docs-refresh: target hyperindex test markdown
Kzoeps Jun 12, 2026
db9a90f
docs-refresh: allow gainforest hyperindex source
Kzoeps Jun 12, 2026
e150893
docs-refresh: support explicit raw doc sources
Kzoeps Jun 12, 2026
34f79db
docs-refresh: use hyperindex as remote test source
Kzoeps Jun 12, 2026
6229277
docs-refresh: render remote docs at build time
Kzoeps Jun 12, 2026
95e4ef7
Merge pull request #2 from Kzoeps/docs-refresh-registry
Kzoeps Jun 12, 2026
0e8dd55
tools: document Hypercerts agent skills
Kzoeps Jun 26, 2026
2f37774
Merge pull request #3 from Kzoeps/hypercerts-agent-skills
Kzoeps Jun 26, 2026
1887c03
external-docs: simplify build-time source rendering
Kzoeps Jul 14, 2026
0b23a4f
github-actions: replace refresh dry run with docs CI
Kzoeps Jul 14, 2026
bd4d4c7
external-docs: address confirmed review findings
Kzoeps Jul 14, 2026
3749606
external-docs: document new functions and constants
Kzoeps Jul 14, 2026
915fd80
Merge branch 'main' into docs-refresh-registry
Kzoeps Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/docs-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Docs CI

on:
pull_request:
branches:
- main
paths:
- '.github/workflows/docs*.yml'
- 'docs-sources.yml'
- 'docs/remote-markdown.md'
- 'components/**'
- 'lib/external-docs*.js'
- 'lib/generate-docs-fingerprint.js'
- 'lib/generate-external-docs-manifest.js'
- 'lib/generate-raw-pages.js'
- 'lib/generate-search-index.js'
- 'lib/compare-docs-fingerprint.js'
- 'markdoc/**'
- 'next.config.js'
- 'pages/**'
- 'test/**'
- 'package.json'
- 'package-lock.json'

permissions:
contents: read

concurrency:
group: docs-ci-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
checks:
name: Test and build documentation
runs-on: ubuntu-latest

steps:
- name: Checkout docs repo
uses: actions/checkout@v6.0.2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@v6.4.0
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build static documentation
run: npm run build
env:
GITHUB_TOKEN: ${{ github.token }}
129 changes: 129 additions & 0 deletions .github/workflows/docs-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Refresh external docs

on:
schedule:
- cron: '17 * * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Compare fingerprints without posting to the Vercel deploy hook.'
required: false
default: true
type: boolean
force_deploy:
description: 'Mark the run as changed even when the deployed fingerprint matches.'
required: false
default: false
type: boolean

permissions:
contents: read

concurrency:
group: docs-refresh
cancel-in-progress: false

jobs:
check-docs:
name: Check external docs fingerprint
runs-on: ubuntu-latest

steps:
- name: Checkout docs repo
uses: actions/checkout@v6.0.2
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@v6.4.0
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Generate current docs fingerprint
run: npm run docs:fingerprint -- --output current-docs-fingerprint.json
env:
DOCS_SOURCE_TOKEN: ${{ secrets.DOCS_SOURCE_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}

- name: Download deployed docs fingerprint
env:
DOCS_FINGERPRINT_URL: ${{ vars.DOCS_FINGERPRINT_URL || 'https://staging.docs.hypercerts.org/docs-fingerprint.json' }}
run: |
set +e
http_code=$(curl -sS -L -w "%{http_code}" \
-o deployed-docs-fingerprint.json \
"$DOCS_FINGERPRINT_URL")
curl_status=$?
set -e

if [ "$curl_status" -ne 0 ]; then
echo "::error::Failed to download deployed docs fingerprint from $DOCS_FINGERPRINT_URL. Refusing to deploy on an unknown diff."
exit 1
fi

case "$http_code" in
200)
;;
404)
echo '{}' > deployed-docs-fingerprint.json
;;
*)
echo "::error::Unexpected HTTP $http_code while downloading $DOCS_FINGERPRINT_URL. Refusing to deploy on an unknown diff."
exit 1
;;
esac

- name: Check if external docs changed
id: diff
env:
FORCE_DEPLOY: ${{ github.event_name == 'workflow_dispatch' && inputs.force_deploy == true }}
run: |
node lib/compare-docs-fingerprint.js \
current-docs-fingerprint.json \
deployed-docs-fingerprint.json >> "$GITHUB_OUTPUT"

if [ "$FORCE_DEPLOY" = "true" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "reason=forced by workflow_dispatch input" >> "$GITHUB_OUTPUT"
fi

- name: Trigger Vercel deployment
if: steps.diff.outputs.changed == 'true'
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_URL }}
run: |
if [ "$DRY_RUN" = "true" ]; then
echo "workflow_dispatch dry_run=true; skipping Vercel deploy hook."
exit 0
fi

if [ -z "$VERCEL_DEPLOY_HOOK_URL" ]; then
echo "::error::VERCEL_DEPLOY_HOOK_URL is required when external docs have changed. Create a Vercel Deploy Hook for the production branch and store its URL as a GitHub Actions secret."
exit 1
fi

curl -fsS -X POST "$VERCEL_DEPLOY_HOOK_URL"

- name: Write summary
if: always()
env:
DIFF_CHANGED: ${{ steps.diff.outputs.changed || 'unknown' }}
DIFF_REASON: ${{ steps.diff.outputs.reason || 'not computed' }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
CURRENT_FINGERPRINT: ${{ steps.diff.outputs.current_fingerprint || 'n/a' }}
DEPLOYED_FINGERPRINT: ${{ steps.diff.outputs.deployed_fingerprint || 'n/a' }}
run: |
{
echo "### External docs refresh"
echo ""
echo "Changed: $DIFF_CHANGED"
echo "Reason: $DIFF_REASON"
echo "Dry run: $DRY_RUN"
echo "Current: $CURRENT_FINGERPRINT"
echo "Deployed: $DEPLOYED_FINGERPRINT"
} >> "$GITHUB_STEP_SUMMARY"
Comment thread
Kzoeps marked this conversation as resolved.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ out/

# Generated at build time by lib/generate-*.js — no need to track
public/raw/
public/docs-fingerprint.json
public/search-index.json
public/sitemap.xml
current-docs-fingerprint.json
deployed-docs-fingerprint.json
lib/external-docs-content.json
lib/lastUpdated.json
11 changes: 9 additions & 2 deletions components/CopyRawButton.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { useState } from 'react';
import { useRouter } from 'next/router';

function getRawUrl(currentPath) {
/**
* Map a documentation route to the generated local Markdown artifact used by page actions.
*/
function getGeneratedRawUrl(currentPath) {
if (currentPath === '/') return '/raw/index.md';
return `/raw${currentPath}.md`;
}

/**
* Render page-level actions for copying or viewing the generated Markdown for the current docs page.
* External docs are included in the same local /raw output as ordinary pages.
*/
export function CopyRawButton() {
const [copied, setCopied] = useState(false);
const [copyError, setCopyError] = useState(false);
const [isCopying, setIsCopying] = useState(false);
const router = useRouter();
const currentPath = router.asPath.split('#')[0].split('?')[0] || '/';
const rawUrl = getRawUrl(currentPath);
const rawUrl = getGeneratedRawUrl(currentPath);

const handleCopy = async () => {
setIsCopying(true);
Expand Down
133 changes: 133 additions & 0 deletions components/MermaidDiagram.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useEffect, useMemo, useState } from 'react';
import { CodeBlock } from './CodeBlock';

/** Cached Mermaid module load shared by every diagram on the current page. */
let mermaidModulePromise;
/** Monotonic counter used to give Mermaid render targets unique DOM identifiers. */
let diagramIdCounter = 0;

/**
* Load Mermaid once per browser session while allowing a failed chunk load to be retried.
*/
function getMermaid() {
if (!mermaidModulePromise) {
mermaidModulePromise = import('mermaid')
.then((module) => module.default || module)
.catch((error) => {
mermaidModulePromise = undefined;
throw error;
});
}

return mermaidModulePromise;
}
Comment thread
Kzoeps marked this conversation as resolved.

/**
* Allocate a unique identifier for one Mermaid render target.
*/
function getDiagramId() {
diagramIdCounter += 1;
return `mermaid-diagram-${diagramIdCounter}`;
}

/**
* Select the Mermaid theme that matches the current documentation color scheme.
*/
function getPreferredMermaidTheme() {
if (typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) {
return 'dark';
}

return 'neutral';
}

/**
* Render a Mermaid fenced code block as an SVG diagram in the browser.
* Use this for Markdown fences with `mermaid` as the language; invalid diagrams fall back to copyable code with an actionable syntax error.
*/
export function MermaidDiagram({ chart, children }) {
const source = (chart || children || '').trim();
const diagramId = useMemo(getDiagramId, []);
const [svg, setSvg] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(Boolean(source));
const [theme, setTheme] = useState('neutral');

useEffect(() => {
/** Synchronize the diagram theme when the page color-scheme class changes. */
const updateTheme = () => setTheme(getPreferredMermaidTheme());
updateTheme();

const observer = new MutationObserver(updateTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

return () => observer.disconnect();
}, []);

useEffect(() => {
if (!source) {
setLoading(false);
setError(new Error('No Mermaid source was provided. Add diagram text inside the mermaid code fence.'));
return undefined;
}

let cancelled = false;
setLoading(true);
setError(null);
setSvg('');

/**
* Render the current source with Mermaid unless this React effect has been cancelled.
*/
async function renderDiagram() {
try {
const mermaid = await getMermaid();
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme,
});
const result = await mermaid.render(diagramId, source);
if (cancelled) return;
setSvg(result.svg);
} catch (err) {
if (cancelled) return;
setError(err);
} finally {
if (!cancelled) setLoading(false);
}
}

renderDiagram();

return () => {
cancelled = true;
};
}, [diagramId, source, theme]);

if (error) {
return (
<div className="mermaid-diagram mermaid-diagram--error">
<p>
<strong>Could not render Mermaid diagram.</strong> Check the diagram syntax in the source Markdown and reload the page. Details: {error.message}
</p>
<CodeBlock content={source} language="mermaid" />
</div>
);
}

if (loading) {
return (
<div className="mermaid-diagram mermaid-diagram--loading" role="status">
Rendering Mermaid diagram…
</div>
);
}

return (
<div
className="mermaid-diagram"
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
Loading