From e7804f9b08118d469f5df3c11b746e6ab9cd5a78 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Fri, 12 Jun 2026 10:53:30 -0700 Subject: [PATCH 1/2] feat: make docs builder reusable for ClawHub --- scripts/docs-site/assets.mjs | 8 +- scripts/docs-site/build.mjs | 184 +++++++++++++++++++------ scripts/docs-site/og-card-template.mjs | 25 +++- 3 files changed, 167 insertions(+), 50 deletions(-) diff --git a/scripts/docs-site/assets.mjs b/scripts/docs-site/assets.mjs index 43c6a06e19..333376d94b 100644 --- a/scripts/docs-site/assets.mjs +++ b/scripts/docs-site/assets.mjs @@ -32,7 +32,9 @@ export function siteCss() { `; } -export function siteJs() { +export function siteJs(options = {}) { + const authUrl = JSON.stringify(options.authUrl ?? "https://hub.openclaw.ai/docs/auth"); + const assistantName = JSON.stringify(options.assistantName ?? "OpenClaw"); return ` const root=document.documentElement;const saved=localStorage.getItem("theme");root.dataset.theme=saved||"dark"; const modal=document.querySelector(".search-modal");const input=document.querySelector("[data-search-input]");const results=document.querySelector("[data-search-results]");const moltySearch=document.querySelector("[data-search-molty]");const moltySearchPrefix=document.querySelector("[data-search-molty-prefix]");const moltySearchTerm=document.querySelector("[data-search-molty-term]");let pagefindReady;let pagefindLoadAttempt=0;let navigating=false;let searchItems=[];let searchActiveIndex=-1; @@ -101,7 +103,7 @@ function clearChat(){const log=document.querySelector("[data-chat-log]");if(!log function isAuthFetchError(err){const msg=String(err?.message||err||"").toLowerCase();return msg==="auth_required"||msg.includes("load failed")||msg.includes("failed to fetch")||msg.includes("networkerror")} function chatFailureMessage(message){const msg=String(message||"").trim();const serverError=msg.toLowerCase().startsWith("docs agent returned 5");if(serverError)return"Molty is temporarily unavailable. Try again in a moment.";return msg||"Docs agent failed."} const pendingMoltyQuestionKey="openclaw-docs-pending-molty-question"; -function chatSignInUrl(){const url=new URL("https://hub.openclaw.ai/docs/auth");const back=new URL(location.href);if(localStorage.getItem(pendingMoltyQuestionKey))back.searchParams.set("molty_resume","1");url.searchParams.set("return_to",back.href);return url.href} +function chatSignInUrl(){const url=new URL(${authUrl},location.href);const back=new URL(location.href);if(localStorage.getItem(pendingMoltyQuestionKey))back.searchParams.set("molty_resume","1");url.searchParams.set("return_to",back.href);return url.href} function initChat(){ const chat=document.querySelector("[data-docs-chat]"); const auth=document.querySelector("[data-chat-auth]"); @@ -124,7 +126,7 @@ const ensureAuth=async()=>{if(authState==="ready")return true;if(authPromise)ret const chatBaseRight=()=>window.matchMedia("(max-width:820px)").matches?14:18;let chatPanelRight=chatBaseRight();const chatTargetWidth=expanded=>Math.min(expanded?760:420,window.innerWidth-28);const clampChatRight=(right,width=chat.getBoundingClientRect().width)=>{const pad=14;return Math.min(Math.max(pad,right),Math.max(pad,window.innerWidth-width-pad))};const applyChatRight=(right,width)=>{chatPanelRight=clampChatRight(right,width);chat.style.left="";chat.style.right=chatPanelRight+"px"};if(head){let dragStart=null;head.addEventListener("pointerdown",e=>{if(e.button!==0||e.target.closest("button,a,input,textarea"))return;const rect=chat.getBoundingClientRect();dragStart={pointerX:e.clientX,left:rect.left,width:rect.width,dragging:false};head.setPointerCapture?.(e.pointerId)});head.addEventListener("pointermove",e=>{if(!dragStart)return;const delta=e.clientX-dragStart.pointerX;if(!dragStart.dragging&&Math.abs(delta)<6)return;if(!dragStart.dragging){dragStart.dragging=true;chat.classList.add("dragging")}const left=Math.min(Math.max(14,dragStart.left+delta),Math.max(14,window.innerWidth-dragStart.width-14));applyChatRight(window.innerWidth-left-dragStart.width,dragStart.width);e.preventDefault()});const stopDrag=e=>{if(!dragStart)return;if(dragStart.dragging)applyChatRight(chatPanelRight,dragStart.width);chat.classList.remove("dragging");head.releasePointerCapture?.(e.pointerId);dragStart=null};head.addEventListener("pointerup",stopDrag);head.addEventListener("pointercancel",stopDrag);head.addEventListener("dblclick",e=>{if(e.target.closest("button,a,input,textarea"))return;setExpanded(!chat.classList.contains("expanded"))})} const maximizeIcon='';const collapseIcon='';const keepChatInViewport=()=>{if(!chat.classList.contains("open")){chatPanelRight=chatBaseRight();chat.style.left="";chat.style.right=chatPanelRight+"px";return}applyChatRight(chatPanelRight,chat.getBoundingClientRect().width)};const setExpanded=expanded=>{const targetWidth=chatTargetWidth(expanded);applyChatRight(chatPanelRight,targetWidth);chat.classList.toggle("expanded",expanded);if(maximize){maximize.setAttribute("aria-pressed",String(expanded));maximize.setAttribute("aria-label",expanded?"Restore docs assistant size":"Maximize docs assistant");maximize.innerHTML=expanded?collapseIcon:maximizeIcon}};window.addEventListener("resize",()=>keepChatInViewport()); let resetChatTimer=0;const resetChatLauncherPosition=()=>{chatPanelRight=chatBaseRight();chat.style.left="";chat.style.right=chatPanelRight+"px";chat.classList.remove("closing")};const setOpen=open=>{clearTimeout(resetChatTimer);chat.classList.toggle("open",open);if(open){chat.classList.remove("closing");applyChatRight(chatPanelRight,chat.getBoundingClientRect().width)}else{chat.classList.add("closing");resetChatTimer=setTimeout(resetChatLauncherPosition,220)}panel?.toggleAttribute("inert",!open);panel?.setAttribute("aria-hidden",String(!open));document.querySelector("[data-chat-toggle]")?.setAttribute("aria-expanded",String(open));if(open)ensureAuth().then(ok=>{if(ok)setTimeout(()=>input.focus(),0)})}; -const chatText=()=>[...document.querySelectorAll(".docs-chat-message")].filter(item=>!item.classList.contains("thinking")).map(item=>(item.classList.contains("user")?"You: ":"OpenClaw: ")+item.innerText.trim()).filter(Boolean).join("\\n\\n"); +const chatText=()=>[...document.querySelectorAll(".docs-chat-message")].filter(item=>!item.classList.contains("thinking")).map(item=>(item.classList.contains("user")?"You: ":${assistantName}+": ")+item.innerText.trim()).filter(Boolean).join("\\n\\n"); const chatErrorMessage=async res=>{const fallback="Docs agent returned "+res.status;if(res.status>=500)return fallback;try{const data=await res.clone().json();return typeof data?.error==="string"&&data.error.trim()?data.error.trim():fallback}catch{return fallback}}; const sendChatMessage=async(message,{echoUser=true}={})=>{const api=window.OPENCLAW_DOCS_CHAT_API;if(!api||!message)return;if(!await ensureAuth())return;lastMessage=message;setRetryEnabled();if(echoUser)addChatMessage("user",message);setChatClearVisible(true);const reply=addChatThinking();if(submit)submit.disabled=true;if(retry)retry.disabled=true;try{const res=await fetch(api,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",redirect:"manual",body:JSON.stringify({message,retrieval:"auto",confidenceThreshold:.3})});if(res.type==="opaqueredirect"||res.redirected||res.status===0||res.status===401||res.status===403)throw new Error("AUTH_REQUIRED");if(!res.ok)throw new Error(await chatErrorMessage(res));if(!res.body)throw new Error("Docs agent did not stream a response");const reader=res.body.getReader();const decoder=new TextDecoder();let raw="";while(true){const {done,value}=await reader.read();if(done)break;raw+=decoder.decode(value,{stream:true});if(reply&&raw.trim()){if(reply.classList.contains("thinking"))reply.classList.remove("thinking");reply.innerHTML=renderChatText(raw);reply.parentElement.scrollTop=reply.parentElement.scrollHeight}}if(!raw&&reply){reply.classList.remove("thinking");reply.innerHTML="

No response.

"}}catch(err){if(reply){const msg=isAuthFetchError(err)?"[Verify with GitHub]("+chatSignInUrl()+"), then ask again.":chatFailureMessage(err?.message);reply.className="docs-chat-message error";reply.innerHTML=renderChatText(msg);if(isAuthFetchError(err))setAuthState("required")}}finally{if(submit)submit.disabled=authState!=="ready";setRetryEnabled();if(authState==="ready")input.focus()}}; const askMoltyQuestion=async message=>{const question=String(message||"").trim();setOpen(true);if(!question){localStorage.removeItem(pendingMoltyQuestionKey);await ensureAuth();return}localStorage.setItem(pendingMoltyQuestionKey,question);if(!await ensureAuth())return;localStorage.removeItem(pendingMoltyQuestionKey);await sendChatMessage(question)}; diff --git a/scripts/docs-site/build.mjs b/scripts/docs-site/build.mjs index d91d2f9fd4..06b907e6c4 100644 --- a/scripts/docs-site/build.mjs +++ b/scripts/docs-site/build.mjs @@ -26,12 +26,39 @@ const canonicalOrigin = (process.env.DOCS_SITE_CANONICAL_ORIGIN ?? (process.env.DOCS_SITE_CNAME ? `https://${process.env.DOCS_SITE_CNAME}` : "https://docs.openclaw.ai")) .replace(/\/$/, ""); const feedbackIssueRepository = normalizeRepository(process.env.DOCS_FEEDBACK_ISSUE_REPO ?? "openclaw/openclaw"); +const sourceRepositoryUrl = normalizeRepositoryUrl( + process.env.DOCS_SOURCE_REPO_URL ?? sourceMetadata.repository ?? "https://github.com/openclaw/openclaw", +); const llmsFullAvailable = process.env.DOCS_SITE_LLMS_FULL_AVAILABLE === "1"; -const ogImagePath = "/og-card.png"; +const faviconPath = config.favicon ?? config.logo?.dark ?? config.logo?.light ?? "/assets/pixel-lobster.svg"; +const logoPath = config.logo?.dark ?? config.logo?.light ?? config.favicon ?? "/assets/pixel-lobster.svg"; +const ogImagePath = config.og?.image ?? "/og-card.png"; +const siteName = config.name || "OpenClaw"; +const siteDescription = config.description || `${siteName} documentation.`; +const siteThemeColor = config.colors?.primary ?? "#FF5A36"; +const siteChatName = config.chat?.name ?? "Molty"; +const siteChatLabel = config.chat?.label ?? `${siteName} docs assistant`; +const siteChatButtonLabel = config.chat?.buttonLabel ?? `Ask ${siteChatName}`; +const siteSearchPlaceholder = config.search?.placeholder ?? "Search commands, channels, config..."; +const siteSearchSuggestions = Array.isArray(config.search?.suggestions) + ? config.search.suggestions + : ["Install OpenClaw", "Set up Telegram", "Fix Gateway", "Build a plugin"]; +const defaultSiteHeaderLinks = [ + { label: "GitHub", href: "https://github.com/openclaw/openclaw", icon: "github" }, + { label: "Releases", href: "https://github.com/openclaw/openclaw/releases", icon: "package" }, + { label: "Discord", href: "https://discord.com/invite/clawd", icon: "discord" }, +]; +const siteHeaderLinks = Array.isArray(config.navbar?.links) + ? config.navbar.links + : Array.isArray(config.headerLinks) ? config.headerLinks : defaultSiteHeaderLinks; const renderedPageOgCards = new Set(); const chatApiUrl = process.env.DOCS_SITE_CHAT_API_URL ?? "/ask-molty/api/chat"; const shellCss = siteCss(); -const shellJs = siteJs(); +const shellJs = siteJs({ + assistantName: siteChatName, + authUrl: process.env.DOCS_SITE_CHAT_AUTH_URL ?? config.chat?.authUrl, + repositoryUrl: sourceRepositoryUrl, +}); const defaultShellAssetVersion = createHash("sha256") .update(shellCss) .update("\0") @@ -263,7 +290,7 @@ function writePage(page) { const activeTab = activeTabTitle(nav, page.slug); const prev = activeIndex > 0 ? flat[activeIndex - 1] : null; const next = activeIndex >= 0 && activeIndex < flat.length - 1 ? flat[activeIndex + 1] : null; - const html = rewriteInternalUrls(renderMdxish(expandSnippets(page.body, page.file), md), page.locale); + const html = rewriteInternalUrls(renderMdxish(expandSnippets(page.body, page.file), md), page); const toc = tableOfContents(html); const outPath = path.join(outDir, pageRoute(page).replace(/^\//, ""), "index.html"); fs.mkdirSync(path.dirname(outPath), { recursive: true }); @@ -308,8 +335,8 @@ ${canonicalUrl ? ` - - + + @@ -353,9 +380,9 @@ function siteHeader(page, nav, activeTab) { }).join(""); return `