import { Link } from 'react-router'; import { ContextualMenu } from '@/components/ContextualMenu'; import { Badge } from '@/components/docs/Badge'; import { ArrowLeft, ArrowRight, FileText } from '@/components/icons'; import { OpenApiOperation } from '@/components/OpenApiOperation'; import { getLastModified } from '@/lib/content'; import { getScopeForPage } from '@/lib/docs-config'; import { resolveLocale } from '@/lib/locale'; import { getOperation, methodColor } from '@/lib/openapi'; import { docsSite, getPageByPathname } from '@/lib/site-config'; import { resolveContextualOptions } from '@/lib/site-model'; import type { DocModule, NormalizedDocsPage, RelatedEntry, SiteModel } from '@/lib/types'; import { cn } from '@/lib/utils'; interface RelatedLink { href: string; title: string; external: boolean; } /** * Related-topics entries from frontmatter. Bare paths resolve their title from * the docs registry; unknown internal paths without an explicit title are * skipped so dead links never render. */ function resolveRelated(entries: unknown): RelatedLink[] { if (!Array.isArray(entries)) { return []; } const links: RelatedLink[] = []; for (const entry of entries as RelatedEntry[]) { const href = typeof entry === 'string' ? entry : entry?.href; if (!href || typeof href !== 'string') { continue; } const external = !href.startsWith('/'); const explicitTitle = typeof entry === 'object' ? entry.title : undefined; const title = explicitTitle || (external ? href : getPageByPathname(href)?.label); if (!title) { if (import.meta.env.DEV) { console.warn( `[shiso] Related topic "${href}" does not match a page and has no title — skipped.`, ); } continue; } links.push({ href, title, external }); } return links; } export interface DocContentProps { page: NormalizedDocsPage; doc: DocModule; site: SiteModel; } export function DocContent({ page, doc, site }: DocContentProps) { const scope = getScopeForPage(docsSite, page); // Prev/next paging never crosses a version or language boundary. const pagerPages = scope.docs.pages.filter(item => !item.hidden); const pageIndex = pagerPages.findIndex(item => item.slug === page.slug); const prev = pageIndex > 0 ? pagerPages[pageIndex - 1] : undefined; const next = pageIndex >= 0 ? pagerPages[pageIndex + 1] : undefined; const title = doc.frontmatter?.title || page.label; const description = doc.frontmatter?.description; const Content = doc.default; const shouldShowTimestamp = typeof doc.frontmatter?.timestamp === 'boolean' ? doc.frontmatter.timestamp : site.showTimestamp; const lastModified = shouldShowTimestamp ? getLastModified(page.filePath) : undefined; // `styling.eyebrows`: the section name alone, or the full navigation path. const eyebrow = site.styling.eyebrows === 'breadcrumbs' ? [...new Set([page.tabLabel, page.section])].filter(Boolean).join(' / ') : page.section; const contextualOptions = resolveContextualOptions(site.contextualOptions, page, site.labels); const related = resolveRelated(doc.frontmatter?.related); const operation = getOperation(doc.frontmatter?.openapi); // Dates follow the page's language when it is a valid locale code. const dateFormat = new Intl.DateTimeFormat(resolveLocale(page.language, site.locale), { dateStyle: 'medium', timeZone: 'UTC', }); // Pagefind indexing markers on the prerendered HTML. Inert unless the site // uses the pagefind provider. Hidden pages/scopes mirror the local index's // search visibility rules; the scope filter matches the id Search.tsx sends. const pagefindAttrs = page.hidden || scope.hidden ? {} : { 'data-pagefind-body': '', ...(page.scopeId !== 'default' ? { 'data-pagefind-filter': 'scope[data-scope]', 'data-scope': page.scopeId } : {}), }; return (
{eyebrow &&
{eyebrow}
}
{title && (

{title}

)}
{operation && (
{operation.method} {operation.path} {operation.deprecated && ( deprecated )}
)} {description && (

{description}

)}
{operation && } {lastModified && (
{site.labels.lastUpdated}{' '}
)} {related.length > 0 && ( )}
); } const NavigationButton = ({ label, url, eyebrow, isPrev, }: { label?: string; url?: string; /** Direction caption ("Previous" / "Next") shown above the page title. */ eyebrow: string; isPrev?: boolean; }) => { if (!url || !label) { return
; } return ( {isPrev && ( )} {eyebrow} {label} {!isPrev && ( )} ); };