225 lines
9.1 KiB
Plaintext
225 lines
9.1 KiB
Plaintext
---
|
|
import '../styles/global.css';
|
|
import UserStatus from '../components/UserStatus.vue';
|
|
|
|
interface Props {
|
|
title: string;
|
|
description?: string;
|
|
image?: string;
|
|
canonicalURL?: string;
|
|
}
|
|
|
|
const {
|
|
title,
|
|
description = 'NovaBlog - 一个极简、高效的程序员博客系统',
|
|
image = '/og-default.png',
|
|
canonicalURL = Astro.url.href,
|
|
} = Astro.props;
|
|
|
|
const { site } = Astro;
|
|
const socialImageURL = image.startsWith('http') ? image : new URL(image, site).href;
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="generator" content={Astro.generator} />
|
|
|
|
<!-- SEO Meta Tags -->
|
|
<title>{title}</title>
|
|
<meta name="description" content={description} />
|
|
<link rel="canonical" href={canonicalURL} />
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content={canonicalURL} />
|
|
<meta property="og:title" content={title} />
|
|
<meta property="og:description" content={description} />
|
|
<meta property="og:image" content={socialImageURL} />
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content={title} />
|
|
<meta name="twitter:description" content={description} />
|
|
<meta name="twitter:image" content={socialImageURL} />
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
|
|
<!-- Preload Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Noto+Serif+SC:wght@400;600&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
|
|
<!-- Dark Mode Script (防止闪烁) -->
|
|
<script is:inline>
|
|
const theme = (() => {
|
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
|
return localStorage.getItem('theme');
|
|
}
|
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return 'dark';
|
|
}
|
|
return 'light';
|
|
})();
|
|
|
|
if (theme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="min-h-screen flex flex-col bg-background text-foreground">
|
|
<!-- Header -->
|
|
<header class="sticky top-0 z-50 glass border-b border-border">
|
|
<nav class="content-width h-16 flex items-center justify-between">
|
|
<!-- Logo -->
|
|
<a href="/" class="flex items-center gap-2 text-xl font-bold hover:text-primary-500 transition-colors">
|
|
<svg class="w-8 h-8" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="32" height="32" rx="8" fill="url(#logo-gradient)" />
|
|
<path
|
|
d="M8 12L16 8L24 12V20L16 24L8 20V12Z"
|
|
stroke="white"
|
|
stroke-width="2"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<circle cx="16" cy="16" r="3" fill="white" />
|
|
<defs>
|
|
<linearGradient id="logo-gradient" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
|
<stop stop-color="#0ea5e9" />
|
|
<stop offset="1" stop-color="#8b5cf6" />
|
|
</linearGradient>
|
|
</defs>
|
|
</svg>
|
|
<span class="gradient-text">NovaBlog</span>
|
|
</a>
|
|
|
|
<!-- Navigation Links -->
|
|
<div class="hidden md:flex items-center gap-6">
|
|
<a href="/" class="text-foreground/70 hover:text-foreground transition-colors">首页</a>
|
|
<a href="/blog" class="text-foreground/70 hover:text-foreground transition-colors">博客</a>
|
|
<a href="/categories" class="text-foreground/70 hover:text-foreground transition-colors">分类</a>
|
|
<a href="/tags" class="text-foreground/70 hover:text-foreground transition-colors">标签</a>
|
|
<a href="/about" class="text-foreground/70 hover:text-foreground transition-colors">关于</a>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center gap-3">
|
|
<!-- User Status (登录状态) -->
|
|
<UserStatus client:load />
|
|
|
|
<!-- Theme Toggle -->
|
|
<button
|
|
id="theme-toggle"
|
|
class="btn-ghost p-2 rounded-lg"
|
|
aria-label="切换主题"
|
|
>
|
|
<svg class="w-5 h-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
|
/>
|
|
</svg>
|
|
<svg class="w-5 h-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Mobile Menu Toggle -->
|
|
<button
|
|
id="mobile-menu-toggle"
|
|
class="btn-ghost p-2 rounded-lg md:hidden"
|
|
aria-label="打开菜单"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Mobile Menu -->
|
|
<div id="mobile-menu" class="hidden md:hidden border-t border-border">
|
|
<div class="content-width py-4 flex flex-col gap-3">
|
|
<a href="/" class="text-foreground/70 hover:text-foreground transition-colors py-2">首页</a>
|
|
<a href="/blog" class="text-foreground/70 hover:text-foreground transition-colors py-2">博客</a>
|
|
<a href="/categories" class="text-foreground/70 hover:text-foreground transition-colors py-2">分类</a>
|
|
<a href="/tags" class="text-foreground/70 hover:text-foreground transition-colors py-2">标签</a>
|
|
<a href="/about" class="text-foreground/70 hover:text-foreground transition-colors py-2">关于</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-1">
|
|
<slot />
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="border-t border-border bg-muted/30">
|
|
<div class="content-width py-12">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
<!-- About -->
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-4">NovaBlog</h3>
|
|
<p class="text-foreground/60 text-sm leading-relaxed">
|
|
一个极简、高效的程序员博客系统。支持 MDX、Typst 学术排版,静态渲染 + 轻量级微服务架构。
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Links -->
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-4">快速链接</h3>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><a href="/blog" class="text-foreground/60 hover:text-primary-500 transition-colors">全部文章</a></li>
|
|
<li><a href="/tags" class="text-foreground/60 hover:text-primary-500 transition-colors">标签分类</a></li>
|
|
<li><a href="/about" class="text-foreground/60 hover:text-primary-500 transition-colors">关于我</a></li>
|
|
<li><a href="/rss.xml" class="text-foreground/60 hover:text-primary-500 transition-colors">RSS 订阅</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Tech Stack -->
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-4">技术栈</h3>
|
|
<ul class="space-y-2 text-sm text-foreground/60">
|
|
<li>前端: Astro + Vue 3 + Tailwind CSS</li>
|
|
<li>后端: Go + Gin + SQLite</li>
|
|
<li>部署: Docker + Nginx</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Copyright -->
|
|
<div class="mt-8 pt-8 border-t border-border text-center text-sm text-foreground/40">
|
|
<p>© {new Date().getFullYear()} NovaBlog. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Theme Toggle Script -->
|
|
<script>
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
// Theme toggle
|
|
themeToggle?.addEventListener('click', () => {
|
|
const isDark = document.documentElement.classList.toggle('dark');
|
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
});
|
|
|
|
// Mobile menu toggle
|
|
mobileMenuToggle?.addEventListener('click', () => {
|
|
mobileMenu?.classList.toggle('hidden');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |