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-pinterest"> <Icon name="brand-pinterest" /> Pinterest </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-bitbucket"> <Icon name="brand-bitbucket" /> Bitbucket </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'
: 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<string, string> = {
'/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<string, string>();
@@ -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;
+28 -7
View File
@@ -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, () => 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<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. */
@@ -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, '')
// <Example> slots hold the markup the page is actually documenting
const examplePattern = /<Example\b[^>]*>([\s\S]*?)<\/Example>/g
// <Example> 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 = /<Example\b(?:[^>]*?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()
}
+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'
---
<RedirectLayout />
<RedirectLayout url="/prose.html" />
+5 -3
View File
@@ -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, {
+4 -2
View File
@@ -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}`
})
+1 -1
View File
@@ -18,7 +18,7 @@ import Icon from '@ui/Icon.astro'
<Card>
<CardHeader title="Product Tour Example">
<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>
</CardHeader>
<CardBody>
+1 -1
View File
@@ -314,7 +314,7 @@
},
"marketing": {
"title": "Marketing",
"url": "marketing/index.html"
"url": "marketing.html"
},
"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.
const themeAttributes = themeDataAttributes(themeSettings)
// Development build: unminified assets, dev favicon.
const environment = 'development'
const min = environment === 'development' ? '' : '.min'
// Development build: unminified assets, dev favicon (same rule as DocsLayout).
const environment = process.env.NODE_ENV || 'production'
const isDevelopment = environment === 'development'
const min = isDevelopment ? '' : '.min'
// RTL swaps tabler/plugin stylesheets for their .rtl.css variants (demo stays LTR)
const rtlSuffix = rtl ? '.rtl' : ''
@@ -55,8 +56,8 @@ const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean
<title>{title ? `${title} - ` : ''}{site.title} - {site.descriptionShort}</title>
<meta name="description" content={description ?? site.descriptionShort} />
<link rel="icon" href={`${base}/favicon-dev.ico`} type="image/x-icon" />
<link rel="shortcut 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${isDevelopment ? '-dev' : ''}.ico`} type="image/x-icon" />
<!-- 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.
*/
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]
if (!icon) return undefined
const icon = (icons as unknown as Record<string, IconRecord | undefined>)[name]
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'] ?? ''
svg = svg.replace(FILLER_PATH, '')
svg = svg.replace(/class="[^"]+"/, `aria-hidden="true" focusable="false" class="${classes}"`)
+5 -3
View File
@@ -4,24 +4,25 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/banner/**"],
"outputs": ["dist/**"],
"cache": true
},
"@tabler/preview#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/**"],
"cache": true
},
"@tabler/docs#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/**"],
"cache": true
},
"@tabler/screenshots#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/**"],
"cache": true
},
@@ -66,6 +67,7 @@
},
"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
},
"test": {