feat: 添加底部链接配置
version: <4.0.0
src/components/layout/Footer.astro
前面部分添加
import { footerConfig, siteConfig } from '@constants/site-config';
const footerSocial = footerConfig?.social || [];
在这部分中
<footer class={cn('mt-auto pb-6', className)}>
// 其它代码
<div class={cn('mx-auto flex flex-col items-center gap-3 px-6 md:pb-10 md:px-3', MAX_WIDTH.content)}>
// 需要添加的代码
</div>
// 其它代码
</footer>
添加
<!-- Social Links -->
<div class="text-muted-foreground flex items-center gap-4 text-sm">
{
footerSocial.map((item) => (
item.url ? (
<a
href={item.url}
target="_blank"
class="footer-link font-medium transition-all duration-300"
>
{item.icon ? (
<Icon name={item.icon} class="h-4 w-4" />
) : item.img ? (
<img src={item.img} alt={item.title} class="h-6 w-auto" />
) : (
item.title
)}
</a>
) : (
<span class="footer-link font-medium transition-all duration-300">
{item.icon ? (
<Icon name={item.icon} class="h-4 w-4" />
) : item.img ? (
<img src={item.img} alt={item.title} class="h-6 w-auto" />
) : (
item.title
)}
</span>
)
))
}
</div>
src/constants/site-config.ts
最后一行添加
// =============================================================================
// Footer
// =============================================================================
export const footerConfig = yamlConfig.footer || {};
src/lib/config/types.ts
添加类型
// =============================================================================
// Footer Configuration
// =============================================================================
export interface FooterConfig {
social?: {
title: string;
url?: string;
icon?: string;
img?: string;
}[];
}
SiteYamlConfig 中添加 footer: FooterConfig
export interface SiteYamlConfig {
/** Footer configuration */
footer?: FooterConfig;
}
feat: 添加 Service Worker 支持
- 添加 astrojs-service-worker 依赖
- 在 site.yaml 中配置 Service Worker 开关
- 在 astro.config.mjs 中根据配置启用 Service Worker
安装依赖
npm install astrojs-service-worker
修改 astro.config.mjs
添加
import serviceWorker from "astrojs-service-worker";
// Get Service Worker config from YAML
const serviceWorkerConfig = yamlConfig.serviceWorker;
const serviceWorkerEnabled = serviceWorkerConfig?.enabled ?? false;
修改 defineConfig 中 integrations 数组,添加 Service Worker 插件:
export default defineConfig({
integrations: [
...(serviceWorkerEnabled ? [serviceWorker()] : []),
]
})
config/site.yaml 中添加 Service Worker 配置
serviceWorker:
enabled: true
feat(配置): 添加注入配置功能
version: <4.0.0
添加了注入配置功能,允许通过 site.yaml 配置在页面头部和主体注入自定义内容。包括:
- 在 site.yaml 中添加 inject 配置节
- 新增 InjectConfig 类型定义
- 在 Layout.astro 中实现内容注入逻辑
图片居中
version: <4.0.0
修改 src/styles/theme/markdown.css
.prose .markdown-image-wrapper {
margin: 1.5rem auto;
}
移除追番导航自动添加到导航栏,改为手动添加
version: <4.0.0
修改 src/constants/site-config.ts 中的 routers 数组,移除追番导航路由。
export const routers: RouterItem[] = baseRouters;
添加 cdn 配置
version: <4.0.0
astro.config.mjs
在前面部分添加以下代码:
// CDN config from YAML
const cdnConfig = yamlConfig.site?.assetsCdn;
config/site.yaml 添加 site.assetsCdn 字段
site:
assetsCdn: https://cdn.example.com
src/constants/site-config.ts 在 SiteConfig 添加 assetsCdn 字段
/**
* Runtime site configuration
* Extends SiteBasicConfig with runtime-specific fields
*/
type SiteConfig = Omit<SiteBasicConfig, 'url'> & {
/** Site URL (mapped from SiteBasicConfig.url) */
site: string;
featuredCategories?: FeaturedCategory[];
/** Normalized array of featured series */
featuredSeries: FeaturedSeriesItem[];
/** CDN for static assets */
assetsCdn?: string;
};
src/lib/config/types.ts 在 SiteBasicConfig 添加 assetsCdn 字段
export interface SiteBasicConfig {
/** CDN for static assets */
assetsCdn?: string;
}
src/components/layout/SearchPortal.astro 配置 Pagefind 搜索组件的 baseUrl,使搜索结果链接指向正确的站点根路径:
import { siteConfig } from '@constants/site-config';
import Search from 'astro-pagefind/components/Search';
uiOptions={{
baseUrl: siteConfig.site,
showImages: false,
}}
字体 cdn 配置
src/layouts/Layout.astro
把下面的代码替换为:
{/* Async load rounded fonts — Japanese pages use Gen Jyuu Gothic P, others use ChillRoundF (fix #113) */}
{locale === 'ja' ? (
<>
<link rel="stylesheet" href="/fonts/GenJyuuGothic-P-Regular/result.css" media="print" onload="this.setAttribute('media','all')" />
<link rel="stylesheet" href="/fonts/GenJyuuGothic-P-Bold/result.css" media="print" onload="this.setAttribute('media','all')" />
</>
) : (
<>
<link rel="stylesheet" href="/fonts/ChillRoundFRegular/result.css" media="print" onload="this.setAttribute('media','all')" />
<link rel="stylesheet" href="/fonts/ChillRoundFBold/result.css" media="print" onload="this.setAttribute('media','all')" />
</>
)}
替换为:
<link rel="stylesheet" href={fonts_url_1} media="print" onload="this.setAttribute('media','all')" />
<link rel="stylesheet" href={fonts_url_2} media="print" onload="this.setAttribute('media','all')" />
再在 Frontmatter 中添加:
// cdn 前缀
const cdn_prefix = (() => {
if (siteConfig.assetsCdn) {
const url = new URL(siteConfig.assetsCdn);
return url.href;
} else {
return '/';
}
})();
// Async load rounded fonts — Japanese pages use Gen Jyuu Gothic P, others use ChillRoundF (fix #113)
const fonts_url_1 =
locale === 'ja'
? cdn_prefix + 'fonts/GenJyuuGothic-P-Regular/result.css'
: cdn_prefix + 'fonts/ChillRoundFRegular/result.css';
const fonts_url_2 =
locale === 'ja' ? cdn_prefix + 'fonts/GenJyuuGothic-P-Bold/result.css' : cdn_prefix + 'fonts/ChillRoundFBold/result.css';
为移动端添加单独的 LogoText
version: <4.0.0
避免 LogoText 太长,导致移动端两行显示
config/site.yaml 中 site 添加 mobileLogoText 字段
site:
mobileLogoText: mobileLogoText
src/components/layout/Header.astro
// 添加 mobileLogoText 导入
const { alternate, title, showLogo, mobileLogoText } = siteConfig;
// ------
// 修改 MobilePostHeader 组件的 logoText 参数
<MobilePostHeader
client:only="react"
isPostPage={isPostPage}
enableNumbering={tocNumbering}
logoElement={showLogo ? 'svg' : 'text'}
logoText={mobileLogoText || title}
logoSrc={logoSrc}
/>
src/constants/site-config.ts 中 SiteConfig 添加 mobileLogoText 字段
export interface SiteConfig {
/** Mobile logo text */
mobileLogoText?: string;
}
src/lib/config/types.ts 中 SiteBasicConfig 添加 mobileLogoText 字段
export interface SiteBasicConfig {
/** Mobile logo text */
mobileLogoText?: string;
}
feat: 添加文章赞赏按钮
version: 4.0.0
在文章底部评论区上方显示赞赏按钮,点击弹出赞赏二维码图片。通过 config/site.yaml 中 content.donate 字段配置赞赏图片链接。
src/components/post/DonateButton.astro
新增 Astro 组件,完整代码如下:
---
/**
* Donate Button Component
*
* Displays a pink donation button below posts when a donate image URL is configured.
* Clicking the button shows a popup with the donation QR code / image.
*/
import { defaultContentConfig } from '@constants/content-config';
import { getLocaleFromUrl, t } from '@/i18n';
const donateUrl = defaultContentConfig.donate;
if (!donateUrl) return;
const locale = getLocaleFromUrl(Astro.url.pathname);
---
<div class="donate-wrapper relative flex flex-col items-center pt-6 pb-4">
<button
id="donate-toggle"
class="donate-btn inline-flex items-center gap-1.5 rounded-xl px-5 py-2 text-sm font-medium text-white transition-all duration-300 hover:scale-105 hover:shadow-lg active:scale-95"
style="background: linear-gradient(135deg, #f9a8d4, #ec4899); box-shadow: 0 2px 8px rgba(236, 72, 153, 0.35);"
aria-label={t(locale, 'donate.button')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
{t(locale, 'donate.button')}
</button>
<div
id="donate-popup"
class="donate-popup invisible absolute bottom-full left-1/2 z-50 mb-3 -translate-x-1/2 opacity-0 transition-all duration-300 ease-out"
>
<div class="rounded-xl bg-white p-4 shadow-2xl ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{t(locale, 'donate.button')}</span>
<button
id="donate-close"
class="ml-4 rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
aria-label={t(locale, 'donate.close')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<img
src={donateUrl}
alt={t(locale, 'donate.button')}
class="h-auto w-56 rounded-lg object-contain"
loading="lazy"
decoding="async"
/>
</div>
<!-- Arrow -->
<div class="absolute left-1/2 top-full -translate-x-1/2 border-8 border-transparent border-t-white dark:border-t-gray-800"></div>
</div>
</div>
<script>
let globalClickHandler: ((e: Event) => void) | null = null;
function initDonate() {
const btn = document.getElementById('donate-toggle');
const popup = document.getElementById('donate-popup');
const close = document.getElementById('donate-close');
if (!btn || !popup) return;
const show = () => {
popup.classList.remove('invisible', 'opacity-0');
popup.classList.add('visible', 'opacity-100');
};
const hide = () => {
popup.classList.add('invisible', 'opacity-0');
popup.classList.remove('visible', 'opacity-100');
};
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (popup.classList.contains('opacity-100')) {
hide();
} else {
show();
}
});
close?.addEventListener('click', (e) => {
e.stopPropagation();
hide();
});
if (globalClickHandler) {
document.removeEventListener('click', globalClickHandler);
}
globalClickHandler = (e) => {
if (!popup.contains(e.target as Node) && e.target !== btn) {
hide();
}
};
document.addEventListener('click', globalClickHandler);
}
initDonate();
document.addEventListener('astro:after-swap', initDonate);
</script>
<style>
.donate-btn:hover {
box-shadow: 0 4px 16px rgba(236, 72, 153, 0.5) !important;
}
</style>
核心逻辑:
- 若
donateUrl未配置则直接return不渲染 - 使用 vanilla JS 控制弹窗显示/隐藏,避免引入额外依赖
- 点击外部区域自动关闭弹窗
- 监听
astro:after-swap事件,确保 SPA 无刷新切换页面后重新绑定事件 - 通过
globalClickHandler引用去重document全局点击监听器,避免 SPA 导航累积
src/pages/post/[...slug].astro
在 <article> 与 <Comment /> 之间插入 <DonateButton />:
import DonateButton from '@/components/post/DonateButton.astro';
<article class="prose md:prose-sm dark:prose-invert" data-pagefind-body>
<CustomContent Content={Content} />
</article>
<DonateButton />
<Comment />
config/site.yaml 中添加赞赏图片配置
content:
donate: "https://ik.imagekit.io/ziw9wtigz/me/WeChat-Appreciation-code-3.png?updatedAt=1778394979458"
类型定义
src/lib/config/types.ts 中 ContentConfig 接口添加:
/** 赞赏图片链接,设置后文章下方显示赞赏按钮 */
donate?: string;
国际化
src/i18n/translations/{zh,en,ja}.ts 各添加 2 个翻译键:
| 键 | zh | en | ja |
|---|---|---|---|
donate.button | 赞赏 | Donate | 応援する |
donate.close | 关闭 | Close | 閉じる |
// ── Donate ───────────────────────────────────────────────────
'donate.button': '赞赏',
'donate.close': '关闭',
src/constants/content-config.ts
默认值中添加 donate: undefined:
export const defaultContentConfig: ContentConfig = yamlConfig.content ?? {
// ...
donate: undefined,
};
feat: 添加 hideFrom frontmatter 字段控制文章可见性
version: 4.0.0
在文章 frontmatter 中添加 hideFrom 字段,控制文章在各类聚合列表页的可见性,支持四种隐藏级别:
| 值 | 隐藏范围 |
|---|---|
home | 仅在首页隐藏 |
rss | 仅在 RSS 隐藏 |
home_rss | 首页 + RSS 隐藏 |
all | 所有聚合列表页隐藏(首页、RSS、分类页、标签页、归档页、系列文章列表、相关文章推荐) |
文章详情页不受影响,直接 URL 始终可访问。
src/types/blog.ts
hideFrom?: 'home' | 'home_rss' | 'rss' | 'all';
src/content/config.ts
hideFrom: z.enum(['home', 'home_rss', 'rss', 'all']).optional(),
src/lib/content/posts.ts
新增工具函数和类型:
export type PostListContext = 'home' | 'rss' | 'anyList';
/** 检查文章是否在给定上下文中隐藏 */
export function isHiddenFromList(post: BlogPost, context: PostListContext): boolean {
const ef = post.data.hideFrom;
if (!ef) return false;
if (ef === 'all') return true;
if (ef === 'home') return context === 'home';
if (ef === 'rss') return context === 'rss';
if (ef === 'home_rss') return context === 'home' || context === 'rss';
return false;
}
/** 过滤数组中隐藏的文章 */
export function filterHiddenPosts(posts: BlogPost[], context: PostListContext): BlogPost[] {
return posts.filter((p) => !isHiddenFromList(p, context));
}
在以下函数中应用过滤:
getHomePagePosts()— 遍历时isHiddenFromList(post, 'home')跳过getNonFeaturedPosts()— 返回前filterHiddenPosts(result, 'anyList')getPostsByCategory()— 返回前filterHiddenPosts(matched, 'anyList')
src/lib/content/categories.ts
getCategoryList() 计数前过滤不可见文章:
const visiblePosts = filterHiddenPosts(allBlogPosts, 'anyList');
RSS 页面过滤
src/pages/rss.xml.ts 和 src/pages/[lang]/rss.xml.ts:
const posts = filterHiddenPosts(allPosts, 'rss');
标签页过滤
src/pages/tags/[tag].astro 和 src/pages/tags/index.astro:
const visiblePosts = filterHiddenPosts(allPosts, 'anyList');
归档页过滤
src/pages/archives.astro:
const posts = filterHiddenPosts(allPosts, 'anyList');
相关文章过滤
src/components/post/PostFooter.astro:
const allPosts = filterHiddenPosts(allPostsRaw, 'anyList');
src/lib/content.ts
导出新增的函数和类型:
export { filterHiddenPosts, isHiddenFromList } from './content/posts';
export type { PostListContext } from './content/posts';
feat(i18n): 添加浏览器语言检测及切换提示功能
version: 4.0.0
实现浏览器语言检测功能,当检测到用户浏览器语言与当前页面语言不一致时,显示切换提示弹窗。弹窗提供切换至浏览器语言的选项和不再提醒功能。
src/store/language-detect.ts
新增 Nanostores 状态管理文件:
/**
* Language Detection State Management
*
* Detects browser language and compares with current site locale.
* Shows a popup suggesting the user switch to their preferred language.
* Persists dismissal in localStorage.
*/
import { atom } from 'nanostores';
import { defaultLocale, isI18nEnabled, localeList } from '@/i18n/config';
const STORAGE_KEY = 'language-detect-dismissed';
/** Whether the language suggestion popup is visible */
export const languagePopupOpen = atom<boolean>(false);
/** The target locale code to suggest switching to */
export const suggestedLocale = atom<string>('');
/** The display label for the suggested language */
export const suggestedLabel = atom<string>('');
/**
* Map a browser language code (e.g. "en-US", "zh-CN") to a site locale code.
* Returns null if the browser language doesn't match any supported locale.
*/
function mapBrowserLang(lang: string): string | null {
const primary = lang.split('-')[0].toLowerCase();
if (localeList.includes(primary)) return primary;
// Full match (e.g. "zh-CN" → "zh" is already handled above, "zh-TW" → "zh")
if (primary === 'zh' && localeList.includes('zh')) return 'zh';
if (primary === 'ja' && localeList.includes('ja')) return 'ja';
if (primary === 'en' && localeList.includes('en')) return 'en';
return null;
}
/**
* Get a human-readable label for a locale code in the locale's own language.
*/
function getLocaleSelfLabel(code: string): string {
const labels: Record<string, string> = {
zh: '中文',
en: 'English',
ja: '日本語',
};
return labels[code] ?? code;
}
/**
* Initialize language detection.
* Compares browser language with current site locale and opens popup if they differ.
* Does nothing if i18n is disabled or the user has dismissed before.
*/
export function initLanguageDetect(): void {
if (typeof window === 'undefined') return;
if (!isI18nEnabled) return;
// Check if user has dismissed
try {
if (localStorage.getItem(STORAGE_KEY) === 'true') return;
} catch {
// localStorage unavailable, skip
return;
}
const browserLang = navigator.language || (navigator as { userLanguage?: string }).userLanguage || '';
const targetLocale = mapBrowserLang(browserLang);
if (!targetLocale) return;
// Get current locale from URL
const pathname = window.location.pathname;
const segments = pathname.split('/').filter(Boolean);
const firstSegment = segments[0];
const currentLocale = firstSegment && localeList.includes(firstSegment) ? firstSegment : defaultLocale;
if (targetLocale === currentLocale) return;
suggestedLocale.set(targetLocale);
suggestedLabel.set(getLocaleSelfLabel(targetLocale));
languagePopupOpen.set(true);
}
/**
* Dismiss the popup permanently (save to localStorage)
*/
export function dismissLanguagePopup(): void {
languagePopupOpen.set(false);
try {
localStorage.setItem(STORAGE_KEY, 'true');
} catch {
// localStorage unavailable
}
}
/**
* Close the popup without dismissing permanently
*/
export function closeLanguagePopup(): void {
languagePopupOpen.set(false);
}
核心逻辑:
mapBrowserLang()— 将navigator.language(如zh-CN)映射到站点 locale(zh)initLanguageDetect()— 检测三条件:i18n 已启用 / 未被永久关闭 / 浏览器语言与当前 locale 不同dismissLanguagePopup()— 写入localStorage的language-detect-dismissed键,永久不再提醒closeLanguagePopup()— 仅关闭弹窗,下次页面加载会重新检测
src/components/language/LanguageDetectPopup.tsx
新增 React 弹窗组件:
/**
* LanguageDetectPopup Component
*
* Detects browser language and shows a popup suggesting the user
* switch to their preferred language. Styled to match the announcement popup.
*
* Popup text is displayed in the user's browser language first,
* falling back to the current page language.
*/
import { animation, zIndex } from '@constants/design-tokens';
import { Icon } from '@iconify/react';
import { useStore } from '@nanostores/react';
import {
closeLanguagePopup,
dismissLanguagePopup,
languagePopupOpen,
suggestedLabel,
suggestedLocale,
} from '@store/language-detect';
import { $locale } from '@store/locale';
import { AnimatePresence, motion } from 'motion/react';
import { getAlternateUrl, t as translate } from '@/i18n';
import type { TranslationKey } from '@/i18n/types';
export default function LanguageDetectPopup() {
const isOpen = useStore(languagePopupOpen);
const targetLocale = useStore(suggestedLocale);
const label = useStore(suggestedLabel);
const pageLocale = useStore($locale);
const targetUrl = typeof window !== 'undefined' ? getAlternateUrl(window.location.pathname, targetLocale) : '/';
/** Translate using browser language first, fall back to page language */
const t = (key: TranslationKey, params?: Record<string, string>) => {
const result = translate(targetLocale, key, params);
if (result === key) {
return translate(pageLocale, key, params);
}
return result;
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
style={{ zIndex: zIndex.modalBackdrop }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={closeLanguagePopup}
/>
{/* Popup */}
<motion.div
className="fixed inset-x-3 top-1/2 mx-auto max-w-md overflow-hidden rounded-xl bg-card shadow-2xl md:inset-x-4 md:rounded-2xl"
style={{ zIndex: zIndex.modal }}
initial={{ opacity: 0, y: '-45%', scale: 0.95 }}
animate={{ opacity: 1, y: '-50%', scale: 1 }}
exit={{ opacity: 0, y: '-45%', scale: 0.95 }}
transition={animation.spring.default}
>
{/* Header */}
<div className="flex items-center justify-between border-border border-b bg-linear-to-r from-primary/5 to-transparent p-3 md:p-4">
<div className="flex items-center gap-2 md:gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 md:h-10 md:w-10 md:rounded-xl">
<Icon icon="ri:translate-2" className="h-4 w-4 text-primary md:h-5 md:w-5" />
</div>
<h3 className="font-semibold text-sm md:text-base">{t('languageDetect.title')}</h3>
</div>
<button
onClick={closeLanguagePopup}
className="rounded-md p-1.5 transition-colors hover:bg-black/5 md:rounded-lg md:p-2 dark:hover:bg-white/10"
aria-label={t('common.close')}
type="button"
>
<Icon icon="ri:close-line" className="h-4 w-4 md:h-5 md:w-5" />
</button>
</div>
{/* Content */}
<div className="p-4 md:p-6">
<p className="text-muted-foreground text-sm leading-relaxed md:text-base">
{t('languageDetect.message', { lang: label })}
</p>
<div className="mt-5 flex items-center gap-3 md:mt-6">
<a
href={targetUrl}
onClick={closeLanguagePopup}
className="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-primary px-4 py-2.5 font-medium text-primary-foreground text-sm transition-all hover:opacity-90 md:rounded-xl md:px-5 md:py-3 md:text-base"
>
<Icon icon="ri:arrow-right-line" className="h-4 w-4 md:h-5 md:w-5" />
{t('languageDetect.switch', { lang: label })}
</a>
<button
onClick={dismissLanguagePopup}
className="rounded-lg px-4 py-2.5 text-muted-foreground text-sm transition-colors hover:bg-muted md:rounded-xl md:px-5 md:py-3 md:text-base"
type="button"
>
{t('languageDetect.dismiss')}
</button>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
关键设计:
- 翻译文本优先使用浏览器语言显示,若缺失则回退到页面语言
- 遮罩层点击关闭(不永久),弹窗内"不再提醒"按钮才写入
localStorage - 使用
motion/react的AnimatePresence实现进入/退出动画
src/components/language/LanguageDetectProvider.astro
新增 Astro 桥接组件:
---
import LanguageDetectPopup from './LanguageDetectPopup';
/**
* LanguageDetectProvider Component
*
* Initializes browser language detection and mounts the React popup component.
* Should be placed in Layout.astro alongside other global components.
*/
---
<LanguageDetectPopup client:load />
<script>
import { initLanguageDetect } from '@store/language-detect';
function init() {
initLanguageDetect();
}
if (document.readyState !== 'loading') {
init();
}
document.addEventListener('astro:page-load', init);
</script>
- 通过
document.readyState判断,避免 DOM 未就绪时执行 - 监听
astro:page-load事件确保 SPA 导航后重新检测
src/layouts/Layout.astro
在全局组件区域引入 LanguageDetectProvider:
import LanguageDetectProvider from '@components/language/LanguageDetectProvider.astro';
<ImageLightbox client:idle />
<Toaster client:idle />
<AnnouncementProvider />
<LanguageDetectProvider />
国际化
src/i18n/translations/{zh,en,ja}.ts 各添加 4 个翻译键:
| 键 | zh | en | ja |
|---|---|---|---|
languageDetect.title | 语言切换 | Language Switch | 言語切替 |
languageDetect.message | 检测到您的浏览器语言是{lang},是否切换到{lang}版? | Your browser language is {lang}. Switch to {lang}? | お使いのブラウザ言語は{lang}です。{lang}版に切り替えますか? |
languageDetect.switch | 切换到{lang} | Switch to {lang} | {lang}に切り替える |
languageDetect.dismiss | 不再提醒 | Don't show again | 今後表示しない |
zh.ts 代码块:
// ── Language Detect ─────────────────────────────────────────
'languageDetect.title': '语言切换',
'languageDetect.message': '检测到您的浏览器语言是{lang},是否切换到{lang}版?',
'languageDetect.switch': '切换到{lang}',
'languageDetect.dismiss': '不再提醒',
mdx 支持
目前不完善,部分主题语法不支持,构建时还会出现以下警告
14:34:50 [200] /post/test 1626ms
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.
- 安装
@astrojs/mdx插件
pnpm install @astrojs/mdx
- 配置
astro.config.mjs启用 MDX 支持
// astro.config.mjs
import mdx from '@astrojs/mdx';
// ...
integrations: [
react(),
...(mdxSupport ? [mdx()] : []),
// ...
],
移除 @yeskunall/astro-umami
pnpm uninstall @yeskunall/astro-umami
在 astro.config.mjs 中移除 @yeskunall/astro-umami 插件
// ...
// import umami from '@yeskunall/astro-umami';
// ...
// // Bundle analysis mode: ANALYZE=true pnpm build
// // Use loadEnv to read .env file (astro.config.mjs runs before Vite loads .env)
// const { ANALYZE } = loadEnv(process.env.NODE_ENV || 'production', process.cwd(), '');
// const isAnalyze = ANALYZE === 'true';
// // Get Umami analytics config from YAML
// const umamiConfig = yamlConfig.analytics?.umami;
// const umamiEnabled = umamiConfig?.enabled ?? false;
// const umamiId = umamiConfig?.id;
// // Normalize endpoint URL to remove trailing slashes
// const umamiEndpoint = normalizeUrl(umamiConfig?.endpoint);
// ...
// Umami analytics - configured via config/site.yaml
// ...(umamiEnabled && umamiId
// ? [
// umami({
// id: umamiId,
// endpointUrl: umamiEndpoint,
// hostUrl: umamiEndpoint,
// }),
// ]
// : []),
// ...
喜欢的话,留下你的评论吧~