mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-10-30 18:22:59 +00:00
The site was on Cloudflare Pages as a direct upload — no git connection, last deployed 2026-03-28 — while a GitHub Pages workflow ran on every push against a repo that has Pages disabled (the API 404s). Two deploy paths, neither of which shipped what main said. One way now: a Dockerfile builds the static export and serves it with ghcr.io/hanzoai/static, exactly like every other Hanzo static site, on our own runners and our own ingress. Along the way, drop what those two vendors left behind: - output:'export' was gated on GITHUB_ACTIONS || CF_PAGES, so a build anywhere else silently produced no export at all. The site is a static export wherever it is built. - basePath pointed at a '/react-sdk' GitHub Pages subdirectory that is not this project. - highlight-code keyed off GITHUB_ACTIONS to pick server vs client highlighting; it means 'is this a production export', so it says that. - pages/api/components returned a JSON blob and 404s in production, because a static export cannot serve an API route. The live index is the generated /api/registry/components.json.
82 lines
2.2 KiB
TypeScript
82 lines
2.2 KiB
TypeScript
// Removed "use server" for static export compatibility
|
|
import { promises as fs } from "fs"
|
|
import path from "path"
|
|
import type { Highlighter } from "shiki"
|
|
|
|
// Highlight on the server while developing. A production build is a static
|
|
// export, which highlights on the client instead — so the build stays cheap.
|
|
const highlightCodeEnabled = process.env.NODE_ENV === "development"
|
|
|
|
// Singleton highlighter instance to prevent memory leaks
|
|
let highlighterInstance: Highlighter | null = null
|
|
let highlighterPromise: Promise<Highlighter> | null = null
|
|
|
|
async function getHighlighter(): Promise<Highlighter> {
|
|
if (highlighterInstance) {
|
|
return highlighterInstance
|
|
}
|
|
|
|
if (highlighterPromise) {
|
|
return highlighterPromise
|
|
}
|
|
|
|
highlighterPromise = (async () => {
|
|
const { createHighlighter } = await import("shiki")
|
|
|
|
const editorTheme = await fs.readFile(
|
|
path.join(process.cwd(), "lib/themes/dark.json"),
|
|
"utf-8"
|
|
)
|
|
|
|
const highlighter = await createHighlighter({
|
|
langs: ["typescript"],
|
|
themes: [JSON.parse(editorTheme)],
|
|
})
|
|
|
|
highlighterInstance = highlighter
|
|
return highlighter
|
|
})()
|
|
|
|
return highlighterPromise
|
|
}
|
|
|
|
export async function highlightCode(code: string) {
|
|
if (!highlightCodeEnabled) {
|
|
// Return code wrapped in basic HTML for static export
|
|
return `<pre><code class="language-typescript">${escapeHtml(code)}</code></pre>`
|
|
}
|
|
|
|
try {
|
|
const highlighter = await getHighlighter()
|
|
|
|
const html = await highlighter.codeToHtml(code, {
|
|
lang: "typescript",
|
|
theme: "Lambda Studio — Blackout",
|
|
})
|
|
|
|
return html
|
|
} catch (error) {
|
|
console.error("Syntax highlighting failed:", error)
|
|
return `<pre><code class="language-typescript">${escapeHtml(code)}</code></pre>`
|
|
}
|
|
}
|
|
|
|
// Clean up highlighter on process exit (for server)
|
|
if (typeof process !== "undefined") {
|
|
process.on("exit", () => {
|
|
if (highlighterInstance) {
|
|
highlighterInstance.dispose()
|
|
highlighterInstance = null
|
|
}
|
|
})
|
|
}
|
|
|
|
function escapeHtml(unsafe: string): string {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
}
|