@jet-w/astro-blog 0.1.1 → 0.1.3

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 (46) hide show
  1. package/dist/chunk-MQXPSOYB.js +124 -0
  2. package/dist/config/index.d.ts +2 -92
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.js +6 -0
  5. package/dist/integration.d.ts +25 -0
  6. package/dist/integration.js +8 -0
  7. package/dist/sidebar-DNdiCKBw.d.ts +92 -0
  8. package/dist/utils/sidebar.d.ts +98 -0
  9. package/dist/utils/sidebar.js +305 -0
  10. package/package.json +6 -3
  11. package/src/components/about/SocialLinks.astro +1 -1
  12. package/src/components/blog/Hero.astro +1 -1
  13. package/src/components/layout/Footer.astro +1 -2
  14. package/src/components/layout/Header.astro +4 -4
  15. package/src/components/layout/Sidebar.astro +3 -3
  16. package/src/components/ui/MobileMenu.vue +1 -1
  17. package/src/components/ui/SearchInterface.vue +1 -1
  18. package/src/layouts/BaseLayout.astro +3 -3
  19. package/src/layouts/SlidesLayout.astro +2 -2
  20. package/{templates/default/src → src}/pages/rss.xml.ts +1 -1
  21. package/templates/default/astro.config.mjs +3 -1
  22. package/templates/default/src/config/footer.ts +31 -0
  23. package/templates/default/src/config/index.ts +13 -110
  24. package/templates/default/src/config/sidebar.ts +33 -0
  25. package/templates/default/src/config/site.ts +56 -0
  26. package/templates/default/src/config/social.ts +15 -0
  27. package/src/utils/sidebar.ts +0 -492
  28. /package/{templates/default/src → src}/pages/[...slug].astro +0 -0
  29. /package/{templates/default/src → src}/pages/archives/[year]/[month]/page/[page].astro +0 -0
  30. /package/{templates/default/src → src}/pages/archives/[year]/[month].astro +0 -0
  31. /package/{templates/default/src → src}/pages/archives/index.astro +0 -0
  32. /package/{templates/default/src → src}/pages/categories/[category]/page/[page].astro +0 -0
  33. /package/{templates/default/src → src}/pages/categories/[category].astro +0 -0
  34. /package/{templates/default/src → src}/pages/categories/index.astro +0 -0
  35. /package/{templates/default/src → src}/pages/container-test.astro +0 -0
  36. /package/{templates/default/src → src}/pages/mermaid-direct.html +0 -0
  37. /package/{templates/default/src → src}/pages/posts/[...slug].astro +0 -0
  38. /package/{templates/default/src → src}/pages/posts/index.astro +0 -0
  39. /package/{templates/default/src → src}/pages/posts/page/[page].astro +0 -0
  40. /package/{templates/default/src → src}/pages/search-index.json.ts +0 -0
  41. /package/{templates/default/src → src}/pages/search.astro +0 -0
  42. /package/{templates/default/src → src}/pages/slides/[...slug].astro +0 -0
  43. /package/{templates/default/src → src}/pages/slides/index.astro +0 -0
  44. /package/{templates/default/src → src}/pages/tags/[tag]/page/[page].astro +0 -0
  45. /package/{templates/default/src → src}/pages/tags/[tag].astro +0 -0
  46. /package/{templates/default/src → src}/pages/tags/index.astro +0 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Site Configuration
3
+ *
4
+ * Basic site settings including title, description, author info, etc.
5
+ */
6
+
7
+ import type { SiteConfig } from '@jet-w/astro-blog';
8
+
9
+ /**
10
+ * Site configuration
11
+ */
12
+ export const siteConfig: SiteConfig = {
13
+ title: 'My Astro Blog',
14
+ description: '基于 Astro + Vue + Tailwind 构建的个人技术博客',
15
+ author: 'Author',
16
+ email: 'email@example.com',
17
+ avatar: '/images/avatar.svg',
18
+ social: {
19
+ github: 'https://github.com/username',
20
+ twitter: '',
21
+ linkedin: '',
22
+ email: 'mailto:email@example.com'
23
+ },
24
+ menu: [
25
+ {
26
+ name: '首页',
27
+ href: '/',
28
+ icon: 'home'
29
+ },
30
+ {
31
+ name: '博客教学',
32
+ href: '/posts/blog_docs',
33
+ icon: 'posts'
34
+ },
35
+ {
36
+ name: '演示',
37
+ href: '/slides',
38
+ icon: 'slides'
39
+ },
40
+ {
41
+ name: '关于',
42
+ href: '/about',
43
+ icon: 'about'
44
+ }
45
+ ]
46
+ };
47
+
48
+ /**
49
+ * Default SEO settings
50
+ */
51
+ export const defaultSEO = {
52
+ title: siteConfig.title,
53
+ description: siteConfig.description,
54
+ image: '/images/og-image.jpg',
55
+ type: 'website' as const
56
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Social Links Configuration
3
+ *
4
+ * Configure social media links for the blog
5
+ */
6
+
7
+ import type { SocialLink } from '@jet-w/astro-blog';
8
+
9
+ /**
10
+ * Social links
11
+ */
12
+ export const socialLinks: SocialLink[] = [
13
+ { type: 'github', url: 'https://github.com/username', label: 'GitHub' },
14
+ { type: 'email', url: 'mailto:email@example.com', label: 'Email' }
15
+ ];
@@ -1,492 +0,0 @@
1
- /**
2
- * 侧边栏工具函数
3
- * 处理配置解析和树形结构生成
4
- */
5
-
6
- import type { CollectionEntry } from 'astro:content';
7
- import type {
8
- SidebarConfig,
9
- SidebarGroup,
10
- SidebarItem,
11
- ScanConfig,
12
- ManualConfig,
13
- MixedConfig,
14
- } from '../config/sidebar';
15
-
16
- // 树节点类型
17
- export interface TreeNode {
18
- name: string;
19
- slug?: string;
20
- title?: string;
21
- displayName?: string;
22
- icon?: string;
23
- badge?: string;
24
- badgeType?: 'info' | 'success' | 'warning' | 'error';
25
- children: TreeNode[];
26
- isFolder: boolean;
27
- isReadme?: boolean;
28
- link?: string;
29
- collapsed?: boolean;
30
- }
31
-
32
- // 处理后的侧边栏组
33
- export interface ProcessedGroup {
34
- type: 'tree' | 'items' | 'divider';
35
- title: string;
36
- icon?: string;
37
- collapsed?: boolean;
38
- tree?: TreeNode[];
39
- items?: SidebarItem[];
40
- }
41
-
42
- /**
43
- * 从文章集合构建树形结构
44
- */
45
- export function buildTreeFromPosts(
46
- posts: CollectionEntry<'posts'>[],
47
- scanPath: string = '',
48
- options: {
49
- maxDepth?: number;
50
- exclude?: string[];
51
- include?: string[];
52
- sortBy?: 'name' | 'date' | 'title' | 'custom';
53
- sortOrder?: 'asc' | 'desc';
54
- } = {}
55
- ): TreeNode[] {
56
- const { maxDepth, exclude = [], include = [], sortBy = 'name', sortOrder = 'asc' } = options;
57
-
58
- // 过滤出指定路径下的文章
59
- const filteredPosts = posts.filter(post => {
60
- const postPath = post.id.toLowerCase();
61
- const targetPath = scanPath.toLowerCase();
62
-
63
- // 检查是否在指定路径下
64
- if (targetPath && !postPath.startsWith(targetPath + '/') && postPath !== targetPath) {
65
- return false;
66
- }
67
-
68
- // 检查排除规则
69
- const pathParts = post.id.split('/');
70
- for (const part of pathParts) {
71
- if (exclude.some(pattern => matchPattern(part, pattern))) {
72
- return false;
73
- }
74
- }
75
-
76
- // 检查包含规则
77
- if (include.length > 0) {
78
- const matchesInclude = pathParts.some(part =>
79
- include.some(pattern => matchPattern(part, pattern))
80
- );
81
- if (!matchesInclude) {
82
- return false;
83
- }
84
- }
85
-
86
- return true;
87
- });
88
-
89
- // 收集文件夹的 README 标题和图标
90
- const folderTitles: Record<string, string> = {};
91
- const folderIcons: Record<string, string> = {};
92
-
93
- filteredPosts.forEach(post => {
94
- const pathParts = post.id.split('/');
95
- const fileName = pathParts[pathParts.length - 1].toLowerCase();
96
-
97
- if (fileName === 'readme' || fileName === 'readme.md') {
98
- const folderPath = pathParts.slice(0, -1).join('/');
99
- if (folderPath) {
100
- if (post.data.title) {
101
- folderTitles[folderPath] = post.data.title;
102
- }
103
- if (post.data.icon) {
104
- folderIcons[folderPath] = post.data.icon;
105
- }
106
- }
107
- }
108
- });
109
-
110
- // 构建树
111
- const tree: TreeNode[] = [];
112
-
113
- filteredPosts.forEach(post => {
114
- // 移除 scanPath 前缀
115
- let relativePath = post.id;
116
- if (scanPath) {
117
- const scanPathLower = scanPath.toLowerCase();
118
- const postIdLower = post.id.toLowerCase();
119
- if (postIdLower.startsWith(scanPathLower + '/')) {
120
- relativePath = post.id.slice(scanPath.length + 1);
121
- } else if (postIdLower === scanPathLower) {
122
- relativePath = post.id.split('/').pop() || post.id;
123
- }
124
- }
125
-
126
- const pathParts = relativePath.split('/');
127
-
128
- // 检查深度限制
129
- if (maxDepth !== undefined && pathParts.length > maxDepth) {
130
- return;
131
- }
132
-
133
- let currentLevel = tree;
134
- let currentPath = scanPath;
135
-
136
- pathParts.forEach((part, index) => {
137
- const isLast = index === pathParts.length - 1;
138
- const existing = currentLevel.find(n => n.name.toLowerCase() === part.toLowerCase());
139
-
140
- currentPath = currentPath ? `${currentPath}/${part}` : part;
141
- const isReadme = isLast && (part.toLowerCase() === 'readme' || part.toLowerCase() === 'readme.md');
142
-
143
- if (existing) {
144
- if (isLast) {
145
- existing.slug = post.id;
146
- existing.title = post.data.title;
147
- existing.icon = post.data.icon;
148
- existing.isReadme = isReadme;
149
- } else {
150
- // 更新文件夹信息
151
- const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join('/')}` : pathParts.slice(0, index + 1).join('/');
152
- if (folderTitles[folderPath]) {
153
- existing.displayName = folderTitles[folderPath];
154
- }
155
- if (folderIcons[folderPath]) {
156
- existing.icon = folderIcons[folderPath];
157
- }
158
- }
159
- currentLevel = existing.children;
160
- } else {
161
- const folderPath = scanPath ? `${scanPath}/${pathParts.slice(0, index + 1).join('/')}` : pathParts.slice(0, index + 1).join('/');
162
- const newNode: TreeNode = {
163
- name: part,
164
- slug: isLast ? post.id : undefined,
165
- title: isLast ? post.data.title : undefined,
166
- displayName: isLast ? post.data.title : folderTitles[folderPath],
167
- icon: isLast ? post.data.icon : folderIcons[folderPath],
168
- children: [],
169
- isFolder: !isLast,
170
- isReadme: isReadme,
171
- };
172
- currentLevel.push(newNode);
173
- currentLevel = newNode.children;
174
- }
175
- });
176
- });
177
-
178
- // 排序并过滤 README
179
- return sortTree(tree, sortBy, sortOrder);
180
- }
181
-
182
- /**
183
- * 排序树并过滤 README 文件
184
- */
185
- function sortTree(
186
- nodes: TreeNode[],
187
- sortBy: 'name' | 'date' | 'title' | 'custom' = 'name',
188
- sortOrder: 'asc' | 'desc' = 'asc'
189
- ): TreeNode[] {
190
- const filtered = nodes.filter(node => !node.isReadme);
191
-
192
- const sorted = filtered.sort((a, b) => {
193
- // 文件夹优先
194
- if (a.isFolder && !b.isFolder) return -1;
195
- if (!a.isFolder && b.isFolder) return 1;
196
-
197
- let comparison = 0;
198
- switch (sortBy) {
199
- case 'title':
200
- comparison = (a.displayName || a.title || a.name).localeCompare(
201
- b.displayName || b.title || b.name,
202
- 'zh-CN'
203
- );
204
- break;
205
- case 'name':
206
- default:
207
- comparison = a.name.localeCompare(b.name, 'zh-CN', { numeric: true });
208
- break;
209
- }
210
-
211
- return sortOrder === 'desc' ? -comparison : comparison;
212
- });
213
-
214
- return sorted.map(node => ({
215
- ...node,
216
- children: sortTree(node.children, sortBy, sortOrder),
217
- }));
218
- }
219
-
220
- /**
221
- * 简单的模式匹配(支持 * 通配符)
222
- */
223
- function matchPattern(str: string, pattern: string): boolean {
224
- if (pattern === '*') return true;
225
- if (pattern.includes('*')) {
226
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
227
- return regex.test(str);
228
- }
229
- return str.toLowerCase() === pattern.toLowerCase();
230
- }
231
-
232
- /**
233
- * 路径 glob 模式匹配
234
- * 支持:
235
- * - /posts/tech/** 匹配 /posts/tech 及其所有子路径
236
- * - /posts/tech/* 匹配 /posts/tech 的直接子路径
237
- * - /posts/tech 精确匹配
238
- */
239
- export function matchPathPattern(currentPath: string, pattern: string): boolean {
240
- // 规范化路径
241
- const normalizedPath = currentPath.replace(/\/$/, '').toLowerCase();
242
- const normalizedPattern = pattern.replace(/\/$/, '').toLowerCase();
243
-
244
- // ** 匹配任意深度
245
- if (normalizedPattern.endsWith('/**')) {
246
- const basePath = normalizedPattern.slice(0, -3);
247
- return normalizedPath === basePath || normalizedPath.startsWith(basePath + '/');
248
- }
249
-
250
- // * 匹配单层
251
- if (normalizedPattern.endsWith('/*')) {
252
- const basePath = normalizedPattern.slice(0, -2);
253
- if (normalizedPath === basePath) return true;
254
- // 检查是否是直接子路径
255
- if (normalizedPath.startsWith(basePath + '/')) {
256
- const remaining = normalizedPath.slice(basePath.length + 1);
257
- return !remaining.includes('/');
258
- }
259
- return false;
260
- }
261
-
262
- // 精确匹配
263
- return normalizedPath === normalizedPattern;
264
- }
265
-
266
- /**
267
- * 检查侧边栏组是否应该在当前路径显示
268
- */
269
- export function shouldShowGroup(
270
- group: { showForPaths?: string[]; hideForPaths?: string[] },
271
- currentPath: string
272
- ): boolean {
273
- // 如果没有配置路径规则,默认显示
274
- if (!group.showForPaths && !group.hideForPaths) {
275
- return true;
276
- }
277
-
278
- // 检查隐藏规则
279
- if (group.hideForPaths && group.hideForPaths.length > 0) {
280
- for (const pattern of group.hideForPaths) {
281
- if (matchPathPattern(currentPath, pattern)) {
282
- return false;
283
- }
284
- }
285
- }
286
-
287
- // 检查显示规则
288
- if (group.showForPaths && group.showForPaths.length > 0) {
289
- for (const pattern of group.showForPaths) {
290
- if (matchPathPattern(currentPath, pattern)) {
291
- return true;
292
- }
293
- }
294
- // 配置了 showForPaths 但没有匹配,则不显示
295
- return false;
296
- }
297
-
298
- // 默认显示
299
- return true;
300
- }
301
-
302
- /**
303
- * 根据当前路径过滤侧边栏组
304
- */
305
- export function filterGroupsByPath(
306
- groups: SidebarGroup[],
307
- currentPath: string
308
- ): SidebarGroup[] {
309
- return groups.filter(group => shouldShowGroup(group, currentPath));
310
- }
311
-
312
- /**
313
- * 将手动配置的项目转换为树节点
314
- */
315
- export function manualItemsToTree(items: SidebarItem[]): TreeNode[] {
316
- return items.map(item => ({
317
- name: item.title,
318
- slug: item.slug,
319
- title: item.title,
320
- displayName: item.title,
321
- icon: item.icon,
322
- badge: item.badge,
323
- badgeType: item.badgeType,
324
- link: item.link,
325
- children: item.children ? manualItemsToTree(item.children) : [],
326
- isFolder: !!(item.children && item.children.length > 0),
327
- collapsed: item.collapsed,
328
- }));
329
- }
330
-
331
- /**
332
- * 处理侧边栏配置,生成可渲染的数据结构
333
- */
334
- export async function processSidebarConfig(
335
- config: SidebarConfig,
336
- posts: CollectionEntry<'posts'>[]
337
- ): Promise<ProcessedGroup[]> {
338
- const processedGroups: ProcessedGroup[] = [];
339
-
340
- for (const group of config.groups) {
341
- const processed = await processGroup(group, posts);
342
- if (processed) {
343
- processedGroups.push(processed);
344
- }
345
- }
346
-
347
- return processedGroups;
348
- }
349
-
350
- /**
351
- * 处理单个侧边栏组
352
- */
353
- async function processGroup(
354
- group: SidebarGroup,
355
- posts: CollectionEntry<'posts'>[]
356
- ): Promise<ProcessedGroup | null> {
357
- switch (group.type) {
358
- case 'scan': {
359
- const scanConfig = group as ScanConfig;
360
- const tree = buildTreeFromPosts(posts, scanConfig.scanPath, {
361
- maxDepth: scanConfig.maxDepth,
362
- exclude: scanConfig.exclude,
363
- include: scanConfig.include,
364
- sortBy: scanConfig.sortBy,
365
- sortOrder: scanConfig.sortOrder,
366
- });
367
-
368
- return {
369
- type: 'tree',
370
- title: scanConfig.title,
371
- icon: scanConfig.icon,
372
- collapsed: scanConfig.collapsed,
373
- tree,
374
- };
375
- }
376
-
377
- case 'manual': {
378
- const manualConfig = group as ManualConfig;
379
- const tree = manualItemsToTree(manualConfig.items);
380
-
381
- return {
382
- type: 'tree',
383
- title: manualConfig.title,
384
- icon: manualConfig.icon,
385
- collapsed: manualConfig.collapsed,
386
- tree,
387
- };
388
- }
389
-
390
- case 'mixed': {
391
- const mixedConfig = group as MixedConfig;
392
- const combinedTree: TreeNode[] = [];
393
-
394
- for (const section of mixedConfig.sections) {
395
- const processed = await processGroup(section, posts);
396
- if (processed && processed.tree) {
397
- // 将每个子部分作为一个文件夹节点
398
- combinedTree.push({
399
- name: section.title,
400
- displayName: section.title,
401
- icon: section.icon,
402
- children: processed.tree,
403
- isFolder: true,
404
- collapsed: section.collapsed,
405
- });
406
- }
407
- }
408
-
409
- return {
410
- type: 'tree',
411
- title: mixedConfig.title,
412
- icon: mixedConfig.icon,
413
- collapsed: mixedConfig.collapsed,
414
- tree: combinedTree,
415
- };
416
- }
417
-
418
- case 'divider': {
419
- return {
420
- type: 'divider',
421
- title: group.title || '',
422
- };
423
- }
424
-
425
- default:
426
- return null;
427
- }
428
- }
429
-
430
- /**
431
- * 获取最新文章
432
- */
433
- export function getRecentPosts(
434
- posts: CollectionEntry<'posts'>[],
435
- count: number = 5
436
- ): CollectionEntry<'posts'>[] {
437
- return posts
438
- .filter(p => p.data.pubDate)
439
- .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0))
440
- .slice(0, count);
441
- }
442
-
443
- /**
444
- * 获取热门标签
445
- */
446
- export function getPopularTags(
447
- posts: CollectionEntry<'posts'>[],
448
- count: number = 8
449
- ): Array<{ name: string; count: number; slug: string }> {
450
- const tagCounts: Record<string, number> = {};
451
-
452
- posts.forEach(post => {
453
- (post.data.tags || []).forEach(tag => {
454
- tagCounts[tag] = (tagCounts[tag] || 0) + 1;
455
- });
456
- });
457
-
458
- return Object.entries(tagCounts)
459
- .sort((a, b) => b[1] - a[1])
460
- .slice(0, count)
461
- .map(([name, count]) => ({
462
- name,
463
- count,
464
- slug: name.toLowerCase().replace(/\s+/g, '-'),
465
- }));
466
- }
467
-
468
- /**
469
- * 获取归档数据
470
- */
471
- export function getArchives(
472
- posts: CollectionEntry<'posts'>[],
473
- count: number = 6
474
- ): Array<{ year: number; month: number; count: number }> {
475
- const archiveMap: Record<string, number> = {};
476
-
477
- posts.forEach(post => {
478
- if (post.data.pubDate) {
479
- const date = new Date(post.data.pubDate);
480
- const key = `${date.getFullYear()}-${date.getMonth() + 1}`;
481
- archiveMap[key] = (archiveMap[key] || 0) + 1;
482
- }
483
- });
484
-
485
- return Object.entries(archiveMap)
486
- .sort((a, b) => b[0].localeCompare(a[0]))
487
- .slice(0, count)
488
- .map(([key, count]) => {
489
- const [year, month] = key.split('-').map(Number);
490
- return { year, month, count };
491
- });
492
- }