methanol 0.0.14 → 0.0.15

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.
Files changed (38) hide show
  1. package/index.js +1 -0
  2. package/package.json +1 -1
  3. package/src/build-system.js +1 -0
  4. package/src/config.js +33 -3
  5. package/src/dev-server.js +21 -13
  6. package/src/pages-index.js +42 -0
  7. package/src/pages.js +1 -0
  8. package/src/reframe.js +1 -1
  9. package/src/state.js +8 -0
  10. package/src/text-utils.js +60 -0
  11. package/src/vite-plugins.js +9 -0
  12. package/src/workers/build-pool.js +1 -0
  13. package/themes/blog/README.md +26 -0
  14. package/themes/blog/components/CategoryView.client.jsx +164 -0
  15. package/themes/blog/components/CategoryView.static.jsx +35 -0
  16. package/themes/blog/components/CollectionView.client.jsx +151 -0
  17. package/themes/blog/components/CollectionView.static.jsx +37 -0
  18. package/themes/blog/components/PostList.client.jsx +92 -0
  19. package/themes/blog/components/PostList.static.jsx +36 -0
  20. package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
  21. package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
  22. package/themes/blog/index.js +40 -0
  23. package/themes/blog/pages/404.mdx +12 -0
  24. package/themes/blog/pages/about.mdx +14 -0
  25. package/themes/blog/pages/categories.mdx +6 -0
  26. package/themes/blog/pages/collections.mdx +6 -0
  27. package/themes/blog/pages/index.mdx +16 -0
  28. package/themes/blog/pages/offline.mdx +11 -0
  29. package/themes/blog/sources/style.css +579 -0
  30. package/themes/blog/src/date-utils.js +28 -0
  31. package/themes/blog/src/heading.jsx +37 -0
  32. package/themes/blog/src/layout-categories.jsx +66 -0
  33. package/themes/blog/src/layout-collections.jsx +65 -0
  34. package/themes/blog/src/layout-home.jsx +66 -0
  35. package/themes/blog/src/layout-post.jsx +42 -0
  36. package/themes/blog/src/page.jsx +152 -0
  37. package/themes/blog/src/post-utils.js +83 -0
  38. package/themes/default/src/page.jsx +1 -1
@@ -0,0 +1,579 @@
1
+ @import '@wooorm/starry-night/style/core';
2
+
3
+ html {
4
+ scroll-behavior: smooth;
5
+ }
6
+
7
+ :root {
8
+ --bg: #ffffff;
9
+ --text: #111827;
10
+ --text-muted: #6b7280;
11
+ --primary: #2563eb;
12
+ --primary-soft: #eff6ff;
13
+ --border: #f3f4f6;
14
+ --bg-soft: #f9fafb;
15
+ --selection-bg: #dbeafe;
16
+
17
+ --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
18
+ --max-width: 820px;
19
+ --container-px: 1.5rem;
20
+ }
21
+
22
+ @media (prefers-color-scheme: dark) {
23
+ :root {
24
+ --bg: #030712;
25
+ --text: #f9fafb;
26
+ --text-muted: #9ca3af;
27
+ --primary: #60a5fa;
28
+ --primary-soft: #1e3a8a33;
29
+ --border: #1f2937;
30
+ --bg-soft: #111827;
31
+ --selection-bg: #1e3a8a;
32
+ }
33
+ }
34
+
35
+ *, *::before, *::after {
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ ::selection {
40
+ background: var(--selection-bg);
41
+ }
42
+
43
+ body {
44
+ margin: 0;
45
+ font-family: var(--font-sans);
46
+ background: var(--bg);
47
+ color: var(--text);
48
+ line-height: 1.6;
49
+ -webkit-font-smoothing: antialiased;
50
+ }
51
+
52
+ a {
53
+ color: inherit;
54
+ text-decoration: none;
55
+ transition: all 0.2s;
56
+ }
57
+
58
+ .container {
59
+ max-width: var(--max-width);
60
+ margin: 0 auto;
61
+ padding: 0 var(--container-px);
62
+ width: 100%;
63
+ }
64
+
65
+ /* Header */
66
+ .blog-header {
67
+ padding: 1.5rem 0;
68
+ border-bottom: 1px solid var(--border);
69
+ margin-bottom: 2rem;
70
+ background: var(--bg);
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: 50;
74
+ }
75
+
76
+ .header-container {
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ gap: 1.5rem;
81
+ }
82
+
83
+ .blog-logo {
84
+ font-size: 1.25rem;
85
+ font-weight: 700;
86
+ letter-spacing: -0.02em;
87
+ flex-shrink: 1;
88
+ overflow: hidden;
89
+ text-overflow: ellipsis;
90
+ white-space: nowrap;
91
+ min-width: 0;
92
+ }
93
+
94
+ .header-actions {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 1rem;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .blog-nav {
102
+ display: flex;
103
+ gap: 1.25rem;
104
+ }
105
+
106
+ .blog-nav a {
107
+ font-size: 0.9rem;
108
+ font-weight: 500;
109
+ color: var(--text-muted);
110
+ white-space: nowrap;
111
+ }
112
+
113
+ .blog-nav a:hover {
114
+ color: var(--primary);
115
+ }
116
+
117
+ /* Nav Toggle */
118
+ .nav-toggle {
119
+ display: none;
120
+ }
121
+
122
+ .nav-toggle-label {
123
+ display: none;
124
+ cursor: pointer;
125
+ color: var(--text-muted);
126
+ padding: 0.25rem;
127
+ }
128
+
129
+ .nav-toggle-label:hover {
130
+ color: var(--text);
131
+ }
132
+
133
+ /* Search Box */
134
+ .search-box {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.5rem;
138
+ padding: 0.35rem 0.5rem;
139
+ background: transparent;
140
+ border: none;
141
+ color: var(--text-muted);
142
+ font-size: 0.85rem;
143
+ cursor: pointer;
144
+ transition: all 0.2s;
145
+ white-space: nowrap;
146
+ min-width: 8.5rem; /* Reserve space for icon + Search + kbd */
147
+ justify-content: flex-start;
148
+ }
149
+
150
+ .search-box:hover {
151
+ color: var(--text);
152
+ }
153
+
154
+ .search-box kbd {
155
+ font-family: inherit;
156
+ font-size: 0.75rem;
157
+ opacity: 0.5;
158
+ background: var(--bg-soft);
159
+ padding: 0 0.25rem;
160
+ border-radius: 4px;
161
+ border: 1px solid var(--border);
162
+ display: inline-flex;
163
+ justify-content: center;
164
+ }
165
+
166
+ /* Post Items */
167
+ .post-list {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 3.5rem;
171
+ }
172
+
173
+ .post-item {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 0.5rem;
177
+ }
178
+
179
+ .post-meta {
180
+ font-size: 0.875rem;
181
+ color: var(--text-muted);
182
+ font-weight: 500;
183
+ }
184
+
185
+ .post-item-title {
186
+ margin: 0;
187
+ font-size: 1.75rem;
188
+ line-height: 1.25;
189
+ font-weight: 700;
190
+ letter-spacing: -0.02em;
191
+ }
192
+
193
+ .post-item-title a:hover {
194
+ color: var(--primary);
195
+ }
196
+
197
+ .post-excerpt {
198
+ color: var(--text-muted);
199
+ font-size: 1.05rem;
200
+ line-height: 1.6;
201
+ margin-top: 0.5rem;
202
+ display: -webkit-box;
203
+ -webkit-line-clamp: 3;
204
+ -webkit-box-orient: vertical;
205
+ overflow: hidden;
206
+ }
207
+
208
+ /* Categories/Collections Buttons */
209
+ .category-list {
210
+ display: flex;
211
+ flex-wrap: wrap;
212
+ gap: 0.5rem;
213
+ margin-bottom: 2.5rem;
214
+ }
215
+
216
+ .category-tag {
217
+ padding: 0.4rem 0.9rem;
218
+ background: var(--bg-soft);
219
+ border: 1px solid var(--border);
220
+ border-radius: 99px;
221
+ color: var(--text-muted);
222
+ font-size: 0.875rem;
223
+ font-weight: 500;
224
+ cursor: pointer;
225
+ transition: all 0.2s;
226
+ }
227
+
228
+ .category-tag:hover {
229
+ border-color: var(--primary);
230
+ color: var(--primary);
231
+ }
232
+
233
+ .category-tag.active {
234
+ background: var(--primary);
235
+ color: #fff;
236
+ border-color: var(--primary);
237
+ }
238
+
239
+ /* Post Content */
240
+ .post-header {
241
+ margin-bottom: 3rem;
242
+ padding-bottom: 2rem;
243
+ border-bottom: 1px solid var(--border);
244
+ }
245
+
246
+ .post-title,
247
+ .post-body h1, .post-body h2, .post-body h3, .post-body h4, .post-body h5, .post-body h6 {
248
+ position: relative;
249
+ }
250
+
251
+ .post-title {
252
+ font-size: 2.5rem;
253
+ line-height: 1.2;
254
+ margin: 0.5rem 0 1rem;
255
+ font-weight: 800;
256
+ letter-spacing: -0.03em;
257
+ }
258
+
259
+ .post-body {
260
+ font-size: 1.125rem;
261
+ line-height: 1.75;
262
+ }
263
+
264
+ .post-body h2 {
265
+ margin-top: 3rem;
266
+ margin-bottom: 1rem;
267
+ font-weight: 700;
268
+ }
269
+
270
+ .post-body p { margin-bottom: 1.5rem; }
271
+
272
+ .post-body img {
273
+ max-width: 100%;
274
+ border-radius: 12px;
275
+ margin: 2.5rem 0;
276
+ }
277
+
278
+ .post-body blockquote {
279
+ border-left: 4px solid var(--primary);
280
+ margin: 2rem 0;
281
+ padding: 0.5rem 0 0.5rem 1.5rem;
282
+ color: var(--text-muted);
283
+ font-style: italic;
284
+ background: var(--bg-soft);
285
+ }
286
+
287
+ .post-body table {
288
+ display: block;
289
+ width: 100%;
290
+ width: max-content;
291
+ max-width: 100%;
292
+ overflow: auto;
293
+ margin-top: 0;
294
+ margin-bottom: 2rem;
295
+ border-spacing: 0;
296
+ border-collapse: collapse;
297
+ }
298
+
299
+ .post-body table tr {
300
+ background-color: var(--bg);
301
+ border-top: 1px solid var(--border);
302
+ }
303
+
304
+ .post-body table tr:nth-child(2n) {
305
+ background-color: var(--bg-soft);
306
+ }
307
+
308
+ .post-body table th,
309
+ .post-body table td {
310
+ padding: 6px 13px;
311
+ border: 1px solid var(--border);
312
+ }
313
+
314
+ .post-body table th {
315
+ font-weight: 600;
316
+ }
317
+
318
+ .post-body pre {
319
+ background: var(--bg-soft);
320
+ padding: 1.25rem;
321
+ border-radius: 12px;
322
+ overflow-x: auto;
323
+ border: 1px solid var(--border);
324
+ margin: 2rem 0;
325
+ font-size: 0.875rem;
326
+ line-height: 1.5;
327
+ }
328
+
329
+ .post-body :not(pre) > code {
330
+ background: var(--bg-soft);
331
+ padding: 0.2em 0.4em;
332
+ font-size: 0.85em;
333
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
334
+ border: 1px solid var(--border);
335
+ border-radius: 6px;
336
+ }
337
+
338
+ /* Heading Anchors */
339
+ .heading-anchor {
340
+ text-decoration: none;
341
+ color: var(--text-muted);
342
+ position: absolute;
343
+ right: 100%;
344
+ padding-right: 0.5rem;
345
+ opacity: 0;
346
+ transition: opacity 0.2s;
347
+ font-weight: 400;
348
+ user-select: none;
349
+ }
350
+
351
+ .heading-anchor::before { content: "#"; }
352
+
353
+ .post-title:hover .heading-anchor,
354
+ .post-title:active .heading-anchor,
355
+ h1:hover .heading-anchor, h1:active .heading-anchor,
356
+ h2:hover .heading-anchor, h2:active .heading-anchor,
357
+ h3:hover .heading-anchor, h3:active .heading-anchor,
358
+ h4:hover .heading-anchor, h4:active .heading-anchor,
359
+ h5:hover .heading-anchor, h5:active .heading-anchor,
360
+ h6:hover .heading-anchor, h6:active .heading-anchor {
361
+ opacity: 1;
362
+ }
363
+
364
+ /* Search Modal */
365
+ .search-modal {
366
+ position: fixed;
367
+ inset: 0;
368
+ display: none;
369
+ align-items: flex-start;
370
+ justify-content: center;
371
+ z-index: 100;
372
+ padding-top: 15vh;
373
+ }
374
+
375
+ .search-modal.open { display: flex; }
376
+
377
+ .search-modal__scrim {
378
+ position: absolute;
379
+ inset: 0;
380
+ background: rgba(0, 0, 0, 0.2);
381
+ backdrop-filter: blur(4px);
382
+ }
383
+
384
+ .search-modal__panel {
385
+ position: relative;
386
+ width: min(600px, 92vw);
387
+ max-height: 60vh;
388
+ background: var(--bg);
389
+ border: 1px solid var(--border);
390
+ border-radius: 16px;
391
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
392
+ display: flex;
393
+ flex-direction: column;
394
+ overflow: hidden;
395
+ }
396
+
397
+ .search-input-wrapper {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 0.75rem;
401
+ padding: 1.25rem;
402
+ border-bottom: 1px solid var(--border);
403
+ }
404
+
405
+ .search-input {
406
+ flex: 1;
407
+ background: transparent;
408
+ border: none;
409
+ font-size: 1.125rem;
410
+ color: var(--text);
411
+ outline: none;
412
+ }
413
+
414
+ .search-results {
415
+ flex: 1;
416
+ overflow-y: auto;
417
+ padding: 0.75rem;
418
+ }
419
+
420
+ .search-result-item {
421
+ display: block;
422
+ padding: 1rem;
423
+ border-radius: 10px;
424
+ margin-bottom: 0.25rem;
425
+ }
426
+
427
+ .search-result-item:hover, .search-result-item.active {
428
+ background: var(--bg-soft);
429
+ }
430
+
431
+ .search-result-title {
432
+ font-weight: 600;
433
+ color: var(--primary);
434
+ margin-bottom: 0.25rem;
435
+ }
436
+
437
+ .search-result-excerpt {
438
+ font-size: 0.875rem;
439
+ color: var(--text-muted);
440
+ }
441
+
442
+ .search-result-excerpt mark {
443
+ background: var(--primary-soft);
444
+ color: var(--primary);
445
+ border-radius: 2px;
446
+ }
447
+
448
+ .search-status {
449
+ padding: 2rem;
450
+ text-align: center;
451
+ color: var(--text-muted);
452
+ }
453
+
454
+ /* Pagination */
455
+ .pagination-container {
456
+ margin-top: 4rem;
457
+ display: flex;
458
+ justify-content: center;
459
+ }
460
+
461
+ .load-more-btn {
462
+ background: var(--bg);
463
+ border: 1px solid var(--border);
464
+ padding: 0.75rem 2rem;
465
+ font-size: 0.95rem;
466
+ font-weight: 600;
467
+ cursor: pointer;
468
+ color: var(--text);
469
+ border-radius: 99px;
470
+ transition: all 0.2s;
471
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
472
+ }
473
+
474
+ .load-more-btn:hover {
475
+ border-color: var(--primary);
476
+ color: var(--primary);
477
+ background: var(--primary-soft);
478
+ transform: translateY(-1px);
479
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
480
+ }
481
+
482
+ .load-more-btn:active {
483
+ transform: translateY(0);
484
+ }
485
+
486
+ /* Footer */
487
+ .blog-footer {
488
+ padding: 4rem 0;
489
+ border-top: 1px solid var(--border);
490
+ text-align: center;
491
+ color: var(--text-muted);
492
+ font-size: 0.875rem;
493
+ margin-top: 4rem;
494
+ }
495
+
496
+ .blog-footer a {
497
+ color: var(--text);
498
+ font-weight: 500;
499
+ }
500
+
501
+ .blog-footer a[href*="methanol"] {
502
+ color: var(--primary);
503
+ font-weight: 700;
504
+ text-decoration: underline;
505
+ text-decoration-thickness: 2px;
506
+ text-underline-offset: 4px;
507
+ }
508
+
509
+ /* Responsive */
510
+ @media (max-width: 900px) {
511
+ .heading-anchor {
512
+ position: relative;
513
+ right: auto;
514
+ padding-right: 0.5rem;
515
+ }
516
+ }
517
+
518
+ @media (max-width: 768px) {
519
+ .nav-toggle-label {
520
+ display: flex;
521
+ align-items: center;
522
+ }
523
+
524
+ .blog-nav {
525
+ position: absolute;
526
+ top: 100%;
527
+ left: 0;
528
+ right: 0;
529
+ background: var(--bg);
530
+ border-bottom: 1px solid var(--border);
531
+ flex-direction: column;
532
+ gap: 0;
533
+ padding: 1rem 0;
534
+ transform: translateY(-1rem);
535
+ opacity: 0;
536
+ visibility: hidden;
537
+ transition: all 0.2s ease;
538
+ z-index: 40;
539
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
540
+ }
541
+
542
+ .blog-nav a {
543
+ padding: 0.75rem var(--container-px);
544
+ width: 100%;
545
+ border-bottom: 1px solid transparent;
546
+ }
547
+
548
+ .nav-toggle:checked ~ .blog-nav {
549
+ transform: translateY(0);
550
+ opacity: 1;
551
+ visibility: visible;
552
+ }
553
+
554
+ .nav-toggle:checked ~ .nav-toggle-label {
555
+ color: var(--primary);
556
+ }
557
+ }
558
+
559
+ @media (max-width: 640px) {
560
+ .blog-header { padding: 1rem 0; }
561
+ .header-container { gap: 1rem; }
562
+ .header-actions { gap: 0.75rem; }
563
+
564
+ .search-box {
565
+ padding: 0;
566
+ width: 2.25rem;
567
+ height: 2.25rem;
568
+ min-width: 2.25rem;
569
+ justify-content: center;
570
+ flex-shrink: 0;
571
+ }
572
+ .search-box span, .search-box kbd {
573
+ display: none;
574
+ }
575
+
576
+ .post-item-title { font-size: 1.5rem; }
577
+ .post-title { font-size: 2rem; }
578
+ .search-modal__panel { width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; padding-top: 0; }
579
+ }
@@ -0,0 +1,28 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ const pad = (value) => String(value).padStart(2, '0')
22
+
23
+ export const formatDate = (value) => {
24
+ if (!value) return ''
25
+ const date = value instanceof Date ? value : new Date(value)
26
+ if (Number.isNaN(date.valueOf())) return ''
27
+ return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())}`
28
+ }
@@ -0,0 +1,37 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ export function heading(Tag) {
22
+ return (props, ...children) => (
23
+ <Tag {...props}>
24
+ {props.id && <a class="heading-anchor" href={`#${props.id}`} aria-label={`Link to ${props.id}`} />}
25
+ {...children}
26
+ </Tag>
27
+ )
28
+ }
29
+
30
+ export function createHeadings() {
31
+ return Object.fromEntries(
32
+ [1, 2, 3, 4, 5, 6].map((i) => {
33
+ const tag = `h${i}`
34
+ return [tag, heading(tag)]
35
+ })
36
+ )
37
+ }
@@ -0,0 +1,66 @@
1
+ /* Copyright Yukino Song, SudoMaker Ltd.
2
+ *
3
+ * Licensed to the Apache Software Foundation (ASF) under one
4
+ * or more contributor license agreements. See the NOTICE file
5
+ * distributed with this work for additional information
6
+ * regarding copyright ownership. The ASF licenses this file
7
+ * to you under the Apache License, Version 2.0 (the
8
+ * "License"); you may not use this file except in compliance
9
+ * with the License. You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing,
14
+ * software distributed under the License is distributed on an
15
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ * KIND, either express or implied. See the License for the
17
+ * specific language governing permissions and limitations
18
+ * under the License.
19
+ */
20
+
21
+ import { HTMLRenderer as R } from 'methanol'
22
+ import { filterBlogPosts, mapStaticPosts, collectCategories } from './post-utils.js'
23
+ import { formatDate } from '../src/date-utils.js'
24
+
25
+ const renderPostCards = (posts = []) =>
26
+ posts.map((p) => {
27
+ const dateStr = formatDate(p.frontmatter?.date || p.stats?.createdAt)
28
+ const categories = p.frontmatter?.categories
29
+ const categoryLabel = Array.isArray(categories) ? categories.join(', ') : categories || ''
30
+ return (
31
+ <article class="post-item">
32
+ <div class="post-meta">
33
+ {dateStr && <span class="post-date">{dateStr}</span>}
34
+ {categoryLabel && <span class="post-categories"> &middot; {categoryLabel}</span>}
35
+ </div>
36
+ <h2 class="post-item-title">
37
+ <a href={p.routeHref}>{p.title || 'Untitled'}</a>
38
+ </h2>
39
+ <div class="post-excerpt">{p.excerpt || p.frontmatter?.excerpt || 'No excerpt available.'}</div>
40
+ </article>
41
+ )
42
+ })
43
+
44
+ export const LayoutCategories = ({ PageContent, title, pages, navLinks, components }) => {
45
+ const { CategoryView } = components || {}
46
+ const filteredPosts = filterBlogPosts(pages, navLinks)
47
+ const staticPosts = mapStaticPosts(filteredPosts)
48
+ const categories = collectCategories(filteredPosts)
49
+ const visiblePosts = staticPosts.slice(0, 10)
50
+ const staticCards = renderPostCards(visiblePosts)
51
+ return (
52
+ <div class="categories-container">
53
+ <header class="post-header">
54
+ <h1 class="post-title">{title}</h1>
55
+ </header>
56
+ <div class="categories-content">
57
+ <PageContent />
58
+ {CategoryView ? (
59
+ <CategoryView categories={categories}>{...staticCards}</CategoryView>
60
+ ) : (
61
+ <p>Error: CategoryView component not found.</p>
62
+ )}
63
+ </div>
64
+ </div>
65
+ )
66
+ }