--- // Docs layout for the @tabler/docs app. // Standalone layout with its own ; does NOT use BaseLayout.astro. // Development uses the unminified assets; production additionally emits the SEO metadata. import { site } from '@shared/lib/site'; import Analytics from '@shared/components/Analytics.astro'; import { isExternal } from '@shared/lib/url'; import libs from '@tabler/core/libs.json'; import docs from '@data/docs.json'; import Icon from '@ui/Icon.astro'; import DocsNavbar from '@components/DocsNavbar.astro'; import DocsMenu from '@components/DocsMenu.astro'; import DocsSearch from '@components/DocsSearch.astro'; import DocsClassnames from '@components/DocsClassnames.astro'; import DocsCssVars from '@components/DocsCssVars.astro'; import DocsToc from '@components/DocsToc.astro'; import type { TocItem } from '@components/DocsToc.astro'; import DocsCopyMarkdownLink from '@components/DocsCopyMarkdownLink.astro'; import DocsPagination from '@components/DocsPagination.astro'; import DocsCard from '@components/DocsCard.astro'; import { getDocsPage } from '@lib/docs-pages'; import Prose from '@ui/Prose.astro'; import PageScripts from '@shared/components/PageScripts.astro'; interface Props { /** front matter: the page's h1 heading */ title: string; /** front matter: optional SEO title override */ seoTitle?: string | undefined; /** front matter: lead below the title (p.text-secondary.fs-3.lh-lg) */ summary?: string | undefined; /** repo-relative path of the file implementing the component, linked next to the edit link */ sourcePath?: string | undefined; /** class reference table, rendered above the page body */ classnames?: Record | undefined; /** front matter `css-vars`: custom-property family (`badge` for `--tblr-badge-*`), rendered below the class reference */ cssVars?: string | undefined; /** front matter `sass-vars`: the `-variables` marker in `_variables.scss`, defaults to `cssVars` */ sassVars?: string | undefined; /** front matter: SEO description */ description?: string | undefined; /** front matter: optional SEO description override */ seoDescription?: string | undefined; /** front matter `added-in`: renders the "Added in X" badge next to the h1 */ addedIn?: string | undefined; /** Table of contents (h2/h3 from the markdown content) — see DocsToc.astro */ toc?: TocItem[]; /** * Page URL in the docs namespace, e.g. "/ui/components/alert". * By default derived from Astro.url.pathname by stripping the /docs prefix. * Drives the active menu entry, pagination, the "Edit this page" link, and * the relative favicon path. */ url?: string | undefined; /** Extra libraries from libs.json (css+js) */ docsLibs?: string[]; /** front matter `css-plugins`: extra tabler-.css sheets for this page's demos */ cssPlugins?: string[]; /** front matter `related`: docs URLs of related pages, rendered above the pagination */ related?: string[]; /** hides child and previous/next page navigation */ hidePagination?: boolean | undefined; /** repo-relative source path for the "Edit this page" link, e.g. "docs/content/ui/components/alert.mdx" */ editPath?: string | undefined; /** url of this page's markdown mirror (see pages/[...slug].md.ts); omitted for pages outside the docs collection */ markdownUrl?: string | undefined; } const { title, seoTitle, summary, classnames, cssVars, sassVars, sourcePath, description, seoDescription, addedIn, toc = [], docsLibs = [], cssPlugins = [], related = [], hidePagination = false, editPath, markdownUrl, } = Astro.props; // Always-on plugin styles. Heavy demo-only sheets (flags, payments, socials) // are opt-in per page via the `css-plugins` frontmatter key; marketing is not // used by any docs page. const basePlugins = ['themes', 'vendors']; const pagePlugins = [...basePlugins, ...cssPlugins.filter((plugin) => !basePlugins.includes(plugin))]; const environment = process.env.NODE_ENV || 'production'; // Vercel preview/branch deployments must not be indexed (VERCEL_ENV is unset locally). const isVercelPreview = !!process.env.VERCEL_ENV && process.env.VERCEL_ENV !== 'production'; // The promo top banner loads on Vercel production deploys only, never on branch previews or local builds. const isProduction = process.env.VERCEL_ENV === 'production'; // Strip /docs prefix (and optional .html) for the docs namespace URL. // Canonical form has no trailing slash (matching vercel.json trailingSlash: false); // the root stays "/". const docsUrl = Astro.props.url ?? (() => { const p = Astro.url.pathname .replace(/^\/docs(?=\/|$)/, '') .replace(/\.html$/, '') .replace(/\/+$/, ''); return p || '/'; })(); const inSection = (prefix: string) => docsUrl === prefix || docsUrl.startsWith(`${prefix}/`); const pageSection = inSection('/ui') ? 'UI' : inSection('/icons') ? 'Icons' : inSection('/illustrations') ? 'Illustrations' : inSection('/emails') ? 'Emails' : inSection('/payments') ? 'Payments' : ''; const metaTitle = seoTitle ?? title; const metaDescription = seoDescription ?? description; const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation'; const canonicalUrl = new URL(docsUrl, Astro.site ?? site.docsUrl).href; const ogImageUrl = new URL('/static/og.png', Astro.site ?? site.docsUrl).href; // Absolute .md url: fetch() uses the relative markdownUrl (same-origin), but the // "Ask ChatGPT" link needs a url ChatGPT can actually resolve on its own. const canonicalMarkdownUrl = markdownUrl ? new URL(markdownUrl, Astro.site ?? site.docsUrl).href : undefined; // Relative asset base: for /ui/components/alert/ -> "../../.." const depth = docsUrl.split('/').filter(Boolean).length; const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1); // Source path for the "Edit this page" link. Prefer the real path passed by the // route (from the collection entry); fall back to the flat-page convention. const editFilePath = editPath ?? (docsUrl === '/' ? 'docs/content/index.mdx' : `docs/content${docsUrl.replace(/\/$/, '')}.mdx`); // docs-libs for this page. In dev, file names stay unchanged // (e.g. dist/plyr.min.js keeps .min). type Lib = { npm?: string; js?: string[]; css?: string[] }; const libEntries = Object.entries(libs as Record); const libUrl = (lib: Lib, file: string) => isExternal(file) ? file : `/dist/libs/${lib.npm}/${file}`; const docsLibCss = libEntries .filter(([name]) => docsLibs.includes(name)) .flatMap(([, lib]) => (lib.css ?? []).map((file) => libUrl(lib, file))); const docsLibJs = libEntries .filter(([name]) => docsLibs.includes(name)) .flatMap(([, lib]) => (lib.js ?? []).map((file) => libUrl(lib, file))); const opencollectiveUrl = 'https://opencollective.com/tabler'; const xUrl = 'https://x.com/tabler_io'; const linkedinUrl = 'https://www.linkedin.com/company/tabler-io'; const year = new Date().getFullYear(); // JSON-LD breadcrumbs. Segment names come from the docs.json menu tree, except // the top-level sections: the menu labels their index pages "Introduction", // so they get explicit names that win over the menu. const sectionNames: Record = { '/ui': 'Tabler UI', '/icons': 'Tabler Icons', '/illustrations': 'Tabler Illustrations', '/emails': 'Tabler Emails', '/payments': 'Tabler Payments', }; type MenuNode = { title?: string; url?: string; children?: MenuNode[] }; const menuTitleByUrl = new Map(); const collectTitles = (nodes: MenuNode[]) => { for (const node of nodes) { if (node.url?.startsWith('/') && node.title) menuTitleByUrl.set(node.url, node.title); if (node.children) collectTitles(node.children); } }; collectTitles(docs.menu as MenuNode[]); // WebSite schema on the homepage only — lets Google pick the right site name. // No SearchAction: docs search is a modal, there is no /search?q= results URL. const websiteJsonLd = docsUrl === '/' ? JSON.stringify({ '@context': 'https://schema.org', '@type': 'WebSite', 'name': 'Tabler Documentation', 'alternateName': 'Tabler Docs', 'url': `${site.docsUrl}/`, }) : null; // Breadcrumb trail — one source for the visible nav above the h1 and the // JSON-LD, so the structured data mirrors what the page shows. const breadcrumbs = (() => { if (docsUrl === '/' || docsUrl === '/404') return null; const segments = docsUrl.split('/').filter(Boolean); const items = [{ name: 'Docs', path: '/' }]; let path = ''; segments.forEach((segment, index) => { path += `/${segment}`; const isLast = index === segments.length - 1; const name = isLast ? title : (sectionNames[path] ?? menuTitleByUrl.get(path) ?? segment); items.push({ name, path }); }); return items; })(); const breadcrumbJsonLd = breadcrumbs ? JSON.stringify({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: breadcrumbs.map((item, index) => ({ '@type': 'ListItem', position: index + 1, name: item.name, item: item.path === '/' ? `${site.docsUrl}/` : `${site.docsUrl}${item.path}`, })), }) : null; type DocsLink = { title: string; url: string; icon: string }; const docsLinks = docs.links as DocsLink[]; // Card data for the "Related" section (title/description from the target // page's frontmatter; menu title as fallback). const relatedPages = await Promise.all( related.map( async (url) => (await getDocsPage(url)) ?? { url, title: menuTitleByUrl.get(url) ?? url, description: '', order: 0 }, ), ); --- {metaTitle} | {siteName} {isVercelPreview && } {markdownUrl && } { environment === 'production' && ( {metaDescription && } {metaDescription && } {metaDescription && } {websiteJsonLd &&

Menu

{/* Machine-readable index of the docs — see /ui/getting-started/llms */}
{ (breadcrumbs || markdownUrl) && (
{breadcrumbs && ( )} {markdownUrl && }
) } 0 ? 'scroll' : undefined} data-bs-target={toc.length > 0 ? '#toc' : undefined} data-bs-root-margin={toc.length > 0 ? '50px 0px -0%' : undefined} data-bs-smooth-scroll={toc.length > 0 ? 'true' : undefined} tabindex="0" >

{title}

{ addedIn && (
Added in {addedIn}
) }

{summary}

{/* Page must supply headings with ids (MDX does this by default). */} {classnames && } {(cssVars || sassVars) && } {relatedPages.length > 0 && ( )} {!hidePagination && }
{!hidePagination && }
© {year} Tabler. All rights reserved.
{ docsLibJs.length > 0 && ( '} /> {docsLibJs.map((src) => {isProduction &&