@x-wave/blog 2.9.1 → 2.10.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.
package/README.md CHANGED
@@ -300,6 +300,29 @@ The blog system supports standard Markdown and extended syntax via GitHub Flavor
300
300
  ~~strikethrough~~
301
301
  ```
302
302
 
303
+ #### Text Alignment
304
+
305
+ Wrap content in `[left]`, `[center]`, or `[right]` tags. Inline Markdown remains
306
+ available inside aligned content:
307
+
308
+ ```mdx
309
+ [center]This paragraph is **centered**.[/center]
310
+
311
+ [right]
312
+ This paragraph is right-aligned.
313
+ [/right]
314
+
315
+ [left]
316
+
317
+ - A left-aligned list item
318
+ - Another item
319
+
320
+ [/left]
321
+ ```
322
+
323
+ Use blank lines between the alignment tags and content when wrapping multiple
324
+ Markdown blocks such as headings, lists, or several paragraphs.
325
+
303
326
  #### Lists
304
327
 
305
328
  **Unordered lists:**
@@ -633,7 +656,8 @@ The home page displays:
633
656
  - **Title**: "Latest Posts" (i18n translated)
634
657
  - **Header metadata**: Author and date from the most recent article
635
658
  - **Article cards**: Recent articles with title, description, author, and date
636
- - **Sorting**: Articles sorted by date (newest first), limited to 50 articles
659
+ - **Sorting**: Articles sorted by date (newest first), with 20 articles per page
660
+ - **Pagination**: Crawlable archive URLs at `/{language}/page/{page}`
637
661
 
638
662
  Article metadata comes from MDX frontmatter:
639
663
  ```mdx
@@ -673,13 +697,21 @@ Or add it as a project script:
673
697
  |---|---|
674
698
  | `--docs` | Path to the docs directory (must contain language sub-directories, e.g. `en/`, `es/`) |
675
699
  | `--output` | Path to the output directory where JSON index files will be written |
700
+ | `--page-size` | Optional number of articles per page (default: `20`) |
701
+
702
+ If you customize `--page-size`, pass the same value as `articlesPerPage` to
703
+ `setupSSG()` so the JSON and statically generated archive pages stay aligned.
676
704
 
677
705
  **Output structure** (one set per language):
678
706
 
679
707
  ```
680
708
  public/blog-index/
681
709
  ├── en/
682
- │ ├── latest.json # Latest 20 articles (home page)
710
+ │ ├── latest.json # Latest 100 articles (legacy index)
711
+ │ ├── pages-index.json # Page count and page-size metadata
712
+ │ ├── pages/
713
+ │ │ ├── 1.json # First 20 articles
714
+ │ │ └── 2.json # Next 20 articles
683
715
  │ ├── months-index.json # Sorted list of YYYY-MM strings
684
716
  │ └── months/
685
717
  │ └── 2026-02.json # Articles for that month
package/cli/index.js CHANGED
@@ -11,9 +11,12 @@
11
11
  * Options:
12
12
  * --docs Path to the docs directory (must contain language sub-directories, e.g. en/, es/)
13
13
  * --output Path to the output directory where JSON index files will be written
14
+ * --page-size Number of articles per home page (default: 20)
14
15
  *
15
16
  * Output structure (one set of files per language):
16
- * <output>/<lang>/latest.json - Latest 50 articles (for the home page)
17
+ * <output>/<lang>/latest.json - Latest 100 articles (legacy home page index)
18
+ * <output>/<lang>/pages-index.json - Pagination metadata for the home page
19
+ * <output>/<lang>/pages/<N>.json - Articles for a home page page
17
20
  * <output>/<lang>/months-index.json - Sorted list of YYYY-MM strings with articles
18
21
  * <output>/<lang>/months/<YYYY-MM>.json - Articles for a specific month
19
22
  *
@@ -25,6 +28,7 @@ import fs from 'node:fs'
25
28
  import path from 'node:path'
26
29
 
27
30
  const LATEST_COUNT = 100
31
+ const DEFAULT_PAGE_SIZE = 20
28
32
 
29
33
  /**
30
34
  * Parse CLI arguments from process.argv.
@@ -36,6 +40,8 @@ function parseArgs(argv) {
36
40
  args.docs = argv[++i]
37
41
  } else if (argv[i] === '--output' && argv[i + 1]) {
38
42
  args.output = argv[++i]
43
+ } else if (argv[i] === '--page-size' && argv[i + 1]) {
44
+ args.pageSize = Number(argv[++i])
39
45
  }
40
46
  }
41
47
  return args
@@ -202,9 +208,13 @@ function processLanguage(docsDir, language) {
202
208
  /**
203
209
  * Write data as formatted JSON to a file, creating any missing directories.
204
210
  */
205
- function writeJson(filePath, data) {
211
+ function writeJson(filePath, data, indentation = '\t') {
206
212
  fs.mkdirSync(path.dirname(filePath), { recursive: true })
207
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8')
213
+ fs.writeFileSync(
214
+ filePath,
215
+ `${JSON.stringify(data, null, indentation)}\n`,
216
+ 'utf-8',
217
+ )
208
218
  }
209
219
 
210
220
  /**
@@ -214,12 +224,23 @@ async function main() {
214
224
  const args = parseArgs(process.argv)
215
225
 
216
226
  if (!args.docs || !args.output) {
217
- console.error('Usage: blog-indexer --docs <docs-dir> --output <output-dir>')
227
+ console.error(
228
+ 'Usage: blog-indexer --docs <docs-dir> --output <output-dir> [--page-size <number>]',
229
+ )
230
+ process.exit(1)
231
+ }
232
+
233
+ if (
234
+ args.pageSize !== undefined &&
235
+ (!Number.isInteger(args.pageSize) || args.pageSize < 1)
236
+ ) {
237
+ console.error('--page-size must be a positive integer')
218
238
  process.exit(1)
219
239
  }
220
240
 
221
241
  const docsDir = path.resolve(args.docs)
222
242
  const outputDir = path.resolve(args.output)
243
+ const pageSize = args.pageSize ?? DEFAULT_PAGE_SIZE
223
244
 
224
245
  if (!fs.existsSync(docsDir)) {
225
246
  console.error(`Docs directory not found: ${docsDir}`)
@@ -247,6 +268,28 @@ async function main() {
247
268
  writeJson(path.join(langOutputDir, 'latest.json'), latest)
248
269
  console.log(` ✓ latest.json (${latest.length} articles)`)
249
270
 
271
+ // Write paginated indexes for the home page. Keeping the pages separate means
272
+ // the browser only downloads the metadata it needs for the current page.
273
+ const totalPages = Math.ceil(sorted.length / pageSize)
274
+ writeJson(path.join(langOutputDir, 'pages-index.json'), {
275
+ pageSize,
276
+ totalArticles: sorted.length,
277
+ totalPages,
278
+ })
279
+
280
+ const pagesDir = path.join(langOutputDir, 'pages')
281
+ fs.rmSync(pagesDir, { recursive: true, force: true })
282
+ for (let page = 1; page <= totalPages; page++) {
283
+ const start = (page - 1) * pageSize
284
+ writeJson(
285
+ path.join(pagesDir, `${page}.json`),
286
+ sorted.slice(start, start + pageSize),
287
+ )
288
+ }
289
+ console.log(
290
+ ` ✓ pages-index.json and ${totalPages} page file(s) (${pageSize} per page)`,
291
+ )
292
+
250
293
  // Group articles by month (only articles with a parseable date)
251
294
  /** @type {Map<string, typeof sorted>} */
252
295
  const monthMap = new Map()
@@ -266,7 +309,7 @@ async function main() {
266
309
  const monthsIndex = Array.from(monthMap.keys()).sort((a, b) =>
267
310
  b.localeCompare(a),
268
311
  )
269
- writeJson(path.join(langOutputDir, 'months-index.json'), monthsIndex)
312
+ writeJson(path.join(langOutputDir, 'months-index.json'), monthsIndex, 0)
270
313
  console.log(` ✓ months-index.json (${monthsIndex.length} months)`)
271
314
 
272
315
  // Write one file per month