@x-wave/blog 2.0.0 → 2.1.0

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.
@@ -0,0 +1,105 @@
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
+ }
@@ -0,0 +1,98 @@
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
+ }
@@ -0,0 +1,32 @@
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
+ }