Fix robots.txt, minified assets and dead links in the preview build (#3079)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-17 10:57:08 +02:00
1 parent 39e73cea60
commit 084d2021d3
13 files changed
+80 -30

No files matched your search

@@ -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.
+5
View File
@@ -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.
+1 -1
View File
@@ -231,7 +231,7 @@ Social buttons use the brand colors and icons of popular services, so a share or
<a href="#" class="btn btn-instagram"> <Icon name="brand-instagram" /> Instagram </a> <a href="#" class="btn btn-instagram"> <Icon name="brand-instagram" /> Instagram </a>
<a href="#" class="btn btn-pinterest"> <Icon name="brand-pinterest" /> Pinterest </a> <a href="#" class="btn btn-pinterest"> <Icon name="brand-pinterest" /> Pinterest </a>
<a href="#" class="btn btn-vk"> <Icon name="brand-vk" /> VK </a> <a href="#" class="btn btn-vk"> <Icon name="brand-vk" /> VK </a>
<a href="#" class="btn btn-rss"> <Icon name="brand-rss" /> RSS </a> <a href="#" class="btn btn-rss"> <Icon name="rss" /> RSS </a>
<a href="#" class="btn btn-flickr"> <Icon name="brand-flickr" /> Flickr </a> <a href="#" class="btn btn-flickr"> <Icon name="brand-flickr" /> Flickr </a>
<a href="#" class="btn btn-bitbucket"> <Icon name="brand-bitbucket" /> Bitbucket </a> <a href="#" class="btn btn-bitbucket"> <Icon name="brand-bitbucket" /> Bitbucket </a>
<a href="#" class="btn btn-tabler"> <Icon name="brand-tabler" /> Tabler </a> <a href="#" class="btn btn-tabler"> <Icon name="brand-tabler" /> Tabler </a>
+8 -4
View File
@@ -109,7 +109,9 @@ const pageSection = inSection('/ui')
? 'Illustrations' ? 'Illustrations'
: inSection('/emails') : inSection('/emails')
? 'Emails' ? 'Emails'
: ''; : inSection('/payments')
? 'Payments'
: '';
const metaTitle = seoTitle ?? title; const metaTitle = seoTitle ?? title;
const metaDescription = seoDescription ?? description; const metaDescription = seoDescription ?? description;
const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation'; 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(); const year = new Date().getFullYear();
// JSON-LD breadcrumbs. Segment names come from the docs.json menu tree; // JSON-LD breadcrumbs. Segment names come from the docs.json menu tree, except
// top-level sections are not in the menu, so they get explicit names. // the top-level sections: the menu labels their index pages "Introduction",
// so they get explicit names that win over the menu.
const sectionNames: Record<string, string> = { const sectionNames: Record<string, string> = {
'/ui': 'Tabler UI', '/ui': 'Tabler UI',
'/icons': 'Tabler Icons', '/icons': 'Tabler Icons',
'/illustrations': 'Tabler Illustrations', '/illustrations': 'Tabler Illustrations',
'/emails': 'Tabler Emails', '/emails': 'Tabler Emails',
'/payments': 'Tabler Payments',
}; };
type MenuNode = { title?: string; url?: string; children?: MenuNode[] }; type MenuNode = { title?: string; url?: string; children?: MenuNode[] };
const menuTitleByUrl = new Map<string, string>(); const menuTitleByUrl = new Map<string, string>();
@@ -190,7 +194,7 @@ const breadcrumbs = (() => {
segments.forEach((segment, index) => { segments.forEach((segment, index) => {
path += `/${segment}`; path += `/${segment}`;
const isLast = index === segments.length - 1; 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 }); items.push({ name, path });
}); });
return items; return items;
+28 -7
View File
@@ -23,6 +23,7 @@ import { site } from '@shared/lib/site'
import { callouts } from '@components/callouts/index.ts' import { callouts } from '@components/callouts/index.ts'
import docs from '@data/docs.json' import docs from '@data/docs.json'
import packageManagers from '@data/package-managers.json' import packageManagers from '@data/package-managers.json'
import payments from '@data/payments.json'
import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet } from './cdn-snippets.ts' import { cdnCssTag, cdnJsTag, cdnPackageSnippet, cdnPluginSnippet } from './cdn-snippets.ts'
// Lazy raw imports, same as CodeDocs.astro — node:fs paths break once this is // 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 { function resolveCodeTokens(snippet: string): string {
const tokens: Record<string, () => string> = { const tokens: Record<string, () => string> = {
'${site.cdnUrl}': () => site.cdnUrl,
'${cdnCssTag()}': cdnCssTag, '${cdnCssTag()}': cdnCssTag,
'${cdnJsTag()}': cdnJsTag, '${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<string, unknown>)[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. */ /** 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 // top-level imports are wiring, not content
text = text.replace(/^import\s+.+?from\s+['"][^'"]+['"];?[ \t]*$/gm, '') text = text.replace(/^import\s+.+?from\s+['"][^'"]+['"];?[ \t]*$/gm, '')
// <Example> slots hold the markup the page is actually documenting // <Example> slots hold the markup the page is actually documenting: either
const examplePattern = /<Example\b[^>]*>([\s\S]*?)<\/Example>/g // 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 = /<Example\b(?:[^>]*?html=\{`([\s\S]*?)`\})?[^>]*?(?:\/>|>([\s\S]*?)<\/Example>)/g
const useRendered = examples.length === (text.match(examplePattern)?.length ?? 0) const useRendered = examples.length === (text.match(examplePattern)?.length ?? 0)
let exampleIndex = 0 let exampleIndex = 0
text = text.replace(examplePattern, (_match, inner: string) => { text = text.replace(examplePattern, (_match, html: string | undefined, inner: string | undefined) => {
const snippet = (useRendered ? examples[exampleIndex++] : null) ?? dedent(inner) const snippet = (useRendered ? examples[exampleIndex++] : null) ?? dedent(html ?? inner ?? '')
return snippet ? `\n${fence(snippet)}\n` : '' 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 // MDX comments and leftover JSX expressions
text = text.replace(/\{\/\*[\s\S]*?\*\/\}/g, '') text = text.replace(/\{\/\*[\s\S]*?\*\/\}/g, '')
return restoreCode(text, code) return resolveExpressions(restoreCode(text, code))
.replace(/\n{3,}/g, '\n\n') .replace(/\n{3,}/g, '\n\n')
.trim() .trim()
} }
+3 -1
View File
@@ -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' import RedirectLayout from '@shared/layouts/RedirectLayout.astro'
--- ---
<RedirectLayout /> <RedirectLayout url="/prose.html" />
+5 -3
View File
@@ -3,15 +3,17 @@ import { site } from '@shared/lib/site'
export const prerender = true 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 = () => { export const GET: APIRoute = () => {
const environment = process.env.NODE_ENV || 'production' const environment = process.env.NODE_ENV || 'production'
const isProduction = process.env.VERCEL_ENV === 'production'
const sitemapBase = environment !== 'development' ? site.previewUrl : '' const sitemapBase = environment !== 'development' ? site.previewUrl : ''
const body = `Sitemap: ${sitemapBase}/sitemap.xml const body = `Sitemap: ${sitemapBase}/sitemap.xml
User-agent: * User-agent: *
Disallow:${environment === 'preview' ? '' : ' /'} Disallow:${isProduction ? '' : ' /'}
` `
return new Response(body, { return new Response(body, {
+4 -2
View File
@@ -3,7 +3,9 @@ import { site } from '@shared/lib/site'
export const prerender = true 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 pages = import.meta.glob('./**/*.astro')
const urls = Object.keys(pages) const urls = Object.keys(pages)
@@ -16,7 +18,7 @@ const urls = Object.keys(pages)
}) })
.map((path) => { .map((path) => {
if (path === 'index.html') return '/' 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}` return `/${path}`
}) })
+1 -1
View File
@@ -18,7 +18,7 @@ import Icon from '@ui/Icon.astro'
<Card> <Card>
<CardHeader title="Product Tour Example"> <CardHeader title="Product Tour Example">
<CardActions> <CardActions>
<Button id="start-tour" text="Start Tour" icon="play" color="primary" element="button" /> <Button id="start-tour" text="Start Tour" icon="player-play" color="primary" element="button" />
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
+1 -1
View File
@@ -314,7 +314,7 @@
}, },
"marketing": { "marketing": {
"title": "Marketing", "title": "Marketing",
"url": "marketing/index.html" "url": "marketing.html"
}, },
"music": { "music": {
"title": "Music", "title": "Music",
+6 -5
View File
@@ -28,9 +28,10 @@ const { title, pageLibs = [], bodyClass, rtl = false, base = '.', description, t
// script runs, so a page keeps its variant until the visitor chooses otherwise. // script runs, so a page keeps its variant until the visitor chooses otherwise.
const themeAttributes = themeDataAttributes(themeSettings) const themeAttributes = themeDataAttributes(themeSettings)
// Development build: unminified assets, dev favicon. // Development build: unminified assets, dev favicon (same rule as DocsLayout).
const environment = 'development' const environment = process.env.NODE_ENV || 'production'
const min = environment === 'development' ? '' : '.min' const isDevelopment = environment === 'development'
const min = isDevelopment ? '' : '.min'
// RTL swaps tabler/plugin stylesheets for their .rtl.css variants (demo stays LTR) // RTL swaps tabler/plugin stylesheets for their .rtl.css variants (demo stays LTR)
const rtlSuffix = rtl ? '.rtl' : '' const rtlSuffix = rtl ? '.rtl' : ''
@@ -55,8 +56,8 @@ const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean
<title>{title ? `${title} - ` : ''}{site.title} - {site.descriptionShort}</title> <title>{title ? `${title} - ` : ''}{site.title} - {site.descriptionShort}</title>
<meta name="description" content={description ?? site.descriptionShort} /> <meta name="description" content={description ?? site.descriptionShort} />
<link rel="icon" href={`${base}/favicon-dev.ico`} type="image/x-icon" /> <link rel="icon" href={`${base}/favicon${isDevelopment ? '-dev' : ''}.ico`} type="image/x-icon" />
<link rel="shortcut icon" href={`${base}/favicon-dev.ico`} type="image/x-icon" /> <link rel="shortcut icon" href={`${base}/favicon${isDevelopment ? '-dev' : ''}.ico`} type="image/x-icon" />
<!-- END META --> <!-- END META -->
{ {
+7 -2
View File
@@ -24,8 +24,13 @@ const FILLER_PATH = /<path stroke="none" d="M0 0h24v24H0z" fill="none"\s*\/>/
* aria-hidden="true" focusable="false" and the given classes. * aria-hidden="true" focusable="false" and the given classes.
*/ */
export function iconSvg(name: string, { filled = false, classes = 'icon' }: { filled?: boolean | undefined; classes?: string | undefined } = {}): string | undefined { export function iconSvg(name: string, { filled = false, classes = 'icon' }: { filled?: boolean | undefined; classes?: string | undefined } = {}): string | undefined {
const icon = (icons as unknown as Record<string, IconRecord>)[name] const icon = (icons as unknown as Record<string, IconRecord | undefined>)[name]
if (!icon) return undefined if (!icon) {
// Icon.astro renders nothing for an unknown name, which is invisible in the
// page; say so in the build log instead of failing silently.
console.warn(`[svg] unknown Tabler icon "${name}"`)
return undefined
}
let svg = icon.svg?.[filled ? 'filled' : 'outline'] ?? '' let svg = icon.svg?.[filled ? 'filled' : 'outline'] ?? ''
svg = svg.replace(FILLER_PATH, '') svg = svg.replace(FILLER_PATH, '')
svg = svg.replace(/class="[^"]+"/, `aria-hidden="true" focusable="false" class="${classes}"`) svg = svg.replace(/class="[^"]+"/, `aria-hidden="true" focusable="false" class="${classes}"`)
+5 -3
View File
@@ -4,24 +4,25 @@
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/banner/**"],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"cache": true "cache": true
}, },
"@tabler/preview#build": { "@tabler/preview#build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared/**"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/**", "!$TURBO_ROOT$/shared/.turbo/**", "!$TURBO_ROOT$/shared/.astro/**", "!$TURBO_ROOT$/shared/**/node_modules/**"],
"outputs": ["dist/**", "tmp-assets/**"], "outputs": ["dist/**", "tmp-assets/**"],
"cache": true "cache": true
}, },
"@tabler/docs#build": { "@tabler/docs#build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared/**"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/**", "!$TURBO_ROOT$/shared/.turbo/**", "!$TURBO_ROOT$/shared/.astro/**", "!$TURBO_ROOT$/shared/**/node_modules/**"],
"outputs": ["dist/**", "tmp-assets/**"], "outputs": ["dist/**", "tmp-assets/**"],
"cache": true "cache": true
}, },
"@tabler/screenshots#build": { "@tabler/screenshots#build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared/**"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/**", "!$TURBO_ROOT$/shared/.turbo/**", "!$TURBO_ROOT$/shared/.astro/**", "!$TURBO_ROOT$/shared/**/node_modules/**"],
"outputs": ["dist/**"], "outputs": ["dist/**"],
"cache": true "cache": true
}, },
@@ -66,6 +67,7 @@
}, },
"type-check": { "type-check": {
"dependsOn": ["^type-check"], "dependsOn": ["^type-check"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared/**", "!$TURBO_ROOT$/shared/.turbo/**", "!$TURBO_ROOT$/shared/.astro/**", "!$TURBO_ROOT$/shared/**/node_modules/**"],
"cache": true "cache": true
}, },
"test": { "test": {