@jet-w/astro-blog 0.1.6 → 0.2.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.
Files changed (61) hide show
  1. package/dist/chunk-ATRISB7B.js +206 -0
  2. package/dist/chunk-HVQKQN6B.js +145 -0
  3. package/dist/config/index.d.ts +3 -47
  4. package/dist/config/index.js +18 -2
  5. package/dist/i18n-5H4W145i.d.ts +202 -0
  6. package/dist/index.d.ts +186 -7
  7. package/dist/index.js +238 -3
  8. package/dist/integration.d.ts +9 -1
  9. package/dist/integration.js +2 -1
  10. package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
  11. package/dist/utils/sidebar.d.ts +1 -1
  12. package/package.json +1 -1
  13. package/src/components/layout/Footer.astro +36 -20
  14. package/src/components/layout/Header.astro +69 -15
  15. package/src/components/layout/Sidebar.astro +27 -15
  16. package/src/components/ui/LanguageSwitcher.vue +183 -0
  17. package/src/layouts/BaseLayout.astro +77 -52
  18. package/src/layouts/PageLayout.astro +22 -27
  19. package/src/layouts/SlidesLayout.astro +14 -2
  20. package/src/pages/rss.xml.ts +18 -6
  21. package/templates/default/astro.config.mjs +22 -2
  22. package/templates/default/content/posts/blog_docs/12-i18n.md +355 -0
  23. package/templates/default/content/posts/blog_docs/README.md +1 -0
  24. package/templates/default/content/posts/blog_docs_en/README.md +78 -0
  25. package/templates/default/content/posts/blog_docs_en/config/01-site.md +208 -0
  26. package/templates/default/content/posts/blog_docs_en/config/02-sidebar.md +240 -0
  27. package/templates/default/content/posts/blog_docs_en/config/03-i18n.md +285 -0
  28. package/templates/default/content/posts/blog_docs_en/config/README.md +85 -0
  29. package/templates/default/content/posts/blog_docs_en/get-started/01-intro.md +81 -0
  30. package/templates/default/content/posts/blog_docs_en/get-started/02-install.md +137 -0
  31. package/templates/default/content/posts/blog_docs_en/get-started/03-create-post.md +176 -0
  32. package/templates/default/content/posts/blog_docs_en/get-started/04-structure.md +173 -0
  33. package/templates/default/content/posts/blog_docs_en/get-started/05-deploy.md +197 -0
  34. package/templates/default/content/posts/blog_docs_en/get-started/README.md +52 -0
  35. package/templates/default/content/posts/blog_docs_en/guide/README.md +59 -0
  36. package/templates/default/content/posts/blog_docs_en/guide/features/01-mermaid.md +194 -0
  37. package/templates/default/content/posts/blog_docs_en/guide/features/02-latex.md +233 -0
  38. package/templates/default/content/posts/blog_docs_en/guide/features/03-video.md +184 -0
  39. package/templates/default/content/posts/blog_docs_en/guide/features/04-icons.md +227 -0
  40. package/templates/default/content/posts/blog_docs_en/guide/features/README.md +51 -0
  41. package/templates/default/content/posts/blog_docs_en/guide/markdown/02-containers.md +226 -0
  42. package/templates/default/content/posts/blog_docs_en/guide/markdown/03-code-blocks.md +206 -0
  43. package/templates/default/content/posts/blog_docs_en/guide/markdown/README.md +194 -0
  44. package/templates/default/package-lock.json +9667 -0
  45. package/templates/default/package.json +1 -1
  46. package/templates/default/src/config/footer.ts +14 -11
  47. package/templates/default/src/config/locales/en/footer.ts +17 -0
  48. package/templates/default/src/config/locales/en/index.ts +16 -0
  49. package/templates/default/src/config/locales/en/menu.ts +12 -0
  50. package/templates/default/src/config/locales/en/sidebar.ts +18 -0
  51. package/templates/default/src/config/locales/en/site.ts +7 -0
  52. package/templates/default/src/config/locales/index.ts +7 -0
  53. package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
  54. package/templates/default/src/config/locales/zh-CN/index.ts +16 -0
  55. package/templates/default/src/config/locales/zh-CN/menu.ts +12 -0
  56. package/templates/default/src/config/locales/zh-CN/sidebar.ts +18 -0
  57. package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
  58. package/templates/default/src/config/sidebar.ts +10 -12
  59. package/templates/default/src/env.d.ts +7 -0
  60. package/dist/chunk-MQXPSOYB.js +0 -124
  61. /package/dist/{chunk-GYLSY3OJ.js → chunk-AZHCNNAC.js} +0 -0
@@ -3,37 +3,69 @@ import { siteConfig } from '@jet-w/astro-blog/config';
3
3
  import ThemeToggle from '../ui/ThemeToggle.vue';
4
4
  import SearchBox from '../ui/SearchBox.vue';
5
5
  import MobileMenu from '../ui/MobileMenu.vue';
6
+ import LanguageSwitcher from '../ui/LanguageSwitcher.vue';
7
+ import type { I18nConfig } from '../../config/i18n';
8
+ import { defaultI18nConfig } from '../../config/i18n';
9
+ import {
10
+ getLocaleFromPath,
11
+ getLocaleConfig,
12
+ removeLocalePrefix,
13
+ isMultiLanguageEnabled,
14
+ } from '../../utils/i18n';
15
+
16
+ export interface Props {
17
+ i18nConfig?: I18nConfig;
18
+ }
19
+
20
+ const { i18nConfig = defaultI18nConfig } = Astro.props;
6
21
 
7
22
  const currentPath = Astro.url.pathname;
23
+ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
24
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
25
+ const ui = localeConfig.ui;
26
+
27
+ // Use locale-specific site config and menu
28
+ const localeSiteConfig = localeConfig.site;
29
+ const menu = localeConfig.menu;
30
+
31
+ // Get base path without locale prefix for language switcher
32
+ const basePath = removeLocalePrefix(currentPath, i18nConfig);
33
+
34
+ // Check if multi-language is enabled
35
+ const showLanguageSwitcher = isMultiLanguageEnabled(i18nConfig);
36
+
37
+ // Prepare locales for language switcher
38
+ const localesForSwitcher = i18nConfig.locales.map(l => ({
39
+ code: l.code,
40
+ name: l.name,
41
+ }));
8
42
  ---
9
43
 
10
44
  <header class="sticky top-0 z-50 bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm border-b border-slate-200 dark:border-slate-700">
11
45
  <div class="container mx-auto px-4">
12
46
  <nav class="flex items-center justify-between h-16">
13
- <!-- Logo 和站点名称 -->
14
47
  <div class="flex items-center space-x-4">
15
48
  <a href="/" class="flex items-center space-x-3 hover:opacity-80 transition-opacity">
16
- {siteConfig.avatar && (
49
+ {(localeSiteConfig.avatar || siteConfig.avatar) && (
17
50
  <img
18
- src={siteConfig.avatar}
19
- alt={siteConfig.title}
51
+ src={localeSiteConfig.avatar || siteConfig.avatar}
52
+ alt={localeSiteConfig.title || siteConfig.title}
20
53
  class="w-8 h-8 rounded-full"
21
54
  />
22
55
  )}
23
56
  <div class="hidden sm:block">
24
57
  <h1 class="text-xl font-bold text-slate-900 dark:text-slate-100">
25
- {siteConfig.title}
58
+ {localeSiteConfig.title || siteConfig.title}
26
59
  </h1>
27
60
  <p class="text-xs text-slate-600 dark:text-slate-400 -mt-1">
28
- 技术博客
61
+ {localeSiteConfig.description || siteConfig.description}
29
62
  </p>
30
63
  </div>
31
64
  </a>
32
65
  </div>
33
66
 
34
- <!-- 桌面端导航 -->
35
67
  <div class="hidden md:flex items-center space-x-8">
36
- {siteConfig.menu.map((item) => (
68
+ {menu.map((item) => (
37
69
  <a
38
70
  href={item.href}
39
71
  class={`text-sm font-medium transition-colors hover:text-primary-500 ${
@@ -47,22 +79,44 @@ const currentPath = Astro.url.pathname;
47
79
  ))}
48
80
  </div>
49
81
 
50
- <!-- 右侧功能区 -->
51
82
  <div class="flex items-center">
52
- <!-- 搜索框 -->
53
83
  <div class="hidden sm:block mr-2">
54
- <SearchBox client:load />
84
+ <SearchBox
85
+ client:load
86
+ placeholder={ui.searchPlaceholder}
87
+ searchLabel={ui.search}
88
+ />
55
89
  </div>
56
90
 
91
+ {showLanguageSwitcher && (
92
+ <div class="hidden sm:block mr-2">
93
+ <LanguageSwitcher
94
+ client:load
95
+ locales={localesForSwitcher}
96
+ currentLocale={currentLocale}
97
+ currentPath={basePath}
98
+ defaultLocale={i18nConfig.defaultLocale}
99
+ prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
100
+ />
101
+ </div>
102
+ )}
103
+
57
104
  <div class="hidden sm:block mr-2">
58
- <!-- 主题切换 -->
59
105
  <ThemeToggle client:load />
60
106
  </div>
61
- <!-- 移动端菜单 -->
107
+
62
108
  <div class="md:hidden">
63
- <MobileMenu client:load navigation={siteConfig.menu} />
109
+ <MobileMenu
110
+ client:load
111
+ navigation={menu}
112
+ locales={showLanguageSwitcher ? localesForSwitcher : []}
113
+ currentLocale={currentLocale}
114
+ currentPath={basePath}
115
+ defaultLocale={i18nConfig.defaultLocale}
116
+ prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
117
+ />
64
118
  </div>
65
119
  </div>
66
120
  </nav>
67
121
  </div>
68
- </header>
122
+ </header>
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  import { getCollection } from 'astro:content';
3
3
  import Icon from '../ui/Icon.astro';
4
- import { sidebarConfig } from '@jet-w/astro-blog/config';
5
4
  import {
6
5
  processSidebarConfig,
7
6
  getRecentPosts,
@@ -9,12 +8,25 @@ import {
9
8
  getArchives,
10
9
  filterGroupsByPath,
11
10
  } from '@jet-w/astro-blog/utils/sidebar';
11
+ import type { I18nConfig } from '../../config/i18n';
12
+ import { defaultI18nConfig } from '../../config/i18n';
13
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, formatDate } from '../../utils/i18n';
12
14
 
13
15
  interface Props {
14
16
  currentPath?: string;
17
+ i18nConfig?: I18nConfig;
15
18
  }
16
19
 
17
- const { currentPath = Astro.url.pathname } = Astro.props;
20
+ const { currentPath = Astro.url.pathname, i18nConfig = defaultI18nConfig } = Astro.props;
21
+
22
+ // Get current locale and translations
23
+ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
24
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
25
+ const ui = localeConfig.ui;
26
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
27
+
28
+ // Get locale-specific sidebar config (already merged with defaults in getLocaleConfig)
29
+ const sidebarConfig = localeConfig.sidebar;
18
30
 
19
31
  // 获取所有文章
20
32
  const allPosts = await getCollection('posts', ({ data }) => !data.draft);
@@ -362,17 +374,17 @@ const archives = sidebarConfig.showArchives
362
374
  )
363
375
  ))}
364
376
 
365
- {/* 最新文章 */}
377
+ {/* Recent Posts */}
366
378
  {sidebarConfig.showRecentPosts && recentPosts.length > 0 && (
367
379
  <section>
368
380
  <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
369
- 最新文章
381
+ {ui.recentPosts}
370
382
  </h3>
371
383
  <div class="space-y-3">
372
384
  {recentPosts.map((post) => (
373
385
  <article class="group">
374
386
  <a
375
- href={`/posts/${post.id.toLowerCase()}`}
387
+ href={`${localePrefix}/posts/${post.id.toLowerCase()}`}
376
388
  class="block p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
377
389
  >
378
390
  <h4 class="text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 line-clamp-2 mb-1">
@@ -380,7 +392,7 @@ const archives = sidebarConfig.showArchives
380
392
  </h4>
381
393
  {post.data.pubDate && (
382
394
  <time class="text-xs text-slate-500 dark:text-slate-400">
383
- {new Date(post.data.pubDate).toLocaleDateString('zh-CN')}
395
+ {formatDate(post.data.pubDate, localeConfig.locale.dateLocale)}
384
396
  </time>
385
397
  )}
386
398
  </a>
@@ -390,16 +402,16 @@ const archives = sidebarConfig.showArchives
390
402
  </section>
391
403
  )}
392
404
 
393
- {/* 热门标签 */}
405
+ {/* Popular Tags */}
394
406
  {sidebarConfig.showPopularTags && popularTags.length > 0 && (
395
407
  <section>
396
408
  <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
397
- 热门标签
409
+ {ui.popularTags}
398
410
  </h3>
399
411
  <div class="flex flex-wrap gap-2">
400
412
  {popularTags.map((tag) => (
401
413
  <a
402
- href={`/tags/${tag.slug}`}
414
+ href={`${localePrefix}/tags/${tag.slug}`}
403
415
  class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors"
404
416
  style={`font-size: ${Math.min(14, 10 + tag.count * 0.5)}px`}
405
417
  >
@@ -411,20 +423,20 @@ const archives = sidebarConfig.showArchives
411
423
  </section>
412
424
  )}
413
425
 
414
- {/* 归档 */}
426
+ {/* Archives */}
415
427
  {sidebarConfig.showArchives && archives.length > 0 && (
416
428
  <section>
417
429
  <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
418
- 文章归档
430
+ {ui.archives}
419
431
  </h3>
420
432
  <div class="space-y-2">
421
433
  {archives.map((archive) => (
422
434
  <a
423
- href={`/archives/${archive.year}/${String(archive.month).padStart(2, '0')}`}
435
+ href={`${localePrefix}/archives/${archive.year}/${String(archive.month).padStart(2, '0')}`}
424
436
  class="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors group"
425
437
  >
426
438
  <span class="text-sm text-slate-700 dark:text-slate-300 group-hover:text-primary-500">
427
- {archive.year}年{archive.month}
439
+ {new Date(archive.year, archive.month - 1).toLocaleDateString(localeConfig.locale.dateLocale, { year: 'numeric', month: 'long' })}
428
440
  </span>
429
441
  <span class="text-xs text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded-full">
430
442
  {archive.count}
@@ -435,11 +447,11 @@ const archives = sidebarConfig.showArchives
435
447
  </section>
436
448
  )}
437
449
 
438
- {/* 友情链接 */}
450
+ {/* Friend Links */}
439
451
  {sidebarConfig.showFriendLinks && sidebarConfig.friendLinks && sidebarConfig.friendLinks.length > 0 && (
440
452
  <section>
441
453
  <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
442
- 友情链接
454
+ {ui.friendLinks}
443
455
  </h3>
444
456
  <div class="space-y-2">
445
457
  {sidebarConfig.friendLinks.map((link) => (
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <div class="relative" ref="dropdownRef">
3
+ <button
4
+ @click="toggleDropdown"
5
+ class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
6
+ :aria-expanded="isOpen"
7
+ aria-haspopup="listbox"
8
+ :aria-label="ariaLabel"
9
+ >
10
+ <svg
11
+ class="w-4 h-4"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ viewBox="0 0 24 24"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ >
17
+ <path
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ stroke-width="2"
21
+ d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
22
+ />
23
+ </svg>
24
+ <span>{{ currentLocaleName }}</span>
25
+ <svg
26
+ class="w-3 h-3 transition-transform"
27
+ :class="{ 'rotate-180': isOpen }"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ viewBox="0 0 24 24"
31
+ >
32
+ <path
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ stroke-width="2"
36
+ d="M19 9l-7 7-7-7"
37
+ />
38
+ </svg>
39
+ </button>
40
+
41
+ <Transition
42
+ enter-active-class="transition ease-out duration-100"
43
+ enter-from-class="transform opacity-0 scale-95"
44
+ enter-to-class="transform opacity-100 scale-100"
45
+ leave-active-class="transition ease-in duration-75"
46
+ leave-from-class="transform opacity-100 scale-100"
47
+ leave-to-class="transform opacity-0 scale-95"
48
+ >
49
+ <div
50
+ v-if="isOpen"
51
+ class="absolute right-0 mt-2 w-40 origin-top-right rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 dark:ring-gray-700 focus:outline-none z-50"
52
+ role="listbox"
53
+ :aria-label="listLabel"
54
+ >
55
+ <div class="py-1">
56
+ <a
57
+ v-for="locale in locales"
58
+ :key="locale.code"
59
+ :href="getLocalizedUrl(locale.code)"
60
+ class="flex items-center gap-2 px-4 py-2 text-sm transition-colors"
61
+ :class="[
62
+ locale.code === currentLocale
63
+ ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
64
+ : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
65
+ ]"
66
+ role="option"
67
+ :aria-selected="locale.code === currentLocale"
68
+ @click="closeDropdown"
69
+ >
70
+ <span
71
+ v-if="locale.code === currentLocale"
72
+ class="w-4 h-4 flex items-center justify-center"
73
+ >
74
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
75
+ <path
76
+ fill-rule="evenodd"
77
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
78
+ clip-rule="evenodd"
79
+ />
80
+ </svg>
81
+ </span>
82
+ <span v-else class="w-4 h-4" />
83
+ <span>{{ locale.name }}</span>
84
+ </a>
85
+ </div>
86
+ </div>
87
+ </Transition>
88
+ </div>
89
+ </template>
90
+
91
+ <script setup lang="ts">
92
+ import { ref, computed, onMounted, onUnmounted } from 'vue';
93
+
94
+ interface LocaleInfo {
95
+ code: string;
96
+ name: string;
97
+ }
98
+
99
+ interface Props {
100
+ /** Available locales */
101
+ locales: LocaleInfo[];
102
+ /** Current locale code */
103
+ currentLocale: string;
104
+ /** Current page path (without locale prefix) */
105
+ currentPath: string;
106
+ /** Default locale code */
107
+ defaultLocale: string;
108
+ /** Whether to prefix the default locale in URLs */
109
+ prefixDefaultLocale?: boolean;
110
+ }
111
+
112
+ const props = withDefaults(defineProps<Props>(), {
113
+ prefixDefaultLocale: false,
114
+ });
115
+
116
+ const isOpen = ref(false);
117
+ const dropdownRef = ref<HTMLElement | null>(null);
118
+
119
+ const currentLocaleName = computed(() => {
120
+ const locale = props.locales.find((l) => l.code === props.currentLocale);
121
+ return locale?.name || props.currentLocale;
122
+ });
123
+
124
+ const ariaLabel = computed(() => {
125
+ return `Select language, current: ${currentLocaleName.value}`;
126
+ });
127
+
128
+ const listLabel = computed(() => {
129
+ return 'Available languages';
130
+ });
131
+
132
+ function toggleDropdown() {
133
+ isOpen.value = !isOpen.value;
134
+ }
135
+
136
+ function closeDropdown() {
137
+ isOpen.value = false;
138
+ }
139
+
140
+ function getLocalizedUrl(targetLocale: string): string {
141
+ // Normalize the path
142
+ let basePath = props.currentPath;
143
+ if (!basePath.startsWith('/')) {
144
+ basePath = '/' + basePath;
145
+ }
146
+
147
+ // If target is default locale and prefix is not required
148
+ if (targetLocale === props.defaultLocale && !props.prefixDefaultLocale) {
149
+ return basePath === '/' ? '/' : basePath;
150
+ }
151
+
152
+ // Add locale prefix
153
+ if (basePath === '/') {
154
+ return `/${targetLocale}`;
155
+ }
156
+
157
+ return `/${targetLocale}${basePath}`;
158
+ }
159
+
160
+ // Close dropdown when clicking outside
161
+ function handleClickOutside(event: MouseEvent) {
162
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
163
+ closeDropdown();
164
+ }
165
+ }
166
+
167
+ // Close dropdown on escape key
168
+ function handleKeyDown(event: KeyboardEvent) {
169
+ if (event.key === 'Escape') {
170
+ closeDropdown();
171
+ }
172
+ }
173
+
174
+ onMounted(() => {
175
+ document.addEventListener('click', handleClickOutside);
176
+ document.addEventListener('keydown', handleKeyDown);
177
+ });
178
+
179
+ onUnmounted(() => {
180
+ document.removeEventListener('click', handleClickOutside);
181
+ document.removeEventListener('keydown', handleKeyDown);
182
+ });
183
+ </script>