@jet-w/astro-blog 0.2.4 → 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 (41) hide show
  1. package/dist/{chunk-Z3O3JK56.js → chunk-CEZBWSMU.js} +34 -5
  2. package/dist/index.js +1 -1
  3. package/dist/utils/i18n.d.ts +34 -2
  4. package/dist/utils/i18n.js +6 -2
  5. package/package.json +1 -1
  6. package/src/components/blog/Hero.astro +13 -6
  7. package/src/components/blog/PostCard.astro +1 -1
  8. package/src/components/home/FeaturedPostsSection.astro +10 -4
  9. package/src/components/home/QuickNavSection.astro +10 -4
  10. package/src/components/home/RecentPostsSection.astro +10 -4
  11. package/src/components/home/StatsSection.astro +9 -3
  12. package/src/components/layout/Footer.astro +17 -7
  13. package/src/components/layout/Header.astro +20 -13
  14. package/src/components/layout/Sidebar.astro +12 -6
  15. package/src/components/media/Slides.astro +22 -6
  16. package/src/components/ui/LanguageSwitcher.vue +24 -12
  17. package/src/components/ui/MobileMenu.vue +20 -3
  18. package/src/layouts/BaseLayout.astro +19 -8
  19. package/src/layouts/PageLayout.astro +9 -3
  20. package/src/layouts/SlidesLayout.astro +34 -16
  21. package/src/pages/archives/[year]/[month]/page/[page].astro +42 -18
  22. package/src/pages/archives/[year]/[month].astro +8 -2
  23. package/src/pages/archives/index.astro +8 -3
  24. package/src/pages/categories/[category]/page/[page].astro +40 -18
  25. package/src/pages/categories/[category].astro +8 -2
  26. package/src/pages/categories/index.astro +8 -3
  27. package/src/pages/posts/[...slug].astro +8 -3
  28. package/src/pages/posts/index.astro +8 -3
  29. package/src/pages/posts/page/[page].astro +8 -2
  30. package/src/pages/search.astro +9 -3
  31. package/src/pages/slides/index.astro +8 -3
  32. package/src/pages/tags/[tag]/page/[page].astro +39 -17
  33. package/src/pages/tags/[tag].astro +8 -2
  34. package/src/pages/tags/index.astro +8 -3
  35. package/src/plugins/rehype-relative-links.mjs +90 -14
  36. package/src/utils/i18n.ts +83 -4
  37. package/templates/default/.claude/ralph-loop.local.md +4 -43
  38. package/templates/default/Makefile +37 -0
  39. package/templates/default/astro.config.mjs +7 -3
  40. package/templates/default/{package.dev.json → package.prod.json} +1 -1
  41. package/templates/default/package-lock.json +0 -9667
@@ -6,14 +6,19 @@ import NavigationTabs from '@jet-w/astro-blog/components/blog/NavigationTabs.vue
6
6
  import { getCollection } from 'astro:content';
7
7
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
8
8
  import { defaultI18nConfig } from '../../config/i18n';
9
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
9
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../utils/i18n';
10
10
 
11
11
  // Get i18n config
12
12
  const i18nConfig = virtualI18nConfig || defaultI18nConfig;
13
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
13
+ const base = import.meta.env.BASE_URL;
14
+
15
+ // Remove base URL from current path for locale detection
16
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
17
+
18
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
14
19
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
15
20
  const ui = localeConfig.ui;
16
- const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
21
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
17
22
 
18
23
  // 从新的内容目录获取文章数据
19
24
  const allPostsCollection = await getCollection('posts');
@@ -25,10 +25,16 @@ const currentPage = parseInt(page as string, 10);
25
25
 
26
26
  // Get i18n config
27
27
  const i18nConfig = virtualI18nConfig || defaultI18nConfig;
28
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
28
+ const base = import.meta.env.BASE_URL;
29
+
30
+ // Remove base URL from current path for locale detection
31
+ const { removeBase } = await import('../../../utils/i18n');
32
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
33
+
34
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
29
35
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
30
36
  const ui = localeConfig.ui;
31
- const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
37
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
32
38
 
33
39
  // 从新的内容目录获取文章数据
34
40
  const allPostsCollection = await getCollection('posts');
@@ -4,7 +4,7 @@ import SearchInterface from '@jet-w/astro-blog/components/ui/SearchInterface.vue
4
4
  import type { I18nConfig } from '../config/i18n';
5
5
  import { defaultI18nConfig } from '../config/i18n';
6
6
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
7
- import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
7
+ import { getLocaleFromPath, getLocaleConfig, removeBase } from '../utils/i18n';
8
8
 
9
9
  export interface Props {
10
10
  i18nConfig?: I18nConfig;
@@ -12,8 +12,14 @@ export interface Props {
12
12
 
13
13
  const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
14
14
 
15
- // Get current locale from URL
16
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
15
+ // Get base URL for prefixing links
16
+ const base = import.meta.env.BASE_URL;
17
+
18
+ // Remove base URL from current path for locale detection
19
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
20
+
21
+ // Get current locale from URL (use path without base)
22
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
17
23
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
18
24
  const ui = localeConfig.ui;
19
25
 
@@ -3,14 +3,19 @@ import { getCollection } from 'astro:content';
3
3
  import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
4
4
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
5
5
  import { defaultI18nConfig } from '../../config/i18n';
6
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix } from '../../utils/i18n';
6
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, removeBase } from '../../utils/i18n';
7
7
 
8
8
  // Get i18n config
9
9
  const i18nConfig = virtualI18nConfig || defaultI18nConfig;
10
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
10
+ const base = import.meta.env.BASE_URL;
11
+
12
+ // Remove base URL from current path for locale detection
13
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
14
+
15
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
11
16
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
12
17
  const ui = localeConfig.ui;
13
- const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
18
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
14
19
 
15
20
  // 获取所有非草稿的 slides
16
21
  const slides = (await getCollection('slides', ({ data }) => !data.draft))
@@ -3,6 +3,9 @@ import { getCollection } from 'astro:content';
3
3
  import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
4
4
  import PostCard from '@jet-w/astro-blog/components/blog/PostCard.astro';
5
5
  import Pagination from '@jet-w/astro-blog/components/ui/Pagination.astro';
6
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
7
+ import { defaultI18nConfig } from '../../../../config/i18n';
8
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../../../utils/i18n';
6
9
 
7
10
  export async function getStaticPaths() {
8
11
  const postsPerPage = 10;
@@ -53,8 +56,23 @@ export async function getStaticPaths() {
53
56
  const { tagSlug, tagName, page: currentPage, totalPages } = Astro.props;
54
57
  const postsPerPage = 10;
55
58
 
59
+ // Get i18n config
60
+ const i18nConfig = virtualI18nConfig || defaultI18nConfig;
61
+ const base = import.meta.env.BASE_URL;
62
+
63
+ // Remove base URL from current path for locale detection
64
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
65
+
66
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
67
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
68
+ const ui = localeConfig.ui;
69
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
70
+
56
71
  // 获取所有文章并筛选包含该标签的文章
57
- const allPosts = await getCollection('posts', ({ data }) => !data.draft);
72
+ const allPostsCollection = await getCollection('posts', ({ data }) => !data.draft);
73
+
74
+ // Filter posts by current locale
75
+ const allPosts = filterPostsByLocale(allPostsCollection, currentLocale, i18nConfig);
58
76
 
59
77
  // 筛选包含该标签的文章
60
78
  const filteredPosts = allPosts
@@ -95,25 +113,26 @@ const relatedTags = Array.from(relatedTagMap.entries())
95
113
  ---
96
114
 
97
115
  <PageLayout
98
- title={`标签: ${tagName} - ${currentPage} 页`}
99
- description={`浏览所有关于 ${tagName} 的文章 - ${currentPage} 页`}
116
+ title={`${ui.taggedWith}: ${tagName} - ${ui.page} ${currentPage}`}
117
+ description={`${ui.taggedWith} ${tagName} - ${ui.page} ${currentPage}`}
100
118
  showSidebar={true}
119
+ i18nConfig={i18nConfig}
101
120
  >
102
121
  <!-- 面包屑导航 -->
103
122
  <nav class="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400 mb-8">
104
- <a href="/" class="hover:text-primary-500 transition-colors">首页</a>
123
+ <a href={`${localePrefix}/`} class="hover:text-primary-500 transition-colors">{ui.home}</a>
105
124
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
106
125
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
107
126
  </svg>
108
- <a href="/tags" class="hover:text-primary-500 transition-colors">标签</a>
127
+ <a href={`${localePrefix}/tags`} class="hover:text-primary-500 transition-colors">{ui.allTags}</a>
109
128
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
110
129
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
111
130
  </svg>
112
- <a href={`/tags/${tagSlug}`} class="hover:text-primary-500 transition-colors">{tagName}</a>
131
+ <a href={`${localePrefix}/tags/${tagSlug}`} class="hover:text-primary-500 transition-colors">{tagName}</a>
113
132
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
133
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
115
134
  </svg>
116
- <span class="text-slate-900 dark:text-slate-100">第 {currentPage} 页</span>
135
+ <span class="text-slate-900 dark:text-slate-100">{ui.page} {currentPage}</span>
117
136
  </nav>
118
137
 
119
138
  <!-- 页面头部 -->
@@ -129,16 +148,16 @@ const relatedTags = Array.from(relatedTagMap.entries())
129
148
  </h1>
130
149
 
131
150
  <p class="text-xl text-slate-600 dark:text-slate-400 mb-8">
132
- 找到 {totalPosts} 篇关于 {tagName} 的文章
151
+ {totalPosts} {ui.posts}
133
152
  </p>
134
153
 
135
154
  <!-- 标签统计 -->
136
155
  <div class="inline-flex items-center space-x-6 text-sm text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800 px-6 py-3 rounded-lg">
137
156
  <span>#{tagName}</span>
138
157
  <span>•</span>
139
- <span>{totalPosts} 篇文章</span>
158
+ <span>{totalPosts} {ui.posts}</span>
140
159
  <span>•</span>
141
- <span>第 {currentPage} / {totalPages} 页</span>
160
+ <span>{ui.page} {currentPage} / {totalPages}</span>
142
161
  </div>
143
162
  </div>
144
163
 
@@ -158,6 +177,9 @@ const relatedTags = Array.from(relatedTagMap.entries())
158
177
  image: post.data.image
159
178
  }}
160
179
  layout="horizontal"
180
+ localePrefix={localePrefix}
181
+ locale={localeConfig.locale.dateLocale}
182
+ ui={ui}
161
183
  />
162
184
  ))}
163
185
  </div>
@@ -165,13 +187,13 @@ const relatedTags = Array.from(relatedTagMap.entries())
165
187
  <div class="text-center py-16">
166
188
  <div class="text-6xl mb-4">📝</div>
167
189
  <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-2">
168
- 暂无相关文章
190
+ {ui.noPostsFound}
169
191
  </h3>
170
192
  <p class="text-slate-600 dark:text-slate-400 mb-6">
171
- 目前还没有关于 {tagName} 的文章,请查看其他标签。
193
+ {ui.noPostsFound}
172
194
  </p>
173
- <a href="/tags" class="btn-secondary">
174
- 浏览所有标签
195
+ <a href={`${localePrefix}/tags`} class="btn-secondary">
196
+ {ui.allTags}
175
197
  </a>
176
198
  </div>
177
199
  )}
@@ -181,7 +203,7 @@ const relatedTags = Array.from(relatedTagMap.entries())
181
203
  <Pagination
182
204
  currentPage={currentPage}
183
205
  totalPages={totalPages}
184
- baseUrl={`/tags/${tagSlug}`}
206
+ baseUrl={`${localePrefix}/tags/${tagSlug}`}
185
207
  />
186
208
  )}
187
209
 
@@ -189,12 +211,12 @@ const relatedTags = Array.from(relatedTagMap.entries())
189
211
  {relatedTags.length > 0 && (
190
212
  <section class="mt-16 pt-8 border-t border-slate-200 dark:border-slate-700">
191
213
  <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-6">
192
- 相关标签
214
+ {ui.popularTags}
193
215
  </h2>
194
216
  <div class="flex flex-wrap gap-3">
195
217
  {relatedTags.map((relatedTag) => (
196
218
  <a
197
- href={`/tags/${relatedTag.slug}`}
219
+ href={`${localePrefix}/tags/${relatedTag.slug}`}
198
220
  class="inline-flex items-center px-4 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg hover:border-primary-300 dark:hover:border-primary-600 hover:shadow-md transition-all duration-200 group"
199
221
  >
200
222
  <span class="font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 transition-colors">
@@ -35,10 +35,16 @@ const { tagSlug, tagName, tagCount } = Astro.props;
35
35
 
36
36
  // Get i18n config
37
37
  const i18nConfig = virtualI18nConfig || defaultI18nConfig;
38
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
38
+ const base = import.meta.env.BASE_URL;
39
+
40
+ // Remove base URL from current path for locale detection
41
+ const { removeBase } = await import('../../utils/i18n');
42
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
43
+
44
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
39
45
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
40
46
  const ui = localeConfig.ui;
41
- const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
47
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
42
48
 
43
49
  // 获取所有文章并筛选包含该标签的文章
44
50
  const allPostsCollection = await getCollection('posts', ({ data }) => !data.draft);
@@ -4,14 +4,19 @@ import PageLayout from '@jet-w/astro-blog/layouts/PageLayout.astro';
4
4
  import TagCloud from '@jet-w/astro-blog/components/blog/TagCloud.astro';
5
5
  import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
6
6
  import { defaultI18nConfig } from '../../config/i18n';
7
- import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
7
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale, removeBase } from '../../utils/i18n';
8
8
 
9
9
  // Get i18n config
10
10
  const i18nConfig = virtualI18nConfig || defaultI18nConfig;
11
- const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
11
+ const base = import.meta.env.BASE_URL;
12
+
13
+ // Remove base URL from current path for locale detection
14
+ const pathWithoutBase = removeBase(Astro.url.pathname, base);
15
+
16
+ const currentLocale = getLocaleFromPath(pathWithoutBase, i18nConfig);
12
17
  const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
13
18
  const ui = localeConfig.ui;
14
- const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
19
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig, base);
15
20
 
16
21
  // 从文章集合动态获取所有标签
17
22
  const allPostsCollection = await getCollection('posts', ({ data }) => !data.draft);
@@ -1,10 +1,63 @@
1
1
  /**
2
- * Rehype 插件:修复 Markdown 中的相对链接
3
- * 将相对链接转换为正确的绝对路径
2
+ * Rehype 插件:修复 Markdown 中的相对链接和图片链接
3
+ * 将相对链接转换为正确的绝对路径,支持 base URL
4
+ * 同时处理绝对路径链接,添加 base URL 前缀
4
5
  */
5
6
  import { visit } from 'unist-util-visit';
6
7
 
8
+ /**
9
+ * Helper function to check if a path is a relative link
10
+ * @param {string} path - The path to check
11
+ * @returns {boolean} - True if the path is relative
12
+ */
13
+ function isRelativePath(path) {
14
+ return (
15
+ path.startsWith('./') ||
16
+ path.startsWith('../') ||
17
+ (!path.startsWith('/') &&
18
+ !path.startsWith('#') &&
19
+ !path.startsWith('http://') &&
20
+ !path.startsWith('https://') &&
21
+ !path.startsWith('mailto:') &&
22
+ !path.startsWith('data:'))
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Helper function to check if a path is an internal absolute path
28
+ * (starts with / but not an external URL or special protocol)
29
+ * @param {string} path - The path to check
30
+ * @returns {boolean} - True if the path is an internal absolute path
31
+ */
32
+ function isInternalAbsolutePath(path) {
33
+ return (
34
+ path.startsWith('/') &&
35
+ !path.startsWith('//') // Exclude protocol-relative URLs like //example.com
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Helper function to normalize the base URL
41
+ * @param {string} base - The base URL
42
+ * @returns {string} - Normalized base URL (with leading slash, without trailing slash)
43
+ */
44
+ function normalizeBase(base) {
45
+ if (!base || base === '/') return '';
46
+ // Ensure leading slash, remove trailing slash
47
+ let normalized = base.startsWith('/') ? base : `/${base}`;
48
+ normalized = normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
49
+ return normalized;
50
+ }
51
+
52
+ /**
53
+ * Rehype plugin to fix relative links in Markdown
54
+ * @param {Object} options - Plugin options
55
+ * @param {string} options.base - The base URL path (e.g., '/blog')
56
+ * @returns {Function} - The plugin function
57
+ */
7
58
  export function rehypeRelativeLinks(options = {}) {
59
+ const base = normalizeBase(options.base || process.env.BASE_PATH || '');
60
+
8
61
  return (tree, file) => {
9
62
  // 获取当前文件的路径信息
10
63
  const filePath = file.history[0] || '';
@@ -12,28 +65,51 @@ export function rehypeRelativeLinks(options = {}) {
12
65
  // 从文件路径中提取目录信息
13
66
  // 例如:/path/to/content/posts/blog_docs/README.md -> blog_docs
14
67
  const match = filePath.match(/content\/posts\/(.+?)\/[^/]+\.(md|mdx)$/i);
15
- if (!match) return;
16
68
 
17
- const dirPath = match[1]; // 例如:blog_docs 或 tech/subfolder
69
+ // dirPath 用于相对链接的解析
70
+ const dirPath = match ? match[1] : ''; // 例如:blog_docs 或 tech/subfolder
18
71
 
19
72
  visit(tree, 'element', (node) => {
73
+ // Handle anchor links
20
74
  if (node.tagName === 'a' && node.properties?.href) {
21
75
  const href = node.properties.href;
22
76
 
23
- // 只处理相对链接(以 ./ 或不以 / # http 开头的链接)
24
- if (href.startsWith('./') ||
25
- (!href.startsWith('/') &&
26
- !href.startsWith('#') &&
27
- !href.startsWith('http://') &&
28
- !href.startsWith('https://') &&
29
- !href.startsWith('mailto:'))) {
30
-
77
+ if (isRelativePath(href) && dirPath) {
31
78
  // 移除 ./ 前缀
32
79
  const cleanHref = href.replace(/^\.\//, '');
33
80
 
34
- // 构建新的绝对路径
35
- const newHref = `/posts/${dirPath}/${cleanHref}`;
81
+ // 构建新的绝对路径(包含 base URL)
82
+ const newHref = `${base}/posts/${dirPath}/${cleanHref}`;
36
83
  node.properties.href = newHref;
84
+ } else if (isInternalAbsolutePath(href) && base) {
85
+ // 处理内部绝对路径:添加 base URL 前缀
86
+ // 例如:/posts/xxx -> /blog/posts/xxx
87
+ // 避免重复添加 base(如果已经包含)
88
+ if (!href.startsWith(base + '/') && href !== base) {
89
+ node.properties.href = base + href;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Handle image links
95
+ if (node.tagName === 'img' && node.properties?.src) {
96
+ const src = node.properties.src;
97
+
98
+ if (isRelativePath(src) && dirPath) {
99
+ // 移除 ./ 前缀
100
+ const cleanSrc = src.replace(/^\.\//, '');
101
+
102
+ // 构建新的绝对路径(包含 base URL)
103
+ // 图片通常相对于当前文档,保持目录结构
104
+ const newSrc = `${base}/posts/${dirPath}/${cleanSrc}`;
105
+ node.properties.src = newSrc;
106
+ } else if (isInternalAbsolutePath(src) && base) {
107
+ // 处理内部绝对路径图片:添加 base URL 前缀
108
+ // 例如:/images/xxx.png -> /blog/images/xxx.png
109
+ // 避免重复添加 base(如果已经包含)
110
+ if (!src.startsWith(base + '/') && src !== base) {
111
+ node.properties.src = base + src;
112
+ }
37
113
  }
38
114
  }
39
115
  });
package/src/utils/i18n.ts CHANGED
@@ -353,15 +353,38 @@ export function isMultiLanguageEnabled(
353
353
  /**
354
354
  * Get prefix for a locale in routes
355
355
  * Returns empty string for default locale if prefixDefaultLocale is false
356
+ * If base is provided, it will be prepended to the locale prefix
357
+ *
358
+ * @example
359
+ * // Without base
360
+ * getLocalePrefix('en', config) // '' (for default locale)
361
+ * getLocalePrefix('zh-CN', config) // '/zh-CN'
362
+ *
363
+ * // With base '/my-blog'
364
+ * getLocalePrefix('en', config, '/my-blog') // '/my-blog'
365
+ * getLocalePrefix('zh-CN', config, '/my-blog') // '/my-blog/zh-CN'
356
366
  */
357
367
  export function getLocalePrefix(
358
368
  locale: string,
359
- config: I18nConfig = defaultI18nConfig
369
+ config: I18nConfig = defaultI18nConfig,
370
+ base?: string
360
371
  ): string {
361
- if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
362
- return '';
372
+ // Normalize base: remove trailing slash
373
+ const normalizedBase = base ? base.replace(/\/$/, '') : '';
374
+
375
+ // Get locale prefix
376
+ let localePrefix = '';
377
+ if (locale !== config.defaultLocale || config.routing.prefixDefaultLocale) {
378
+ localePrefix = `/${locale}`;
379
+ }
380
+
381
+ // If base is just '/' or empty, return locale prefix as before
382
+ if (!normalizedBase || normalizedBase === '') {
383
+ return localePrefix;
363
384
  }
364
- return `/${locale}`;
385
+
386
+ // Combine base and locale prefix
387
+ return `${normalizedBase}${localePrefix}`;
365
388
  }
366
389
 
367
390
  /**
@@ -406,6 +429,62 @@ export function filterPostsByLocale<T extends { id: string }>(
406
429
  });
407
430
  }
408
431
 
432
+ /**
433
+ * Add base URL prefix to a path
434
+ * This is used when the site is deployed to a subdirectory (e.g., /jet-w.astro-blog/)
435
+ *
436
+ * @example
437
+ * // If BASE_URL is '/jet-w.astro-blog'
438
+ * withBase('/posts') // '/jet-w.astro-blog/posts'
439
+ * withBase('/') // '/jet-w.astro-blog/'
440
+ *
441
+ * // If BASE_URL is '/'
442
+ * withBase('/posts') // '/posts'
443
+ */
444
+ export function withBase(path: string, base?: string): string {
445
+ // Get base URL - in Astro components, pass import.meta.env.BASE_URL
446
+ // Remove trailing slash from base
447
+ const baseUrl = (base || '/').replace(/\/$/, '');
448
+
449
+ // If base is empty or just '/', return path as-is
450
+ if (!baseUrl || baseUrl === '') {
451
+ return path;
452
+ }
453
+
454
+ // Ensure path starts with /
455
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
456
+
457
+ // If path is just '/', return base with trailing slash
458
+ if (normalizedPath === '/') {
459
+ return `${baseUrl}/`;
460
+ }
461
+
462
+ return `${baseUrl}${normalizedPath}`;
463
+ }
464
+
465
+ /**
466
+ * Remove base URL prefix from a path
467
+ * Useful for getting the actual path without base prefix
468
+ *
469
+ * @example
470
+ * // If BASE_URL is '/jet-w.astro-blog'
471
+ * removeBase('/jet-w.astro-blog/posts', '/jet-w.astro-blog') // '/posts'
472
+ */
473
+ export function removeBase(path: string, base?: string): string {
474
+ const baseUrl = (base || '/').replace(/\/$/, '');
475
+
476
+ if (!baseUrl || baseUrl === '') {
477
+ return path;
478
+ }
479
+
480
+ if (path.startsWith(baseUrl)) {
481
+ const rest = path.slice(baseUrl.length);
482
+ return rest || '/';
483
+ }
484
+
485
+ return path;
486
+ }
487
+
409
488
  // Re-export types and config functions
410
489
  export type { I18nConfig, Locale, LocaleConfig, UITranslations } from '../config/i18n';
411
490
  export {
@@ -1,48 +1,9 @@
1
1
  ---
2
- active: false
3
- iteration: 2
2
+ active: true
3
+ iteration: 1
4
4
  max_iterations: 20
5
5
  completion_promise: "DONE"
6
- started_at: "2026-01-29T01:25:08Z"
7
- completed_at: "2026-01-29T01:48:00Z"
6
+ started_at: "2026-01-29T23:41:34Z"
8
7
  ---
9
8
 
10
- DONE
11
-
12
- ## Issue Fixed
13
-
14
- The i18n URL routing and navigation link issue has been resolved.
15
-
16
- ### Root Cause Analysis
17
-
18
- When English is set as the default locale with `prefixDefaultLocale: false`:
19
- - English pages use URLs without prefix: `/posts/...`
20
- - Chinese pages should use URLs with prefix: `/zh-CN/posts/...`
21
-
22
- However, the navigation menu and sidebar tree links in the zh-CN locale config were missing the `/zh-CN/` prefix, causing navigation links to point to the wrong locale.
23
-
24
- ### Files Modified
25
-
26
- 1. **`templates/default/src/config/locales/zh-CN/menu.ts`**
27
- - Updated all menu links to include `/zh-CN/` prefix
28
- - Changed `href: '/'` to `href: '/zh-CN/'`
29
- - Changed `href: '/posts/...'` to `href: '/zh-CN/posts/...'`
30
- - Changed `href: '/slides'` to `href: '/zh-CN/slides'`
31
- - Changed `href: '/about'` to `href: '/zh-CN/about'`
32
-
33
- 2. **`templates/default/src/config/locales/zh-CN/sidebar.ts`**
34
- - Updated sidebar group titles to Chinese: '快速入门', '使用指南', '配置文档'
35
- - Updated comments to Chinese
36
-
37
- 3. **`src/components/layout/Sidebar.astro`**
38
- - Fixed all tree links to use `${localePrefix}/posts/...` instead of hardcoded `/posts/...`
39
- - This ensures sidebar tree navigation respects the current locale
40
-
41
- ### URL Behavior (Correct)
42
-
43
- | Language | URL Pattern |
44
- |----------|-------------|
45
- | English (default) | `/posts/...`, `/slides`, `/about` |
46
- | Chinese | `/zh-CN/posts/...`, `/zh-CN/slides`, `/zh-CN/about` |
47
-
48
- This is the expected behavior with `prefixDefaultLocale: false`.
9
+ When the base url was set up, the hyper link in markdown, such as [/xxx/fse](text) and image link cannot change accordingly, please help to check similar link at the same time. please help to fix it
@@ -0,0 +1,37 @@
1
+ .PHONY: dev prod install clean help
2
+
3
+ # Default target
4
+ help:
5
+ @echo "Usage:"
6
+ @echo " make dev - Switch to development mode (uses local @jet-w/astro-blog)"
7
+ @echo " make prod - Switch to production mode (uses npm package @jet-w/astro-blog)"
8
+ @echo " make install - Run npm install after switching modes"
9
+ @echo " make clean - Remove node_modules and package-lock.json"
10
+
11
+ # Switch to development mode (local package)
12
+ dev:
13
+ @echo "Switching to development mode..."
14
+ @cp package.dev.json package.json
15
+ @echo "Done! Run 'make install' or 'npm install' to install dependencies."
16
+
17
+ # Switch to production mode (npm package)
18
+ prod:
19
+ @echo "Switching to production mode..."
20
+ @cp package.prod.json package.json
21
+ @echo "Done! Run 'make install' or 'npm install' to install dependencies."
22
+
23
+ # Install dependencies
24
+ install:
25
+ npm install
26
+
27
+ # Clean node_modules and package-lock.json
28
+ clean:
29
+ @echo "Cleaning..."
30
+ @rm -rf node_modules package-lock.json
31
+ @echo "Done!"
32
+
33
+ # Switch to dev and install
34
+ dev-install: dev install
35
+
36
+ # Switch to prod and install
37
+ prod-install: prod install
@@ -1,4 +1,5 @@
1
1
  import { defineConfig } from 'astro/config';
2
+ import { loadEnv } from 'vite';
2
3
  import vue from '@astrojs/vue';
3
4
  import tailwind from '@astrojs/tailwind';
4
5
  import mdx from '@astrojs/mdx';
@@ -7,6 +8,9 @@ import remarkMath from 'remark-math';
7
8
  import rehypeKatex from 'rehype-katex';
8
9
  import rehypeRaw from 'rehype-raw';
9
10
 
11
+ // Load environment variables from .env file
12
+ const { SITE_URL, BASE_PATH } = loadEnv(process.env.NODE_ENV || 'production', process.cwd(), '');
13
+
10
14
  // Import plugins and integration from @jet-w/astro-blog
11
15
  import { astroBlog, defineI18nConfig } from '@jet-w/astro-blog';
12
16
  import { remarkProtectCode, rehypeRestoreCode } from '@jet-w/astro-blog/plugins/remark-protect-code.mjs';
@@ -59,7 +63,7 @@ export default defineConfig({
59
63
  rehypeKatex,
60
64
  rehypeCleanContainers,
61
65
  rehypeTabs,
62
- rehypeRelativeLinks,
66
+ [rehypeRelativeLinks, { base: BASE_PATH || process.env.BASE_PATH || '/' }],
63
67
  rehypeRestoreCode, // Must run LAST to restore ::: in code blocks
64
68
  ],
65
69
  shikiConfig: {
@@ -68,8 +72,8 @@ export default defineConfig({
68
72
  wrap: true
69
73
  }
70
74
  },
71
- site: 'https://example.com',
72
- base: '/',
75
+ site: SITE_URL || process.env.SITE_URL || 'https://example.com',
76
+ base: BASE_PATH || process.env.BASE_PATH || '/',
73
77
  build: {
74
78
  assets: 'assets'
75
79
  },
@@ -14,7 +14,7 @@
14
14
  "@astrojs/rss": "^4.0.14",
15
15
  "@astrojs/tailwind": "^5.1.3",
16
16
  "@astrojs/vue": "^5.0.6",
17
- "@jet-w/astro-blog": "file:../../",
17
+ "@jet-w/astro-blog": "^0.2.1",
18
18
  "@tailwindcss/typography": "^0.5.15",
19
19
  "astro": "^5.14.1",
20
20
  "echarts": "^6.0.0",