@x-wave/blog 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-wave/blog",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -20,8 +20,8 @@
20
20
  "types": "./types/index.d.ts"
21
21
  },
22
22
  "./vite-config": {
23
- "import": "./vite-config/index.ts",
24
- "types": "./vite-config/index.ts"
23
+ "import": "./vite-config/index.js",
24
+ "types": "./vite-config/index.d.ts"
25
25
  },
26
26
  "./styles": {
27
27
  "import": "./styles/index.css"
@@ -0,0 +1,15 @@
1
+ import type { BlogDiscoveryResult, BlogPostMetadata, BlogSSGConfig } from './types.js';
2
+ /**
3
+ * Discover all blog posts from Vite glob import
4
+ *
5
+ * @param mdxFiles - Object from import.meta.glob('./docs/**\/*.mdx', { query: '?raw', import: 'default', eager: false })
6
+ * @param config - SSG configuration options
7
+ * @returns Collection of discovered blog posts organized by language and slug
8
+ */
9
+ export declare function discoverBlogPosts(mdxFiles: Record<string, () => Promise<unknown>>, _config?: BlogSSGConfig): Promise<BlogDiscoveryResult>;
10
+ /**
11
+ * Filter blog posts by specific criteria
12
+ * Useful for separating blog posts from docs
13
+ */
14
+ export declare function filterBlogPosts(posts: BlogPostMetadata[], predicate: (post: BlogPostMetadata) => boolean): BlogPostMetadata[];
15
+ //# sourceMappingURL=blog-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-discovery.d.ts","sourceRoot":"","sources":["../src/blog-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,MAAM,YAAY,CAAA;AAgFnB;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,EAChD,OAAO,GAAE,aAAkB,GACzB,OAAO,CAAC,mBAAmB,CAAC,CAqD9B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,KAAK,EAAE,gBAAgB,EAAE,EACzB,SAAS,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,OAAO,GAC5C,gBAAgB,EAAE,CAEpB"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Parse YAML frontmatter from MDX content
3
+ */
4
+ function parseFrontmatter(content) {
5
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
6
+ if (!match)
7
+ return {};
8
+ const frontmatter = {};
9
+ const frontmatterText = match[1];
10
+ let currentKey = '';
11
+ let isArrayContext = false;
12
+ const arrayValues = [];
13
+ for (const line of frontmatterText.split('\n')) {
14
+ const trimmed = line.trim();
15
+ // Handle array items (lines starting with -)
16
+ if (trimmed.startsWith('-')) {
17
+ if (isArrayContext) {
18
+ const value = trimmed.substring(1).trim();
19
+ arrayValues.push(value);
20
+ }
21
+ continue;
22
+ }
23
+ // If we were in array context and hit a non-array line, save the array
24
+ if (isArrayContext && !trimmed.startsWith('-')) {
25
+ frontmatter[currentKey] = arrayValues.slice();
26
+ arrayValues.length = 0;
27
+ isArrayContext = false;
28
+ }
29
+ // Handle key-value pairs
30
+ if (trimmed?.includes(':')) {
31
+ const [key, ...valueParts] = trimmed.split(':');
32
+ const value = valueParts.join(':').trim();
33
+ currentKey = key.trim();
34
+ // If value is empty, this might be an array declaration
35
+ if (!value) {
36
+ isArrayContext = true;
37
+ continue;
38
+ }
39
+ // Parse boolean values
40
+ if (value === 'true')
41
+ frontmatter[currentKey] = true;
42
+ else if (value === 'false')
43
+ frontmatter[currentKey] = false;
44
+ else
45
+ frontmatter[currentKey] = value;
46
+ }
47
+ }
48
+ // Save any remaining array
49
+ if (isArrayContext) {
50
+ frontmatter[currentKey] = arrayValues.slice();
51
+ }
52
+ return frontmatter;
53
+ }
54
+ /**
55
+ * Extract language from file path based on doc structure
56
+ * Expected: docs/[language]/[slug].mdx
57
+ */
58
+ function extractLanguageFromPath(filePath) {
59
+ const match = filePath.match(/\/docs\/([a-z]{2})\//);
60
+ return match ? match[1] : 'en';
61
+ }
62
+ /**
63
+ * Extract slug from file path
64
+ * Expected: docs/[language]/[slug].mdx
65
+ */
66
+ function extractSlugFromPath(filePath) {
67
+ const match = filePath.match(/\/([a-z0-9-]+)\.mdx$/);
68
+ return match ? match[1] : '';
69
+ }
70
+ /**
71
+ * Discover all blog posts from Vite glob import
72
+ *
73
+ * @param mdxFiles - Object from import.meta.glob('./docs/**\/*.mdx', { query: '?raw', import: 'default', eager: false })
74
+ * @param config - SSG configuration options
75
+ * @returns Collection of discovered blog posts organized by language and slug
76
+ */
77
+ export async function discoverBlogPosts(mdxFiles, _config = {}) {
78
+ const posts = [];
79
+ const postsByLanguage = {};
80
+ const postsBySlug = {};
81
+ // Iterate through all MDX files
82
+ for (const [filePath, getContent] of Object.entries(mdxFiles)) {
83
+ try {
84
+ // Load content
85
+ const content = (await getContent());
86
+ // Extract language and slug
87
+ const language = extractLanguageFromPath(filePath);
88
+ const slug = extractSlugFromPath(filePath);
89
+ if (!slug)
90
+ continue;
91
+ // Parse frontmatter
92
+ const frontmatter = parseFrontmatter(content);
93
+ // Create metadata object
94
+ const metadata = {
95
+ slug,
96
+ title: frontmatter.title || slug,
97
+ description: frontmatter.description,
98
+ ogImage: frontmatter.ogImage,
99
+ keywords: frontmatter.keywords,
100
+ date: frontmatter.date,
101
+ author: frontmatter.author,
102
+ language,
103
+ filePath,
104
+ };
105
+ posts.push(metadata);
106
+ // Index by language
107
+ if (!postsByLanguage[language]) {
108
+ postsByLanguage[language] = [];
109
+ }
110
+ postsByLanguage[language].push(metadata);
111
+ // Index by slug (use [language-slug] composite key to avoid conflicts)
112
+ postsBySlug[`${language}:${slug}`] = metadata;
113
+ }
114
+ catch (error) {
115
+ console.warn(`Warning: Failed to process ${filePath}:`, error);
116
+ }
117
+ }
118
+ return {
119
+ posts,
120
+ postsByLanguage,
121
+ postsBySlug,
122
+ };
123
+ }
124
+ /**
125
+ * Filter blog posts by specific criteria
126
+ * Useful for separating blog posts from docs
127
+ */
128
+ export function filterBlogPosts(posts, predicate) {
129
+ return posts.filter(predicate);
130
+ }
131
+ //# sourceMappingURL=blog-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-discovery.js","sourceRoot":"","sources":["../src/blog-discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { discoverBlogPosts, filterBlogPosts } from './blog-discovery.js';
2
+ export { generateHtmlWithMetaTags, generateMetaTags, generateMetaTagsObject, type MetaTagsOptions, } from './meta-tags.js';
3
+ export { type SetupSSGOptions, setupSSG } from './setup-ssg.js';
4
+ export { createStaticGenPlugin, type StaticGenPluginOptions, } from './static-gen-plugin.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAGxE,OAAO,EACN,wBAAwB,EACxB,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,eAAe,GACpB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE/D,OAAO,EACN,qBAAqB,EACrB,KAAK,sBAAsB,GAC3B,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,9 @@
1
+ // Blog discovery
2
+ export { discoverBlogPosts, filterBlogPosts } from './blog-discovery.js';
3
+ // Meta tag generation
4
+ export { generateHtmlWithMetaTags, generateMetaTags, generateMetaTagsObject, } from './meta-tags.js';
5
+ // SSG setup utilities (recommended for consumers)
6
+ export { setupSSG } from './setup-ssg.js';
7
+ // Vite plugin
8
+ export { createStaticGenPlugin, } from './static-gen-plugin.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iBAAiB;AACjB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAExE,sBAAsB;AACtB,OAAO,EACN,wBAAwB,EACxB,gBAAgB,EAChB,sBAAsB,GAEtB,MAAM,gBAAgB,CAAA;AACvB,kDAAkD;AAClD,OAAO,EAAwB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC/D,cAAc;AACd,OAAO,EACN,qBAAqB,GAErB,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,28 @@
1
+ import type { BlogPostMetadata } from './types.js';
2
+ export interface MetaTagsOptions {
3
+ /** Base URL for absolute OG image URLs */
4
+ baseUrl?: string;
5
+ /** Default OG image URL if post doesn't have one */
6
+ defaultOgImage?: string;
7
+ /** Site title for Twitter Card */
8
+ siteTitle?: string;
9
+ /** Site name for OG tags */
10
+ siteName?: string;
11
+ /** Twitter handle for Twitter Card */
12
+ twitterHandle?: string;
13
+ }
14
+ /**
15
+ * Generate meta tags HTML string from blog post metadata
16
+ */
17
+ export declare function generateMetaTags(post: BlogPostMetadata, options?: MetaTagsOptions): string;
18
+ /**
19
+ * Generate an HTML template with injected meta tags
20
+ * Used for static page generation
21
+ */
22
+ export declare function generateHtmlWithMetaTags(baseHtml: string, post: BlogPostMetadata, options?: MetaTagsOptions): string;
23
+ /**
24
+ * Generate meta tags as a JSON object for dynamic injection
25
+ * Useful if you want to update meta tags on client-side navigation
26
+ */
27
+ export declare function generateMetaTagsObject(post: BlogPostMetadata, options?: MetaTagsOptions): Record<string, string>;
28
+ //# sourceMappingURL=meta-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-tags.d.ts","sourceRoot":"","sources":["../src/meta-tags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,WAAW,eAAe;IAC/B,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,eAAoB,GAC3B,MAAM,CA4DR;AAgBD;;;GAGG;AACH,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,eAAoB,GAC3B,MAAM,CA8BR;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,eAAoB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCxB"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Generate meta tags HTML string from blog post metadata
3
+ */
4
+ export function generateMetaTags(post, options = {}) {
5
+ const { baseUrl = 'https://docs.staking.polkadot.cloud', defaultOgImage = '/img/og-image.png', siteTitle = 'Polkadot Cloud Staking', siteName = 'Polkadot Cloud Staking Documentation', twitterHandle = '@PolkadotCloud', } = options;
6
+ const postUrl = `${baseUrl}/${post.language}/docs/${post.slug}`;
7
+ const ogImage = post.ogImage
8
+ ? `${baseUrl}${post.ogImage.startsWith('/') ? '' : '/'}${post.ogImage}`
9
+ : `${baseUrl}${defaultOgImage}`;
10
+ // Normalize keywords
11
+ let keywordsString = '';
12
+ if (post.keywords) {
13
+ if (Array.isArray(post.keywords)) {
14
+ keywordsString = post.keywords.join(', ');
15
+ }
16
+ else {
17
+ keywordsString = post.keywords;
18
+ }
19
+ }
20
+ const tags = [
21
+ // Basic meta tags
22
+ `<meta name="description" content="${escapeHtml(post.description || siteTitle)}">`,
23
+ `<meta name="viewport" content="width=device-width, initial-scale=1.0">`,
24
+ // Open Graph (OG) tags
25
+ `<meta property="og:type" content="article">`,
26
+ `<meta property="og:title" content="${escapeHtml(post.title)} | ${escapeHtml(siteName)}">`,
27
+ `<meta property="og:description" content="${escapeHtml(post.description || siteTitle)}">`,
28
+ `<meta property="og:url" content="${escapeHtml(postUrl)}">`,
29
+ `<meta property="og:image" content="${escapeHtml(ogImage)}">`,
30
+ `<meta property="og:site_name" content="${escapeHtml(siteName)}">`,
31
+ // Article-specific meta tags
32
+ `<meta property="article:published_time" content="${post.date || new Date().toISOString()}">`,
33
+ post.author
34
+ ? `<meta property="article:author" content="${escapeHtml(post.author)}">`
35
+ : '',
36
+ // Twitter Card tags
37
+ `<meta name="twitter:card" content="summary_large_image">`,
38
+ `<meta name="twitter:site" content="${twitterHandle}">`,
39
+ `<meta name="twitter:title" content="${escapeHtml(post.title)} | ${escapeHtml(siteName)}">`,
40
+ `<meta name="twitter:description" content="${escapeHtml(post.description || siteTitle)}">`,
41
+ `<meta name="twitter:image" content="${escapeHtml(ogImage)}">`,
42
+ // Keywords
43
+ keywordsString
44
+ ? `<meta name="keywords" content="${escapeHtml(keywordsString)}">`
45
+ : '',
46
+ // Canonical URL
47
+ `<link rel="canonical" href="${escapeHtml(postUrl)}">`,
48
+ ];
49
+ return tags.filter(Boolean).join('\n\t');
50
+ }
51
+ /**
52
+ * Escape HTML special characters in attribute values
53
+ */
54
+ function escapeHtml(text) {
55
+ const map = {
56
+ '&': '&amp;',
57
+ '<': '&lt;',
58
+ '>': '&gt;',
59
+ '"': '&quot;',
60
+ "'": '&#039;',
61
+ };
62
+ return text.replace(/[&<>"']/g, (char) => map[char]);
63
+ }
64
+ /**
65
+ * Generate an HTML template with injected meta tags
66
+ * Used for static page generation
67
+ */
68
+ export function generateHtmlWithMetaTags(baseHtml, post, options = {}) {
69
+ const metaTags = generateMetaTags(post, options);
70
+ // Remove default OG and Twitter meta tags to replace them with custom ones
71
+ let html = baseHtml
72
+ // Remove default og:* meta tags (handles multiline)
73
+ .replace(/<meta\s+property="og:[^"]*"[^>]*>/gis, '')
74
+ // Remove default article:* meta tags (handles multiline)
75
+ .replace(/<meta\s+property="article:[^"]*"[^>]*>/gis, '')
76
+ // Remove default twitter:* meta tags with either 'name' or 'property' attribute (handles multiline)
77
+ .replace(/<meta\s+(?:name|property)="twitter:[^"]*"[^>]*>/gis, '')
78
+ // Remove default description tag
79
+ .replace(/<meta\s+name="description"[^>]*>/gis, '')
80
+ // Remove canonical link if present
81
+ .replace(/<link\s+rel="canonical"[^>]*>/gis, '');
82
+ // Clean up excess blank lines left by removed tags
83
+ html = html.replace(/\n\s*\n\s*\n/g, '\n\n');
84
+ // Remove orphaned section comments followed by blank lines
85
+ html = html.replace(/\s*<!--\s*(?:Open Graph|Facebook|Twitter)[^>]*-->\s*\n/g, '');
86
+ // Final cleanup: remove any remaining multiple blank lines
87
+ html = html.replace(/\n\s*\n\s*\n/g, '\n\n');
88
+ // Inject custom meta tags before closing </head> tag
89
+ return html.replace('</head>', `\t${metaTags}\n</head>`);
90
+ }
91
+ /**
92
+ * Generate meta tags as a JSON object for dynamic injection
93
+ * Useful if you want to update meta tags on client-side navigation
94
+ */
95
+ export function generateMetaTagsObject(post, options = {}) {
96
+ const { baseUrl = 'https://docs.staking.polkadot.cloud', defaultOgImage = '/img/og-image.png', siteName = 'Polkadot Cloud Staking Documentation', } = options;
97
+ const postUrl = `${baseUrl}/${post.language}/docs/${post.slug}`;
98
+ const ogImage = post.ogImage
99
+ ? `${baseUrl}${post.ogImage.startsWith('/') ? '' : '/'}${post.ogImage}`
100
+ : `${baseUrl}${defaultOgImage}`;
101
+ // Normalize keywords
102
+ let keywordsArray = [];
103
+ if (post.keywords) {
104
+ if (Array.isArray(post.keywords)) {
105
+ keywordsArray = post.keywords;
106
+ }
107
+ else {
108
+ keywordsArray = post.keywords.split(',').map((k) => k.trim());
109
+ }
110
+ }
111
+ return {
112
+ description: post.description || siteName,
113
+ 'og:type': 'article',
114
+ 'og:title': `${post.title} | ${siteName}`,
115
+ 'og:description': post.description || siteName,
116
+ 'og:url': postUrl,
117
+ 'og:image': ogImage,
118
+ 'og:site_name': siteName,
119
+ 'article:published_time': post.date || new Date().toISOString(),
120
+ ...(post.author && { 'article:author': post.author }),
121
+ 'twitter:card': 'summary_large_image',
122
+ 'twitter:title': `${post.title} | ${siteName}`,
123
+ 'twitter:description': post.description || siteName,
124
+ 'twitter:image': ogImage,
125
+ ...(keywordsArray.length > 0 && { keywords: keywordsArray.join(', ') }),
126
+ canonical: postUrl,
127
+ };
128
+ }
129
+ //# sourceMappingURL=meta-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-tags.js","sourceRoot":"","sources":["../src/meta-tags.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { PluginOption } from 'vite';
2
+ import type { MetaTagsOptions } from './meta-tags.js';
3
+ export interface SetupSSGOptions {
4
+ /** Path to the docs directory (e.g., 'src/docs') */
5
+ docsPath: string;
6
+ /** Base path segment for SSG output (e.g., 'blog') */
7
+ docsBase?: string;
8
+ /** Output directory for generated static pages */
9
+ outputDir?: string;
10
+ /** Meta tags configuration */
11
+ metaTagsOptions?: MetaTagsOptions;
12
+ }
13
+ /**
14
+ * Setup static site generation for blog posts
15
+ *
16
+ * This is a convenience function that handles the entire SSG setup process.
17
+ * It discovers blog posts from your docs directory and creates a Vite plugin.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { setupSSG } from '@x-wave/blog/vite-config'
22
+ *
23
+ * export default defineConfig(async (env) => {
24
+ * const ssgPlugin = env.command === 'build'
25
+ * ? await setupSSG({
26
+ * docsPath: 'src/docs',
27
+ * outputDir: 'dist/docs',
28
+ * metaTagsOptions: {
29
+ * baseUrl: 'https://example.com',
30
+ * siteName: 'My Documentation',
31
+ * }
32
+ * })
33
+ * : undefined
34
+ *
35
+ * return {
36
+ * plugins: [react(), ssgPlugin].filter(Boolean),
37
+ * // ... rest of config
38
+ * }
39
+ * })
40
+ * ```
41
+ */
42
+ export declare function setupSSG(options: SetupSSGOptions): Promise<PluginOption | undefined>;
43
+ //# sourceMappingURL=setup-ssg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-ssg.d.ts","sourceRoot":"","sources":["../src/setup-ssg.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAA;AAExC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAGrD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAA;IAChB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,eAAe,CAAC,EAAE,eAAe,CAAA;CACjC;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,QAAQ,CAC7B,OAAO,EAAE,eAAe,GACtB,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CA2BnC"}
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { discoverBlogPosts } from './blog-discovery.js';
4
+ import { createStaticGenPlugin } from './static-gen-plugin.js';
5
+ /**
6
+ * Read all MDX files from a docs directory
7
+ * @param docsDir - Path to the docs directory
8
+ * @returns Object mapping file paths to content loaders
9
+ */
10
+ function readMdxFiles(docsDir) {
11
+ const mdxFiles = {};
12
+ function walkDir(dir, relativePath = '') {
13
+ if (!fs.existsSync(dir))
14
+ return;
15
+ const files = fs.readdirSync(dir);
16
+ for (const file of files) {
17
+ const fullPath = path.join(dir, file);
18
+ const relPath = path.join(relativePath, file);
19
+ const stat = fs.statSync(fullPath);
20
+ if (stat.isDirectory()) {
21
+ walkDir(fullPath, relPath);
22
+ }
23
+ else if (file.endsWith('.mdx')) {
24
+ // Create a lazy loader function
25
+ mdxFiles[path.join(docsDir, relPath)] = async () => fs.readFileSync(fullPath, 'utf-8');
26
+ }
27
+ }
28
+ }
29
+ walkDir(docsDir);
30
+ return mdxFiles;
31
+ }
32
+ /**
33
+ * Setup static site generation for blog posts
34
+ *
35
+ * This is a convenience function that handles the entire SSG setup process.
36
+ * It discovers blog posts from your docs directory and creates a Vite plugin.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { setupSSG } from '@x-wave/blog/vite-config'
41
+ *
42
+ * export default defineConfig(async (env) => {
43
+ * const ssgPlugin = env.command === 'build'
44
+ * ? await setupSSG({
45
+ * docsPath: 'src/docs',
46
+ * outputDir: 'dist/docs',
47
+ * metaTagsOptions: {
48
+ * baseUrl: 'https://example.com',
49
+ * siteName: 'My Documentation',
50
+ * }
51
+ * })
52
+ * : undefined
53
+ *
54
+ * return {
55
+ * plugins: [react(), ssgPlugin].filter(Boolean),
56
+ * // ... rest of config
57
+ * }
58
+ * })
59
+ * ```
60
+ */
61
+ export async function setupSSG(options) {
62
+ try {
63
+ const { docsPath, docsBase, outputDir, metaTagsOptions = {} } = options;
64
+ const mdxFiles = readMdxFiles(docsPath);
65
+ const { posts } = await discoverBlogPosts(mdxFiles, {
66
+ blogContentPath: docsPath,
67
+ });
68
+ if (posts.length === 0) {
69
+ console.log('📝 No blog posts found, SSG disabled');
70
+ return undefined;
71
+ }
72
+ console.log(`📝 Discovered ${posts.length} blog posts for SSG`);
73
+ return createStaticGenPlugin({
74
+ posts,
75
+ outputDir,
76
+ basePath: docsBase,
77
+ docsBase,
78
+ metaTagsOptions,
79
+ });
80
+ }
81
+ catch (error) {
82
+ console.warn('⚠️ SSG initialization failed:', error);
83
+ return undefined;
84
+ }
85
+ }
86
+ //# sourceMappingURL=setup-ssg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-ssg.js","sourceRoot":"","sources":["../src/setup-ssg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAa9D;;;;GAIG;AACH,SAAS,YAAY,CAAC,OAAe;IACpC,MAAM,QAAQ,GAA0C,EAAE,CAAA;IAE1D,SAAS,OAAO,CAAC,GAAW,EAAE,YAAY,GAAG,EAAE;QAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAM;QAE/B,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QAEjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;YAC7C,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;YAElC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;YAC3B,CAAC;iBAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClC,gCAAgC;gBAChC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,GAAG,KAAK,IAAI,EAAE,CAClD,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;YACpC,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,CAAC,OAAO,CAAC,CAAA;IAChB,OAAO,QAAQ,CAAA;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC7B,OAAwB;IAExB,IAAI,CAAC;QACJ,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;QAEvE,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;QACvC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,iBAAiB,CAAC,QAAQ,EAAE;YACnD,eAAe,EAAE,QAAQ;SACzB,CAAC,CAAA;QAEF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAA;YACnD,OAAO,SAAS,CAAA;QACjB,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,CAAC,MAAM,qBAAqB,CAAC,CAAA;QAE/D,OAAO,qBAAqB,CAAC;YAC5B,KAAK;YACL,SAAS;YACT,QAAQ,EAAE,QAAQ;YAClB,QAAQ;YACR,eAAe;SACf,CAAC,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,gCAAgC,EAAE,KAAK,CAAC,CAAA;QACrD,OAAO,SAAS,CAAA;IACjB,CAAC;AACF,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from 'vite';
2
+ import { type MetaTagsOptions } from './meta-tags.js';
3
+ import type { BlogPostMetadata, BlogSSGConfig } from './types.js';
4
+ export interface StaticGenPluginOptions extends BlogSSGConfig {
5
+ /** Blog posts to generate static pages for */
6
+ posts: BlogPostMetadata[];
7
+ /** Meta tag generation options */
8
+ metaTagsOptions?: MetaTagsOptions;
9
+ /** Reference to original index.html path */
10
+ indexHtmlPath?: string;
11
+ }
12
+ /**
13
+ * Vite plugin for static site generation of blog posts
14
+ *
15
+ * Generates individual HTML files for each blog post with injected meta tags
16
+ * These files are created after the main build completes
17
+ */
18
+ export declare function createStaticGenPlugin(options: StaticGenPluginOptions): Plugin;
19
+ //# sourceMappingURL=static-gen-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-gen-plugin.d.ts","sourceRoot":"","sources":["../src/static-gen-plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAClC,OAAO,EAA4B,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEjE,MAAM,WAAW,sBAAuB,SAAQ,aAAa;IAC5D,8CAA8C;IAC9C,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,kCAAkC;IAClC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAqG7E"}
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { generateHtmlWithMetaTags } from './meta-tags.js';
4
+ /**
5
+ * Vite plugin for static site generation of blog posts
6
+ *
7
+ * Generates individual HTML files for each blog post with injected meta tags
8
+ * These files are created after the main build completes
9
+ */
10
+ export function createStaticGenPlugin(options) {
11
+ // biome-ignore lint/suspicious/noExplicitAny: <ignore>
12
+ let config;
13
+ const { posts, outputDir, metaTagsOptions = {}, indexHtmlPath = 'index.html', } = options;
14
+ return {
15
+ name: 'vite-plugin-blog-static-gen',
16
+ configResolved(resolvedConfig) {
17
+ config = resolvedConfig;
18
+ },
19
+ async generateBundle() {
20
+ // This hook runs during the bundle generation phase
21
+ // We'll actually write files in writeBundle to ensure index.html exists
22
+ },
23
+ async writeBundle() {
24
+ if (!posts || posts.length === 0) {
25
+ console.log('📝 No blog posts to generate static files for');
26
+ return;
27
+ }
28
+ try {
29
+ const resolvedIndexHtmlPath = path.isAbsolute(indexHtmlPath)
30
+ ? indexHtmlPath
31
+ : path.resolve(config.build.outDir, indexHtmlPath);
32
+ const docsBasePath = typeof options.docsBase === 'string'
33
+ ? options.docsBase
34
+ : typeof options.basePath === 'string'
35
+ ? options.basePath
36
+ : typeof config.base === 'string'
37
+ ? config.base
38
+ : '/';
39
+ const baseSegment = docsBasePath.replace(/^\//, '').replace(/\/$/, '');
40
+ const defaultOutputDir = baseSegment
41
+ ? path.resolve(config.build.outDir, baseSegment)
42
+ : config.build.outDir;
43
+ const resolvedOutputDir = outputDir
44
+ ? path.isAbsolute(outputDir)
45
+ ? outputDir
46
+ : outputDir.startsWith(config.build.outDir)
47
+ ? outputDir
48
+ : path.resolve(config.build.outDir, outputDir)
49
+ : defaultOutputDir;
50
+ // Read the generated index.html
51
+ if (!fs.existsSync(resolvedIndexHtmlPath)) {
52
+ console.warn(`⚠️ Index HTML not found at ${resolvedIndexHtmlPath}, skipping static generation`);
53
+ return;
54
+ }
55
+ const indexHtmlContent = fs.readFileSync(resolvedIndexHtmlPath, 'utf-8');
56
+ console.log(`📝 Generating static pages for ${posts.length} blog posts...`);
57
+ // Create static HTML file for each blog post
58
+ for (const post of posts) {
59
+ const htmlWithMeta = generateHtmlWithMetaTags(indexHtmlContent, post, metaTagsOptions);
60
+ // Create directory structure: <outputDir>/[language]/[slug]/index.html
61
+ const postDir = path.resolve(resolvedOutputDir, post.language, post.slug);
62
+ fs.mkdirSync(postDir, { recursive: true });
63
+ // Write the static HTML file
64
+ const htmlPath = path.resolve(postDir, 'index.html');
65
+ fs.writeFileSync(htmlPath, htmlWithMeta, 'utf-8');
66
+ console.log(` ✓ Generated ${post.language}/${post.slug}/`);
67
+ }
68
+ console.log(`✨ Static generation complete for ${posts.length} blog posts`);
69
+ }
70
+ catch (error) {
71
+ console.error('❌ Error generating static blog posts:', error);
72
+ throw error;
73
+ }
74
+ },
75
+ };
76
+ }
77
+ //# sourceMappingURL=static-gen-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-gen-plugin.js","sourceRoot":"","sources":["../src/static-gen-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,wBAAwB,EAAwB,MAAM,gBAAgB,CAAA;AAY/E;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAA+B;IACpE,uDAAuD;IACvD,IAAI,MAAW,CAAA;IACf,MAAM,EACL,KAAK,EACL,SAAS,EACT,eAAe,GAAG,EAAE,EACpB,aAAa,GAAG,YAAY,GAC5B,GAAG,OAAO,CAAA;IAEX,OAAO;QACN,IAAI,EAAE,6BAA6B;QAEnC,cAAc,CAAC,cAAc;YAC5B,MAAM,GAAG,cAAc,CAAA;QACxB,CAAC;QAED,KAAK,CAAC,cAAc;YACnB,oDAAoD;YACpD,wEAAwE;QACzE,CAAC;QAED,KAAK,CAAC,WAAW;YAChB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAClC,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAA;gBAC5D,OAAM;YACP,CAAC;YAED,IAAI,CAAC;gBACJ,MAAM,qBAAqB,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;oBAC3D,CAAC,CAAC,aAAa;oBACf,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;gBAEnD,MAAM,YAAY,GACjB,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ;oBACnC,CAAC,CAAC,OAAO,CAAC,QAAQ;oBAClB,CAAC,CAAC,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ;wBACrC,CAAC,CAAC,OAAO,CAAC,QAAQ;wBAClB,CAAC,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;4BAChC,CAAC,CAAC,MAAM,CAAC,IAAI;4BACb,CAAC,CAAC,GAAG,CAAA;gBACT,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;gBACtE,MAAM,gBAAgB,GAAG,WAAW;oBACnC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC;oBAChD,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAA;gBAEtB,MAAM,iBAAiB,GAAG,SAAS;oBAClC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;wBAC3B,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;4BAC1C,CAAC,CAAC,SAAS;4BACX,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC;oBAChD,CAAC,CAAC,gBAAgB,CAAA;gBAEnB,gCAAgC;gBAChC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,qBAAqB,CAAC,EAAE,CAAC;oBAC3C,OAAO,CAAC,IAAI,CACX,+BAA+B,qBAAqB,8BAA8B,CAClF,CAAA;oBACD,OAAM;gBACP,CAAC;gBAED,MAAM,gBAAgB,GAAG,EAAE,CAAC,YAAY,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAA;gBAExE,OAAO,CAAC,GAAG,CACV,kCAAkC,KAAK,CAAC,MAAM,gBAAgB,CAC9D,CAAA;gBAED,6CAA6C;gBAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBAC1B,MAAM,YAAY,GAAG,wBAAwB,CAC5C,gBAAgB,EAChB,IAAI,EACJ,eAAe,CACf,CAAA;oBAED,uEAAuE;oBACvE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAC3B,iBAAiB,EACjB,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,IAAI,CACT,CAAA;oBAED,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;oBAE1C,6BAA6B;oBAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;oBACpD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,CAAA;oBAEjD,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;gBAC5D,CAAC;gBAED,OAAO,CAAC,GAAG,CACV,oCAAoC,KAAK,CAAC,MAAM,aAAa,CAC7D,CAAA;YACF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAA;gBAC7D,MAAM,KAAK,CAAA;YACZ,CAAC;QACF,CAAC;KACD,CAAA;AACF,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared types for blog SSG functionality
3
+ */
4
+ export interface BlogPostMetadata {
5
+ slug: string;
6
+ title: string;
7
+ description?: string;
8
+ ogImage?: string;
9
+ keywords?: string[] | string;
10
+ date?: string;
11
+ author?: string;
12
+ language: string;
13
+ filePath: string;
14
+ }
15
+ export interface BlogDiscoveryResult {
16
+ posts: BlogPostMetadata[];
17
+ postsByLanguage: Record<string, BlogPostMetadata[]>;
18
+ postsBySlug: Record<string, BlogPostMetadata>;
19
+ }
20
+ export interface BlogSSGConfig {
21
+ /** Path to blog MDX files relative to app root */
22
+ blogContentPath?: string;
23
+ /** Whether to generate static files */
24
+ generateStatic?: boolean;
25
+ /** Output directory for generated blog posts */
26
+ outputDir?: string;
27
+ /** Base path for blog routes (deprecated: use docsBase) */
28
+ basePath?: string;
29
+ /** Base path segment for SSG output (e.g., 'blog') */
30
+ docsBase?: string;
31
+ }
32
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IACnD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAC7C;AAED,MAAM,WAAW,aAAa;IAC7B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types for blog SSG functionality
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -1,160 +0,0 @@
1
- import path from 'node:path'
2
- import type {
3
- BlogDiscoveryResult,
4
- BlogPostMetadata,
5
- BlogSSGConfig,
6
- } from './types.ts'
7
-
8
- /**
9
- * Parse YAML frontmatter from MDX content
10
- */
11
- function parseFrontmatter(content: string): Record<string, unknown> {
12
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/)
13
-
14
- if (!match) return {}
15
-
16
- const frontmatter: Record<string, unknown> = {}
17
- const frontmatterText = match[1]
18
- let currentKey = ''
19
- let isArrayContext = false
20
- const arrayValues: string[] = []
21
-
22
- for (const line of frontmatterText.split('\n')) {
23
- const trimmed = line.trim()
24
-
25
- // Handle array items (lines starting with -)
26
- if (trimmed.startsWith('-')) {
27
- if (isArrayContext) {
28
- const value = trimmed.substring(1).trim()
29
- arrayValues.push(value)
30
- }
31
- continue
32
- }
33
-
34
- // If we were in array context and hit a non-array line, save the array
35
- if (isArrayContext && !trimmed.startsWith('-')) {
36
- frontmatter[currentKey] = arrayValues.slice()
37
- arrayValues.length = 0
38
- isArrayContext = false
39
- }
40
-
41
- // Handle key-value pairs
42
- if (trimmed?.includes(':')) {
43
- const [key, ...valueParts] = trimmed.split(':')
44
- const value = valueParts.join(':').trim()
45
- currentKey = key.trim()
46
-
47
- // If value is empty, this might be an array declaration
48
- if (!value) {
49
- isArrayContext = true
50
- continue
51
- }
52
-
53
- // Parse boolean values
54
- if (value === 'true') frontmatter[currentKey] = true
55
- else if (value === 'false') frontmatter[currentKey] = false
56
- else frontmatter[currentKey] = value
57
- }
58
- }
59
-
60
- // Save any remaining array
61
- if (isArrayContext) {
62
- frontmatter[currentKey] = arrayValues.slice()
63
- }
64
-
65
- return frontmatter
66
- }
67
-
68
- /**
69
- * Extract language from file path based on doc structure
70
- * Expected: docs/[language]/[slug].mdx
71
- */
72
- function extractLanguageFromPath(filePath: string): string {
73
- const match = filePath.match(/\/docs\/([a-z]{2})\//)
74
- return match ? match[1] : 'en'
75
- }
76
-
77
- /**
78
- * Extract slug from file path
79
- * Expected: docs/[language]/[slug].mdx
80
- */
81
- function extractSlugFromPath(filePath: string): string {
82
- const match = filePath.match(/\/([a-z0-9-]+)\.mdx$/)
83
- return match ? match[1] : ''
84
- }
85
-
86
- /**
87
- * Discover all blog posts from Vite glob import
88
- *
89
- * @param mdxFiles - Object from import.meta.glob('./docs/**\/*.mdx', { query: '?raw', import: 'default', eager: false })
90
- * @param config - SSG configuration options
91
- * @returns Collection of discovered blog posts organized by language and slug
92
- */
93
- export async function discoverBlogPosts(
94
- mdxFiles: Record<string, () => Promise<unknown>>,
95
- config: BlogSSGConfig = {},
96
- ): Promise<BlogDiscoveryResult> {
97
- const posts: BlogPostMetadata[] = []
98
- const postsByLanguage: Record<string, BlogPostMetadata[]> = {}
99
- const postsBySlug: Record<string, BlogPostMetadata> = {}
100
-
101
- // Iterate through all MDX files
102
- for (const [filePath, getContent] of Object.entries(mdxFiles)) {
103
- try {
104
- // Load content
105
- const content = (await getContent()) as string
106
-
107
- // Extract language and slug
108
- const language = extractLanguageFromPath(filePath)
109
- const slug = extractSlugFromPath(filePath)
110
-
111
- if (!slug) continue
112
-
113
- // Parse frontmatter
114
- const frontmatter = parseFrontmatter(content)
115
-
116
- // Create metadata object
117
- const metadata: BlogPostMetadata = {
118
- slug,
119
- title: (frontmatter.title as string) || slug,
120
- description: frontmatter.description as string | undefined,
121
- ogImage: frontmatter.ogImage as string | undefined,
122
- keywords: frontmatter.keywords as string[] | string | undefined,
123
- date: frontmatter.date as string | undefined,
124
- author: frontmatter.author as string | undefined,
125
- language,
126
- filePath,
127
- }
128
-
129
- posts.push(metadata)
130
-
131
- // Index by language
132
- if (!postsByLanguage[language]) {
133
- postsByLanguage[language] = []
134
- }
135
- postsByLanguage[language].push(metadata)
136
-
137
- // Index by slug (use [language-slug] composite key to avoid conflicts)
138
- postsBySlug[`${language}:${slug}`] = metadata
139
- } catch (error) {
140
- console.warn(`Warning: Failed to process ${filePath}:`, error)
141
- }
142
- }
143
-
144
- return {
145
- posts,
146
- postsByLanguage,
147
- postsBySlug,
148
- }
149
- }
150
-
151
- /**
152
- * Filter blog posts by specific criteria
153
- * Useful for separating blog posts from docs
154
- */
155
- export function filterBlogPosts(
156
- posts: BlogPostMetadata[],
157
- predicate: (post: BlogPostMetadata) => boolean,
158
- ): BlogPostMetadata[] {
159
- return posts.filter(predicate)
160
- }
@@ -1,17 +0,0 @@
1
- // Blog discovery
2
- export { discoverBlogPosts, filterBlogPosts } from './blog-discovery.ts'
3
-
4
- // Meta tag generation
5
- export {
6
- generateHtmlWithMetaTags,
7
- generateMetaTags,
8
- generateMetaTagsObject,
9
- type MetaTagsOptions,
10
- } from './meta-tags.ts'
11
- // SSG setup utilities (recommended for consumers)
12
- export { type SetupSSGOptions, setupSSG } from './setup-ssg.ts'
13
- // Vite plugin
14
- export {
15
- createStaticGenPlugin,
16
- type StaticGenPluginOptions,
17
- } from './static-gen-plugin.ts'
@@ -1,184 +0,0 @@
1
- import type { BlogPostMetadata } from './types.ts'
2
-
3
- export interface MetaTagsOptions {
4
- /** Base URL for absolute OG image URLs */
5
- baseUrl?: string
6
- /** Default OG image URL if post doesn't have one */
7
- defaultOgImage?: string
8
- /** Site title for Twitter Card */
9
- siteTitle?: string
10
- /** Site name for OG tags */
11
- siteName?: string
12
- /** Twitter handle for Twitter Card */
13
- twitterHandle?: string
14
- }
15
-
16
- /**
17
- * Generate meta tags HTML string from blog post metadata
18
- */
19
- export function generateMetaTags(
20
- post: BlogPostMetadata,
21
- options: MetaTagsOptions = {},
22
- ): string {
23
- const {
24
- baseUrl = 'https://docs.staking.polkadot.cloud',
25
- defaultOgImage = '/img/og-image.png',
26
- siteTitle = 'Polkadot Cloud Staking',
27
- siteName = 'Polkadot Cloud Staking Documentation',
28
- twitterHandle = '@PolkadotCloud',
29
- } = options
30
-
31
- const postUrl = `${baseUrl}/${post.language}/docs/${post.slug}`
32
- const ogImage = post.ogImage
33
- ? `${baseUrl}${post.ogImage.startsWith('/') ? '' : '/'}${post.ogImage}`
34
- : `${baseUrl}${defaultOgImage}`
35
-
36
- // Normalize keywords
37
- let keywordsString = ''
38
- if (post.keywords) {
39
- if (Array.isArray(post.keywords)) {
40
- keywordsString = post.keywords.join(', ')
41
- } else {
42
- keywordsString = post.keywords
43
- }
44
- }
45
-
46
- const tags: string[] = [
47
- // Basic meta tags
48
- `<meta name="description" content="${escapeHtml(post.description || siteTitle)}">`,
49
- `<meta name="viewport" content="width=device-width, initial-scale=1.0">`,
50
-
51
- // Open Graph (OG) tags
52
- `<meta property="og:type" content="article">`,
53
- `<meta property="og:title" content="${escapeHtml(post.title)} | ${escapeHtml(siteName)}">`,
54
- `<meta property="og:description" content="${escapeHtml(post.description || siteTitle)}">`,
55
- `<meta property="og:url" content="${escapeHtml(postUrl)}">`,
56
- `<meta property="og:image" content="${escapeHtml(ogImage)}">`,
57
- `<meta property="og:site_name" content="${escapeHtml(siteName)}">`,
58
-
59
- // Article-specific meta tags
60
- `<meta property="article:published_time" content="${post.date || new Date().toISOString()}">`,
61
- post.author
62
- ? `<meta property="article:author" content="${escapeHtml(post.author)}">`
63
- : '',
64
-
65
- // Twitter Card tags
66
- `<meta name="twitter:card" content="summary_large_image">`,
67
- `<meta name="twitter:site" content="${twitterHandle}">`,
68
- `<meta name="twitter:title" content="${escapeHtml(post.title)} | ${escapeHtml(siteName)}">`,
69
- `<meta name="twitter:description" content="${escapeHtml(post.description || siteTitle)}">`,
70
- `<meta name="twitter:image" content="${escapeHtml(ogImage)}">`,
71
-
72
- // Keywords
73
- keywordsString
74
- ? `<meta name="keywords" content="${escapeHtml(keywordsString)}">`
75
- : '',
76
-
77
- // Canonical URL
78
- `<link rel="canonical" href="${escapeHtml(postUrl)}">`,
79
- ]
80
-
81
- return tags.filter(Boolean).join('\n\t')
82
- }
83
-
84
- /**
85
- * Escape HTML special characters in attribute values
86
- */
87
- function escapeHtml(text: string): string {
88
- const map: Record<string, string> = {
89
- '&': '&amp;',
90
- '<': '&lt;',
91
- '>': '&gt;',
92
- '"': '&quot;',
93
- "'": '&#039;',
94
- }
95
- return text.replace(/[&<>"']/g, (char) => map[char])
96
- }
97
-
98
- /**
99
- * Generate an HTML template with injected meta tags
100
- * Used for static page generation
101
- */
102
- export function generateHtmlWithMetaTags(
103
- baseHtml: string,
104
- post: BlogPostMetadata,
105
- options: MetaTagsOptions = {},
106
- ): string {
107
- const metaTags = generateMetaTags(post, options)
108
-
109
- // Remove default OG and Twitter meta tags to replace them with custom ones
110
- let html = baseHtml
111
- // Remove default og:* meta tags (handles multiline)
112
- .replace(/<meta\s+property="og:[^"]*"[^>]*>/gis, '')
113
- // Remove default article:* meta tags (handles multiline)
114
- .replace(/<meta\s+property="article:[^"]*"[^>]*>/gis, '')
115
- // Remove default twitter:* meta tags with either 'name' or 'property' attribute (handles multiline)
116
- .replace(/<meta\s+(?:name|property)="twitter:[^"]*"[^>]*>/gis, '')
117
- // Remove default description tag
118
- .replace(/<meta\s+name="description"[^>]*>/gis, '')
119
- // Remove canonical link if present
120
- .replace(/<link\s+rel="canonical"[^>]*>/gis, '')
121
-
122
- // Clean up excess blank lines left by removed tags
123
- html = html.replace(/\n\s*\n\s*\n/g, '\n\n')
124
-
125
- // Remove orphaned section comments followed by blank lines
126
- html = html.replace(
127
- /\s*<!--\s*(?:Open Graph|Facebook|Twitter)[^>]*-->\s*\n/g,
128
- '',
129
- )
130
-
131
- // Final cleanup: remove any remaining multiple blank lines
132
- html = html.replace(/\n\s*\n\s*\n/g, '\n\n')
133
-
134
- // Inject custom meta tags before closing </head> tag
135
- return html.replace('</head>', `\t${metaTags}\n</head>`)
136
- }
137
-
138
- /**
139
- * Generate meta tags as a JSON object for dynamic injection
140
- * Useful if you want to update meta tags on client-side navigation
141
- */
142
- export function generateMetaTagsObject(
143
- post: BlogPostMetadata,
144
- options: MetaTagsOptions = {},
145
- ): Record<string, string> {
146
- const {
147
- baseUrl = 'https://docs.staking.polkadot.cloud',
148
- defaultOgImage = '/img/og-image.png',
149
- siteName = 'Polkadot Cloud Staking Documentation',
150
- } = options
151
-
152
- const postUrl = `${baseUrl}/${post.language}/docs/${post.slug}`
153
- const ogImage = post.ogImage
154
- ? `${baseUrl}${post.ogImage.startsWith('/') ? '' : '/'}${post.ogImage}`
155
- : `${baseUrl}${defaultOgImage}`
156
-
157
- // Normalize keywords
158
- let keywordsArray: string[] = []
159
- if (post.keywords) {
160
- if (Array.isArray(post.keywords)) {
161
- keywordsArray = post.keywords
162
- } else {
163
- keywordsArray = post.keywords.split(',').map((k) => k.trim())
164
- }
165
- }
166
-
167
- return {
168
- description: post.description || siteName,
169
- 'og:type': 'article',
170
- 'og:title': `${post.title} | ${siteName}`,
171
- 'og:description': post.description || siteName,
172
- 'og:url': postUrl,
173
- 'og:image': ogImage,
174
- 'og:site_name': siteName,
175
- 'article:published_time': post.date || new Date().toISOString(),
176
- ...(post.author && { 'article:author': post.author }),
177
- 'twitter:card': 'summary_large_image',
178
- 'twitter:title': `${post.title} | ${siteName}`,
179
- 'twitter:description': post.description || siteName,
180
- 'twitter:image': ogImage,
181
- ...(keywordsArray.length > 0 && { keywords: keywordsArray.join(', ') }),
182
- canonical: postUrl,
183
- }
184
- }
@@ -1,105 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import type { PluginOption } from 'vite'
4
- import { discoverBlogPosts } from './blog-discovery.ts'
5
- import type { MetaTagsOptions } from './meta-tags.ts'
6
- import { createStaticGenPlugin } from './static-gen-plugin.ts'
7
-
8
- export interface SetupSSGOptions {
9
- /** Path to the docs directory (e.g., 'src/docs') */
10
- docsPath: string
11
- /** Output directory for generated static pages (default: 'dist/docs') */
12
- outputDir?: string
13
- /** Meta tags configuration */
14
- metaTagsOptions?: MetaTagsOptions
15
- }
16
-
17
- /**
18
- * Read all MDX files from a docs directory
19
- * @param docsDir - Path to the docs directory
20
- * @returns Object mapping file paths to content loaders
21
- */
22
- function readMdxFiles(docsDir: string): Record<string, () => Promise<string>> {
23
- const mdxFiles: Record<string, () => Promise<string>> = {}
24
-
25
- function walkDir(dir: string, relativePath = '') {
26
- if (!fs.existsSync(dir)) return
27
-
28
- const files = fs.readdirSync(dir)
29
-
30
- for (const file of files) {
31
- const fullPath = path.join(dir, file)
32
- const relPath = path.join(relativePath, file)
33
- const stat = fs.statSync(fullPath)
34
-
35
- if (stat.isDirectory()) {
36
- walkDir(fullPath, relPath)
37
- } else if (file.endsWith('.mdx')) {
38
- // Create a lazy loader function
39
- mdxFiles[path.join(docsDir, relPath)] = async () =>
40
- fs.readFileSync(fullPath, 'utf-8')
41
- }
42
- }
43
- }
44
-
45
- walkDir(docsDir)
46
- return mdxFiles
47
- }
48
-
49
- /**
50
- * Setup static site generation for blog posts
51
- *
52
- * This is a convenience function that handles the entire SSG setup process.
53
- * It discovers blog posts from your docs directory and creates a Vite plugin.
54
- *
55
- * @example
56
- * ```typescript
57
- * import { setupSSG } from '@x-wave/blog/vite-config'
58
- *
59
- * export default defineConfig(async (env) => {
60
- * const ssgPlugin = env.command === 'build'
61
- * ? await setupSSG({
62
- * docsPath: 'src/docs',
63
- * outputDir: 'dist/docs',
64
- * metaTagsOptions: {
65
- * baseUrl: 'https://example.com',
66
- * siteName: 'My Documentation',
67
- * }
68
- * })
69
- * : undefined
70
- *
71
- * return {
72
- * plugins: [react(), ssgPlugin].filter(Boolean),
73
- * // ... rest of config
74
- * }
75
- * })
76
- * ```
77
- */
78
- export async function setupSSG(
79
- options: SetupSSGOptions,
80
- ): Promise<PluginOption | undefined> {
81
- try {
82
- const { docsPath, outputDir = 'dist/docs', metaTagsOptions = {} } = options
83
-
84
- const mdxFiles = readMdxFiles(docsPath)
85
- const { posts } = await discoverBlogPosts(mdxFiles, {
86
- blogContentPath: docsPath,
87
- })
88
-
89
- if (posts.length === 0) {
90
- console.log('📝 No blog posts found, SSG disabled')
91
- return undefined
92
- }
93
-
94
- console.log(`📝 Discovered ${posts.length} blog posts for SSG`)
95
-
96
- return createStaticGenPlugin({
97
- posts,
98
- outputDir,
99
- metaTagsOptions,
100
- })
101
- } catch (error) {
102
- console.warn('⚠️ SSG initialization failed:', error)
103
- return undefined
104
- }
105
- }
@@ -1,98 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import type { Plugin } from 'vite'
4
- import { generateHtmlWithMetaTags, type MetaTagsOptions } from './meta-tags.ts'
5
- import type { BlogPostMetadata, BlogSSGConfig } from './types.ts'
6
-
7
- export interface StaticGenPluginOptions extends BlogSSGConfig {
8
- /** Blog posts to generate static pages for */
9
- posts: BlogPostMetadata[]
10
- /** Meta tag generation options */
11
- metaTagsOptions?: MetaTagsOptions
12
- /** Reference to original index.html path */
13
- indexHtmlPath?: string
14
- }
15
-
16
- /**
17
- * Vite plugin for static site generation of blog posts
18
- *
19
- * Generates individual HTML files for each blog post with injected meta tags
20
- * These files are created after the main build completes
21
- */
22
- export function createStaticGenPlugin(options: StaticGenPluginOptions): Plugin {
23
- let config: any
24
- const {
25
- posts,
26
- outputDir = 'dist/docs',
27
- metaTagsOptions = {},
28
- indexHtmlPath = 'index.html',
29
- } = options
30
-
31
- return {
32
- name: 'vite-plugin-blog-static-gen',
33
-
34
- configResolved(resolvedConfig) {
35
- config = resolvedConfig
36
- },
37
-
38
- async generateBundle() {
39
- // This hook runs during the bundle generation phase
40
- // We'll actually write files in writeBundle to ensure index.html exists
41
- },
42
-
43
- async writeBundle() {
44
- if (!posts || posts.length === 0) {
45
- console.log('📝 No blog posts to generate static files for')
46
- return
47
- }
48
-
49
- try {
50
- // Read the generated index.html
51
- const indexHtmlPath_ = path.resolve(config.build.outDir, 'index.html')
52
- if (!fs.existsSync(indexHtmlPath_)) {
53
- console.warn(
54
- `⚠️ Index HTML not found at ${indexHtmlPath_}, skipping static generation`,
55
- )
56
- return
57
- }
58
-
59
- const indexHtmlContent = fs.readFileSync(indexHtmlPath_, 'utf-8')
60
-
61
- console.log(
62
- `📝 Generating static pages for ${posts.length} blog posts...`,
63
- )
64
-
65
- // Create static HTML file for each blog post
66
- for (const post of posts) {
67
- const htmlWithMeta = generateHtmlWithMetaTags(
68
- indexHtmlContent,
69
- post,
70
- metaTagsOptions,
71
- )
72
-
73
- // Create directory structure: dist/docs/[language]/[slug]/index.html
74
- const postDir = path.resolve(
75
- config.build.outDir,
76
- post.language,
77
- post.slug,
78
- )
79
-
80
- fs.mkdirSync(postDir, { recursive: true })
81
-
82
- // Write the static HTML file
83
- const htmlPath = path.resolve(postDir, 'index.html')
84
- fs.writeFileSync(htmlPath, htmlWithMeta, 'utf-8')
85
-
86
- console.log(` ✓ Generated ${post.language}/${post.slug}/`)
87
- }
88
-
89
- console.log(
90
- `✨ Static generation complete for ${posts.length} blog posts`,
91
- )
92
- } catch (error) {
93
- console.error('❌ Error generating static blog posts:', error)
94
- throw error
95
- }
96
- },
97
- }
98
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Shared types for blog SSG functionality
3
- */
4
-
5
- export interface BlogPostMetadata {
6
- slug: string
7
- title: string
8
- description?: string
9
- ogImage?: string
10
- keywords?: string[] | string
11
- date?: string
12
- author?: string
13
- language: string
14
- filePath: string
15
- }
16
-
17
- export interface BlogDiscoveryResult {
18
- posts: BlogPostMetadata[]
19
- postsByLanguage: Record<string, BlogPostMetadata[]>
20
- postsBySlug: Record<string, BlogPostMetadata>
21
- }
22
-
23
- export interface BlogSSGConfig {
24
- /** Path to blog MDX files relative to app root */
25
- blogContentPath?: string
26
- /** Whether to generate static files */
27
- generateStatic?: boolean
28
- /** Output directory for generated blog posts */
29
- outputDir?: string
30
- /** Base path for blog routes */
31
- basePath?: string
32
- }