From 084d2021d331352f004178c51bfe98c38ad20835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:57:08 +0200 Subject: [PATCH] Fix robots.txt, minified assets and dead links in the preview build (#3079) --- .changeset/fix-docs-breadcrumbs-mirrors.md | 6 ++++ .changeset/fix-preview-build-urls.md | 5 ++++ docs/content/ui/components/button.mdx | 2 +- docs/layouts/DocsLayout.astro | 12 +++++--- docs/lib/llms.ts | 35 +++++++++++++++++----- preview/pages/markdown.astro | 4 ++- preview/pages/robots.txt.ts | 8 +++-- preview/pages/sitemap.xml.ts | 6 ++-- preview/pages/tour.astro | 2 +- shared/data/menu.json | 2 +- shared/layouts/BaseLayout.astro | 11 +++---- shared/lib/svg.ts | 9 ++++-- turbo.json | 8 +++-- 13 files changed, 80 insertions(+), 30 deletions(-) create mode 100644 .changeset/fix-docs-breadcrumbs-mirrors.md create mode 100644 .changeset/fix-preview-build-urls.md diff --git a/.changeset/fix-docs-breadcrumbs-mirrors.md b/.changeset/fix-docs-breadcrumbs-mirrors.md new file mode 100644 index 000000000..61765e666 --- /dev/null +++ b/.changeset/fix-docs-breadcrumbs-mirrors.md @@ -0,0 +1,6 @@ +--- +"@tabler/docs": patch +"@tabler/preview": patch +--- + +Fixed docs breadcrumbs showing "Introduction", unresolved `site` expressions in the `.md` mirrors and the missing `play` and `rss` icons. diff --git a/.changeset/fix-preview-build-urls.md b/.changeset/fix-preview-build-urls.md new file mode 100644 index 000000000..4d4f29cbf --- /dev/null +++ b/.changeset/fix-preview-build-urls.md @@ -0,0 +1,5 @@ +--- +"@tabler/preview": patch +--- + +Fixed `robots.txt`, minified assets in `BaseLayout`, the marketing menu link and the `markdown.html` redirect in the preview build. diff --git a/docs/content/ui/components/button.mdx b/docs/content/ui/components/button.mdx index 32e90d3e6..b3ddf8a97 100644 --- a/docs/content/ui/components/button.mdx +++ b/docs/content/ui/components/button.mdx @@ -231,7 +231,7 @@ Social buttons use the brand colors and icons of popular services, so a share or Instagram Pinterest VK - RSS + RSS Flickr Bitbucket Tabler diff --git a/docs/layouts/DocsLayout.astro b/docs/layouts/DocsLayout.astro index 07ced28f7..5df2496cd 100644 --- a/docs/layouts/DocsLayout.astro +++ b/docs/layouts/DocsLayout.astro @@ -109,7 +109,9 @@ const pageSection = inSection('/ui') ? 'Illustrations' : inSection('/emails') ? 'Emails' - : ''; + : inSection('/payments') + ? 'Payments' + : ''; const metaTitle = seoTitle ?? title; const metaDescription = seoDescription ?? description; const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation'; @@ -149,13 +151,15 @@ 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; -// top-level sections are not in the menu, so they get explicit names. +// 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(); @@ -190,7 +194,7 @@ const breadcrumbs = (() => { segments.forEach((segment, index) => { path += `/${segment}`; const isLast = index === segments.length - 1; - const name = isLast ? title : (menuTitleByUrl.get(path) ?? sectionNames[path] ?? segment); + const name = isLast ? title : (sectionNames[path] ?? menuTitleByUrl.get(path) ?? segment); items.push({ name, path }); }); return items; diff --git a/docs/lib/llms.ts b/docs/lib/llms.ts index 5e76688f5..51487b99a 100644 --- a/docs/lib/llms.ts +++ b/docs/lib/llms.ts @@ -23,6 +23,7 @@ import { site } from '@shared/lib/site' import { callouts } from '@components/callouts/index.ts' import docs from '@data/docs.json' import packageManagers from '@data/package-managers.json' +import payments from '@data/payments.json' import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet } from './cdn-snippets.ts' // Lazy raw imports, same as CodeDocs.astro — node:fs paths break once this is @@ -38,12 +39,30 @@ const fence = (code: string, lang = 'html') => `\`\`\`${lang}\n${code.trim()}\n\ */ function resolveCodeTokens(snippet: string): string { const tokens: Record string> = { - '${site.cdnUrl}': () => site.cdnUrl, '${cdnCssTag()}': cdnCssTag, '${cdnJsTag()}': cdnJsTag, } - return Object.entries(tokens).reduce((text, [token, resolve]) => text.replaceAll(token, resolve()), snippet) + return Object.entries(tokens) + .reduce((text, [token, resolve]) => text.replaceAll(token, resolve()), snippet) + .replace(/\$\{site\.(\w+)\}/g, (match, key: string) => siteValue(key) ?? match) +} + +/** A string or number field of `site`, as the MDX pages read it; undefined for anything else. */ +function siteValue(key: string): string | undefined { + const value = (site as Record)[key] + return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined +} + +/** + * JSX expressions the pages use in prose — `{ site.iconsCount }`, + * `{ payments.length }` — and `${site.…}` inside template literals. MDX + * evaluates them in the browser; the markdown mirror has to do it here. + */ +function resolveExpressions(text: string): string { + return resolveCodeTokens(text) + .replace(/\{\s*site\.(\w+)\s*\}/g, (match, key: string) => siteValue(key) ?? match) + .replace(/\{\s*payments\.length\s*\}/g, String(payments.length)) } /** Strip the common leading indentation from a block and trim blank edges. */ @@ -199,12 +218,14 @@ export async function mdxToMarkdown(body: string, examples: (string | null)[] = // top-level imports are wiring, not content text = text.replace(/^import\s+.+?from\s+['"][^'"]+['"];?[ \t]*$/gm, '') - // slots hold the markup the page is actually documenting - const examplePattern = /]*>([\s\S]*?)<\/Example>/g + // slots hold the markup the page is actually documenting: either + // wrapped in the tag, or passed as `html={\`…\`}` on a self-closing one (that + // template literal may contain `>`, so it is matched before the attribute scan) + const examplePattern = /]*?html=\{`([\s\S]*?)`\})?[^>]*?(?:\/>|>([\s\S]*?)<\/Example>)/g const useRendered = examples.length === (text.match(examplePattern)?.length ?? 0) let exampleIndex = 0 - text = text.replace(examplePattern, (_match, inner: string) => { - const snippet = (useRendered ? examples[exampleIndex++] : null) ?? dedent(inner) + text = text.replace(examplePattern, (_match, html: string | undefined, inner: string | undefined) => { + const snippet = (useRendered ? examples[exampleIndex++] : null) ?? dedent(html ?? inner ?? '') return snippet ? `\n${fence(snippet)}\n` : '' }) @@ -244,7 +265,7 @@ export async function mdxToMarkdown(body: string, examples: (string | null)[] = // MDX comments and leftover JSX expressions text = text.replace(/\{\/\*[\s\S]*?\*\/\}/g, '') - return restoreCode(text, code) + return resolveExpressions(restoreCode(text, code)) .replace(/\n{3,}/g, '\n\n') .trim() } diff --git a/preview/pages/markdown.astro b/preview/pages/markdown.astro index c8820110a..479501155 100644 --- a/preview/pages/markdown.astro +++ b/preview/pages/markdown.astro @@ -1,5 +1,7 @@ --- +// Legacy alias kept for old bookmarks and the downloadable package: the page +// moved to prose.html. import RedirectLayout from '@shared/layouts/RedirectLayout.astro' --- - + diff --git a/preview/pages/robots.txt.ts b/preview/pages/robots.txt.ts index bef827a3a..e7342f6b1 100644 --- a/preview/pages/robots.txt.ts +++ b/preview/pages/robots.txt.ts @@ -3,15 +3,17 @@ import { site } from '@shared/lib/site' export const prerender = true -// development, and crawling is allowed only in the preview environment. +// Vercel sets VERCEL_ENV to "production" on preview.tabler.io and to "preview" +// on branch deploys; it is unset locally. Only production may be crawled, +// and the sitemap URL is absolute outside development. export const GET: APIRoute = () => { const environment = process.env.NODE_ENV || 'production' + const isProduction = process.env.VERCEL_ENV === 'production' const sitemapBase = environment !== 'development' ? site.previewUrl : '' const body = `Sitemap: ${sitemapBase}/sitemap.xml - User-agent: * -Disallow:${environment === 'preview' ? '' : ' /'} +Disallow:${isProduction ? '' : ' /'} ` return new Response(body, { diff --git a/preview/pages/sitemap.xml.ts b/preview/pages/sitemap.xml.ts index 3d4b576e0..0f99c003c 100644 --- a/preview/pages/sitemap.xml.ts +++ b/preview/pages/sitemap.xml.ts @@ -3,7 +3,9 @@ import { site } from '@shared/lib/site' export const prerender = true -// Index pages collapse to directory URLs (build.format 'file'). +// build.format 'file' emits x/index.astro as x.html (the hosting serves no +// x/ directory), so nested index pages collapse to that file; the root index +// stays "/". const pages = import.meta.glob('./**/*.astro') const urls = Object.keys(pages) @@ -16,7 +18,7 @@ const urls = Object.keys(pages) }) .map((path) => { if (path === 'index.html') return '/' - if (path.endsWith('/index.html')) return `/${path.slice(0, -'index.html'.length)}` + if (path.endsWith('/index.html')) return `/${path.slice(0, -'/index.html'.length)}.html` return `/${path}` }) diff --git a/preview/pages/tour.astro b/preview/pages/tour.astro index df82e8685..06f726dac 100644 --- a/preview/pages/tour.astro +++ b/preview/pages/tour.astro @@ -18,7 +18,7 @@ import Icon from '@ui/Icon.astro' -