From e5f6ea0b1350ae9dd13369de9b64aa16a71c6b77 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 17 Jun 2025 10:16:46 +0200 Subject: [PATCH] Add canonicals to pages migrated to docs.mermaidchart.com --- .../src/docs/.vitepress/canonical-config.ts | 149 +++++++++++++ .../src/docs/.vitepress/canonical-urls.ts | 198 ++++++++++++++++++ .../mermaid/src/docs/.vitepress/config.ts | 2 + 3 files changed, 349 insertions(+) create mode 100644 packages/mermaid/src/docs/.vitepress/canonical-config.ts create mode 100644 packages/mermaid/src/docs/.vitepress/canonical-urls.ts diff --git a/packages/mermaid/src/docs/.vitepress/canonical-config.ts b/packages/mermaid/src/docs/.vitepress/canonical-config.ts new file mode 100644 index 000000000..7c2b5aaeb --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/canonical-config.ts @@ -0,0 +1,149 @@ +import type { CanonicalUrlConfig } from './canonical-urls.js'; + +/** + * Canonical URL configuration for Mermaid documentation + * + * This file contains the configuration for generating canonical URLs + * for the Mermaid documentation site. + */ +export const canonicalConfig: CanonicalUrlConfig = { + // Base URL for the Mermaid documentation site + baseUrl: 'https://docs.mermaidchart.com', + + // Disable automatic generation - only use specificCanonicalUrls + autoGenerate: false, + + // Patterns for pages to exclude from automatic canonical URL generation + excludePatterns: [ + // Exclude the main index page (handled by VitePress home layout) + 'index.md', + + // Exclude any draft or temporary files + 'draft-*.md', + '**/draft-*.md', + 'temp-*.md', + '**/temp-*.md', + '*.draft.md', + '**/*.draft.md', + + // Exclude any test files + '*.test.md', + '**/*.test.md', + '*.spec.md', + '**/*.spec.md', + + // You can add more patterns here as needed + // Examples: + // '**/internal/**', // Exclude internal documentation + // '**/archive/**', // Exclude archived content + ], + + // URL transformation rules + transformations: { + // Remove index.md from URLs (e.g., /intro/index.md -> /intro/) + //removeIndex: true, + + // Remove .md extension from URLs (e.g., /syntax/flowchart.md -> /syntax/flowchart) + removeMarkdownExtension: true, + + // Custom path transformations + customTransforms: [ + // Example: Redirect old paths to new paths + // { pattern: /^old-syntax\//, replacement: 'syntax/' }, + // Example: Handle special cases + // { pattern: /^config\/setup\/README$/, replacement: 'config/setup/' }, + // Add your custom transformations here + ], + }, +}; + +/** + * Pages that should have specific canonical URLs + * + * Since autoGenerate is set to false, ONLY pages listed here will get canonical URLs. + * + * Usage: Add entries to this object where the key is the relative path + * of the markdown file and the value is the desired canonical URL. + * + * Examples: + * - 'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html' + * - 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html' + * - 'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html' + */ +export const specificCanonicalUrls: Record = { + // Add your specific canonical URLs here + // Example: + // 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html', + + // Intro section + 'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html', + 'intro/getting-started.md': + 'https://docs.mermaidchart.com/mermaid-oss/intro/getting-started.html', + 'intro/syntax-reference.md': + 'https://docs.mermaidchart.com/mermaid-oss/intro/syntax-reference.html', + + // Syntax section + 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html', + 'syntax/sequenceDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/sequenceDiagram.html', + 'syntax/classDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/classDiagram.html', + 'syntax/stateDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/stateDiagram.html', + 'syntax/entityRelationshipDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/entityRelationshipDiagram.html', + 'syntax/userJourney.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/userJourney.html', + 'syntax/gantt.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gantt.html', + 'syntax/pie.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/pie.html', + 'syntax/quadrantChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/quadrantChart.html', + 'syntax/requirementDiagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/syntax/requirementDiagram.html', + 'syntax/mindmap.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/mindmap.html', + 'syntax/timeline.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/timeline.html', + 'syntax/gitgraph.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gitgraph.html', + 'syntax/c4.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/c4.html', + 'syntax/sankey.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/sankey.html', + 'syntax/xyChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/xyChart.html', + 'syntax/block.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/block.html', + 'syntax/packet.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/packet.html', + 'syntax/kanban.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/kanban.html', + 'syntax/architecture.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/architecture.html', + 'syntax/radar.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/radar.html', + 'syntax/examples.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/examples.html', + + // Config section + 'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html', + 'config/usage.md': 'https://docs.mermaidchart.com/mermaid-oss/config/usage.html', + 'config/icons.md': 'https://docs.mermaidchart.com/mermaid-oss/config/icons.html', + 'config/directives.md': 'https://docs.mermaidchart.com/mermaid-oss/config/directives.html', + 'config/theming.md': 'https://docs.mermaidchart.com/mermaid-oss/config/theming.html', + 'config/math.md': 'https://docs.mermaidchart.com/mermaid-oss/config/math.html', + 'config/accessibility.md': 'https://docs.mermaidchart.com/mermaid-oss/config/accessibility.html', + 'config/mermaidCLI.md': 'https://docs.mermaidchart.com/mermaid-oss/config/mermaidCLI.html', + 'config/faq.md': 'https://docs.mermaidchart.com/mermaid-oss/config/faq.html', + + // Ecosystem section + 'ecosystem/mermaid-chart.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/mermaid-chart.html', + 'ecosystem/tutorials.md': 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/tutorials.html', + 'ecosystem/integrations-community.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-community.html', + 'ecosystem/integrations-create.md': + 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-create.html', + + // Community section + 'community/intro.md': 'https://docs.mermaidchart.com/mermaid-oss/community/intro.html', + 'community/contributing.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/contributing.html', + 'community/new-diagram.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/new-diagram.html', + 'community/questions-and-suggestions.md': + 'https://docs.mermaidchart.com/mermaid-oss/community/questions-and-suggestions.html', + 'community/security.md': 'https://docs.mermaidchart.com/mermaid-oss/community/security.html', +}; + +/** + * Helper function to get canonical URL for a specific page + * This can be used in frontmatter or for manual overrides + */ +export function getCanonicalUrl(relativePath: string): string | undefined { + return specificCanonicalUrls[relativePath]; +} diff --git a/packages/mermaid/src/docs/.vitepress/canonical-urls.ts b/packages/mermaid/src/docs/.vitepress/canonical-urls.ts new file mode 100644 index 000000000..6c04e1eb1 --- /dev/null +++ b/packages/mermaid/src/docs/.vitepress/canonical-urls.ts @@ -0,0 +1,198 @@ +import type { PageData } from 'vitepress'; +import { canonicalConfig, specificCanonicalUrls } from './canonical-config.js'; + +/** + * Configuration for canonical URL generation + */ +export interface CanonicalUrlConfig { + /** Base URL for the site (e.g., 'https://mermaid.js.org') */ + baseUrl: string; + /** Whether to automatically generate canonical URLs for pages without explicit ones */ + autoGenerate: boolean; + /** Pages to exclude from automatic canonical URL generation (glob patterns supported) */ + excludePatterns?: string[]; + /** Custom URL transformations */ + transformations?: { + /** Remove index.md from URLs */ + removeIndex?: boolean; + /** Remove .md extension from URLs */ + removeMarkdownExtension?: boolean; + /** Custom path transformations */ + customTransforms?: { + pattern: RegExp; + replacement: string; + }[]; + }; +} + +/** + * Default configuration for canonical URLs + */ +const defaultConfig: CanonicalUrlConfig = { + baseUrl: 'https://mermaid.js.org', + autoGenerate: true, + excludePatterns: [ + // Exclude the home page as it's handled separately + 'index.md', + // Exclude any temporary or draft files + '**/draft-*', + '**/temp-*', + ], + transformations: { + removeIndex: true, + removeMarkdownExtension: true, + customTransforms: [ + // Transform any special cases here + // Example: { pattern: /^old-path\//, replacement: 'new-path/' } + ], + }, +}; + +/** + * Check if a path matches any of the exclude patterns + */ +function shouldExcludePath(relativePath: string, excludePatterns: string[] = []): boolean { + return excludePatterns.some((pattern) => { + // Convert glob pattern to regex + const regexPattern = pattern + .replace(/\*\*/g, '.*') + .replace(/\*/g, '[^/]*') + .replace(/\?/g, '.') + .replace(/\./g, '\\.'); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(relativePath); + }); +} + +/** + * Transform a relative path to a canonical URL path + */ +function transformPath(relativePath: string, config: CanonicalUrlConfig): string { + let transformedPath = relativePath; + + // Apply built-in transformations + if (config.transformations?.removeMarkdownExtension) { + transformedPath = transformedPath.replace(/\.md$/, ''); + } + + if (config.transformations?.removeIndex) { + transformedPath = transformedPath.replace(/\/index$/, '/'); + if (transformedPath === 'index') { + transformedPath = ''; + } + } + + // Apply custom transformations + if (config.transformations?.customTransforms) { + for (const transform of config.transformations.customTransforms) { + transformedPath = transformedPath.replace(transform.pattern, transform.replacement); + } + } + + // Ensure path starts with / + if (transformedPath && !transformedPath.startsWith('/')) { + transformedPath = '/' + transformedPath; + } + + // Handle root path + if (!transformedPath) { + transformedPath = '/'; + } + + return transformedPath; +} + +/** + * Generate a canonical URL for a page + */ +function generateCanonicalUrl(relativePath: string, config: CanonicalUrlConfig): string { + const transformedPath = transformPath(relativePath, config); + return config.baseUrl + transformedPath; +} + +/** + * VitePress transformPageData hook to add canonical URLs + */ +export function addCanonicalUrls(pageData: PageData): void { + const config = canonicalConfig; + + // Check for specific canonical URLs first + const specificUrl = specificCanonicalUrls[pageData.relativePath]; + if (specificUrl) { + addCanonicalToHead(pageData, specificUrl); + return; + } + + // Skip if canonical URL is already explicitly set in frontmatter + if (pageData.frontmatter.canonical) { + // If it's already a full URL, use as-is + if (pageData.frontmatter.canonical.startsWith('http')) { + addCanonicalToHead(pageData, pageData.frontmatter.canonical); + return; + } + // If it's a relative path, convert to absolute URL + const canonicalUrl = config.baseUrl + pageData.frontmatter.canonical; + addCanonicalToHead(pageData, canonicalUrl); + return; + } + + // Skip if canonicalPath is set in frontmatter + if (pageData.frontmatter.canonicalPath) { + const canonicalUrl = config.baseUrl + pageData.frontmatter.canonicalPath; + addCanonicalToHead(pageData, canonicalUrl); + return; + } + + // Skip if auto-generation is disabled + if (!config.autoGenerate) { + return; + } + + // Skip if path should be excluded + if (shouldExcludePath(pageData.relativePath, config.excludePatterns)) { + return; + } + + // Generate canonical URL + const canonicalUrl = generateCanonicalUrl(pageData.relativePath, config); + addCanonicalToHead(pageData, canonicalUrl); +} + +/** + * Add canonical URL to page head + */ +function addCanonicalToHead(pageData: PageData, canonicalUrl: string): void { + // Initialize head array if it doesn't exist + pageData.frontmatter.head = pageData.frontmatter.head || []; + + // Check if canonical link already exists + const hasCanonical = pageData.frontmatter.head.some( + (item: any) => Array.isArray(item) && item[0] === 'link' && item[1]?.rel === 'canonical' + ); + + // Add canonical link if it doesn't exist + if (!hasCanonical) { + pageData.frontmatter.head.push(['link', { rel: 'canonical', href: canonicalUrl }]); + } +} + +/** + * Utility function to create a custom configuration + * This can be used to override the default configuration + */ +export function createCanonicalUrlConfig( + customConfig: Partial +): CanonicalUrlConfig { + return { + ...defaultConfig, + ...customConfig, + transformations: { + ...defaultConfig.transformations, + ...customConfig.transformations, + customTransforms: [ + ...(defaultConfig.transformations?.customTransforms || []), + ...(customConfig.transformations?.customTransforms || []), + ], + }, + }; +} diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index cd66316a3..cdc877c16 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -2,6 +2,7 @@ import type { MarkdownOptions } from 'vitepress'; import { defineConfig } from 'vitepress'; import packageJson from '../../../package.json' assert { type: 'json' }; import MermaidExample from './mermaid-markdown-all.js'; +import { addCanonicalUrls } from './canonical-urls.js'; const allMarkdownTransformers: MarkdownOptions = { // the shiki theme to highlight code blocks @@ -25,6 +26,7 @@ export default defineConfig({ // ignore all localhost links /^https?:\/\/localhost/, ], + transformPageData: addCanonicalUrls, head: [ ['link', { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], ['meta', { property: 'og:title', content: 'Mermaid' }],