@jet-w/astro-blog 0.1.5 → 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.
- package/dist/chunk-ATRISB7B.js +206 -0
- package/dist/chunk-HVQKQN6B.js +145 -0
- package/dist/config/index.d.ts +3 -47
- package/dist/config/index.js +18 -2
- package/dist/i18n-5H4W145i.d.ts +202 -0
- package/dist/index.d.ts +186 -7
- package/dist/index.js +238 -3
- package/dist/integration.d.ts +9 -1
- package/dist/integration.js +2 -1
- package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
- package/dist/utils/sidebar.d.ts +1 -1
- package/package.json +4 -3
- package/src/components/layout/Footer.astro +36 -20
- package/src/components/layout/Header.astro +69 -15
- package/src/components/layout/Sidebar.astro +27 -15
- package/src/components/ui/LanguageSwitcher.vue +183 -0
- package/src/layouts/BaseLayout.astro +77 -52
- package/src/layouts/PageLayout.astro +22 -27
- package/src/layouts/SlidesLayout.astro +14 -2
- package/src/pages/rss.xml.ts +18 -6
- package/templates/default/astro.config.mjs +22 -2
- package/templates/default/content/posts/blog_docs/12-i18n.md +355 -0
- package/templates/default/content/posts/blog_docs/README.md +1 -0
- package/templates/default/content/posts/blog_docs_en/README.md +78 -0
- package/templates/default/content/posts/blog_docs_en/config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_en/config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_en/config/03-i18n.md +285 -0
- package/templates/default/content/posts/blog_docs_en/config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_en/get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_en/get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_en/get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_en/get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_en/get-started/05-deploy.md +197 -0
- package/templates/default/content/posts/blog_docs_en/get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_en/guide/README.md +59 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/01-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/02-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/03-video.md +184 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/04-icons.md +227 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/README.md +51 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/02-containers.md +226 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/03-code-blocks.md +206 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/README.md +194 -0
- package/templates/default/package-lock.json +9667 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/config/footer.ts +14 -11
- package/templates/default/src/config/locales/en/footer.ts +17 -0
- package/templates/default/src/config/locales/en/index.ts +16 -0
- package/templates/default/src/config/locales/en/menu.ts +12 -0
- package/templates/default/src/config/locales/en/sidebar.ts +18 -0
- package/templates/default/src/config/locales/en/site.ts +7 -0
- package/templates/default/src/config/locales/index.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
- package/templates/default/src/config/locales/zh-CN/index.ts +16 -0
- package/templates/default/src/config/locales/zh-CN/menu.ts +12 -0
- package/templates/default/src/config/locales/zh-CN/sidebar.ts +18 -0
- package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
- package/templates/default/src/config/sidebar.ts +10 -12
- package/templates/default/src/env.d.ts +7 -0
- package/dist/chunk-MQXPSOYB.js +0 -124
- /package/dist/{chunk-GYLSY3OJ.js → chunk-AZHCNNAC.js} +0 -0
|
@@ -4,6 +4,18 @@ import { siteConfig, defaultSEO } from '@jet-w/astro-blog/config';
|
|
|
4
4
|
import '@jet-w/astro-blog/styles/global.css';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
getLocaleFromPath,
|
|
9
|
+
getLocaleConfig,
|
|
10
|
+
getAlternateLinks,
|
|
11
|
+
getTextDirection,
|
|
12
|
+
isMultiLanguageEnabled,
|
|
13
|
+
t,
|
|
14
|
+
type I18nConfig,
|
|
15
|
+
} from '../utils/i18n';
|
|
16
|
+
import { defaultI18nConfig } from '../config/i18n';
|
|
17
|
+
// Import i18n config from virtual module (injected by integration)
|
|
18
|
+
import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
|
|
7
19
|
|
|
8
20
|
export interface Props {
|
|
9
21
|
title?: string;
|
|
@@ -13,6 +25,7 @@ export interface Props {
|
|
|
13
25
|
publishedTime?: string;
|
|
14
26
|
modifiedTime?: string;
|
|
15
27
|
tags?: string[];
|
|
28
|
+
i18nConfig?: I18nConfig;
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
const {
|
|
@@ -22,14 +35,35 @@ const {
|
|
|
22
35
|
type = 'website',
|
|
23
36
|
publishedTime,
|
|
24
37
|
modifiedTime,
|
|
25
|
-
tags
|
|
38
|
+
tags,
|
|
39
|
+
i18nConfig = virtualI18nConfig || defaultI18nConfig,
|
|
26
40
|
} = Astro.props;
|
|
27
41
|
|
|
42
|
+
// Get current locale from URL
|
|
43
|
+
const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
|
|
44
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
45
|
+
const localeData = localeConfig.locale;
|
|
46
|
+
const ui = localeConfig.ui;
|
|
47
|
+
|
|
48
|
+
// Use locale-specific site config if available
|
|
49
|
+
const localeSiteConfig = localeConfig.site;
|
|
50
|
+
|
|
28
51
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
|
29
|
-
const
|
|
52
|
+
const siteTitle = localeSiteConfig.title || siteConfig.title;
|
|
53
|
+
const fullTitle = title === siteTitle ? title : `${title} | ${siteTitle}`;
|
|
30
54
|
const fullImage = new URL(image, Astro.site);
|
|
31
55
|
|
|
32
|
-
//
|
|
56
|
+
// Get alternate links for SEO (hreflang)
|
|
57
|
+
const baseUrl = Astro.site?.toString() || '';
|
|
58
|
+
const alternateLinks = isMultiLanguageEnabled(i18nConfig)
|
|
59
|
+
? getAlternateLinks(Astro.url.pathname, baseUrl, i18nConfig)
|
|
60
|
+
: [];
|
|
61
|
+
|
|
62
|
+
// Get locale prefix for RSS link
|
|
63
|
+
const rssPath = currentLocale === i18nConfig.defaultLocale && !i18nConfig.routing.prefixDefaultLocale
|
|
64
|
+
? '/rss.xml'
|
|
65
|
+
: `/${currentLocale}/rss.xml`;
|
|
66
|
+
|
|
33
67
|
const publicDir = path.join(process.cwd(), 'public');
|
|
34
68
|
const faviconSvgExists = fs.existsSync(path.join(publicDir, 'favicon.svg'));
|
|
35
69
|
const faviconIcoExists = fs.existsSync(path.join(publicDir, 'favicon.ico'));
|
|
@@ -38,7 +72,7 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
38
72
|
---
|
|
39
73
|
|
|
40
74
|
<!DOCTYPE html>
|
|
41
|
-
<html lang=
|
|
75
|
+
<html lang={localeData.htmlLang} dir={getTextDirection(currentLocale, i18nConfig)} class="scroll-smooth">
|
|
42
76
|
<head>
|
|
43
77
|
<meta charset="UTF-8" />
|
|
44
78
|
<meta name="description" content={description} />
|
|
@@ -50,13 +84,19 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
50
84
|
<title>{fullTitle}</title>
|
|
51
85
|
<link rel="canonical" href={canonicalURL} />
|
|
52
86
|
|
|
87
|
+
<!-- Alternate language links (hreflang) -->
|
|
88
|
+
{alternateLinks.map(link => (
|
|
89
|
+
<link rel="alternate" hreflang={link.hreflang} href={link.url} />
|
|
90
|
+
))}
|
|
91
|
+
|
|
53
92
|
<!-- Open Graph -->
|
|
54
93
|
<meta property="og:type" content={type} />
|
|
55
94
|
<meta property="og:title" content={fullTitle} />
|
|
56
95
|
<meta property="og:description" content={description} />
|
|
57
96
|
<meta property="og:url" content={canonicalURL} />
|
|
58
97
|
<meta property="og:image" content={fullImage} />
|
|
59
|
-
<meta property="og:site_name" content={
|
|
98
|
+
<meta property="og:site_name" content={siteTitle} />
|
|
99
|
+
<meta property="og:locale" content={localeData.htmlLang} />
|
|
60
100
|
|
|
61
101
|
{publishedTime && (
|
|
62
102
|
<meta property="article:published_time" content={publishedTime} />
|
|
@@ -75,13 +115,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
75
115
|
<meta name="twitter:image" content={fullImage} />
|
|
76
116
|
|
|
77
117
|
<!-- RSS -->
|
|
78
|
-
<link rel="alternate" type="application/rss+xml" title={
|
|
118
|
+
<link rel="alternate" type="application/rss+xml" title={siteTitle} href={rssPath} />
|
|
79
119
|
|
|
80
|
-
<!-- 预加载关键资源 -->
|
|
81
120
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
82
121
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
83
122
|
|
|
84
|
-
<!-- 图标库 -->
|
|
85
123
|
<!-- Font Awesome -->
|
|
86
124
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" />
|
|
87
125
|
<!-- Material Icons -->
|
|
@@ -95,15 +133,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
95
133
|
<!-- Ionicons -->
|
|
96
134
|
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
|
97
135
|
|
|
98
|
-
<!-- 主题检测脚本 -->
|
|
99
136
|
<script is:inline>
|
|
100
|
-
// 在页面加载前检测主题,避免闪烁
|
|
101
|
-
// 默认使用深色模式
|
|
102
137
|
const theme = (() => {
|
|
103
138
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
|
104
139
|
return localStorage.getItem('theme');
|
|
105
140
|
}
|
|
106
|
-
// 默认深色模式,除非用户系统偏好浅色
|
|
107
141
|
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
108
142
|
return 'light';
|
|
109
143
|
}
|
|
@@ -123,15 +157,26 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
123
157
|
<slot />
|
|
124
158
|
</div>
|
|
125
159
|
|
|
126
|
-
<!-- Mermaid
|
|
160
|
+
<!-- Mermaid -->
|
|
127
161
|
<script src="/js/mermaid-container.js" is:inline></script>
|
|
128
162
|
|
|
129
|
-
<!-- Tabs
|
|
163
|
+
<!-- Tabs -->
|
|
130
164
|
<script src="/js/tabs-init.js" is:inline></script>
|
|
131
165
|
|
|
132
|
-
<!--
|
|
166
|
+
<!-- Store i18n data for client-side scripts -->
|
|
167
|
+
<script is:inline define:vars={{ locale: currentLocale, uiTranslations: ui }}>
|
|
168
|
+
window.__i18n = {
|
|
169
|
+
locale: locale,
|
|
170
|
+
ui: uiTranslations
|
|
171
|
+
};
|
|
172
|
+
</script>
|
|
173
|
+
|
|
133
174
|
<script>
|
|
134
|
-
//
|
|
175
|
+
// Get i18n translations
|
|
176
|
+
const i18n = (window as any).__i18n || { locale: 'zh-CN', ui: {} };
|
|
177
|
+
const ui = i18n.ui;
|
|
178
|
+
|
|
179
|
+
// Back to top
|
|
135
180
|
const backToTop = document.querySelector('.back-to-top');
|
|
136
181
|
if (backToTop) {
|
|
137
182
|
window.addEventListener('scroll', () => {
|
|
@@ -147,7 +192,7 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
147
192
|
});
|
|
148
193
|
}
|
|
149
194
|
|
|
150
|
-
//
|
|
195
|
+
// Smooth scroll for anchor links
|
|
151
196
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
152
197
|
anchor.addEventListener('click', function (this: HTMLAnchorElement, e: Event) {
|
|
153
198
|
e.preventDefault();
|
|
@@ -161,20 +206,17 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
161
206
|
});
|
|
162
207
|
});
|
|
163
208
|
|
|
164
|
-
//
|
|
209
|
+
// Code block enhancement
|
|
165
210
|
function enhanceCodeBlocks() {
|
|
166
211
|
const codeBlocks = document.querySelectorAll('pre:not([data-enhanced])');
|
|
167
|
-
const COLLAPSE_THRESHOLD = 15;
|
|
212
|
+
const COLLAPSE_THRESHOLD = 15;
|
|
168
213
|
|
|
169
214
|
codeBlocks.forEach((pre) => {
|
|
170
|
-
// 标记为已处理
|
|
171
215
|
pre.setAttribute('data-enhanced', 'true');
|
|
172
216
|
|
|
173
|
-
// 获取语言信息
|
|
174
217
|
const code = pre.querySelector('code');
|
|
175
218
|
let lang = 'code';
|
|
176
219
|
|
|
177
|
-
// 跳过 mermaid 代码块
|
|
178
220
|
if (code?.classList.contains('language-mermaid') ||
|
|
179
221
|
pre.classList.contains('mermaid') ||
|
|
180
222
|
pre.closest('.mermaid-container')) {
|
|
@@ -182,7 +224,6 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
182
224
|
}
|
|
183
225
|
|
|
184
226
|
if (code) {
|
|
185
|
-
// 从 class 中获取语言
|
|
186
227
|
const classList = code.className.split(' ');
|
|
187
228
|
for (const cls of classList) {
|
|
188
229
|
if (cls.startsWith('language-')) {
|
|
@@ -192,58 +233,50 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
192
233
|
}
|
|
193
234
|
}
|
|
194
235
|
|
|
195
|
-
// 从 data-language 属性获取(shiki 用这个)
|
|
196
236
|
const preEl = pre as HTMLPreElement;
|
|
197
237
|
if (preEl.dataset.language) {
|
|
198
238
|
lang = preEl.dataset.language;
|
|
199
239
|
}
|
|
200
240
|
|
|
201
|
-
// 计算代码行数
|
|
202
241
|
const codeText = code ? code.textContent : pre.textContent;
|
|
203
242
|
const lineCount = (codeText || '').split('\n').length;
|
|
204
243
|
const shouldCollapse = lineCount > COLLAPSE_THRESHOLD;
|
|
205
244
|
|
|
206
|
-
// 创建包装器
|
|
207
245
|
const wrapper = document.createElement('div');
|
|
208
246
|
wrapper.className = 'code-block-wrapper' + (shouldCollapse ? ' collapsed' : '');
|
|
209
247
|
|
|
210
|
-
// 创建头部
|
|
211
248
|
const header = document.createElement('div');
|
|
212
249
|
header.className = 'code-block-header';
|
|
213
250
|
header.innerHTML = `
|
|
214
251
|
<span class="code-block-lang">${lang}</span>
|
|
215
252
|
<div class="code-block-actions">
|
|
216
253
|
${shouldCollapse ? `
|
|
217
|
-
<button class="code-block-btn collapse-btn" title="
|
|
254
|
+
<button class="code-block-btn collapse-btn" title="${ui.expand || 'Expand'}/${ui.collapse || 'Collapse'}">
|
|
218
255
|
<svg class="collapse-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
219
256
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
220
257
|
</svg>
|
|
221
|
-
<span class="collapse-text"
|
|
258
|
+
<span class="collapse-text">${ui.expand || 'Expand'}</span>
|
|
222
259
|
</button>
|
|
223
260
|
` : ''}
|
|
224
|
-
<button class="code-block-btn copy-btn" title="
|
|
261
|
+
<button class="code-block-btn copy-btn" title="${ui.copyCode || 'Copy'}">
|
|
225
262
|
<svg class="copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
226
263
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
227
264
|
</svg>
|
|
228
|
-
<span class="copy-text"
|
|
265
|
+
<span class="copy-text">${ui.copyCode || 'Copy'}</span>
|
|
229
266
|
</button>
|
|
230
267
|
</div>
|
|
231
268
|
`;
|
|
232
269
|
|
|
233
|
-
// 创建内容区
|
|
234
270
|
const content = document.createElement('div');
|
|
235
271
|
content.className = 'code-block-content';
|
|
236
272
|
|
|
237
|
-
// 插入DOM
|
|
238
273
|
if (!pre.parentNode) return;
|
|
239
274
|
pre.parentNode.insertBefore(wrapper, pre);
|
|
240
275
|
wrapper.appendChild(header);
|
|
241
276
|
wrapper.appendChild(content);
|
|
242
277
|
content.appendChild(pre);
|
|
243
278
|
|
|
244
|
-
// 如果需要收缩,添加展开按钮和底部收起按钮
|
|
245
279
|
if (shouldCollapse) {
|
|
246
|
-
// 展开按钮(收缩状态显示)
|
|
247
280
|
const expandOverlay = document.createElement('div');
|
|
248
281
|
expandOverlay.className = 'code-block-expand';
|
|
249
282
|
expandOverlay.innerHTML = `
|
|
@@ -251,12 +284,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
251
284
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
252
285
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
253
286
|
</svg>
|
|
254
|
-
<span
|
|
287
|
+
<span>${ui.expandCode || 'Expand code'} (${lineCount} ${ui.lines || 'lines'})</span>
|
|
255
288
|
</button>
|
|
256
289
|
`;
|
|
257
290
|
content.appendChild(expandOverlay);
|
|
258
291
|
|
|
259
|
-
// 底部收起按钮(展开状态显示)
|
|
260
292
|
const collapseOverlay = document.createElement('div');
|
|
261
293
|
collapseOverlay.className = 'code-block-collapse';
|
|
262
294
|
collapseOverlay.innerHTML = `
|
|
@@ -264,35 +296,31 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
264
296
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
265
297
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
|
266
298
|
</svg>
|
|
267
|
-
<span
|
|
299
|
+
<span>${ui.collapseCode || 'Collapse code'}</span>
|
|
268
300
|
</button>
|
|
269
301
|
`;
|
|
270
302
|
content.appendChild(collapseOverlay);
|
|
271
303
|
|
|
272
|
-
// 展开按钮点击事件
|
|
273
304
|
const expandBtn = expandOverlay.querySelector('.expand-btn');
|
|
274
305
|
if (expandBtn) {
|
|
275
306
|
expandBtn.addEventListener('click', () => {
|
|
276
307
|
wrapper.classList.remove('collapsed');
|
|
277
308
|
const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
|
|
278
|
-
if (collapseText) collapseText.textContent = '
|
|
309
|
+
if (collapseText) collapseText.textContent = ui.collapse || 'Collapse';
|
|
279
310
|
});
|
|
280
311
|
}
|
|
281
312
|
|
|
282
|
-
// 底部收起按钮点击事件
|
|
283
313
|
const collapseBottomBtn = collapseOverlay.querySelector('.collapse-bottom-btn');
|
|
284
314
|
if (collapseBottomBtn) {
|
|
285
315
|
collapseBottomBtn.addEventListener('click', () => {
|
|
286
316
|
wrapper.classList.add('collapsed');
|
|
287
317
|
const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
|
|
288
|
-
if (collapseText) collapseText.textContent = '
|
|
289
|
-
// 滚动到代码块顶部
|
|
318
|
+
if (collapseText) collapseText.textContent = ui.expand || 'Expand';
|
|
290
319
|
wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
291
320
|
});
|
|
292
321
|
}
|
|
293
322
|
}
|
|
294
323
|
|
|
295
|
-
// 复制按钮点击事件
|
|
296
324
|
const copyBtn = header.querySelector('.copy-btn') as HTMLElement | null;
|
|
297
325
|
if (copyBtn) {
|
|
298
326
|
copyBtn.addEventListener('click', async () => {
|
|
@@ -302,40 +330,37 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
302
330
|
copyBtn.classList.add('copied');
|
|
303
331
|
const copyText = copyBtn.querySelector('.copy-text') as HTMLElement | null;
|
|
304
332
|
const copyIcon = copyBtn.querySelector('.copy-icon') as HTMLElement | null;
|
|
305
|
-
if (copyText) copyText.textContent = '
|
|
333
|
+
if (copyText) copyText.textContent = ui.copied || 'Copied';
|
|
306
334
|
if (copyIcon) copyIcon.innerHTML = `
|
|
307
335
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
308
336
|
`;
|
|
309
337
|
|
|
310
338
|
setTimeout(() => {
|
|
311
339
|
copyBtn.classList.remove('copied');
|
|
312
|
-
if (copyText) copyText.textContent = '
|
|
340
|
+
if (copyText) copyText.textContent = ui.copyCode || 'Copy';
|
|
313
341
|
if (copyIcon) copyIcon.innerHTML = `
|
|
314
342
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
315
343
|
`;
|
|
316
344
|
}, 2000);
|
|
317
345
|
} catch (err) {
|
|
318
|
-
console.error('
|
|
346
|
+
console.error('Copy failed:', err);
|
|
319
347
|
}
|
|
320
348
|
});
|
|
321
349
|
}
|
|
322
350
|
|
|
323
|
-
// 收缩按钮点击事件
|
|
324
351
|
const collapseBtn = header.querySelector('.collapse-btn') as HTMLElement | null;
|
|
325
352
|
if (collapseBtn) {
|
|
326
353
|
collapseBtn.addEventListener('click', () => {
|
|
327
354
|
const isCollapsed = wrapper.classList.toggle('collapsed');
|
|
328
355
|
const collapseText = collapseBtn.querySelector('.collapse-text') as HTMLElement | null;
|
|
329
|
-
if (collapseText) collapseText.textContent = isCollapsed ? '
|
|
356
|
+
if (collapseText) collapseText.textContent = isCollapsed ? (ui.expand || 'Expand') : (ui.collapse || 'Collapse');
|
|
330
357
|
});
|
|
331
358
|
}
|
|
332
359
|
});
|
|
333
360
|
}
|
|
334
361
|
|
|
335
|
-
// 页面加载后执行
|
|
336
362
|
enhanceCodeBlocks();
|
|
337
363
|
|
|
338
|
-
// 监听DOM变化(处理动态加载的内容)
|
|
339
364
|
const observer = new MutationObserver((mutations) => {
|
|
340
365
|
let hasNewCodeBlocks = false;
|
|
341
366
|
mutations.forEach((mutation) => {
|
|
@@ -359,4 +384,4 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
|
|
|
359
384
|
</script>
|
|
360
385
|
|
|
361
386
|
</body>
|
|
362
|
-
</html>
|
|
387
|
+
</html>
|
|
@@ -4,6 +4,11 @@ import Header from '../components/layout/Header.astro';
|
|
|
4
4
|
import Footer from '../components/layout/Footer.astro';
|
|
5
5
|
import Sidebar from '../components/layout/Sidebar.astro';
|
|
6
6
|
import SidebarToggle from '../components/ui/SidebarToggle.vue';
|
|
7
|
+
import type { I18nConfig } from '../config/i18n';
|
|
8
|
+
import { defaultI18nConfig } from '../config/i18n';
|
|
9
|
+
import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
|
|
10
|
+
// Import i18n config from virtual module (injected by integration)
|
|
11
|
+
import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
|
|
7
12
|
|
|
8
13
|
export interface Props {
|
|
9
14
|
title?: string;
|
|
@@ -15,6 +20,7 @@ export interface Props {
|
|
|
15
20
|
tags?: string[];
|
|
16
21
|
showSidebar?: boolean;
|
|
17
22
|
showToc?: boolean;
|
|
23
|
+
i18nConfig?: I18nConfig;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const {
|
|
@@ -26,8 +32,14 @@ const {
|
|
|
26
32
|
modifiedTime,
|
|
27
33
|
tags,
|
|
28
34
|
showSidebar = true,
|
|
29
|
-
showToc = false
|
|
35
|
+
showToc = false,
|
|
36
|
+
i18nConfig = virtualI18nConfig || defaultI18nConfig,
|
|
30
37
|
} = Astro.props;
|
|
38
|
+
|
|
39
|
+
// Get current locale
|
|
40
|
+
const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
|
|
41
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
42
|
+
const ui = localeConfig.ui;
|
|
31
43
|
---
|
|
32
44
|
|
|
33
45
|
<BaseLayout
|
|
@@ -38,17 +50,16 @@ const {
|
|
|
38
50
|
publishedTime={publishedTime}
|
|
39
51
|
modifiedTime={modifiedTime}
|
|
40
52
|
tags={tags}
|
|
53
|
+
i18nConfig={i18nConfig}
|
|
41
54
|
>
|
|
42
|
-
<Header />
|
|
55
|
+
<Header i18nConfig={i18nConfig} />
|
|
43
56
|
|
|
44
57
|
<main class="flex-1 flex w-full min-w-0">
|
|
45
|
-
<!-- 左侧边栏 - 桌面端 -->
|
|
46
58
|
{showSidebar && (
|
|
47
59
|
<aside class="hidden lg:block shrink-0 transition-all duration-300 relative" data-sidebar style="width: var(--sidebar-width, 256px);">
|
|
48
60
|
<div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
|
|
49
|
-
<Sidebar />
|
|
61
|
+
<Sidebar i18nConfig={i18nConfig} />
|
|
50
62
|
</div>
|
|
51
|
-
<!-- 拖拽调整宽度的手柄 -->
|
|
52
63
|
<div
|
|
53
64
|
class="absolute top-0 right-0 w-1 h-full cursor-col-resize group z-10"
|
|
54
65
|
data-sidebar-resizer
|
|
@@ -60,7 +71,6 @@ const {
|
|
|
60
71
|
</aside>
|
|
61
72
|
)}
|
|
62
73
|
|
|
63
|
-
<!-- 移动端侧边栏遮罩 -->
|
|
64
74
|
{showSidebar && (
|
|
65
75
|
<div
|
|
66
76
|
class="lg:hidden fixed inset-0 bg-black/50 z-40 hidden"
|
|
@@ -69,33 +79,29 @@ const {
|
|
|
69
79
|
></div>
|
|
70
80
|
)}
|
|
71
81
|
|
|
72
|
-
<!-- 移动端侧边栏 -->
|
|
73
82
|
{showSidebar && (
|
|
74
83
|
<aside
|
|
75
84
|
class="lg:hidden fixed top-16 left-0 w-72 h-[calc(100vh-4rem)] bg-white dark:bg-slate-900 z-50 transform -translate-x-full transition-transform duration-300 shadow-xl overflow-y-auto"
|
|
76
85
|
data-mobile-sidebar
|
|
77
86
|
>
|
|
78
87
|
<div class="p-4">
|
|
79
|
-
<Sidebar />
|
|
88
|
+
<Sidebar i18nConfig={i18nConfig} />
|
|
80
89
|
</div>
|
|
81
90
|
</aside>
|
|
82
91
|
)}
|
|
83
92
|
|
|
84
|
-
<!-- 主内容区 -->
|
|
85
93
|
<div class={`flex-1 transition-all duration-300 ${showSidebar ? 'lg:px-8' : ''} ${showToc ? 'xl:pr-64' : ''} relative`} data-main-content>
|
|
86
|
-
<!-- 侧边栏切换按钮 - 桌面端固定位置 -->
|
|
87
94
|
{showSidebar && (
|
|
88
95
|
<div class="hidden lg:block fixed top-20 left-1 z-40 transition-all duration-300" data-sidebar-toggle>
|
|
89
96
|
<SidebarToggle client:load />
|
|
90
97
|
</div>
|
|
91
98
|
)}
|
|
92
99
|
|
|
93
|
-
<!-- 侧边栏切换按钮 - 移动端浮动按钮 -->
|
|
94
100
|
{showSidebar && (
|
|
95
101
|
<button
|
|
96
102
|
class="lg:hidden fixed bottom-20 left-4 z-40 p-3 bg-primary-500 hover:bg-primary-600 text-white rounded-full shadow-lg transition-colors"
|
|
97
103
|
data-mobile-sidebar-toggle
|
|
98
|
-
aria-label=
|
|
104
|
+
aria-label={ui.documentTree}
|
|
99
105
|
onclick="document.querySelector('[data-sidebar-overlay]').classList.remove('hidden'); document.querySelector('[data-mobile-sidebar]').classList.remove('-translate-x-full');"
|
|
100
106
|
>
|
|
101
107
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -109,7 +115,6 @@ const {
|
|
|
109
115
|
</div>
|
|
110
116
|
</div>
|
|
111
117
|
|
|
112
|
-
<!-- 右侧TOC -->
|
|
113
118
|
{showToc && (
|
|
114
119
|
<aside class="hidden xl:block w-64 shrink-0">
|
|
115
120
|
<div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
|
|
@@ -119,10 +124,9 @@ const {
|
|
|
119
124
|
)}
|
|
120
125
|
</main>
|
|
121
126
|
|
|
122
|
-
<Footer />
|
|
127
|
+
<Footer i18nConfig={i18nConfig} />
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
<button class="back-to-top" aria-label="回到顶部">
|
|
129
|
+
<button class="back-to-top" aria-label={ui.backToTop}>
|
|
126
130
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
127
131
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
128
132
|
</svg>
|
|
@@ -130,14 +134,12 @@ const {
|
|
|
130
134
|
</BaseLayout>
|
|
131
135
|
|
|
132
136
|
<script>
|
|
133
|
-
// 侧边栏拖拽调整宽度
|
|
134
137
|
function initSidebarResizer() {
|
|
135
138
|
const sidebar = document.querySelector('[data-sidebar]') as HTMLElement;
|
|
136
139
|
const resizer = document.querySelector('[data-sidebar-resizer]') as HTMLElement;
|
|
137
140
|
|
|
138
141
|
if (!sidebar || !resizer) return;
|
|
139
142
|
|
|
140
|
-
// 从 localStorage 恢复宽度
|
|
141
143
|
const savedWidth = localStorage.getItem('sidebar-width');
|
|
142
144
|
if (savedWidth) {
|
|
143
145
|
sidebar.style.setProperty('--sidebar-width', savedWidth);
|
|
@@ -152,10 +154,8 @@ const {
|
|
|
152
154
|
startX = e.clientX;
|
|
153
155
|
startWidth = sidebar.offsetWidth;
|
|
154
156
|
|
|
155
|
-
// 禁用过渡效果以获得流畅拖拽
|
|
156
157
|
sidebar.style.transition = 'none';
|
|
157
158
|
|
|
158
|
-
// 添加全局样式防止选中文本
|
|
159
159
|
document.body.style.cursor = 'col-resize';
|
|
160
160
|
document.body.style.userSelect = 'none';
|
|
161
161
|
|
|
@@ -167,7 +167,7 @@ const {
|
|
|
167
167
|
if (!isResizing) return;
|
|
168
168
|
|
|
169
169
|
const diff = e.clientX - startX;
|
|
170
|
-
const newWidth = Math.min(Math.max(startWidth + diff, 200), 500);
|
|
170
|
+
const newWidth = Math.min(Math.max(startWidth + diff, 200), 500);
|
|
171
171
|
|
|
172
172
|
sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
|
|
173
173
|
};
|
|
@@ -177,14 +177,11 @@ const {
|
|
|
177
177
|
|
|
178
178
|
isResizing = false;
|
|
179
179
|
|
|
180
|
-
// 恢复过渡效果
|
|
181
180
|
sidebar.style.transition = '';
|
|
182
181
|
|
|
183
|
-
// 恢复全局样式
|
|
184
182
|
document.body.style.cursor = '';
|
|
185
183
|
document.body.style.userSelect = '';
|
|
186
184
|
|
|
187
|
-
// 保存宽度到 localStorage
|
|
188
185
|
const currentWidth = getComputedStyle(sidebar).getPropertyValue('--sidebar-width');
|
|
189
186
|
if (currentWidth) {
|
|
190
187
|
localStorage.setItem('sidebar-width', currentWidth.trim());
|
|
@@ -197,7 +194,6 @@ const {
|
|
|
197
194
|
resizer.addEventListener('mousedown', startResize);
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
// DOM 加载完成后初始化
|
|
201
197
|
if (document.readyState === 'loading') {
|
|
202
198
|
document.addEventListener('DOMContentLoaded', initSidebarResizer);
|
|
203
199
|
} else {
|
|
@@ -206,7 +202,6 @@ const {
|
|
|
206
202
|
</script>
|
|
207
203
|
|
|
208
204
|
<style>
|
|
209
|
-
/* 拖拽时的视觉反馈 */
|
|
210
205
|
[data-sidebar-resizer]:hover {
|
|
211
206
|
background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.1), transparent);
|
|
212
207
|
}
|
|
@@ -214,4 +209,4 @@ const {
|
|
|
214
209
|
[data-sidebar-resizer]:active {
|
|
215
210
|
background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.2), transparent);
|
|
216
211
|
}
|
|
217
|
-
</style>
|
|
212
|
+
</style>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { siteConfig } from '@jet-w/astro-blog/config';
|
|
3
3
|
import '@jet-w/astro-blog/styles/slides.css';
|
|
4
|
+
import type { I18nConfig } from '../config/i18n';
|
|
5
|
+
import { defaultI18nConfig } from '../config/i18n';
|
|
6
|
+
import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
|
|
4
7
|
|
|
5
8
|
export interface Props {
|
|
6
9
|
title: string;
|
|
@@ -13,6 +16,7 @@ export interface Props {
|
|
|
13
16
|
hash?: boolean;
|
|
14
17
|
slideNumber?: boolean;
|
|
15
18
|
showBackButton?: boolean;
|
|
19
|
+
i18nConfig?: I18nConfig;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
const {
|
|
@@ -26,9 +30,17 @@ const {
|
|
|
26
30
|
hash = true,
|
|
27
31
|
slideNumber = false,
|
|
28
32
|
showBackButton = true,
|
|
33
|
+
i18nConfig = defaultI18nConfig,
|
|
29
34
|
} = Astro.props;
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
// Get current locale
|
|
37
|
+
const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
|
|
38
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
39
|
+
const localeData = localeConfig.locale;
|
|
40
|
+
const localeSiteConfig = localeConfig.site;
|
|
41
|
+
|
|
42
|
+
const siteTitle = localeSiteConfig.title || siteConfig.title;
|
|
43
|
+
const fullTitle = `${title} | ${siteTitle}`;
|
|
32
44
|
|
|
33
45
|
// Reveal.js 配置
|
|
34
46
|
const revealConfig = JSON.stringify({
|
|
@@ -48,7 +60,7 @@ const revealConfig = JSON.stringify({
|
|
|
48
60
|
---
|
|
49
61
|
|
|
50
62
|
<!DOCTYPE html>
|
|
51
|
-
<html lang=
|
|
63
|
+
<html lang={localeData.htmlLang}>
|
|
52
64
|
<head>
|
|
53
65
|
<meta charset="UTF-8" />
|
|
54
66
|
<meta name="description" content={description} />
|
package/src/pages/rss.xml.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import rss from '@astrojs/rss';
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import { siteConfig } from '@jet-w/astro-blog/config';
|
|
4
|
+
import { defaultI18nConfig } from '../config/i18n';
|
|
5
|
+
import { getLocaleFromPath, getLocaleConfig, getLocalePrefix } from '../utils/i18n';
|
|
4
6
|
|
|
5
|
-
export async function GET(context: { site: URL }) {
|
|
7
|
+
export async function GET(context: { site: URL; request: Request }) {
|
|
6
8
|
const posts = await getCollection('posts', ({ data }) => !data.draft);
|
|
7
9
|
|
|
8
|
-
//
|
|
10
|
+
// Get locale from URL path
|
|
11
|
+
const url = new URL(context.request.url);
|
|
12
|
+
const i18nConfig = defaultI18nConfig; // In real usage, this would be passed from integration
|
|
13
|
+
const currentLocale = getLocaleFromPath(url.pathname, i18nConfig);
|
|
14
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
15
|
+
const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
|
|
16
|
+
|
|
17
|
+
// Use locale-specific site config
|
|
18
|
+
const localeSiteConfig = localeConfig.site;
|
|
19
|
+
|
|
20
|
+
// Sort by date, newest first
|
|
9
21
|
const sortedPosts = posts.sort((a, b) => {
|
|
10
22
|
const dateA = new Date(a.data.date || 0);
|
|
11
23
|
const dateB = new Date(b.data.date || 0);
|
|
@@ -13,16 +25,16 @@ export async function GET(context: { site: URL }) {
|
|
|
13
25
|
});
|
|
14
26
|
|
|
15
27
|
return rss({
|
|
16
|
-
title: siteConfig.title,
|
|
17
|
-
description: siteConfig.description,
|
|
28
|
+
title: localeSiteConfig.title || siteConfig.title,
|
|
29
|
+
description: localeSiteConfig.description || siteConfig.description,
|
|
18
30
|
site: context.site,
|
|
19
31
|
items: sortedPosts.map((post) => ({
|
|
20
32
|
title: post.data.title,
|
|
21
33
|
pubDate: post.data.date ? new Date(post.data.date) : new Date(),
|
|
22
34
|
description: post.data.description || '',
|
|
23
|
-
link:
|
|
35
|
+
link: `${localePrefix}/posts/${post.id.toLowerCase()}/`,
|
|
24
36
|
categories: [...(post.data.categories || []), ...(post.data.tags || [])]
|
|
25
37
|
})),
|
|
26
|
-
customData: `<language
|
|
38
|
+
customData: `<language>${localeConfig.locale.htmlLang}</language>`
|
|
27
39
|
});
|
|
28
40
|
}
|