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
-
+
Flickr
Bitbucket
Tabler
diff --git a/docs/layouts/DocsLayout.astro b/docs/layouts/DocsLayout.astro
index 58fd51004..14df07aa3 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 24a6b8ef7..52ca09389 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'
-
+
diff --git a/shared/data/menu.json b/shared/data/menu.json
index 632dd3796..41f1c0f47 100644
--- a/shared/data/menu.json
+++ b/shared/data/menu.json
@@ -343,7 +343,7 @@
},
"marketing": {
"title": "Marketing",
- "url": "marketing/index.html"
+ "url": "marketing.html"
},
"music": {
"title": "Music",
diff --git a/shared/layouts/BaseLayout.astro b/shared/layouts/BaseLayout.astro
index e0f01b1b9..2426dffdd 100644
--- a/shared/layouts/BaseLayout.astro
+++ b/shared/layouts/BaseLayout.astro
@@ -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} - ` : ''}{site.title} - {site.descriptionShort}
-
-
+
+
{
diff --git a/shared/lib/svg.ts b/shared/lib/svg.ts
index 9a139ffd4..0359f75b5 100644
--- a/shared/lib/svg.ts
+++ b/shared/lib/svg.ts
@@ -24,8 +24,13 @@ const FILLER_PATH = //
* 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)[name]
- if (!icon) return undefined
+ const icon = (icons as unknown as Record)[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}"`)
diff --git a/turbo.json b/turbo.json
index d6822fc5c..b83050732 100644
--- a/turbo.json
+++ b/turbo.json
@@ -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": {