@@ -13,6 +13,7 @@ import starlightImageZoom from 'starlight-image-zoom';
1313import starlightGitHubAlerts from 'starlight-github-alerts' ;
1414import { fileURLToPath } from 'node:url' ;
1515import { remarkLinkRewrite } from './src/plugins/remark-link-rewrite.ts' ;
16+ import { latestProductVersion } from './src/versionUtils.ts' ;
1617
1718// Read per-product versions from .versions JSON (written by src/build/integration.ts).
1819// Format: { "owner/repo": { "repo": "...", "branch": "...", "versions": [...], "latestTag": "..." } }
@@ -33,19 +34,24 @@ const productVersions = Object.fromEntries(
3334 return [ p . id , available ] ;
3435 } )
3536) ;
36- const productLatestTags = Object . fromEntries (
37+ const productLatestVersions = Object . fromEntries (
3738 PRODUCTS . flatMap ( p => {
38- const tag = versionsByRepo [ p . repo ] ?. latestTag ;
39- return tag ? [ [ p . id , tag ] ] : [ ] ;
39+ const latest = latestProductVersion ( p , versionsByRepo ) ;
40+ return latest ? [ [ p . id , latest ] ] : [ ] ;
4041 } )
4142) ;
42-
4343// Build remark-link-rewrite options from product configs.
4444// versionedSections provides per-version overrides for archived docs whose
4545// sidebarOrder differs from the current product config.
4646const linkRewriteProducts = PRODUCTS . map ( p => ( {
4747 contentDir : p . contentDir ,
48+ channel : p . latestSource ,
4849 sections : p . sidebarOrder . map ( e => typeof e === 'string' ? e : e . dir ) ,
50+ latestPrefix : p . latestSource
51+ ? productLatestVersions [ p . id ]
52+ ? `/${ p . contentDir } /${ productLatestVersions [ p . id ] . slug } `
53+ : `/${ p . contentDir } /${ p . latestSource } `
54+ : `/${ p . contentDir } ` ,
4955 versionedSections : Object . fromEntries (
5056 ( productVersions [ p . id ] ?? [ ] ) . map ( v => {
5157 const verSidebarOrder = loadVersionSidebarOrder ( p . repo , v . slug ) ?? p . sidebarOrder ;
@@ -83,13 +89,62 @@ function loadVersionSidebarOrder(repo, verSlug) {
8389 }
8490}
8591
86- // One sidebar topic per product (config loaded from .product-configs/).
87- const productTopics = PRODUCTS . map ( ( product ) => ( {
88- id : product . id ,
89- label : product . label ,
90- link : product . link ,
91- items : makeSidebarItems ( product . contentDir , product . sidebarOrder ) ,
92- } ) ) ;
92+ // One sidebar topic per product. Products with a configured channel use the latest
93+ // release content for their sidebar, while the topic link stays at the
94+ // product root so the dropdown can distinguish it from version topics.
95+ const productTopics = PRODUCTS . map ( product => {
96+ const latest = productLatestVersions [ product . id ] ;
97+ const useVersionedLatest = Boolean ( product . latestSource && latest ) ;
98+ const prefix = useVersionedLatest
99+ ? `${ product . contentDir } /${ latest . slug } `
100+ : product . latestSource
101+ ? `${ product . contentDir } /${ product . latestSource } `
102+ : product . contentDir ;
103+ const sidebarOrder = useVersionedLatest
104+ ? loadVersionSidebarOrder ( product . repo , latest . slug ) ?? product . sidebarOrder
105+ : product . sidebarOrder ;
106+
107+ return {
108+ id : product . id ,
109+ label : product . label ,
110+ link : useVersionedLatest ? `/${ prefix } /` : product . link ,
111+ items : makeSidebarItems ( prefix , sidebarOrder ) ,
112+ } ;
113+ } ) ;
114+
115+ const channelTopics = PRODUCTS
116+ . filter ( product => product . latestSource )
117+ . map ( product => ( {
118+ id : `${ product . id } -${ product . latestSource } ` ,
119+ label : product . label ,
120+ link : `${ product . link } ${ product . latestSource } /` ,
121+ items : makeSidebarItems ( `${ product . contentDir } /${ product . latestSource } ` , product . sidebarOrder ) ,
122+ } ) ) ;
123+
124+ function productContentPrefixes ( product ) {
125+ const prefixes = product . latestSource
126+ ? [
127+ productLatestVersions [ product . id ]
128+ ? `${ product . contentDir } /${ productLatestVersions [ product . id ] . slug } `
129+ : `${ product . contentDir } /${ product . latestSource } ` ,
130+ `${ product . contentDir } /${ product . latestSource } ` ,
131+ ]
132+ : [ product . contentDir ] ;
133+
134+ return [ ...new Set ( [
135+ ...prefixes ,
136+ ...( productVersions [ product . id ] ?? [ ] ) . map ( version => `${ product . contentDir } /${ version . slug } ` ) ,
137+ ] ) ] ;
138+ }
139+
140+ function productLatestContentPrefix ( product ) {
141+ if ( product . latestSource ) {
142+ return productLatestVersions [ product . id ]
143+ ? `${ product . contentDir } /${ productLatestVersions [ product . id ] . slug } `
144+ : `${ product . contentDir } /${ product . latestSource } ` ;
145+ }
146+ return product . contentDir ;
147+ }
93148
94149// One sidebar topic per archived version of each product.
95150const versionedTopics = PRODUCTS . flatMap ( product => {
@@ -109,10 +164,25 @@ const versionedTopics = PRODUCTS.flatMap(product => {
109164// sidebar section (product root/index pages, 404 pages, versioned 404 pages).
110165// Computed automatically from products; no manual unlistedPaths needed.
111166const topicsOption = Object . fromEntries ( [
112- ...PRODUCTS . map ( ( p , i ) => [
113- p . id ,
114- [ `/${ p . contentDir } ` , `/${ p . contentDir } /404` , ...( i === 0 ? [ '/404' ] : [ ] ) ] ,
115- ] ) ,
167+ ...PRODUCTS . map ( ( p , i ) => {
168+ const latest = productLatestVersions [ p . id ] ;
169+ const productRoot = p . latestSource && latest
170+ ? `/${ p . contentDir } /${ latest . slug } `
171+ : p . latestSource
172+ ? `/${ p . contentDir } /${ p . latestSource } `
173+ : `/${ p . contentDir } ` ;
174+ const paths = new Set ( [ productRoot , `${ productRoot } /404` , `/${ p . contentDir } /404` ] ) ;
175+ return [
176+ p . id ,
177+ [ ...paths , ...( i === 0 ? [ '/404' ] : [ ] ) ] ,
178+ ] ;
179+ } ) ,
180+ ...PRODUCTS
181+ . filter ( product => product . latestSource )
182+ . map ( product => [
183+ `${ product . id } -${ product . latestSource } ` ,
184+ [ `/${ product . contentDir } /${ product . latestSource } ` , `/${ product . contentDir } /${ product . latestSource } /404` ] ,
185+ ] ) ,
116186 ...PRODUCTS . flatMap ( product => {
117187 const versions = productVersions [ product . id ] ?? [ ] ;
118188 return versions . map ( v => {
@@ -122,14 +192,19 @@ const topicsOption = Object.fromEntries([
122192 } ) ,
123193] ) ;
124194
195+ let generatedRedirects = { } ;
196+ try {
197+ generatedRedirects = JSON . parse ( readFileSync ( '.product-configs/redirects.json' , 'utf8' ) ) ;
198+ } catch { /* not present in local dev */ }
199+
125200// https://astro.build/config
126201export default defineConfig ( {
127202 site : 'https://docs.defenseunicorns.com/docs/' ,
128203 prefetch : true ,
129- redirects :
130- {
204+ redirects : {
131205 '/docs' : '/' ,
132206 '/en' : '/' ,
207+ ...generatedRedirects ,
133208 } ,
134209
135210 integrations : [
@@ -164,7 +239,7 @@ export default defineConfig({
164239 const entry = typeof e === 'string' ? { dir : e , label : titleCase ( e ) } : e ;
165240 return {
166241 label : `${ p . label } > ${ entry . label } ` ,
167- paths : [ `${ p . contentDir } /${ entry . dir } /**` ] ,
242+ paths : [ `${ productLatestContentPrefix ( p ) } /${ entry . dir } /**` ] ,
168243 } ;
169244 } )
170245 ) ,
@@ -173,9 +248,11 @@ export default defineConfig({
173248 // Core is first in products.json, so Core pages sort before CLI pages in llms-full.txt.
174249 promote : [
175250 'index*' ,
176- ...PRODUCTS . map ( p => `${ p . contentDir } /index*` ) ,
251+ ...PRODUCTS . flatMap ( p => productContentPrefixes ( p ) . map ( prefix => `${ prefix } /index*` ) ) ,
177252 ...PRODUCTS . flatMap ( p =>
178- p . sidebarOrder . map ( e => `${ p . contentDir } /${ typeof e === 'string' ? e : e . dir } /**` )
253+ productContentPrefixes ( p ) . flatMap ( prefix =>
254+ p . sidebarOrder . map ( e => `${ prefix } /${ typeof e === 'string' ? e : e . dir } /**` )
255+ )
179256 ) ,
180257 ] ,
181258 minify : { note : true , tip : true , caution : true , danger : true , details : true , whitespace : true } ,
@@ -184,6 +261,7 @@ export default defineConfig({
184261 } ) ,
185262 starlightSidebarTopics ( [
186263 ...productTopics ,
264+ ...channelTopics ,
187265 ...versionedTopics ,
188266 ] , { topics : topicsOption } ) ,
189267 ] ,
@@ -251,10 +329,16 @@ export default defineConfig({
251329 ] )
252330 )
253331 ) ,
254- // Per-product latest release tags for VersionPicker label
255- __PRODUCT_LATEST_TAGS__ : JSON . stringify ( productLatestTags ) ,
332+ // Latest release metadata for VersionPicker
333+ __PRODUCT_LATEST_VERSIONS__ : JSON . stringify ( productLatestVersions ) ,
256334 // Product registry for client-side components (VersionPicker, Search)
257- __PRODUCTS__ : JSON . stringify ( PRODUCTS . map ( ( { id, label, link, repo } ) => ( { id, label, link, githubRepo : repo ?? null } ) ) ) ,
335+ __PRODUCTS__ : JSON . stringify ( PRODUCTS . map ( ( { id, label, link, repo, latestSource } ) => ( {
336+ id,
337+ label,
338+ link,
339+ githubRepo : repo ?? null ,
340+ latestSource : latestSource ?? null ,
341+ } ) ) ) ,
258342 } ,
259343 plugins : [
260344 tailwindcss ( ) ,
0 commit comments