ReviewOS

also looking at this

stacks/drone

fix: keep documentation search results under docs

#106
Merged chris wants to merge codex/docs-search-base-path into main
5 files +107 -1
bun.lockmodified+1-0
Changes to bun.lock
@@ -40,6 +40,7 @@
4040 },
4141 "patchedDependencies": {
4242 "tuf-js@6.0.0": "patches/tuf-js@6.0.0.patch",
43 "@stacksjs/bunpress@0.2.22": "patches/@stacksjs%2Fbunpress@0.2.22.patch",
4344 },
4445 "overrides": {
4546 "@stacksjs/bunpress": "0.2.22",
package.jsonmodified+2-1
Changes to package.json
@@ -84,6 +84,7 @@
8484 "@stacksjs/bunpress": "0.2.22"
8585 },
8686 "patchedDependencies": {
87 "tuf-js@6.0.0": "patches/tuf-js@6.0.0.patch"
87 "tuf-js@6.0.0": "patches/tuf-js@6.0.0.patch",
88 "@stacksjs/bunpress@0.2.22": "patches/@stacksjs%2Fbunpress@0.2.22.patch"
8889 }
8990}
patches/@stacksjs%2Fbunpress@0.2.22.patchadded+45-0
Changes to patches/@stacksjs%2Fbunpress@0.2.22.patch
@@ -0,0 +1,45 @@
1diff --git a/dist/chunk-8rsrw3sw.js b/dist/chunk-8rsrw3sw.js
2index 1155ca4aa2f63eac326bc3373afdecd8cfa8bfc3..faf2b12ba255f9cc8fcbe813abf9f07c77a7549d 100644
3--- a/dist/chunk-8rsrw3sw.js
4+++ b/dist/chunk-8rsrw3sw.js
5@@ -1,4 +1,5 @@
6 // @bun
7+import { getConfiguredBasePath as searchBasePath } from "./search-base-path.js";
8 import{ia as Q0,la as L,ma as Y0}from"./chunk-ynp2pgxy.js";import{loadConfig as z0}from"bunfig";var K0={docsDir:"./docs",outDir:"./dist",theme:"vitepress",nav:[{text:"Guide",link:"/install"},{text:"API",link:"/advanced#api-reference"},{text:"Examples",link:"/examples"}],plugins:[],markdown:{title:"BunPress Documentation",meta:{description:"Documentation built with BunPress",generator:"BunPress",viewport:"width=device-width, initial-scale=1.0"},sidebar:{"/":[{text:"Getting Started",items:[{text:"Introduction",link:"/"},{text:"Installation",link:"/install"},{text:"Quick Start",link:"/quick-start"},{text:"Usage",link:"/usage"},{text:"Configuration",link:"/config"}]},{text:"Core Features",items:[{text:"Features Overview",link:"/features"},{text:"Markdown Extensions",link:"/markdown-extensions"},{text:"Syntax Highlighting",link:"/syntax-highlighting"},{text:"Table of Contents",link:"/table-of-contents"},{text:"CLI Commands",link:"/cli"}]},{text:"SEO & Analytics",items:[{text:"SEO Features",link:"/seo"},{text:"Analytics (Fathom)",link:"/config#fathom-analytics-configuration"}]},{text:"Advanced",items:[{text:"Advanced Usage",link:"/advanced"},{text:"Examples",link:"/examples"},{text:"Best Practices",link:"/best-practices"}]},{text:"More",items:[{text:"Showcase",link:"/showcase"},{text:"Partners",link:"/partners"},{text:"License",link:"/license"},{text:"Postcardware",link:"/postcardware"}]}]},css:`
9 /* Additional BunPress customizations */
10 /* Note: Base styles are provided by the VitePress theme */
11@@ -1735,5 +1736,5 @@ else {
12 }
13 `}var{Glob:i,YAML:m0}=globalThis.Bun;import{join as u0}from"path";var U1="/search-index.json",O={include:["**/*.md"],exclude:["public/**","**/node_modules/**"],searchFields:["title","content","headings","keywords"],maxResults:10,minQueryLength:1,fuzzy:!1,fuzziness:1,stemmer:"none",maxContentLength:800,boost:{title:10,headings:5,content:1}};function X0(X){let Q=X?.options??{};return{...O,...Q,include:Q.include??O.include,exclude:Q.exclude??O.exclude,searchFields:Q.searchFields??O.searchFields,maxResults:Q.maxResults??X?.maxResults??O.maxResults,boost:{...O.boost,...Q.boost??{}}}}var a=[{key:"k",meta:!0,ctrl:!1,alt:!1,shift:!1},{key:"k",meta:!1,ctrl:!0,alt:!1,shift:!1},{key:"/",meta:!1,ctrl:!1,alt:!1,shift:!1}];function p0(X){if(!X)return a;let Q=(Array.isArray(X)?X:[X]).map((J)=>String(J).toLowerCase()),Z={meta:!1,ctrl:!1,alt:!1,shift:!1},$="";for(let J of Q)if(J==="meta"||J==="cmd"||J==="command")Z.meta=!0;else if(J==="ctrl"||J==="control")Z.ctrl=!0;else if(J==="alt"||J==="option")Z.alt=!0;else if(J==="shift")Z.shift=!0;else $=J;if(!$)return a;return[{key:$,...Z}]}function n(X){if(typeof X!=="function")return null;let Q=Function.prototype.toString.call(X);return Q.includes("[native code]")?null:Q}function I1(X,Q){let Z=X?.search??X?.markdown?.search,$=X0(Z),J=Z?.resultOptions??{};return{indexUrl:Q,placeholder:Z?.placeholder??"Search documentation",lazy:Z?.lazy!==!1,maxResults:$.maxResults,minQueryLength:$.minQueryLength,searchFields:$.searchFields,boost:{title:$.boost.title??10,headings:$.boost.headings??5,content:$.boost.content??1},fuzzy:$.fuzzy,fuzziness:$.fuzziness,stemmer:$.stemmer,shortcuts:{keyboard:Z?.keyboardShortcuts!==!1,keys:p0(Z?.shortcut)},result:{showDescription:J.showDescription!==!1,descriptionLength:J.descriptionLength??180,highlightMatches:J.highlightMatches!==!1,showPath:J.showPath!==!1},tokenizeSource:n(Z?.options?.tokenize),onSearchSource:n(Z?.onSearch),locale:null}}function t(X){return p(r(X)).replace(/^:::.*$/gm," ").replace(/^\s{0,3}>\s*\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/gim," ").replace(/<[^>]+>/g," ").replace(/!\[[^\]]*\]\([^)]*\)/g," ").replace(/\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/`([^`]+)`/g,"$1").replace(/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/g,"$1").replace(/^\s{0,3}>\s?/gm,"").replace(/^\s*[-*+]\s+/gm,"").replace(/^\s*\d+\.\s+/gm,"").replace(/^\s*\|?[\s:|-]+\|[\s:|-]*$/gm," ").replace(/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/gm," ").replace(/\|/g," ").replace(/\s+/g," ").trim()}function g0(X){let Q=X.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);if(!Q)return{body:X,data:{}};let Z={};try{Z=m0.parse(Q[1])??{}}catch{Z={}}return{body:X.slice(Q[0].length),data:Z}}function h0(X){let Q=X.replace(/\.md$/,"");if(Q==="index")return"/";if(Q.endsWith("/index"))return`/${Q.slice(0,-6)}`;return`/${Q}`}function d0(X,Q,Z){let{body:$,data:J}=g0(Q);if(J.search===!1)return[];if(J.layout==="home")return[];let V=typeof J.search==="object"&&J.search!==null?J.search:{},W=Array.isArray(V.keywords)?V.keywords.map(String):[],q=h0(X),j=$.split(`
14 `),G=$.match(/^#\s+(.+)$/m),K=V.title||J.title||(G?G[1].trim():q),B=[],z=new Set,Y={title:K,anchor:"",level:1,buffer:[]},M=()=>{let N=t(Y.buffer.join(`
15-`)).slice(0,Z.maxContentLength);if(!N&&!Y.title)return;let U={url:Y.anchor?`${q}#${Y.anchor}`:q,page:K,title:t(Y.title)||K,text:N,level:Y.level};if(W.length)U.keywords=W;B.push(U)},I=E();for(let N of j){let U=I(N)?null:N.match(/^(#{1,6})\s+(.+?)\s*$/);if(!U){Y.buffer.push(N);continue}let H=U[1].length,A=U[2].replace(/\s*\{#([\w-]+)\}\s*$/,""),R=U[2].match(/\{#([\w-]+)\}\s*$/),D=b(R?R[1]:T(v(A)),z);if(H===1){Y.buffer.push("");continue}M(),Y={title:A,anchor:D,level:H,buffer:[]}}return M(),B.filter((N)=>N.text||N.level>1)}function c0(X,Q){let{storeFields:Z}=Q;if(!Z||!Z.length)return X;let $=new Set(["url",...Z]);if(Q.searchFields.includes("title"))$.add("title");if(Q.searchFields.includes("content"))$.add("text");if(Q.searchFields.includes("headings"))$.add("page");if(Q.searchFields.includes("keywords"))$.add("keywords");return X.map((J)=>{let V={};for(let[W,q]of Object.entries(J))if($.has(W)&&q!==void 0)V[W]=q;return V})}function e(X){return X.split("#")[0]}function l0(X,Q){if(!Q.enabled)return{locale:Q.defaultLocale,page:X};let Z=X.split("/");if(Z.length>1&&Q.locales.includes(Z[0])&&Z[0]!==Q.defaultLocale)return{locale:Z[0],page:Z.slice(1).join("/")};let $=X.match(/^(.*)\.([A-Za-z-]+)(\.md)$/);if($&&Q.locales.includes($[2]))return{locale:$[2],page:`${$[1]}${$[3]}`};return{locale:Q.defaultLocale,page:X}}async function _1(X,Q){let Z=Q?.search??Q?.markdown?.search,$=X0(Z),J=Q0(Q),V=$.exclude.map((G)=>new i(G)),W=[],q=new Set,j=new Map;for(let G of J.locales)j.set(G,new Set);for(let G of $.include){let K=new i(G);for await(let B of K.scan(X)){let z=B.replace(/\\/g,"/");if(V.some((Y)=>Y.match(z)))continue;q.add(z)}}for(let G of[...q].sort())try{let K=await Bun.file(u0(X,G)).text(),{locale:B,page:z}=l0(G,J);for(let Y of d0(z,K,$)){if(J.enabled)Y.locale=B,Y.url=L(J,B,Y.url);W.push(Y),j.get(B)?.add(e(Y.url))}}catch{}if(J.enabled){let G=W.filter((K)=>K.locale===J.defaultLocale);for(let K of J.locales){if(K===J.defaultLocale)continue;let B=j.get(K)??new Set;for(let z of G){let Y=L(J,K,z.url);if(B.has(e(Y)))continue;W.push({...z,url:Y,locale:K})}}}return c0(W,$)}function Z0(X){let Q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0"};return X.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi,(Z,$)=>{if($[0]==="#"){let J=$[1]==="x"||$[1]==="X"?Number.parseInt($.slice(2),16):Number.parseInt($.slice(1),10);return Number.isFinite(J)&&J>0&&J<=1114111?String.fromCodePoint(J):Z}return Q[$.toLowerCase()]??Z})}function $0(X){return String(X??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}var{Glob:r0}=globalThis.Bun;import{dirname as s0,isAbsolute as J0,join as o0,relative as V0,resolve as C,sep as W0}from"path";async function L1(X){let Q=[];for await(let Z of new r0("**/*.md").scan({cwd:X,onlyFiles:!0}))Q.push(o0(X,Z));return Q}function q0(X){return X.split("/").map((Q)=>encodeURIComponent(Q)).join("/")}function x(X){let Q=X==="index"?"":X.replace(/\/index$/,"/");return`/${q0(Q)}`}function k(X){return X.split("/").map((Z)=>encodeURIComponent(decodeURIComponent(Z))).join("/").replace(/\/+$/,"")||"/"}function C1(X,Q,Z){let $=C(Q),J={...Z,locales:[...Z.locales]},V=new Set(X.map((z)=>C(z))),W=new Map,q=new Set;for(let z of V){let Y=V0($,z).split(W0).join("/").replace(/\.md$/,"");if(Y===".."||Y.startsWith("../")||J0(Y))throw Error("Markdown source is outside the documentation root");let M=J.defaultLocale,I=!1;if(J.enabled){let N=Y.split("/");if(N.length>1&&J.locales.includes(N[0])&&N[0]!==J.defaultLocale)M=N.shift(),I=!0,Y=N.join("/");else{let U=Y.match(/^(.*)\.([A-Za-z-]+)$/);if(U&&J.locales.includes(U[2]))Y=U[1],M=U[2],I=!0}}W.set(z,{path:Y,locale:M,explicitLocale:I}),q.add(Y)}let j=[],G=new Map,K=new Map,B=new Map;for(let z of J.locales)for(let Y of q){let M=J.enabled?Y0(J,$,z,`/${Y}`).map((U)=>C(U)).find((U)=>V.has(U)):C($,`${Y}.md`);if(!M||!V.has(M))continue;let I=Object.freeze({sourceFile:M,relativePath:Y,locale:z}),N=`${z}\x00${k(x(Y))}`;if(K.has(N))throw Error(`Documentation pages share an output route: ${x(Y)}`);K.set(N,I),G.set(`${z}\x00${Y}`,I),B.set(`${z}\x00${k(`/${q0(Y)}`)}`,I),j.push(I)}return Object.freeze({pages:Object.freeze(j),findPage(z,Y){let M=`${z}\x00${k(Y)}`;return K.get(M)??B.get(M)},resolveLink(z,Y){if(!Y||/^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(Y))return Y;let M=Y.search(/[?#]/),I=M===-1?Y:Y.slice(0,M),N=M===-1?"":Y.slice(M),U=I.replace(/%([\da-f]{2})/gi,(i0,j0)=>String.fromCharCode(Number.parseInt(j0,16)));if(!/\.md$/i.test(U))return Y;let H=decodeURIComponent(I),A=C(s0(z.sourceFile),H),R=V0($,A);if(R===".."||R.startsWith(`..${W0}`)||J0(R))return Y;let D=W.get(A);if(!D)throw Error(`Markdown link has no documentation source: ${I}`);let f=D.explicitLocale?D.locale:z.locale,m=G.get(`${f}\x00${D.path}`);if(!m)throw Error(`Markdown link has no rendered page: ${I}`);return`${L(J,f,x(m.relativePath))}${N}`}})}async function E1(X,Q,Z=new Map){let $=[Q.page],J=new Set(Z.keys()),V=await new HTMLRewriter().onDocument({comments(W){let q=Z.get(W.text);if(q===void 0)return;if(!J.delete(W.text))throw Error("Markdown include source boundary was duplicated");if(q===null){if($.length===1)throw Error("Markdown include source boundaries are unbalanced");$.pop()}else $.push(q);W.remove()}}).on("a[href]",{element(W){let q=Z0(W.getAttribute("href")),j=Q.catalog.resolveLink($[$.length-1],q);if(j!==q)W.setAttribute("href",$0(j))}}).transform(new Response(X)).text();if(J.size||$.length!==1)throw Error("Markdown include source boundaries did not survive rendering");return V}
16+`)).slice(0,Z.maxContentLength);if(!N&&!Y.title)return;let U={url:Y.anchor?`${q}#${Y.anchor}`:q,page:K,title:t(Y.title)||K,text:N,level:Y.level};if(W.length)U.keywords=W;B.push(U)},I=E();for(let N of j){let U=I(N)?null:N.match(/^(#{1,6})\s+(.+?)\s*$/);if(!U){Y.buffer.push(N);continue}let H=U[1].length,A=U[2].replace(/\s*\{#([\w-]+)\}\s*$/,""),R=U[2].match(/\{#([\w-]+)\}\s*$/),D=b(R?R[1]:T(v(A)),z);if(H===1){Y.buffer.push("");continue}M(),Y={title:A,anchor:D,level:H,buffer:[]}}return M(),B.filter((N)=>N.text||N.level>1)}function c0(X,Q){let{storeFields:Z}=Q;if(!Z||!Z.length)return X;let $=new Set(["url",...Z]);if(Q.searchFields.includes("title"))$.add("title");if(Q.searchFields.includes("content"))$.add("text");if(Q.searchFields.includes("headings"))$.add("page");if(Q.searchFields.includes("keywords"))$.add("keywords");return X.map((J)=>{let V={};for(let[W,q]of Object.entries(J))if($.has(W)&&q!==void 0)V[W]=q;return V})}function e(X){return X.split("#")[0]}function l0(X,Q){if(!Q.enabled)return{locale:Q.defaultLocale,page:X};let Z=X.split("/");if(Z.length>1&&Q.locales.includes(Z[0])&&Z[0]!==Q.defaultLocale)return{locale:Z[0],page:Z.slice(1).join("/")};let $=X.match(/^(.*)\.([A-Za-z-]+)(\.md)$/);if($&&Q.locales.includes($[2]))return{locale:$[2],page:`${$[1]}${$[3]}`};return{locale:Q.defaultLocale,page:X}}async function _1(X,Q){let Z=Q?.search??Q?.markdown?.search,$=X0(Z),J=Q0(Q),V=$.exclude.map((G)=>new i(G)),W=[],q=new Set,j=new Map;for(let G of J.locales)j.set(G,new Set);for(let G of $.include){let K=new i(G);for await(let B of K.scan(X)){let z=B.replace(/\\/g,"/");if(V.some((Y)=>Y.match(z)))continue;q.add(z)}}for(let G of[...q].sort())try{let K=await Bun.file(u0(X,G)).text(),{locale:B,page:z}=l0(G,J);for(let Y of d0(z,K,$)){if(J.enabled)Y.locale=B,Y.url=L(J,B,Y.url);W.push(Y),j.get(B)?.add(e(Y.url))}}catch{}if(J.enabled){let G=W.filter((K)=>K.locale===J.defaultLocale);for(let K of J.locales){if(K===J.defaultLocale)continue;let B=j.get(K)??new Set;for(let z of G){let Y=L(J,K,z.url);if(B.has(e(Y)))continue;W.push({...z,url:Y,locale:K})}}}let searchPrefix=Q?searchBasePath(Q):"";if(searchPrefix)for(let record of W)record.url=searchPrefix+record.url;return c0(W,$)}function Z0(X){let Q={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0"};return X.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi,(Z,$)=>{if($[0]==="#"){let J=$[1]==="x"||$[1]==="X"?Number.parseInt($.slice(2),16):Number.parseInt($.slice(1),10);return Number.isFinite(J)&&J>0&&J<=1114111?String.fromCodePoint(J):Z}return Q[$.toLowerCase()]??Z})}function $0(X){return String(X??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}var{Glob:r0}=globalThis.Bun;import{dirname as s0,isAbsolute as J0,join as o0,relative as V0,resolve as C,sep as W0}from"path";async function L1(X){let Q=[];for await(let Z of new r0("**/*.md").scan({cwd:X,onlyFiles:!0}))Q.push(o0(X,Z));return Q}function q0(X){return X.split("/").map((Q)=>encodeURIComponent(Q)).join("/")}function x(X){let Q=X==="index"?"":X.replace(/\/index$/,"/");return`/${q0(Q)}`}function k(X){return X.split("/").map((Z)=>encodeURIComponent(decodeURIComponent(Z))).join("/").replace(/\/+$/,"")||"/"}function C1(X,Q,Z){let $=C(Q),J={...Z,locales:[...Z.locales]},V=new Set(X.map((z)=>C(z))),W=new Map,q=new Set;for(let z of V){let Y=V0($,z).split(W0).join("/").replace(/\.md$/,"");if(Y===".."||Y.startsWith("../")||J0(Y))throw Error("Markdown source is outside the documentation root");let M=J.defaultLocale,I=!1;if(J.enabled){let N=Y.split("/");if(N.length>1&&J.locales.includes(N[0])&&N[0]!==J.defaultLocale)M=N.shift(),I=!0,Y=N.join("/");else{let U=Y.match(/^(.*)\.([A-Za-z-]+)$/);if(U&&J.locales.includes(U[2]))Y=U[1],M=U[2],I=!0}}W.set(z,{path:Y,locale:M,explicitLocale:I}),q.add(Y)}let j=[],G=new Map,K=new Map,B=new Map;for(let z of J.locales)for(let Y of q){let M=J.enabled?Y0(J,$,z,`/${Y}`).map((U)=>C(U)).find((U)=>V.has(U)):C($,`${Y}.md`);if(!M||!V.has(M))continue;let I=Object.freeze({sourceFile:M,relativePath:Y,locale:z}),N=`${z}\x00${k(x(Y))}`;if(K.has(N))throw Error(`Documentation pages share an output route: ${x(Y)}`);K.set(N,I),G.set(`${z}\x00${Y}`,I),B.set(`${z}\x00${k(`/${q0(Y)}`)}`,I),j.push(I)}return Object.freeze({pages:Object.freeze(j),findPage(z,Y){let M=`${z}\x00${k(Y)}`;return K.get(M)??B.get(M)},resolveLink(z,Y){if(!Y||/^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(Y))return Y;let M=Y.search(/[?#]/),I=M===-1?Y:Y.slice(0,M),N=M===-1?"":Y.slice(M),U=I.replace(/%([\da-f]{2})/gi,(i0,j0)=>String.fromCharCode(Number.parseInt(j0,16)));if(!/\.md$/i.test(U))return Y;let H=decodeURIComponent(I),A=C(s0(z.sourceFile),H),R=V0($,A);if(R===".."||R.startsWith(`..${W0}`)||J0(R))return Y;let D=W.get(A);if(!D)throw Error(`Markdown link has no documentation source: ${I}`);let f=D.explicitLocale?D.locale:z.locale,m=G.get(`${f}\x00${D.path}`);if(!m)throw Error(`Markdown link has no rendered page: ${I}`);return`${L(J,f,x(m.relativePath))}${N}`}})}async function E1(X,Q,Z=new Map){let $=[Q.page],J=new Set(Z.keys()),V=await new HTMLRewriter().onDocument({comments(W){let q=Z.get(W.text);if(q===void 0)return;if(!J.delete(W.text))throw Error("Markdown include source boundary was duplicated");if(q===null){if($.length===1)throw Error("Markdown include source boundaries are unbalanced");$.pop()}else $.push(q);W.remove()}}).on("a[href]",{element(W){let q=Z0(W.getAttribute("href")),j=Q.catalog.resolveLink($[$.length-1],q);if(j!==q)W.setAttribute("href",$0(j))}}).transform(new Response(X)).text();if(J.size||$.length!==1)throw Error("Markdown include source boundaries did not survive rendering");return V}
17 export{K0 as A,G0 as B,M0 as C,n0 as D,p as E,e0 as F,X1 as G,Z1 as H,S0 as I,v as J,T as K,b as L,s as M,v0 as N,T0 as O,b0 as P,w as Q,o as R,x0 as S,k0 as T,f0 as U,J1 as V,V1 as W,W1 as X,Y1 as Y,q1 as Z,U1 as _,I1 as $,_1 as aa,Z0 as ba,$0 as ca,L1 as da,C1 as ea,E1 as fa};
18diff --git a/node_modules/@stacksjs/bunpress/dist/search-base-path.js b/dist/search-base-path.js
19new file mode 100644
20index 0000000000000000000000000000000000000000..017c550ed825d5704524923bd510147bfaba1e09
21--- /dev/null
22+++ b/dist/search-base-path.js
23@@ -0,0 +1,22 @@
24+// Deployment path compatibility patch for BunPress 0.2.22.
25+export function getConfiguredBasePath(config) {
26+ if (config.basePath) {
27+ const normalized = config.basePath.trim().replace(/\/+$/, '')
28+ if (!normalized || normalized === '/')
29+ return ''
30+ return normalized.startsWith('/') ? normalized : `/${normalized}`
31+ }
32+
33+ const baseUrl = (config.sitemap?.baseUrl || config.url || '').replace(/\/$/, '')
34+ if (!baseUrl) {
35+ return ''
36+ }
37+
38+ try {
39+ const pathname = new URL(baseUrl).pathname.replace(/\/+$/, '')
40+ return pathname === '/' ? '' : pathname
41+ }
42+ catch {
43+ return ''
44+ }
45+}
patches/README.mdmodified+20-0
Changes to patches/README.md
@@ -33,3 +33,23 @@ carry a patch onto another version.
3333
3434See `docs/runtime-updates.md` for the trust and clock limits. The extensions are
3535maintained here and are not claimed to be unmodified upstream options.
36
37## BunPress 0.2.22
38
39`@stacksjs%2Fbunpress@0.2.22.patch` keeps generated local search results under
40BunPress's configured deployment path. The native page renderer already uses
41`/docs`, but 0.2.22's search index contained origin-root paths, so selecting a
42result navigated to a 404. The patch applies the same base-path resolution after
43locale translation and fallback selection, preserving anchors and root-hosted
44sites. A real source directory named `docs` still receives the deployment prefix.
45
46The patch changes the native index builder and adds a small helper matching the renderer's path resolution;
47it adds no browser redirect or application search implementation. The installed
48package retains BunPress's MIT license. Regression coverage lives in
49`tests/unit/docs-search.test.ts`. Rebuild native docs and verify an actual search
50selection after upgrading or removing this patch. Prepare installed changes with
51`bun patch @stacksjs/bunpress@0.2.22` and retain the generated Bun patch and lockfile.
52
53The shared engine fix is [BunPress PR 91](https://github.com/stacksjs/bunpress/pull/91),
54source commit `096004cc6e7b4800bfd1e90993866c9011ca706d`. Remove the compatibility
55patch only after upgrading to a release that includes it and repeating the checks.
tests/unit/docs-search.test.tsadded+39-0
Changes to tests/unit/docs-search.test.ts
@@ -0,0 +1,39 @@
1import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
3import { tmpdir } from 'node:os'
4import { join } from 'node:path'
5import { buildSearchIndex } from '@stacksjs/bunpress'
6import type { BunPressConfig } from '@stacksjs/bunpress'
7
8let root: string
9beforeAll(async () => {
10 root = await mkdtemp(join(tmpdir(), 'bunpress-search-base-'))
11 await mkdir(join(root, 'docs'))
12 await writeFile(join(root, 'index.md'), '# Home\n\nStart here.\n')
13 await writeFile(join(root, 'guide.md'), '# Guide\n\nLead.\n\n## Details\n\nDetails.\n')
14 await writeFile(join(root, 'docs/repeat.md'), '# Repeat\n\nA real nested docs directory.\n')
15})
16afterAll(async () => { if (root) await rm(root, { recursive: true, force: true }) })
17
18describe('search result deployment paths', () => {
19 const urls = async (config: Partial<BunPressConfig>) => (await buildSearchIndex(root, { verbose: false, ...config } as BunPressConfig)).map(record => record.url)
20 it('keeps page, home and anchored results under an explicit base path', async () => {
21 expect(await urls({ basePath: '/docs' })).toEqual(['/docs/docs/repeat', '/docs/guide', '/docs/guide#details', '/docs/'])
22 })
23 it('normalizes a nested deployment path using the renderer contract', async () => {
24 expect(await urls({ basePath: ' manual/reference/// ' })).toEqual(['/manual/reference/docs/repeat', '/manual/reference/guide', '/manual/reference/guide#details', '/manual/reference/'])
25 })
26 it('uses the configured site URL path when no explicit base path exists', async () => {
27 expect(await urls({ sitemap: { baseUrl: 'https://example.test/help/docs/' } })).toEqual(['/help/docs/docs/repeat', '/help/docs/guide', '/help/docs/guide#details', '/help/docs/'])
28 })
29 it('prefixes locale fallback only after translation selection', async () => {
30 const index = await buildSearchIndex(root, { verbose: false, basePath: '/docs', i18n: { locales: ['en', 'es'], defaultLocale: 'en' } } as BunPressConfig)
31 expect(index.filter(record => record.locale === 'en').map(record => record.url)).toEqual(['/docs/docs/repeat', '/docs/guide', '/docs/guide#details', '/docs/'])
32 expect(index.filter(record => record.locale === 'es').map(record => record.url)).toEqual(['/docs/es/docs/repeat', '/docs/es/guide', '/docs/es/guide#details', '/docs/es'])
33 })
34 it('preserves root deployments and an explicit root override', async () => {
35 const expected = ['/docs/repeat', '/guide', '/guide#details', '/']
36 expect(await urls({})).toEqual(expected)
37 expect(await urls({ basePath: '/', sitemap: { baseUrl: 'https://example.test/help' } })).toEqual(expected)
38 })
39})