diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 1f84fa0e..31fe813f 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress' import container from 'markdown-it-container' import { renderSandbox } from 'vitepress-plugin-sandpack' import { withMermaid } from 'vitepress-plugin-mermaid' -import { head, nav, sidebar, blog, transformHead } from './configs/index.mts' +import { head, nav, sidebar, transformHead } from './configs/index.mts' const LIVE_SANDBOX_CONTAINER_PATTERN = /:{3,}\s*textmode-(?:api-)?sandbox\b[^\n]*\n[\s\S]*?\n:{3,}/g const LIVE_SANDBOX_COMPONENT_PATTERN = /]*\/>/g @@ -65,7 +65,17 @@ export default withMermaid(defineConfig({ lang: 'en-US', appearance: 'dark', lastUpdated: true, - srcExclude: ['docs/examples/**'], + srcExclude: [ + 'docs/examples/**', + 'blog/**', + 'README.md', + 'LICENSE.md', + 'CONTRIBUTING.md', + 'CODE_OF_CONDUCT.md', + 'SECURITY.md', + 'CONTRIBUTORS_AUTOMATION.md', + '.vitepress/data/TESTIMONIALS_README.md', + ], title: "textmode.js", description: "textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.", head, @@ -75,10 +85,6 @@ export default withMermaid(defineConfig({ }, transformHead, - vite: { - plugins: [blog.plugin], - }, - markdown: { config(md) { md diff --git a/.vitepress/configs/head.mts b/.vitepress/configs/head.mts index b4c6123c..1f977085 100644 --- a/.vitepress/configs/head.mts +++ b/.vitepress/configs/head.mts @@ -10,66 +10,145 @@ const baseHeaders: HeadConfig[] = [ export const head: HeadConfig[] = [...baseHeaders] export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { - const canonicalUrl = `https://code.textmode.art/${pageData.relativePath.replace(/index\.md$/, '').replace(/\.md$/, '')}` - .replace(/\/$/, ''); + const relative = pageData.relativePath + + // Normalize canonical URL + let canonicalUrl: string + if (relative === 'index.md') { + canonicalUrl = 'https://code.textmode.art/' + } else if (relative.endsWith('/index.md')) { + canonicalUrl = `https://code.textmode.art/${relative.replace(/\/index\.md$/, '')}/` + } else { + canonicalUrl = `https://code.textmode.art/${relative.replace(/\.md$/, '')}` + } + + // Detect page categories + const isHomePage = relative === 'index.md' + const isDocsIndex = relative === 'docs/index.md' + const isDocsPage = relative.startsWith('docs/') && !isDocsIndex + const isApiIndex = relative === 'api/index.md' + const isApiPage = relative.startsWith('api/') && !isApiIndex - // Breadcrumb Schema Generation - const segments = pageData.relativePath.replace(/\.md$/, '').split('/').filter(s => s !== 'index') + // Construct valid BreadcrumbList (ensuring every item has a resolvable 200 OK URL) const breadcrumbItems = [ { "@type": "ListItem", "position": 1, "name": "Home", - "item": "https://code.textmode.art" + "item": "https://code.textmode.art/" } ] - let currentPath = '' - segments.forEach((segment, index) => { - currentPath += `/${segment}` - const isLast = index === segments.length - 1 - - let name = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ') - let itemUrl = `https://code.textmode.art${currentPath}` + if (isDocsIndex) { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 2, + "name": "Documentation", + "item": "https://code.textmode.art/docs/" + }) + } else if (isDocsPage) { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 2, + "name": "Documentation", + "item": "https://code.textmode.art/docs/" + }) - // Manual override for 'docs' root to prevent 404s - if (segment === 'docs' && !isLast) { - itemUrl = 'https://code.textmode.art/docs/introduction' + if (relative.startsWith('docs/contributing/')) { + if (relative === 'docs/contributing/index.md') { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 3, + "name": "Contributing", + "item": "https://code.textmode.art/docs/contributing/" + }) + } else { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 3, + "name": "Contributing", + "item": "https://code.textmode.art/docs/contributing/" + }) + breadcrumbItems.push({ + "@type": "ListItem", + "position": 4, + "name": pageData.title || "Guide", + "item": canonicalUrl + }) + } + } else { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 3, + "name": pageData.title || "Guide", + "item": canonicalUrl + }) } + } else if (isApiIndex) { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 2, + "name": "API", + "item": "https://code.textmode.art/api/" + }) + } else if (isApiPage) { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 2, + "name": "API", + "item": "https://code.textmode.art/api/" + }) - if (isLast) { - name = pageData.title || name - // Use the canonical URL for the leaf node to ensure consistency (e.g. .html vs directory) - itemUrl = canonicalUrl + // Check if under an API sub-package e.g. api/textmode.export.js/... + const apiMatch = relative.match(/^api\/(textmode(?:\.[a-z]+)?\.js)\//) + if (apiMatch) { + const pkgName = apiMatch[1] + const isPkgIndex = relative === `api/${pkgName}/index.md` + + breadcrumbItems.push({ + "@type": "ListItem", + "position": 3, + "name": pkgName, + "item": `https://code.textmode.art/api/${pkgName}/` + }) + + if (!isPkgIndex) { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 4, + "name": pageData.title || "Reference", + "item": canonicalUrl + }) + } + } else { + breadcrumbItems.push({ + "@type": "ListItem", + "position": 3, + "name": pageData.title || "Reference", + "item": canonicalUrl + }) } - + } else if (!isHomePage) { breadcrumbItems.push({ "@type": "ListItem", - "position": breadcrumbItems.length + 1, - "name": name, - "item": itemUrl + "position": 2, + "name": pageData.title || "Page", + "item": canonicalUrl }) - }) + } const defaultDescription = 'textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.' - const ogTitle = pageData.title || 'textmode.js' + const ogTitle = pageData.title ? `${pageData.title} | textmode.js` : 'textmode.js' const ogDescription = pageData.description || defaultDescription const ogImage = 'https://code.textmode.art/png/readme-og.png' - // Detect page types - const isBlogPost = pageData.relativePath.startsWith('blog/') && pageData.relativePath !== 'blog/index.md'; - const isHomePage = pageData.relativePath === 'index.md'; - const isDocsPage = pageData.relativePath.startsWith('docs/') && !pageData.relativePath.endsWith('/index.md'); - - const ogType = isBlogPost ? 'article' : 'website'; - const head: HeadConfig[] = [ ['link', { rel: 'canonical', href: canonicalUrl }], // Meta ['meta', { name: 'description', content: ogDescription }], ['meta', { name: 'robots', content: 'index, follow' }], // Open Graph - ['meta', { property: 'og:type', content: ogType }], + ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'textmode.js' }], ['meta', { property: 'og:locale', content: 'en_US' }], ['meta', { property: 'og:title', content: ogTitle }], @@ -86,14 +165,14 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { ] // Add Keywords (from frontmatter or default) - const defaultKeywords = 'textmode, ascii art, creative coding, webgl, javascript library, generative art, real-time, canvas, visualization, retro, 8-bit, terminal, petscii, live coding'; - const keywords = pageData.frontmatter.keywords || defaultKeywords; + const defaultKeywords = 'textmode, ascii art, creative coding, webgl, javascript library, generative art, real-time, canvas, visualization, retro, 8-bit, terminal, petscii, live coding' + const keywords = pageData.frontmatter.keywords || defaultKeywords if (keywords) { - head.push(['meta', { name: 'keywords', content: keywords }]); + head.push(['meta', { name: 'keywords', content: keywords }]) } - // Schema: Home page with consolidated @graph + // Schema: Home page if (isHomePage) { head.push(['script', { type: 'application/ld+json' }, JSON.stringify({ "@context": "https://schema.org", @@ -101,7 +180,7 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { { "@type": "Organization", "name": "textmode.js", - "url": "https://code.textmode.art", + "url": "https://code.textmode.art/", "logo": "https://code.textmode.art/svg/doc_logo.svg", "sameAs": [ "https://github.com/humanbydefinition/textmode.js", @@ -111,12 +190,8 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { { "@type": "WebSite", "name": "textmode.js", - "url": "https://code.textmode.art", - "potentialAction": { - "@type": "SearchAction", - "target": "https://code.textmode.art/search?q={search_term_string}", - "query-input": "required name=search_term_string" - } + "url": "https://code.textmode.art/", + "description": defaultDescription }, { "@type": "SoftwareApplication", @@ -128,65 +203,42 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { "price": "0", "priceCurrency": "USD" }, - "description": "textmode.js is a lightweight creative coding library for creating real-time ASCII art on the web.", - "url": "https://code.textmode.art" - }, - { - "@type": "BreadcrumbList", - "itemListElement": breadcrumbItems + "description": defaultDescription, + "url": "https://code.textmode.art/" } ] - })]); + })]) } - // Schema: Blog posts - else if (isBlogPost) { + // Schema: Documentation & API Reference Pages (TechArticle) + else if (isDocsPage || isDocsIndex || isApiPage || isApiIndex) { head.push(['script', { type: 'application/ld+json' }, JSON.stringify({ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": breadcrumbItems - })]); - head.push(['script', { type: 'application/ld+json' }, JSON.stringify({ - "@context": "https://schema.org", - "@type": "BlogPosting", - "headline": pageData.title, - "image": [ogImage], - "datePublished": pageData.frontmatter.date ? new Date(pageData.frontmatter.date).toISOString() : undefined, - "dateModified": pageData.lastUpdated ? new Date(pageData.lastUpdated).toISOString() : undefined, - "author": [{ - "@type": "Person", - "name": pageData.frontmatter.author || "textmode.js team", - "url": "https://code.textmode.art" - }] - })]); - } - // Schema: Documentation pages (TechArticle) - else if (isDocsPage) { - head.push(['script', { type: 'application/ld+json' }, JSON.stringify({ - "@context": "https://schema.org", - "@type": "BreadcrumbList", - "itemListElement": breadcrumbItems - })]); + })]) + const techArticle: Record = { "@context": "https://schema.org", "@type": "TechArticle", - "headline": pageData.title, + "headline": pageData.title || "textmode.js Documentation", "description": ogDescription, "author": { "@type": "Organization", "name": "textmode.js", - "url": "https://code.textmode.art" + "url": "https://code.textmode.art/" }, "publisher": { "@type": "Organization", "name": "textmode.js", - "url": "https://code.textmode.art" + "url": "https://code.textmode.art/" }, "url": canonicalUrl - }; + } + if (pageData.lastUpdated) { - techArticle.dateModified = new Date(pageData.lastUpdated).toISOString(); + techArticle.dateModified = new Date(pageData.lastUpdated).toISOString() } - head.push(['script', { type: 'application/ld+json' }, JSON.stringify(techArticle)]); + head.push(['script', { type: 'application/ld+json' }, JSON.stringify(techArticle)]) } // Schema: All other pages else { @@ -194,8 +246,8 @@ export const transformHead = ({ pageData }: TransformContext): HeadConfig[] => { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": breadcrumbItems - })]); + })]) } - return head; + return head } diff --git a/.vitepress/configs/index.mts b/.vitepress/configs/index.mts index 450ba06a..a23198e9 100644 --- a/.vitepress/configs/index.mts +++ b/.vitepress/configs/index.mts @@ -1,4 +1,4 @@ // Re-export all config modules for convenient importing export { head, transformHead } from './head.mts' export { nav } from './nav.mts' -export { sidebar, blog } from './sidebar.mts' +export { sidebar } from './sidebar.mts' diff --git a/.vitepress/configs/nav.mts b/.vitepress/configs/nav.mts index a7c61439..feaae48d 100644 --- a/.vitepress/configs/nav.mts +++ b/.vitepress/configs/nav.mts @@ -3,9 +3,8 @@ import type { DefaultTheme } from 'vitepress' export const nav: DefaultTheme.NavItem[] = [ { text: 'Home', link: '/' }, { text: 'Gallery', link: '/gallery' }, - { text: 'Documentation', link: '/docs/introduction' }, + { text: 'Documentation', link: '/docs/' }, { text: 'API', link: '/api/' }, - /** { text: 'Blog', link: '/blog/' }, **/ { text: 'Support ♥', link: '/docs/support' }, { text: 'Web Editor', link: 'https://editor.textmode.art' }, ] diff --git a/.vitepress/configs/sidebar.mts b/.vitepress/configs/sidebar.mts index 40140b31..04977290 100644 --- a/.vitepress/configs/sidebar.mts +++ b/.vitepress/configs/sidebar.mts @@ -5,13 +5,6 @@ import typedocSidebarFilters from "../../api/textmode.filters.js/typedoc-sidebar import typedocSidebarExport from "../../api/textmode.export.js/typedoc-sidebar.json"; import typedocSidebarFiglet from "../../api/textmode.figlet.js/typedoc-sidebar.json"; import typedocSidebarOverlay from "../../api/textmode.overlay.js/typedoc-sidebar.json"; -import { defineBlogConfig } from "vitepress-plugin-blog/config"; - -export const blog = defineBlogConfig({ - sidebar: { - recentPostsCount: 5, - }, -}); const docsSidebar: DefaultTheme.SidebarItem[] = [ { @@ -19,8 +12,9 @@ const docsSidebar: DefaultTheme.SidebarItem[] = [ items: [ { text: "Start", + link: "/docs/", items: [ - { text: "Introduction", link: "/docs/introduction" }, + { text: "Introduction", link: "/docs/" }, { text: "Installation", link: "/docs/installation" }, { text: "First sketch", link: "/docs/first-sketch" }, { text: "Examples", link: "/docs/examples" }, @@ -167,5 +161,4 @@ const apiSidebar: DefaultTheme.SidebarItem[] = [ export const sidebar: DefaultTheme.Sidebar = { "/docs/": docsSidebar, "/api/": apiSidebar, - "/blog/": blog.sidebar, }; diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index f1b9e418..95851ed1 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -28,9 +28,6 @@ import ExampleSketchBrowser from './components/ExampleSketchBrowser/ExampleSketc import { GalleryGrid } from './components/Gallery' import { PromoCallout } from './components/PromoCallout' -// Blog -import { withBlogTheme } from 'vitepress-plugin-blog' -import 'vitepress-plugin-blog/style.css' // Composables import { useComments } from './composables' @@ -122,6 +119,4 @@ const baseTheme: Theme = { } } -const theme = withBlogTheme(baseTheme) - -export default theme +export default baseTheme diff --git a/docs/contributing/code.md b/docs/contributing/code.md index 134eac31..9187faf3 100644 --- a/docs/contributing/code.md +++ b/docs/contributing/code.md @@ -11,7 +11,7 @@ Use this guide for changes to `textmode.js` and its official add-ons. | Project | Use it for | Documentation | | --- | --- | --- | -| [`textmode.js`](https://github.com/humanbydefinition/textmode.js) | Core APIs, rendering, layers, fonts, media, input, and plugins | [Guides](/docs/introduction) · [API](/api/textmode.js/) | +| [`textmode.js`](https://github.com/humanbydefinition/textmode.js) | Core APIs, rendering, layers, fonts, media, input, and plugins | [Guides](/docs/) · [API](/api/textmode.js/) | | [`textmode.export.js`](https://github.com/humanbydefinition/textmode.export.js) | Image, document, and animation export | [Guide](/docs/exporting) · [API](/api/textmode.export.js/) | | [`textmode.synth.js`](https://github.com/humanbydefinition/textmode.synth.js) | Synth sources, transforms, and shader composition | [Guide](/docs/live-coding-editor-textmode-art) · [API](/api/textmode.synth.js/) | | [`textmode.figlet.js`](https://github.com/humanbydefinition/textmode.figlet.js) | FIGfont parsing, layout, and rendering | [Guide](/docs/figlet-typography) · [API](/api/textmode.figlet.js/) | diff --git a/docs/introduction.md b/docs/index.md similarity index 100% rename from docs/introduction.md rename to docs/index.md diff --git a/index.md b/index.md index 9e870924..e75d2c19 100644 --- a/index.md +++ b/index.md @@ -9,7 +9,7 @@ hero: actions: - theme: brand text: Get started - link: /docs/introduction + link: /docs/ - theme: alt text: View examples link: /docs/examples diff --git a/public/robots.txt b/public/robots.txt index e89abcb0..b430fae6 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,7 @@ User-agent: * Allow: / +Disallow: /blog/ +Disallow: /blog Sitemap: https://code.textmode.art/sitemap.xml +