feat(wiki): add full in-app wiki browser and llms index (#80)

* feat(wiki): add full in-app wiki browser and llms index

* feat(wiki): auto-generate per-page llms exports

* vuln package

* fix(wiki): guard malformed route decoding

* fix(wiki): preserve markdown anchor fragments across page links

* refactor(markdown): share default render components

* fix(wiki): block unsafe markdown link schemes

* fix(wiki): block unsafe markdown image schemes

* docs(wiki): migrate root docs into wiki pages

* chore(wiki): de-track generated llms exports

* chore(wiki): ignore generated public wiki artifacts

* fix(wiki): align llms urls with per-page endpoint pattern

* fix(wiki): derive llms index from wiki index page

* refactor(markdown): share frontmatter and title helpers

* refactor(wiki): share route and llms path mapping

* ci(pages): add pr verify workflow and tighten deploy triggers
This commit is contained in:
davida-ps
2026-02-26 10:43:36 +02:00
committed by GitHub
parent 8132c23f41
commit fefecaa60a
26 changed files with 1274 additions and 230 deletions
+99
View File
@@ -0,0 +1,99 @@
import React from 'react';
import type { Components } from 'react-markdown';
export const defaultMarkdownComponents: Components = {
h1: ({ children }) => (
<h1 className="text-2xl font-bold text-white border-b border-clawd-700 pb-3 mb-6 mt-0">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-xl font-bold text-white mt-8 mb-4">{children}</h2>
),
h3: ({ children }) => (
<h3 className="text-lg font-semibold text-white mt-6 mb-3">{children}</h3>
),
h4: ({ children }) => (
<h4 className="text-base font-semibold text-white mt-4 mb-2">{children}</h4>
),
p: ({ children }) => (
<p className="text-gray-300 leading-relaxed mb-4">{children}</p>
),
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-clawd-accent hover:underline"
>
{children}
</a>
),
ul: ({ children }) => (
<ul className="list-disc list-inside text-gray-300 space-y-2 mb-4 ml-4">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside text-gray-300 space-y-2 mb-4 ml-4">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-gray-300">{children}</li>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-clawd-accent pl-4 py-2 my-4 bg-clawd-900/50 rounded-r text-gray-400 italic">
{children}
</blockquote>
),
code: ({ className, children }) => {
const isInline = !className;
if (isInline) {
return (
<code className="text-orange-300 bg-clawd-900 px-1.5 py-0.5 rounded text-sm font-mono">
{children}
</code>
);
}
return (
<code className="text-gray-200 text-sm font-mono">{children}</code>
);
},
pre: ({ children }) => (
<pre className="bg-clawd-900 border border-clawd-700 rounded-lg p-3 sm:p-4 overflow-x-auto mb-4 text-xs sm:text-sm max-w-full">
{children}
</pre>
),
table: ({ children }) => (
<div className="overflow-x-auto mb-6 -mx-4 sm:mx-0 px-4 sm:px-0">
<table className="w-full border-collapse text-xs sm:text-sm min-w-[300px]">
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-clawd-900 border-b border-clawd-600">
{children}
</thead>
),
tbody: ({ children }) => <tbody>{children}</tbody>,
tr: ({ children }) => (
<tr className="border-b border-clawd-700/50">{children}</tr>
),
th: ({ children }) => (
<th className="text-left px-4 py-3 text-gray-300 font-semibold">
{children}
</th>
),
td: ({ children }) => (
<td className="px-4 py-3 text-gray-300">{children}</td>
),
hr: () => <hr className="border-clawd-700 my-6" />,
strong: ({ children }) => (
<strong className="text-white font-semibold">{children}</strong>
),
em: ({ children }) => (
<em className="text-gray-200">{children}</em>
),
};
+40
View File
@@ -0,0 +1,40 @@
const FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n/;
/**
* Remove a leading YAML frontmatter block from markdown content.
* @param {string} content
* @returns {string}
*/
export const stripFrontmatter = (content) =>
String(content ?? '').replace(FRONTMATTER_REGEX, '');
/**
* Build a readable fallback title from a markdown file path.
* @param {string} filePath
* @returns {string}
*/
export const fallbackTitleFromPath = (filePath) => {
const normalized = String(filePath ?? '');
const filename = normalized.split('/').pop() ?? normalized;
const stem = filename.replace(/\.md$/i, '');
return stem
.split(/[-_]/)
.filter(Boolean)
.map((part) => {
if (part.toUpperCase() === part && part.length > 1) return part;
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join(' ');
};
/**
* Extract the first H1 title from markdown; fall back to path-derived title.
* @param {string} content
* @param {string} filePath
* @returns {string}
*/
export const extractTitleFromMarkdown = (content, filePath) => {
const cleaned = stripFrontmatter(content).trim();
const match = cleaned.match(/^#\s+(.+)$/m);
return match?.[1]?.trim() || fallbackTitleFromPath(filePath);
};
+38
View File
@@ -0,0 +1,38 @@
/**
* Normalize a wiki slug for route/path construction.
* @param {string} slug
* @returns {string}
*/
const normalizeWikiSlug = (slug) =>
String(slug ?? '')
.replace(/\\/g, '/')
.replace(/^\/+|\/+$/g, '');
/**
* Return whether a slug represents the wiki index page.
* @param {string} slug
* @returns {boolean}
*/
export const isWikiIndexSlug = (slug) => normalizeWikiSlug(slug).toLowerCase() === 'index';
/**
* Convert a wiki slug to app route path.
* @param {string} slug
* @returns {string}
*/
export const toWikiRoute = (slug) => {
const normalized = normalizeWikiSlug(slug);
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki';
return `/wiki/${normalized}`;
};
/**
* Convert a wiki slug to its llms.txt endpoint path.
* @param {string} slug
* @returns {string}
*/
export const toWikiLlmsPath = (slug) => {
const normalized = normalizeWikiSlug(slug);
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki/llms.txt';
return `/wiki/${normalized}/llms.txt`;
};