diff --git a/docs/angular/astro.config.ts b/docs/angular/astro.config.ts index 71b2fec482..d11837e0c2 100644 --- a/docs/angular/astro.config.ts +++ b/docs/angular/astro.config.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createDocsSite, type DocsMode } from 'docs-template/integration'; -import { IGDOCS_PLATFORMS, type NavLang } from 'docs-template/platform'; +import { IGDOCS_PLATFORMS, IGDOCS_HOSTS, type NavLang } from 'docs-template/platform'; import { generateGridTopics } from './src/scripts/generate-grids.mjs'; import mdx from '@astrojs/mdx'; @@ -29,9 +29,10 @@ if (docsEnv !== 'development' && docsEnv !== 'staging' && docsEnv !== 'productio const mode: DocsMode = docsEnv; -// ── Site URL — varies by build mode ───────────────────────────────────────── -const PROD_HOST = 'https://www.infragistics.com'; -const STAGING_HOST = 'https://staging.infragistics.com'; +// ── Site URL — varies by build mode and language ──────────────────────────── +// Hosts source: legacy igniteui-docfx environment.json (per-language). +const PROD_HOST = IGDOCS_HOSTS[docsLang].production; +const STAGING_HOST = IGDOCS_HOSTS[docsLang].staging; const platformKey = docsLang === 'jp' ? 'AngularJP' : 'Angular'; const { base } = IGDOCS_PLATFORMS[platformKey]; @@ -56,6 +57,10 @@ export default createDocsSite({ platform: 'angular', navLang: docsLang, mode, + build: { + format: 'file' + }, + trailingSlash: 'never', productLinks: Object.values(IGDOCS_PLATFORMS) .filter(p => p.lang === docsLang) .map(({ label, key, base: b }) => ({ @@ -66,6 +71,7 @@ export default createDocsSite({ source: { tocPath: `${componentsDocsDir}/toc.json`, docsDir: componentsDocsDir, + slugPrefix: 'components', }, sidebar: { exclude: [/^internal\//] }, starlight: { diff --git a/docs/angular/public/web.config b/docs/angular/public/web.config index 2485d7671f..09b4aef1f9 100644 --- a/docs/angular/public/web.config +++ b/docs/angular/public/web.config @@ -760,7 +760,7 @@ - + @@ -769,6 +769,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/angular/src/content.config.ts b/docs/angular/src/content.config.ts index 41c43926c0..63c6949b96 100644 --- a/docs/angular/src/content.config.ts +++ b/docs/angular/src/content.config.ts @@ -13,7 +13,7 @@ try { lang = cfg.lang ?? lang; } catch { /* use defaults */ } -const docsDir = path.join(root, 'src', 'content', lang, 'components'); +const docsDir = path.join(root, 'src', 'content', lang); const tableOfContentsSchema = z.object({ tableOfContents: z @@ -28,5 +28,5 @@ const tableOfContentsSchema = z.object({ }); export const collections = { - docs: createDocsCollection(docsDir, { exclude: ['**/*.md'], extendSchema: tableOfContentsSchema }), + docs: createDocsCollection(docsDir, { exclude: ['**/*.md', 'grids_templates/**', 'images/**', 'index.mdx'], extendSchema: tableOfContentsSchema }), }; diff --git a/docs/angular/src/pages/index.astro b/docs/angular/src/pages/index.astro index ead380cc72..60eb09a107 100644 --- a/docs/angular/src/pages/index.astro +++ b/docs/angular/src/pages/index.astro @@ -15,7 +15,19 @@ const toc: TocItem[] = existsSync(tocPath) : []; function hrefToSlug(href: string): string { - return href.replace(/\\/g, '/').replace(/\.(md|mdx)$/i, '').toLowerCase().replace(/\/index$/, ''); + if (!href) return ''; + let slug = href + .replace(/\\/g, '/') + .replace(/\.(md|mdx)$/i, '') + .toLowerCase(); + slug = slug.replace(/\/index$/, ''); + return slug === 'index' ? '' : slug; +} + + +function joinSlugPrefix(slugPrefix: string, slug: string): string { + if (!slugPrefix) return slug; + return slug ? `${slugPrefix}/${slug}` : slugPrefix; } function findDefaultSlug(items: TocItem[]): string | null { @@ -52,10 +64,12 @@ function slugExistsInSidebar(items: SidebarEntry[], target: string): boolean { const typedSidebar = sidebar as SidebarEntry[]; const defaultSlug = findDefaultSlug(toc); -const slug = (defaultSlug && slugExistsInSidebar(typedSidebar, defaultSlug)) - ? defaultSlug +const slugPrefix = process.env.DOCS_SLUG_PREFIX ?? ''; +const prefixedDefault = defaultSlug !== null ? joinSlugPrefix(slugPrefix, defaultSlug) : null; +const slug = (prefixedDefault && slugExistsInSidebar(typedSidebar, prefixedDefault)) + ? prefixedDefault : firstSlug(typedSidebar); const base = import.meta.env.BASE_URL.replace(/\/$/, ''); -const redirectTo = slug ? `${base}/${slug}/` : `${base}/404/`; +const redirectTo = slug ? `${base}/${slug}` : `${base}/404`; return Astro.redirect(redirectTo, 301); ---- \ No newline at end of file +--- diff --git a/docs/xplat/astro.config.ts b/docs/xplat/astro.config.ts index 315bd34b72..67ee8a4b00 100644 --- a/docs/xplat/astro.config.ts +++ b/docs/xplat/astro.config.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { createDocsSite, type DocsMode } from 'docs-template/integration'; -import { IGDOCS_PLATFORMS, type NavLang } from 'docs-template/platform'; +import { IGDOCS_PLATFORMS, IGDOCS_HOSTS, type NavLang } from 'docs-template/platform'; import mdx from '@astrojs/mdx'; // --------------------------------------------------------------------------- @@ -304,8 +304,9 @@ function buildFilteredToc(): string { const filteredTocPath = buildFilteredToc(); console.log(`[astro.config] Platform: ${platform} lang: ${lang} mode: ${mode}`); -const PROD_HOST = 'https://www.infragistics.com'; -const STAGING_HOST = 'https://staging.infragistics.com'; +// Hosts source: legacy igniteui-docfx environment.json (per-language). +const PROD_HOST = IGDOCS_HOSTS[lang].production; +const STAGING_HOST = IGDOCS_HOSTS[lang].staging; const platformLangKey = lang === 'jp' ? `${platform}JP` : platform; const p = PLATFORMS[platformLangKey] ?? PLATFORMS[platform]; @@ -325,6 +326,10 @@ export default createDocsSite({ platform: p.key, navLang: lang, mode, + build: { + format: 'file' + }, + trailingSlash: 'never', source: { tocPath: filteredTocPath, docsDir: path.join(XPLAT_ROOT, 'components'), diff --git a/src/integration.ts b/src/integration.ts index f59230cb3a..fda181ef0b 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -638,6 +638,16 @@ export interface DocsSiteSource { tocPath: string; /** Absolute path to the Markdown docs directory. */ docsDir: string; + /** + * Path prefix prepended to every sidebar slug. + * When set, the content collection base is assumed to be a parent of + * `docsDir` so that entry IDs (and therefore routes) include this prefix. + * + * Example: `slugPrefix: 'components'` makes sidebar slugs like + * `components/accordion` instead of `accordion`, matching a content + * collection whose glob base is one directory above `docsDir`. + */ + slugPrefix?: string; } export interface CreateDocsSiteOptions { @@ -720,16 +730,26 @@ export function createDocsSite(options: CreateDocsSiteOptions = {} as CreateDocs ...astroExtra } = options; + const prefixSegments = source.slugPrefix ? source.slugPrefix.split('/').filter(Boolean) : []; + const sidebar = buildSidebarFromToc({ tocPath: source.tocPath!, docsDir: source.docsDir!, exclude: sidebarOptions.exclude ?? [], + slugPrefix: source.slugPrefix, }); + // When slugPrefix is set the content collection base is a parent of docsDir. + // Compute the content root so llms.txt and dev-middleware resolve slugs correctly. + const contentRoot = prefixSegments.length > 0 + ? path.resolve(source.docsDir!, ...prefixSegments.map(() => '..')) + : source.docsDir; + // Expose env vars so consuming content.config.ts and components can read them. if (source.docsDir) { process.env.DOCS_SOURCE_PATH = source.docsDir; } + process.env.DOCS_SLUG_PREFIX = source.slugPrefix ?? ''; process.env.DOCS_BUILD_MODE = mode; process.env.DOCS_BASE = base ? base.replace(/\/$/, '') : ''; if (!process.env.DOCS_ENV) { @@ -808,7 +828,7 @@ export function createDocsSite(options: CreateDocsSiteOptions = {} as CreateDocs siteMetaIntegration({ title, description, - docsDir: source.docsDir, + docsDir: contentRoot, sidebar, platform, navLang, diff --git a/src/platform.ts b/src/platform.ts index ace1d89dc5..787c529f4b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -184,59 +184,81 @@ export const PLATFORM_DEFS: Record = { * Shared per-platform metadata for all Ignite UI docs sites. * Import this in `astro.config.ts` files instead of duplicating the data. */ +// Base paths that match the legacy DocFX URL shape served by IIS. +// Same path is used regardless of language — language is encoded in the host +// (see IGDOCS_HOSTS below). Source: legacy igniteui-docfx and igniteui-xplat-docs +// global.json (_currentBaseUrl: /products/{ProductSpinal}/{PlatformLower}/). +const ANGULAR_BASE = '/products/ignite-ui-angular/angular'; +const REACT_BASE = '/products/ignite-ui-react/react'; +const WEBCOMPONENTS_BASE = '/products/ignite-ui-web-components/web-components'; +const BLAZOR_BASE = '/products/ignite-ui-blazor/blazor'; + export const IGDOCS_PLATFORMS: Record = { // English Angular: { lang: 'en', label: 'Angular', key: 'angular', devPort: 4331, - base: '/angularsite', + base: ANGULAR_BASE, title: 'Ignite UI for Angular', description: 'Component documentation for Ignite UI for Angular.', }, React: { lang: 'en', label: 'React', key: 'react', devPort: 4332, - base: '/reactsite', + base: REACT_BASE, title: 'Ignite UI for React', description: 'Component documentation for Ignite UI for React.', }, WebComponents: { lang: 'en', label: 'Web Components', key: 'web-components', devPort: 4333, - base: '/webcomponentssite', + base: WEBCOMPONENTS_BASE, title: 'Ignite UI for Web Components', description: 'Component documentation for Ignite UI for Web Components.', }, Blazor: { lang: 'en', label: 'Blazor', key: 'blazor', devPort: 4334, - base: '/blazorsite', + base: BLAZOR_BASE, title: 'Ignite UI for Blazor', description: 'Component documentation for Ignite UI for Blazor.', }, // Japanese AngularJP: { lang: 'jp', label: 'Angular', key: 'angular', devPort: 4341, - base: '/angularsite', + base: ANGULAR_BASE, title: 'Ignite UI for Angular', description: 'Component documentation for Ignite UI for Angular.', }, ReactJP: { lang: 'jp', label: 'React', key: 'react', devPort: 4342, - base: '/reactsite', + base: REACT_BASE, title: 'Ignite UI for React', description: 'Component documentation for Ignite UI for React.', }, WebComponentsJP: { lang: 'jp', label: 'Web Components', key: 'web-components', devPort: 4343, - base: '/webcomponentssite', + base: WEBCOMPONENTS_BASE, title: 'Ignite UI for Web Components', description: 'Component documentation for Ignite UI for Web Components.', }, BlazorJP: { lang: 'jp', label: 'Blazor', key: 'blazor', devPort: 4344, - base: '/blazorsite', + base: BLAZOR_BASE, title: 'Ignite UI for Blazor', description: 'Component documentation for Ignite UI for Blazor.', }, }; +/** + * Per-language hostname for staging and production. + * Source: legacy igniteui-docfx environment.json files (one per locale). + * en → www.infragistics.com (staging: staging.infragistics.com) + * jp → jp.infragistics.com (staging: jp.staging.infragistics.com) + * kr → www.infragistics.co.kr (staging: staging.infragistics.co.kr) + */ +export const IGDOCS_HOSTS: Record = { + en: { production: 'https://www.infragistics.com', staging: 'https://staging.infragistics.com' }, + jp: { production: 'https://jp.infragistics.com', staging: 'https://jp.staging.infragistics.com' }, + kr: { production: 'https://www.infragistics.co.kr', staging: 'https://staging.infragistics.co.kr' }, +}; + /** * Returns an array of Starlight `head` entries for the given platform. * Pass the result directly to `starlight({ head: getPlatformHead(...) })`. diff --git a/src/sidebar.ts b/src/sidebar.ts index ca47a8726c..3ad2dc96dd 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -66,6 +66,12 @@ function hrefToSlug(href: string): string { return slug === 'index' ? '' : slug; } + +function joinSlugPrefix(slugPrefix: string, slug: string): string { + if (!slugPrefix) return slug; + return slug ? `${slugPrefix}/${slug}` : slugPrefix; +} + /** * Initial collapsed state by depth: * • depth 0 (root groups, incl. `header:true` sections) → `collapsed: false` @@ -80,6 +86,7 @@ function convertTocItem( item: TocItem, exclude: RegExp[], depth: number, + slugPrefix: string, ): SidebarEntry | null { if (!item.name) return null; @@ -90,10 +97,10 @@ function convertTocItem( collapsed: collapsedForDepth(depth), }; if (item.href && docExists(docsDir, item.href, exclude)) { - group.items.push({ label: 'Overview', slug: hrefToSlug(item.href) }); + group.items.push({ label: 'Overview', slug: joinSlugPrefix(slugPrefix, hrefToSlug(item.href)) }); } for (const child of item.items) { - const entry = convertTocItem(docsDir, child, exclude, depth + 1); + const entry = convertTocItem(docsDir, child, exclude, depth + 1, slugPrefix); if (entry) group.items.push(entry); } return group.items.length > 0 ? group : null; @@ -101,7 +108,7 @@ function convertTocItem( if (item.href) { if (!docExists(docsDir, item.href, exclude)) return null; - const entry: SidebarLink = { label: item.name, slug: hrefToSlug(item.href) }; + const entry: SidebarLink = { label: item.name, slug: joinSlugPrefix(slugPrefix, hrefToSlug(item.href)) }; // Status badge — only one slot available in Starlight, priority order: if (item.new) entry.badge = { text: 'New', variant: 'success' }; else if (item.preview) entry.badge = { text: 'Preview', variant: 'caution' }; @@ -130,16 +137,24 @@ export interface BuildSidebarFromTocOptions { docsDir: string; /** Extra patterns to exclude (matched against the `href`). */ exclude?: RegExp[]; + /** + * Path prefix prepended to every generated slug. + * Use when the content collection base is a parent of `docsDir` so that + * slugs include the intermediate directory (e.g. `'components/'`). + */ + slugPrefix?: string; } /** * Reads a YAML or JSON TOC file and converts it to a Starlight sidebar array. */ -export function buildSidebarFromToc({ tocPath, docsDir, exclude = [] }: BuildSidebarFromTocOptions): SidebarEntry[] { +export function buildSidebarFromToc({ tocPath, docsDir, exclude = [], slugPrefix }: BuildSidebarFromTocOptions): SidebarEntry[] { if (!tocPath || !fs.existsSync(tocPath)) return []; const tocRaw = fs.readFileSync(tocPath, 'utf-8'); const tocItems = tocPath.endsWith('.json') ? JSON.parse(tocRaw) : yaml.load(tocRaw) as TocItem[]; + const prefix = slugPrefix ?? ''; + const sidebar: SidebarEntry[] = []; let currentGroup: SidebarGroup | null = null; @@ -149,14 +164,14 @@ export function buildSidebarFromToc({ tocPath, docsDir, exclude = [] }: BuildSid // Root-level header section — open by default. currentGroup = { label: item.name!, items: [], collapsed: collapsedForDepth(0) }; if (item.href && docExists(docsDir, item.href, exclude)) { - currentGroup.items.push({ label: 'Overview', slug: hrefToSlug(item.href) }); + currentGroup.items.push({ label: 'Overview', slug: joinSlugPrefix(prefix, hrefToSlug(item.href)) }); } continue; } // Items inside a header section are at depth 1 (nested); // items outside any header section are at depth 0 (root). const depth = currentGroup ? 1 : 0; - const entry = convertTocItem(docsDir, item, exclude, depth); + const entry = convertTocItem(docsDir, item, exclude, depth, prefix); if (!entry) continue; if (currentGroup) currentGroup.items.push(entry); else sidebar.push(entry);