@jet-w/astro-blog 0.2.5 → 0.2.6

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/components/blog/Hero.astro +9 -6
  3. package/src/components/blog/PostCard.astro +4 -8
  4. package/src/components/home/FeaturedPostsSection.astro +9 -4
  5. package/src/components/home/QuickNavSection.astro +9 -4
  6. package/src/components/home/RecentPostsSection.astro +9 -4
  7. package/src/components/home/StatsSection.astro +9 -3
  8. package/src/components/layout/Footer.astro +16 -7
  9. package/src/components/layout/Header.astro +15 -12
  10. package/src/components/layout/Sidebar.astro +11 -6
  11. package/src/components/media/Slides.astro +22 -6
  12. package/src/components/ui/LanguageSwitcher.vue +24 -12
  13. package/src/components/ui/MobileMenu.vue +20 -3
  14. package/src/layouts/BaseLayout.astro +14 -7
  15. package/src/layouts/PageLayout.astro +9 -3
  16. package/src/layouts/SlidesLayout.astro +34 -16
  17. package/src/pages/archives/[year]/[month]/page/[page].astro +42 -18
  18. package/src/pages/archives/[year]/[month].astro +7 -2
  19. package/src/pages/archives/index.astro +7 -3
  20. package/src/pages/categories/[category]/page/[page].astro +40 -18
  21. package/src/pages/categories/[category].astro +7 -2
  22. package/src/pages/categories/index.astro +7 -3
  23. package/src/pages/posts/[...slug].astro +7 -3
  24. package/src/pages/posts/index.astro +7 -3
  25. package/src/pages/posts/page/[page].astro +7 -2
  26. package/src/pages/search.astro +9 -3
  27. package/src/pages/slides/index.astro +7 -3
  28. package/src/pages/tags/[tag]/page/[page].astro +39 -17
  29. package/src/pages/tags/[tag].astro +7 -2
  30. package/src/pages/tags/index.astro +7 -3
  31. package/src/plugins/rehype-relative-links.mjs +90 -14
  32. package/templates/default/.claude/ralph-loop.local.md +2 -32
  33. package/templates/default/Makefile +37 -0
  34. package/templates/default/astro.config.mjs +7 -3
  35. package/templates/default/package.prod.json +31 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jet-w/astro-blog",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A modern Astro blog theme with Vue and Tailwind CSS support",
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,7 +3,7 @@ import { siteConfig } from '@jet-w/astro-blog/config';
3
3
  import type { I18nConfig } from '../../config/i18n';
4
4
  import { defaultI18nConfig } from '../../config/i18n';
5
5
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
6
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, withBase } from '../../utils/i18n';
6
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, withBase, removeBase } from '../../utils/i18n';
7
7
 
8
8
  export interface Props {
9
9
  i18nConfig?: I18nConfig;
@@ -11,13 +11,16 @@ export interface Props {
11
11
 
12
12
  const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
13
13
 
14
- // Get current locale from URL
15
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
16
- const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
17
- const ui = localeConfig.ui;
18
-
19
14
  // Get base URL for prefixing links
20
15
  const base = import.meta.env.BASE_URL;
16
+
17
+ // Remove base URL from current path for locale detection
18
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
19
+
20
+ // Get current locale from URL (use path without base)
21
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
22
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
23
+ const ui = localeConfig.ui;
21
24
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
22
25
  ---
23
26
 
@@ -1,6 +1,4 @@
1
1
  ---
2
- import { withBase } from '../../utils/i18n';
3
-
4
2
  export interface Props {
5
3
  post: {
6
4
  slug: string;
@@ -32,8 +30,6 @@ const {
32
30
  ui = { readMore: '阅读更多', minuteRead: '分钟' }
33
31
  } = Astro.props;
34
32
 
35
- const base = import.meta.env.BASE_URL;
36
-
37
33
  const formattedDate = post.pubDate
38
34
  ? new Intl.DateTimeFormat(locale, {
39
35
  year: 'numeric',
@@ -49,10 +45,10 @@ const tagToSlug = (tag: string) => tag.toLowerCase().replace(/\s+/g, '-');
49
45
  // 将分类名转换为 slug 格式
50
46
  const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+/g, '-');
51
47
 
52
- // Build URLs with locale prefix and base URL
53
- const postUrl = withBase(`${localePrefix}/posts/${post.slug}`, base);
54
- const tagUrl = (tag: string) => withBase(`${localePrefix}/tags/${tagToSlug(tag)}`, base);
55
- const categoryUrl = (category: string) => withBase(`${localePrefix}/categories/${categoryToSlug(category)}`, base);
48
+ // Build URLs with locale prefix (localePrefix already contains base URL)
49
+ const postUrl = `${localePrefix}/posts/${post.slug}`;
50
+ const tagUrl = (tag: string) => `${localePrefix}/tags/${tagToSlug(tag)}`;
51
+ const categoryUrl = (category: string) => `${localePrefix}/categories/${categoryToSlug(category)}`;
56
52
  ---
57
53
 
58
54
  <article class={`group card hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 ${isHorizontal ? 'flex gap-6 items-center' : 'block'}`}>
@@ -7,7 +7,7 @@ import { getCollection } from 'astro:content';
7
7
  import type { I18nConfig } from '../../config/i18n';
8
8
  import { defaultI18nConfig } from '../../config/i18n';
9
9
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../utils/i18n';
11
11
 
12
12
  export interface Props {
13
13
  count?: number;
@@ -16,11 +16,16 @@ export interface Props {
16
16
 
17
17
  const { count = 3, i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
18
18
 
19
- // Get current locale from URL
20
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
19
+ // Get base URL for prefixing links
20
+ const base = import.meta.env.BASE_URL;
21
+
22
+ // Remove base URL from current path for locale detection
23
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
24
+
25
+ // Get current locale from URL (use path without base)
26
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
21
27
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
22
28
  const ui = localeConfig.ui;
23
- const base = import.meta.env.BASE_URL;
24
29
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
25
30
 
26
31
  const allPosts = await getCollection('posts');
@@ -7,7 +7,7 @@ import { getCollection } from 'astro:content';
7
7
  import type { I18nConfig } from '../../config/i18n';
8
8
  import { defaultI18nConfig } from '../../config/i18n';
9
9
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../utils/i18n';
11
11
 
12
12
  interface Props {
13
13
  i18nConfig?: I18nConfig;
@@ -15,11 +15,16 @@ interface Props {
15
15
 
16
16
  const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
17
17
 
18
- // Get current locale and translations
19
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
18
+ // Get base URL for prefixing links
19
+ const base = import.meta.env.BASE_URL;
20
+
21
+ // Remove base URL from current path for locale detection
22
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
23
+
24
+ // Get current locale and translations (use path without base)
25
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
20
26
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
21
27
  const ui = localeConfig.ui;
22
- const base = import.meta.env.BASE_URL;
23
28
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
24
29
 
25
30
  const allPosts = await getCollection('posts', ({ data }) => !data.draft);
@@ -7,7 +7,7 @@ import { getCollection } from 'astro:content';
7
7
  import type { I18nConfig } from '../../config/i18n';
8
8
  import { defaultI18nConfig } from '../../config/i18n';
9
9
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../utils/i18n';
11
11
 
12
12
  export interface Props {
13
13
  count?: number;
@@ -16,11 +16,16 @@ export interface Props {
16
16
 
17
17
  const { count = 6, i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
18
18
 
19
- // Get current locale from URL
20
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
19
+ // Get base URL for prefixing links
20
+ const base = import.meta.env.BASE_URL;
21
+
22
+ // Remove base URL from current path for locale detection
23
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
24
+
25
+ // Get current locale from URL (use path without base)
26
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
21
27
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
22
28
  const ui = localeConfig.ui;
23
- const base = import.meta.env.BASE_URL;
24
29
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
25
30
 
26
31
  const allPosts = await getCollection('posts');
@@ -6,7 +6,7 @@ import { getCollection } from 'astro:content';
6
6
  import type { I18nConfig } from '../../config/i18n';
7
7
  import { defaultI18nConfig } from '../../config/i18n';
8
8
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
9
- import { getLocaleFromPath, getLocaleConfig, filterPostsByLocale } from '../../utils/i18n';
9
+ import { getLocaleFromPath, getLocaleConfig, filterPostsByLocale, removeBase } from '../../utils/i18n';
10
10
 
11
11
  export interface Props {
12
12
  i18nConfig?: I18nConfig;
@@ -14,8 +14,14 @@ export interface Props {
14
14
 
15
15
  const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
16
16
 
17
- // Get current locale from URL
18
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
17
+ // Get base URL for prefixing links
18
+ const base = import.meta.env.BASE_URL;
19
+
20
+ // Remove base URL from current path for locale detection
21
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
22
+
23
+ // Get current locale from URL (use path without base)
24
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
19
25
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
20
26
  const ui = localeConfig.ui;
21
27
 
@@ -2,7 +2,7 @@
2
2
  import { siteConfig, footerConfig, defaultIcons } from '@jet-w/astro-blog/config';
3
3
  import type { I18nConfig } from '../../config/i18n';
4
4
  import { defaultI18nConfig } from '../../config/i18n';
5
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix } from '../../utils/i18n';
5
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, removeBase, withBase } from '../../utils/i18n';
6
6
 
7
7
  export interface Props {
8
8
  i18nConfig?: I18nConfig;
@@ -10,7 +10,13 @@ export interface Props {
10
10
 
11
11
  const { i18nConfig = defaultI18nConfig } = Astro.props;
12
12
 
13
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
13
+ // Get base URL for prefixing links
14
+ const base = import.meta.env.BASE_URL;
15
+
16
+ // Remove base URL from current path for locale detection
17
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
18
+
19
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
14
20
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
15
21
  const ui = localeConfig.ui;
16
22
 
@@ -24,10 +30,13 @@ function getIcon(link: { type: string; icon?: string }): string {
24
30
  return link.icon || defaultIcons[link.type] || '';
25
31
  }
26
32
 
27
- // Get locale prefix for RSS link
28
- const base = import.meta.env.BASE_URL;
33
+ // Get locale prefix for RSS link (already includes base)
29
34
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
30
- const rssUrl = localeFooterConfig.rssUrl || footerConfig.rssUrl || `${localePrefix}/rss.xml`;
35
+ // Apply withBase to rssUrl if it's a relative path
36
+ const configRssUrl = localeFooterConfig.rssUrl || footerConfig.rssUrl;
37
+ const rssUrl = configRssUrl
38
+ ? (configRssUrl.startsWith('/') && !configRssUrl.startsWith(base) ? withBase(configRssUrl, base) : configRssUrl)
39
+ : `${localePrefix}/rss.xml`;
31
40
 
32
41
  const copyright = (localeFooterConfig.copyright || footerConfig.copyright)
33
42
  .replace('{year}', String(currentYear))
@@ -41,7 +50,7 @@ const copyright = (localeFooterConfig.copyright || footerConfig.copyright)
41
50
  <div class="flex items-center space-x-3 mb-3">
42
51
  {(localeSiteConfig.avatar || siteConfig.avatar) && (
43
52
  <img
44
- src={localeSiteConfig.avatar || siteConfig.avatar}
53
+ src={withBase(localeSiteConfig.avatar || siteConfig.avatar || '', base)}
45
54
  alt={localeSiteConfig.title || siteConfig.title}
46
55
  class="w-8 h-8 rounded-full"
47
56
  />
@@ -64,7 +73,7 @@ const copyright = (localeFooterConfig.copyright || footerConfig.copyright)
64
73
  <div class="flex flex-wrap gap-x-4 gap-y-1">
65
74
  {(localeFooterConfig.quickLinks || footerConfig.quickLinks).map((item) => (
66
75
  <a
67
- href={item.href}
76
+ href={withBase(item.href, base)}
68
77
  class="text-sm text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
69
78
  >
70
79
  {item.name}
@@ -11,6 +11,7 @@ import {
11
11
  getLocaleFromPath,
12
12
  getLocaleConfig,
13
13
  removeLocalePrefix,
14
+ removeBase,
14
15
  isMultiLanguageEnabled,
15
16
  withBase,
16
17
  } from '../../utils/i18n';
@@ -22,7 +23,15 @@ export interface Props {
22
23
  const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
23
24
 
24
25
  const currentPath = Astro.url.pathname;
25
- const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
26
+
27
+ // Get base URL for prefixing links
28
+ const base = import.meta.env.BASE_URL;
29
+
30
+ // Get base path without base URL - needed for locale detection
31
+ const pathWithoutBase = removeBase(currentPath, base);
32
+
33
+ // Detect current locale from path (without base URL)
34
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
26
35
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
27
36
  const ui = localeConfig.ui;
28
37
 
@@ -30,8 +39,8 @@ const ui = localeConfig.ui;
30
39
  const localeSiteConfig = localeConfig.site;
31
40
  const menu = localeConfig.menu;
32
41
 
33
- // Get base path without locale prefix for language switcher
34
- const basePath = removeLocalePrefix(currentPath, i18nConfig);
42
+ // Get path without locale prefix for language switcher
43
+ const pathWithoutLocale = removeLocalePrefix(pathWithoutBase, i18nConfig);
35
44
 
36
45
  // Check if multi-language is enabled
37
46
  const showLanguageSwitcher = isMultiLanguageEnabled(i18nConfig);
@@ -41,9 +50,6 @@ const localesForSwitcher = i18nConfig.locales.map(l => ({
41
50
  code: l.code,
42
51
  name: l.name,
43
52
  }));
44
-
45
- // Get base URL for prefixing links
46
- const base = import.meta.env.BASE_URL;
47
53
  ---
48
54
 
49
55
  <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">
@@ -102,9 +108,10 @@ const base = import.meta.env.BASE_URL;
102
108
  client:load
103
109
  locales={localesForSwitcher}
104
110
  currentLocale={currentLocale}
105
- currentPath={basePath}
111
+ currentPath={pathWithoutLocale}
106
112
  defaultLocale={i18nConfig.defaultLocale}
107
113
  prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
114
+ base={base}
108
115
  />
109
116
  </div>
110
117
  )}
@@ -117,11 +124,7 @@ const base = import.meta.env.BASE_URL;
117
124
  <MobileMenu
118
125
  client:load
119
126
  navigation={menu}
120
- locales={showLanguageSwitcher ? localesForSwitcher : []}
121
- currentLocale={currentLocale}
122
- currentPath={basePath}
123
- defaultLocale={i18nConfig.defaultLocale}
124
- prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
127
+ base={base}
125
128
  />
126
129
  </div>
127
130
  </div>
@@ -10,7 +10,7 @@ import {
10
10
  } from '@jet-w/astro-blog/utils/sidebar';
11
11
  import type { I18nConfig } from '../../config/i18n';
12
12
  import { defaultI18nConfig } from '../../config/i18n';
13
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, formatDate, filterPostsByLocale } from '../../utils/i18n';
13
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, formatDate, filterPostsByLocale, removeBase } from '../../utils/i18n';
14
14
 
15
15
  interface Props {
16
16
  currentPath?: string;
@@ -19,11 +19,16 @@ interface Props {
19
19
 
20
20
  const { currentPath = Astro.url.pathname, i18nConfig = defaultI18nConfig } = Astro.props;
21
21
 
22
- // Get current locale and translations
23
- const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
22
+ // Get base URL for prefixing links
23
+ const base = import.meta.env.BASE_URL;
24
+
25
+ // Remove base URL from current path for locale detection and path filtering
26
+ const pathWithoutBase = removeBase(currentPath, base);
27
+
28
+ // Get current locale and translations (use path without base)
29
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
24
30
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
25
31
  const ui = localeConfig.ui;
26
- const base = import.meta.env.BASE_URL;
27
32
  const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
28
33
 
29
34
  // Get locale-specific sidebar config (already merged with defaults in getLocaleConfig)
@@ -35,8 +40,8 @@ const allPosts = await getCollection('posts', ({ data }) => !data.draft);
35
40
  // Filter posts by current locale for sidebar widgets
36
41
  const localePosts = filterPostsByLocale(allPosts, currentLocale, i18nConfig);
37
42
 
38
- // 根据当前路径过滤侧边栏组
39
- const filteredGroups = filterGroupsByPath(sidebarConfig.groups, currentPath);
43
+ // 根据当前路径过滤侧边栏组 (use path without base for pattern matching)
44
+ const filteredGroups = filterGroupsByPath(sidebarConfig.groups, pathWithoutBase);
40
45
 
41
46
  // 创建过滤后的配置
42
47
  const filteredConfig = { ...sidebarConfig, groups: filteredGroups };
@@ -78,6 +78,9 @@ const revealConfig = JSON.stringify({
78
78
  touch: true,
79
79
  });
80
80
 
81
+ // Get base URL for static assets
82
+ const base = import.meta.env.BASE_URL;
83
+
81
84
  // 获取 slot 内容
82
85
  const slotContent = await Astro.slots.render('default');
83
86
  const hasInlineContent = slotContent && slotContent.trim().length > 0;
@@ -164,13 +167,26 @@ const slidesData = hasInlineContent ? parseSlides(slotContent) : [];
164
167
  </div>
165
168
 
166
169
  <!-- Reveal.js 资源 -->
167
- <link rel="stylesheet" href="/slides/reveal.css" />
168
- <link rel="stylesheet" href={`/slides/theme/${theme}.css`} />
169
- <link rel="stylesheet" href="/slides/plugin/highlight/monokai.css" />
170
+ <link rel="stylesheet" href={`${base}slides/reveal.css`} />
171
+ <link rel="stylesheet" href={`${base}slides/theme/${theme}.css`} />
172
+ <link rel="stylesheet" href={`${base}slides/plugin/highlight/monokai.css`} />
170
173
 
171
- <script is:inline src="/slides/reveal.js"></script>
172
- <script is:inline src="/slides/plugin/markdown/markdown.js"></script>
173
- <script is:inline src="/slides/plugin/highlight/highlight.js"></script>
174
+ <!-- Reveal.js - dynamically loaded with base URL -->
175
+ <script is:inline define:vars={{ baseUrl: base }}>
176
+ (function() {
177
+ var scripts = [
178
+ baseUrl + 'slides/reveal.js',
179
+ baseUrl + 'slides/plugin/markdown/markdown.js',
180
+ baseUrl + 'slides/plugin/highlight/highlight.js'
181
+ ];
182
+ scripts.forEach(function(src) {
183
+ var script = document.createElement('script');
184
+ script.src = src;
185
+ script.async = false;
186
+ document.head.appendChild(script);
187
+ });
188
+ })();
189
+ </script>
174
190
 
175
191
  <script is:inline define:vars={{ slidesId, revealConfig }}>
176
192
  (function() {
@@ -101,16 +101,19 @@ interface Props {
101
101
  locales: LocaleInfo[];
102
102
  /** Current locale code */
103
103
  currentLocale: string;
104
- /** Current page path (without locale prefix) */
104
+ /** Current page path (without locale prefix and without base) */
105
105
  currentPath: string;
106
106
  /** Default locale code */
107
107
  defaultLocale: string;
108
108
  /** Whether to prefix the default locale in URLs */
109
109
  prefixDefaultLocale?: boolean;
110
+ /** Base URL for the site (e.g., '/jet-w.astro-blog') */
111
+ base?: string;
110
112
  }
111
113
 
112
114
  const props = withDefaults(defineProps<Props>(), {
113
115
  prefixDefaultLocale: false,
116
+ base: '/',
114
117
  });
115
118
 
116
119
  const isOpen = ref(false);
@@ -138,23 +141,32 @@ function closeDropdown() {
138
141
  }
139
142
 
140
143
  function getLocalizedUrl(targetLocale: string): string {
141
- // Normalize the path
142
- let basePath = props.currentPath;
143
- if (!basePath.startsWith('/')) {
144
- basePath = '/' + basePath;
144
+ // Normalize the current path (without base and without locale)
145
+ let pagePath = props.currentPath;
146
+ if (!pagePath.startsWith('/')) {
147
+ pagePath = '/' + pagePath;
145
148
  }
146
149
 
147
- // If target is default locale and prefix is not required
148
- if (targetLocale === props.defaultLocale && !props.prefixDefaultLocale) {
149
- return basePath === '/' ? '/' : basePath;
150
+ // Normalize base URL - remove trailing slash
151
+ const baseUrl = props.base?.replace(/\/$/, '') || '';
152
+
153
+ // Build locale prefix
154
+ let localePrefix = '';
155
+ if (targetLocale !== props.defaultLocale || props.prefixDefaultLocale) {
156
+ localePrefix = `/${targetLocale}`;
150
157
  }
151
158
 
152
- // Add locale prefix
153
- if (basePath === '/') {
154
- return `/${targetLocale}`;
159
+ // Combine: base + locale + path
160
+ if (pagePath === '/') {
161
+ // Home page
162
+ if (localePrefix) {
163
+ return baseUrl ? `${baseUrl}${localePrefix}/` : `${localePrefix}/`;
164
+ }
165
+ return baseUrl ? `${baseUrl}/` : '/';
155
166
  }
156
167
 
157
- return `/${targetLocale}${basePath}`;
168
+ // Other pages
169
+ return `${baseUrl}${localePrefix}${pagePath}`;
158
170
  }
159
171
 
160
172
  // Close dropdown when clicking outside
@@ -71,7 +71,7 @@
71
71
  <a
72
72
  v-for="item in navigation"
73
73
  :key="item.href"
74
- :href="item.href"
74
+ :href="withBase(item.href)"
75
75
  @click="isOpen = false"
76
76
  class="flex items-center px-4 py-3 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
77
77
  :class="{ 'bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400': isActive(item.href) }"
@@ -98,16 +98,33 @@
98
98
  </template>
99
99
 
100
100
  <script setup lang="ts">
101
- import { ref, onMounted, onUnmounted } from 'vue'
101
+ import { ref, onMounted, onUnmounted, withDefaults } from 'vue'
102
102
  import type { NavigationItem } from '@jet-w/astro-blog/types'
103
103
  import SearchBox from './SearchBox.vue'
104
104
  import ThemeToggle from './ThemeToggle.vue'
105
105
 
106
106
  interface Props {
107
107
  navigation: NavigationItem[]
108
+ /** Base URL for the site (e.g., '/jet-w.astro-blog') */
109
+ base?: string
108
110
  }
109
111
 
110
- const props = defineProps<Props>()
112
+ const props = withDefaults(defineProps<Props>(), {
113
+ base: '/',
114
+ })
115
+
116
+ // Helper function to add base URL to paths
117
+ function withBase(path: string): string {
118
+ const baseUrl = props.base?.replace(/\/$/, '') || '';
119
+ if (!baseUrl || baseUrl === '') {
120
+ return path;
121
+ }
122
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
123
+ if (normalizedPath === '/') {
124
+ return `${baseUrl}/`;
125
+ }
126
+ return `${baseUrl}${normalizedPath}`;
127
+ }
111
128
  const isOpen = ref(false)
112
129
  const currentPath = ref('')
113
130
  const isMounted = ref(false)
@@ -11,6 +11,7 @@ import {
11
11
  getTextDirection,
12
12
  isMultiLanguageEnabled,
13
13
  withBase,
14
+ removeBase,
14
15
  t,
15
16
  type I18nConfig,
16
17
  } from '../utils/i18n';
@@ -40,8 +41,14 @@ const {
40
41
  i18nConfig = virtualI18nConfig || defaultI18nConfig,
41
42
  } = Astro.props;
42
43
 
43
- // Get current locale from URL
44
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
44
+ // Get base URL for prefixing links
45
+ const base = import.meta.env.BASE_URL;
46
+
47
+ // Remove base URL from current path for locale detection
48
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
49
+
50
+ // Get current locale from URL (use path without base)
51
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
45
52
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
46
53
  const localeData = localeConfig.locale;
47
54
  const ui = localeConfig.ui;
@@ -55,9 +62,12 @@ const fullTitle = title === siteTitle ? title : `${title} | ${siteTitle}`;
55
62
  const fullImage = new URL(image, Astro.site);
56
63
 
57
64
  // Get alternate links for SEO (hreflang)
58
- const baseUrl = Astro.site?.toString() || '';
65
+ // We need to pass the pathname without base URL to getAlternateLinks
66
+ // Then the baseUrl for combining should include the base path
67
+ const siteUrl = Astro.site?.toString().replace(/\/$/, '') || '';
68
+ const baseUrlWithPath = base && base !== '/' ? `${siteUrl}${base.replace(/\/$/, '')}` : siteUrl;
59
69
  const alternateLinks = isMultiLanguageEnabled(i18nConfig)
60
- ? getAlternateLinks(Astro.url.pathname, baseUrl, i18nConfig)
70
+ ? getAlternateLinks(pathWithoutBase, baseUrlWithPath, i18nConfig)
61
71
  : [];
62
72
 
63
73
  // Get locale prefix for RSS link
@@ -70,9 +80,6 @@ const faviconSvgExists = fs.existsSync(path.join(publicDir, 'favicon.svg'));
70
80
  const faviconIcoExists = fs.existsSync(path.join(publicDir, 'favicon.ico'));
71
81
  const faviconHref = faviconSvgExists ? '/favicon.svg' : faviconIcoExists ? '/favicon.ico' : siteConfig.avatar;
72
82
  const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'image/x-icon' : 'image/jpeg';
73
-
74
- // Get base URL for prefixing links
75
- const base = import.meta.env.BASE_URL;
76
83
  ---
77
84
 
78
85
  <!DOCTYPE html>
@@ -6,7 +6,7 @@ import Sidebar from '../components/layout/Sidebar.astro';
6
6
  import SidebarToggle from '../components/ui/SidebarToggle.vue';
7
7
  import type { I18nConfig } from '../config/i18n';
8
8
  import { defaultI18nConfig } from '../config/i18n';
9
- import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
9
+ import { getLocaleFromPath, getLocaleConfig, removeBase } from '../utils/i18n';
10
10
  // Import i18n config from virtual module (injected by integration)
11
11
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
12
12
 
@@ -36,8 +36,14 @@ const {
36
36
  i18nConfig = virtualI18nConfig || defaultI18nConfig,
37
37
  } = Astro.props;
38
38
 
39
- // Get current locale
40
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
39
+ // Get base URL for prefixing links
40
+ const base = import.meta.env.BASE_URL;
41
+
42
+ // Remove base URL from current path for locale detection
43
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
44
+
45
+ // Get current locale (use path without base)
46
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
41
47
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
42
48
  const ui = localeConfig.ui;
43
49
  ---