@jet-w/astro-blog 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import type { SiteConfig } from '../types';
2
+
3
+ /**
4
+ * Default site configuration
5
+ * Users should override this in their own config
6
+ */
7
+ export const siteConfig: SiteConfig = {
8
+ title: 'My Astro Blog',
9
+ description: '',
10
+ author: 'Author',
11
+ email: '',
12
+ avatar: '/images/avatar.svg',
13
+ social: {
14
+ github: '',
15
+ twitter: '',
16
+ linkedin: '',
17
+ email: ''
18
+ },
19
+ menu: [
20
+ {
21
+ name: '首页',
22
+ href: '/',
23
+ icon: 'home'
24
+ },
25
+ {
26
+ name: '博客',
27
+ href: '/posts',
28
+ icon: 'posts'
29
+ },
30
+ {
31
+ name: '关于',
32
+ href: '/about',
33
+ icon: 'about'
34
+ }
35
+ ]
36
+ };
37
+
38
+ export const defaultSEO = {
39
+ title: siteConfig.title,
40
+ description: siteConfig.description,
41
+ image: '/images/og-image.jpg',
42
+ type: 'website' as const
43
+ };
44
+
45
+ /**
46
+ * Create site config with user overrides
47
+ */
48
+ export function defineSiteConfig(config: Partial<SiteConfig>): SiteConfig {
49
+ return {
50
+ ...siteConfig,
51
+ ...config,
52
+ social: {
53
+ ...siteConfig.social,
54
+ ...config.social
55
+ },
56
+ menu: config.menu || siteConfig.menu
57
+ };
58
+ }
59
+
60
+ // 向后兼容的别名
61
+ export const defaultSiteConfig = siteConfig;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 社交链接配置
3
+ */
4
+
5
+ export interface SocialLink {
6
+ type: string;
7
+ url: string;
8
+ label?: string;
9
+ icon?: string;
10
+ }
11
+
12
+ export const defaultIcons: Record<string, string> = {
13
+ github: 'M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z',
14
+ twitter: 'M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z',
15
+ linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
16
+ email: 'M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
17
+ youtube: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
18
+ discord: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z'
19
+ };
20
+
21
+ export const socialLinks: SocialLink[] = [];
22
+ export const defaultSocialLinks = socialLinks;
23
+
24
+ /**
25
+ * Define social links
26
+ */
27
+ export function defineSocialLinks(links: SocialLink[]): SocialLink[] {
28
+ return links;
29
+ }
@@ -0,0 +1,80 @@
1
+ export interface BlogPost {
2
+ slug: string;
3
+ title: string;
4
+ description: string;
5
+ content: string;
6
+ pubDate: Date;
7
+ updatedDate?: Date;
8
+ tags: string[];
9
+ categories: string[];
10
+ author?: string;
11
+ image?: string;
12
+ draft?: boolean;
13
+ readingTime?: number;
14
+ }
15
+
16
+ export interface PostFrontmatter {
17
+ title: string;
18
+ description: string;
19
+ pubDate: string;
20
+ updatedDate?: string;
21
+ tags?: string[];
22
+ categories?: string[];
23
+ author?: string;
24
+ image?: string;
25
+ draft?: boolean;
26
+ }
27
+
28
+ export interface Tag {
29
+ name: string;
30
+ count: number;
31
+ slug: string;
32
+ }
33
+
34
+ export interface Category {
35
+ name: string;
36
+ count: number;
37
+ slug: string;
38
+ }
39
+
40
+ export interface SearchResult {
41
+ title: string;
42
+ description: string;
43
+ url: string;
44
+ content: string;
45
+ tags: string[];
46
+ categories: string[];
47
+ }
48
+
49
+ export interface NavigationItem {
50
+ name: string;
51
+ href: string;
52
+ icon?: string;
53
+ children?: NavigationItem[];
54
+ }
55
+
56
+ export interface SiteConfig {
57
+ title: string;
58
+ description: string;
59
+ author: string;
60
+ email?: string;
61
+ avatar?: string;
62
+ social?: {
63
+ github?: string;
64
+ twitter?: string;
65
+ linkedin?: string;
66
+ email?: string;
67
+ };
68
+ menu: NavigationItem[];
69
+ }
70
+
71
+ export interface SEOProps {
72
+ title?: string;
73
+ description?: string;
74
+ image?: string;
75
+ url?: string;
76
+ type?: 'website' | 'article';
77
+ publishedTime?: string;
78
+ modifiedTime?: string;
79
+ tags?: string[];
80
+ }
@@ -0,0 +1,418 @@
1
+ /**
2
+ * i18n Utility Functions
3
+ *
4
+ * Provides helper functions for multi-language support.
5
+ */
6
+
7
+ import type {
8
+ I18nConfig,
9
+ Locale,
10
+ UITranslations,
11
+ } from '../config/i18n';
12
+ import {
13
+ defaultI18nConfig,
14
+ getUITranslations,
15
+ } from '../config/i18n';
16
+ import type { SiteConfig, NavigationItem } from '../types';
17
+ import type { FooterConfig } from '../config/footer';
18
+ import type { SidebarConfig } from '../config/sidebar';
19
+ import { defaultSiteConfig } from '../config/site';
20
+ import { defaultMenu } from '../config/menu';
21
+ import { defaultFooterConfig } from '../config/footer';
22
+ import { sidebarConfig as defaultSidebarConfig } from '../config/sidebar';
23
+
24
+ /**
25
+ * Merged locale configuration with all defaults applied
26
+ */
27
+ export interface MergedLocaleConfig {
28
+ locale: Locale;
29
+ site: SiteConfig;
30
+ menu: NavigationItem[];
31
+ footer: FooterConfig;
32
+ sidebar: SidebarConfig;
33
+ ui: UITranslations;
34
+ }
35
+
36
+ /**
37
+ * Alternate link for SEO (hreflang)
38
+ */
39
+ export interface AlternateLink {
40
+ locale: string;
41
+ url: string;
42
+ hreflang: string;
43
+ }
44
+
45
+ /**
46
+ * Get current locale from URL pathname
47
+ *
48
+ * @example
49
+ * getLocaleFromPath('/en/posts', config) // 'en'
50
+ * getLocaleFromPath('/posts', config) // 'zh-CN' (default)
51
+ * getLocaleFromPath('/zh-CN/about', config) // 'zh-CN'
52
+ */
53
+ export function getLocaleFromPath(
54
+ pathname: string,
55
+ config: I18nConfig = defaultI18nConfig
56
+ ): string {
57
+ // Remove leading slash and get first segment
58
+ const segments = pathname.replace(/^\//, '').split('/');
59
+ const firstSegment = segments[0];
60
+
61
+ // Check if first segment matches any locale code
62
+ const matchedLocale = config.locales.find(
63
+ (locale) => locale.code === firstSegment
64
+ );
65
+
66
+ if (matchedLocale) {
67
+ return matchedLocale.code;
68
+ }
69
+
70
+ // Return default locale
71
+ return config.defaultLocale;
72
+ }
73
+
74
+ /**
75
+ * Get locale data by code
76
+ */
77
+ export function getLocaleByCode(
78
+ code: string,
79
+ config: I18nConfig = defaultI18nConfig
80
+ ): Locale | undefined {
81
+ return config.locales.find((locale) => locale.code === code);
82
+ }
83
+
84
+ /**
85
+ * Remove locale prefix from pathname
86
+ *
87
+ * @example
88
+ * removeLocalePrefix('/en/posts', config) // '/posts'
89
+ * removeLocalePrefix('/posts', config) // '/posts'
90
+ */
91
+ export function removeLocalePrefix(
92
+ pathname: string,
93
+ config: I18nConfig = defaultI18nConfig
94
+ ): string {
95
+ const locale = getLocaleFromPath(pathname, config);
96
+
97
+ // If it's default locale and prefix is not required, pathname has no prefix
98
+ if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
99
+ return pathname;
100
+ }
101
+
102
+ // Remove the locale prefix
103
+ const prefix = `/${locale}`;
104
+ if (pathname.startsWith(prefix)) {
105
+ const rest = pathname.slice(prefix.length);
106
+ return rest || '/';
107
+ }
108
+
109
+ return pathname;
110
+ }
111
+
112
+ /**
113
+ * Get localized path for a given locale
114
+ *
115
+ * @example
116
+ * getLocalizedPath('/posts', 'en', config) // '/en/posts'
117
+ * getLocalizedPath('/en/posts', 'zh-CN', config) // '/posts' (if zh-CN is default)
118
+ */
119
+ export function getLocalizedPath(
120
+ pathname: string,
121
+ targetLocale: string,
122
+ config: I18nConfig = defaultI18nConfig
123
+ ): string {
124
+ // First, remove any existing locale prefix
125
+ const basePath = removeLocalePrefix(pathname, config);
126
+
127
+ // If target is default locale and prefix is not required
128
+ if (
129
+ targetLocale === config.defaultLocale &&
130
+ !config.routing.prefixDefaultLocale
131
+ ) {
132
+ return basePath;
133
+ }
134
+
135
+ // Add locale prefix
136
+ if (basePath === '/') {
137
+ return `/${targetLocale}`;
138
+ }
139
+
140
+ return `/${targetLocale}${basePath}`;
141
+ }
142
+
143
+ /**
144
+ * Get all alternate links for SEO (hreflang tags)
145
+ *
146
+ * @example
147
+ * getAlternateLinks('/posts', 'https://example.com', config)
148
+ * // Returns links for all locales
149
+ */
150
+ export function getAlternateLinks(
151
+ pathname: string,
152
+ baseUrl: string,
153
+ config: I18nConfig = defaultI18nConfig
154
+ ): AlternateLink[] {
155
+ const links: AlternateLink[] = [];
156
+
157
+ for (const locale of config.locales) {
158
+ const localizedPath = getLocalizedPath(pathname, locale.code, config);
159
+ links.push({
160
+ locale: locale.code,
161
+ url: `${baseUrl.replace(/\/$/, '')}${localizedPath}`,
162
+ hreflang: locale.htmlLang,
163
+ });
164
+ }
165
+
166
+ // Add x-default pointing to default locale
167
+ const defaultPath = getLocalizedPath(
168
+ pathname,
169
+ config.defaultLocale,
170
+ config
171
+ );
172
+ links.push({
173
+ locale: 'x-default',
174
+ url: `${baseUrl.replace(/\/$/, '')}${defaultPath}`,
175
+ hreflang: 'x-default',
176
+ });
177
+
178
+ return links;
179
+ }
180
+
181
+ /**
182
+ * Deep merge two objects
183
+ */
184
+ function deepMerge<T extends object>(base: T, override: Partial<T>): T {
185
+ const result = { ...base };
186
+
187
+ for (const key in override) {
188
+ if (Object.prototype.hasOwnProperty.call(override, key)) {
189
+ const overrideValue = override[key];
190
+ const baseValue = base[key];
191
+
192
+ if (
193
+ typeof overrideValue === 'object' &&
194
+ overrideValue !== null &&
195
+ !Array.isArray(overrideValue) &&
196
+ typeof baseValue === 'object' &&
197
+ baseValue !== null &&
198
+ !Array.isArray(baseValue)
199
+ ) {
200
+ (result as any)[key] = deepMerge(baseValue as object, overrideValue as object);
201
+ } else if (overrideValue !== undefined) {
202
+ (result as any)[key] = overrideValue;
203
+ }
204
+ }
205
+ }
206
+
207
+ return result;
208
+ }
209
+
210
+ /**
211
+ * Get merged configuration for a specific locale
212
+ * Combines default config with locale-specific overrides
213
+ */
214
+ export function getLocaleConfig(
215
+ locale: string,
216
+ config: I18nConfig = defaultI18nConfig
217
+ ): MergedLocaleConfig {
218
+ const localeData = getLocaleByCode(locale, config);
219
+ const localeOverrides = config.localeConfigs[locale] || {};
220
+
221
+ // Get locale info (fallback to creating one from code)
222
+ const localeInfo: Locale = localeData || {
223
+ code: locale,
224
+ name: locale,
225
+ htmlLang: locale,
226
+ dateLocale: locale,
227
+ direction: 'ltr',
228
+ };
229
+
230
+ // Merge site config
231
+ const site = deepMerge(defaultSiteConfig, localeOverrides.site || {});
232
+
233
+ // Use locale-specific menu or default menu
234
+ const menu = localeOverrides.menu || defaultMenu;
235
+
236
+ // Merge footer config
237
+ const footer = deepMerge(defaultFooterConfig, localeOverrides.footer || {});
238
+
239
+ // Merge sidebar config
240
+ const sidebar = localeOverrides.sidebar
241
+ ? {
242
+ ...defaultSidebarConfig,
243
+ ...localeOverrides.sidebar,
244
+ // Use locale-specific groups if provided, otherwise keep default
245
+ groups: localeOverrides.sidebar.groups || defaultSidebarConfig.groups,
246
+ }
247
+ : defaultSidebarConfig;
248
+
249
+ // Get UI translations
250
+ const ui = getUITranslations(locale, config);
251
+
252
+ return {
253
+ locale: localeInfo,
254
+ site,
255
+ menu,
256
+ footer,
257
+ sidebar,
258
+ ui,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Translation function - get a UI translation string
264
+ *
265
+ * @example
266
+ * t('readMore', 'en') // 'Read more'
267
+ * t('readMore', 'zh-CN') // '阅读更多'
268
+ */
269
+ export function t(
270
+ key: keyof UITranslations,
271
+ locale: string,
272
+ config?: I18nConfig
273
+ ): string {
274
+ const translations = getUITranslations(locale, config);
275
+ return translations[key] || key;
276
+ }
277
+
278
+ /**
279
+ * Format date according to locale
280
+ *
281
+ * @example
282
+ * formatDate(new Date(), 'en') // 'January 1, 2024'
283
+ * formatDate(new Date(), 'zh-CN') // '2024年1月1日'
284
+ */
285
+ export function formatDate(
286
+ date: Date | string,
287
+ locale: string,
288
+ options?: Intl.DateTimeFormatOptions
289
+ ): string {
290
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
291
+
292
+ const defaultOptions: Intl.DateTimeFormatOptions = {
293
+ year: 'numeric',
294
+ month: 'long',
295
+ day: 'numeric',
296
+ };
297
+
298
+ return new Intl.DateTimeFormat(locale, options || defaultOptions).format(
299
+ dateObj
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Format date in short format
305
+ *
306
+ * @example
307
+ * formatDateShort(new Date(), 'en') // '1/1/2024'
308
+ * formatDateShort(new Date(), 'zh-CN') // '2024/1/1'
309
+ */
310
+ export function formatDateShort(
311
+ date: Date | string,
312
+ locale: string
313
+ ): string {
314
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
315
+
316
+ return new Intl.DateTimeFormat(locale, {
317
+ year: 'numeric',
318
+ month: 'numeric',
319
+ day: 'numeric',
320
+ }).format(dateObj);
321
+ }
322
+
323
+ /**
324
+ * Check if a locale is RTL (right-to-left)
325
+ */
326
+ export function isRTL(
327
+ locale: string,
328
+ config: I18nConfig = defaultI18nConfig
329
+ ): boolean {
330
+ const localeData = getLocaleByCode(locale, config);
331
+ return localeData?.direction === 'rtl';
332
+ }
333
+
334
+ /**
335
+ * Get the HTML dir attribute value
336
+ */
337
+ export function getTextDirection(
338
+ locale: string,
339
+ config: I18nConfig = defaultI18nConfig
340
+ ): 'ltr' | 'rtl' {
341
+ return isRTL(locale, config) ? 'rtl' : 'ltr';
342
+ }
343
+
344
+ /**
345
+ * Check if multi-language is enabled (more than one locale)
346
+ */
347
+ export function isMultiLanguageEnabled(
348
+ config: I18nConfig = defaultI18nConfig
349
+ ): boolean {
350
+ return config.locales.length > 1;
351
+ }
352
+
353
+ /**
354
+ * Get prefix for a locale in routes
355
+ * Returns empty string for default locale if prefixDefaultLocale is false
356
+ */
357
+ export function getLocalePrefix(
358
+ locale: string,
359
+ config: I18nConfig = defaultI18nConfig
360
+ ): string {
361
+ if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
362
+ return '';
363
+ }
364
+ return `/${locale}`;
365
+ }
366
+
367
+ /**
368
+ * Get content path prefix for a specific locale
369
+ * Returns the contentPathPrefix from locale config, or undefined if not set
370
+ */
371
+ export function getContentPathPrefix(
372
+ locale: string,
373
+ config: I18nConfig = defaultI18nConfig
374
+ ): string | undefined {
375
+ const localeConfig = config.localeConfigs[locale];
376
+ return localeConfig?.contentPathPrefix;
377
+ }
378
+
379
+ /**
380
+ * Filter posts by locale based on contentPathPrefix
381
+ * If contentPathPrefix is set, only return posts that start with that prefix
382
+ * If not set, return all posts (backward compatible)
383
+ *
384
+ * @example
385
+ * // If en locale has contentPathPrefix: 'blog_docs_en'
386
+ * filterPostsByLocale(posts, 'en', config)
387
+ * // Returns only posts with id starting with 'blog_docs_en/'
388
+ */
389
+ export function filterPostsByLocale<T extends { id: string }>(
390
+ posts: T[],
391
+ locale: string,
392
+ config: I18nConfig = defaultI18nConfig
393
+ ): T[] {
394
+ const contentPathPrefix = getContentPathPrefix(locale, config);
395
+
396
+ // If no contentPathPrefix is set, return all posts (backward compatible)
397
+ if (!contentPathPrefix) {
398
+ return posts;
399
+ }
400
+
401
+ // Filter posts that start with the content path prefix
402
+ return posts.filter((post) => {
403
+ const postPath = post.id.toLowerCase();
404
+ const prefix = contentPathPrefix.toLowerCase();
405
+ return postPath.startsWith(prefix + '/') || postPath === prefix;
406
+ });
407
+ }
408
+
409
+ // Re-export types and config functions
410
+ export type { I18nConfig, Locale, LocaleConfig, UITranslations } from '../config/i18n';
411
+ export {
412
+ defaultI18nConfig,
413
+ defineI18nConfig,
414
+ getUITranslations,
415
+ builtInTranslations,
416
+ zhCNTranslations,
417
+ enTranslations,
418
+ } from '../config/i18n';