Add sitemap and SEO metadata
This commit is contained in:
parent
21551e3a97
commit
b4185eb668
14 changed files with 393 additions and 27 deletions
|
|
@ -3,6 +3,7 @@ const links = [
|
||||||
{ href: '/', label: '首页' },
|
{ href: '/', label: '首页' },
|
||||||
{ href: '/archive/', label: '归档' },
|
{ href: '/archive/', label: '归档' },
|
||||||
{ href: '/tags/', label: '标签' },
|
{ href: '/tags/', label: '标签' },
|
||||||
|
{ href: '/site-map/', label: '站点地图' },
|
||||||
{ href: '/rss.xml', label: 'RSS' }
|
{ href: '/rss.xml', label: 'RSS' }
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
|
||||||
57
frontend/site/src/components/seo/SeoHead.astro
Normal file
57
frontend/site/src/components/seo/SeoHead.astro
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
import { site } from '../../lib/siteConfig';
|
||||||
|
import { absoluteUrl } from '../../lib/seo';
|
||||||
|
|
||||||
|
type JsonLd = Record<string, unknown> | Record<string, unknown>[];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
path?: string;
|
||||||
|
type?: 'website' | 'article';
|
||||||
|
publishedTime?: string;
|
||||||
|
modifiedTime?: string;
|
||||||
|
tags?: string[];
|
||||||
|
jsonLd?: JsonLd;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = site.title,
|
||||||
|
description = site.description,
|
||||||
|
path = '/',
|
||||||
|
type = 'website',
|
||||||
|
publishedTime,
|
||||||
|
modifiedTime,
|
||||||
|
tags = [],
|
||||||
|
jsonLd
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const pageTitle = title === site.title ? site.title : `${title} | ${site.title}`;
|
||||||
|
const canonicalUrl = absoluteUrl(path);
|
||||||
|
const rssUrl = absoluteUrl('/rss.xml');
|
||||||
|
---
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title={`${site.title} RSS`} href={rssUrl} />
|
||||||
|
<meta property="og:site_name" content={site.title} />
|
||||||
|
<meta property="og:title" content={pageTitle} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:type" content={type} />
|
||||||
|
<meta property="og:url" content={canonicalUrl} />
|
||||||
|
<meta property="og:locale" content={site.language.replace('-', '_')} />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content={pageTitle} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
|
||||||
|
{modifiedTime && <meta property="article:modified_time" content={modifiedTime} />}
|
||||||
|
{tags.map((tag) => <meta property="article:tag" content={tag} />)}
|
||||||
|
{
|
||||||
|
jsonLd && (
|
||||||
|
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
21
frontend/site/src/lib/seo.ts
Normal file
21
frontend/site/src/lib/seo.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { site } from './siteConfig';
|
||||||
|
import type { Post } from './posts';
|
||||||
|
|
||||||
|
export function absoluteUrl(path = '/'): string {
|
||||||
|
const base = site.base_url.replace(/\/+$/, '');
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
return `${base}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function postUpdatedAt(post: Pick<Post, 'updated_at' | 'published_at' | 'created_at' | 'date'>): string {
|
||||||
|
return post.updated_at ?? post.published_at ?? post.created_at ?? post.date ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xmlEscape(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
@ -47,14 +47,26 @@ function loadSiteConfig(): SiteConfig {
|
||||||
try {
|
try {
|
||||||
const file = readFileSync(resolve(siteRoot, 'config/site.yaml'), 'utf8');
|
const file = readFileSync(resolve(siteRoot, 'config/site.yaml'), 'utf8');
|
||||||
const parsed = YAML.parse(file) ?? {};
|
const parsed = YAML.parse(file) ?? {};
|
||||||
|
const siteOverrides = {
|
||||||
|
...parsed.site,
|
||||||
|
...(process.env.SITE_URL ? { base_url: process.env.SITE_URL } : {}),
|
||||||
|
...(process.env.PUBLIC_SITE_URL ? { base_url: process.env.PUBLIC_SITE_URL } : {})
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
site: { ...defaults.site, ...parsed.site },
|
site: { ...defaults.site, ...siteOverrides },
|
||||||
content: { ...defaults.content, ...parsed.content },
|
content: { ...defaults.content, ...parsed.content },
|
||||||
build: { ...defaults.build, ...parsed.build }
|
build: { ...defaults.build, ...parsed.build }
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return {
|
||||||
|
...defaults,
|
||||||
|
site: {
|
||||||
|
...defaults.site,
|
||||||
|
...(process.env.SITE_URL ? { base_url: process.env.SITE_URL } : {}),
|
||||||
|
...(process.env.PUBLIC_SITE_URL ? { base_url: process.env.PUBLIC_SITE_URL } : {})
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,20 @@
|
||||||
import '../../styles/normalize.css';
|
import '../../styles/normalize.css';
|
||||||
import DefaultArchive from '../../components/DefaultArchive.astro';
|
import DefaultArchive from '../../components/DefaultArchive.astro';
|
||||||
import YarArchive from '../../components/themes/yar/YarArchive.astro';
|
import YarArchive from '../../components/themes/yar/YarArchive.astro';
|
||||||
|
import SeoHead from '../../components/seo/SeoHead.astro';
|
||||||
import { site } from '../../lib/siteConfig';
|
import { site } from '../../lib/siteConfig';
|
||||||
import { getArchiveYears } from '../../lib/posts';
|
import { getArchiveYears } from '../../lib/posts';
|
||||||
|
|
||||||
const archiveYears = getArchiveYears();
|
const archiveYears = getArchiveYears();
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const ArchiveView = theme === 'yar' ? YarArchive : DefaultArchive;
|
const ArchiveView = theme === 'yar' ? YarArchive : DefaultArchive;
|
||||||
|
const description = `${site.title} 的文章归档`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead title="归档" description={description} path="/archive/" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Archive | {site.title}</title>
|
|
||||||
<meta name="description" content={`Archive of ${site.title}`} />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ArchiveView archiveYears={archiveYears} />
|
<ArchiveView archiveYears={archiveYears} />
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,30 @@
|
||||||
import '../styles/normalize.css';
|
import '../styles/normalize.css';
|
||||||
import DefaultHome from '../components/DefaultHome.astro';
|
import DefaultHome from '../components/DefaultHome.astro';
|
||||||
import YarHome from '../components/themes/yar/YarHome.astro';
|
import YarHome from '../components/themes/yar/YarHome.astro';
|
||||||
|
import SeoHead from '../components/seo/SeoHead.astro';
|
||||||
import { site } from '../lib/siteConfig';
|
import { site } from '../lib/siteConfig';
|
||||||
import { getPaginatedPosts, getPublishedPosts, getTotalPages } from '../lib/posts';
|
import { getPaginatedPosts, getPublishedPosts, getTotalPages } from '../lib/posts';
|
||||||
|
import { absoluteUrl } from '../lib/seo';
|
||||||
|
|
||||||
const allPosts = getPublishedPosts();
|
const allPosts = getPublishedPosts();
|
||||||
const posts = getPaginatedPosts(allPosts, 1);
|
const posts = getPaginatedPosts(allPosts, 1);
|
||||||
const totalPages = getTotalPages(allPosts);
|
const totalPages = getTotalPages(allPosts);
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const Home = theme === 'yar' ? YarHome : DefaultHome;
|
const Home = theme === 'yar' ? YarHome : DefaultHome;
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Blog',
|
||||||
|
name: site.title,
|
||||||
|
description: site.description,
|
||||||
|
url: absoluteUrl('/'),
|
||||||
|
inLanguage: site.language
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead title={site.title} description={site.description} path="/" jsonLd={jsonLd} />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>{site.title}</title>
|
|
||||||
<meta name="description" content={site.description} />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Home posts={posts} currentPage={1} totalPages={totalPages} />
|
<Home posts={posts} currentPage={1} totalPages={totalPages} />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import '../../styles/normalize.css';
|
import '../../styles/normalize.css';
|
||||||
import DefaultHome from '../../components/DefaultHome.astro';
|
import DefaultHome from '../../components/DefaultHome.astro';
|
||||||
import YarHome from '../../components/themes/yar/YarHome.astro';
|
import YarHome from '../../components/themes/yar/YarHome.astro';
|
||||||
|
import SeoHead from '../../components/seo/SeoHead.astro';
|
||||||
import { site } from '../../lib/siteConfig';
|
import { site } from '../../lib/siteConfig';
|
||||||
import { getPaginatedPosts, getPublishedPosts, getTotalPages } from '../../lib/posts';
|
import { getPaginatedPosts, getPublishedPosts, getTotalPages } from '../../lib/posts';
|
||||||
|
|
||||||
|
|
@ -24,15 +25,14 @@ const posts = getPaginatedPosts(allPosts, page);
|
||||||
const totalPages = getTotalPages(allPosts);
|
const totalPages = getTotalPages(allPosts);
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const Home = theme === 'yar' ? YarHome : DefaultHome;
|
const Home = theme === 'yar' ? YarHome : DefaultHome;
|
||||||
|
const title = `第 ${page} 页`;
|
||||||
|
const description = `${site.title} 的第 ${page} 页文章列表`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead title={title} description={description} path={`/page/${page}/`} />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>第 {page} 页 | {site.title}</title>
|
|
||||||
<meta name="description" content={site.description} />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Home posts={posts} currentPage={page} totalPages={totalPages} />
|
<Home posts={posts} currentPage={page} totalPages={totalPages} />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
import '../../styles/normalize.css';
|
import '../../styles/normalize.css';
|
||||||
import DefaultPost from '../../components/DefaultPost.astro';
|
import DefaultPost from '../../components/DefaultPost.astro';
|
||||||
import YarPost from '../../components/themes/yar/YarPost.astro';
|
import YarPost from '../../components/themes/yar/YarPost.astro';
|
||||||
|
import SeoHead from '../../components/seo/SeoHead.astro';
|
||||||
import { site } from '../../lib/siteConfig';
|
import { site } from '../../lib/siteConfig';
|
||||||
|
import { absoluteUrl } from '../../lib/seo';
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
const modules = import.meta.glob('../../../../../content/posts/*.md', { eager: true });
|
const modules = import.meta.glob('../../../../../content/posts/*.md', { eager: true });
|
||||||
|
|
@ -18,15 +20,46 @@ const { post } = Astro.props;
|
||||||
const title = post.frontmatter.title;
|
const title = post.frontmatter.title;
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const PostView = theme === 'yar' ? YarPost : DefaultPost;
|
const PostView = theme === 'yar' ? YarPost : DefaultPost;
|
||||||
|
const description = post.frontmatter.summary ?? site.description;
|
||||||
|
const publishedTime = post.frontmatter.published_at ?? post.frontmatter.created_at;
|
||||||
|
const modifiedTime = post.frontmatter.updated_at ?? publishedTime;
|
||||||
|
const postUrl = `/posts/${post.frontmatter.slug}/`;
|
||||||
|
const tags = post.frontmatter.tags ?? [];
|
||||||
|
const jsonLd = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
headline: title,
|
||||||
|
description,
|
||||||
|
datePublished: publishedTime,
|
||||||
|
dateModified: modifiedTime,
|
||||||
|
url: absoluteUrl(postUrl),
|
||||||
|
mainEntityOfPage: absoluteUrl(postUrl),
|
||||||
|
inLanguage: site.language,
|
||||||
|
author: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: site.title
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: site.title
|
||||||
|
},
|
||||||
|
...(tags.length > 0 ? { keywords: tags.join(', ') } : {})
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead
|
||||||
<meta name="viewport" content="width=device-width" />
|
title={title}
|
||||||
<title>{title} | {site.title}</title>
|
description={description}
|
||||||
{post.frontmatter.summary && <meta name="description" content={post.frontmatter.summary} />}
|
path={postUrl}
|
||||||
|
type="article"
|
||||||
|
publishedTime={publishedTime}
|
||||||
|
modifiedTime={modifiedTime}
|
||||||
|
tags={tags}
|
||||||
|
jsonLd={jsonLd}
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<PostView post={post} />
|
<PostView post={post} />
|
||||||
|
|
|
||||||
14
frontend/site/src/pages/robots.txt.ts
Normal file
14
frontend/site/src/pages/robots.txt.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { absoluteUrl } from '../lib/seo';
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return new Response(`User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: ${absoluteUrl('/sitemap.xml')}
|
||||||
|
`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import rss from '@astrojs/rss';
|
import rss from '@astrojs/rss';
|
||||||
import { getPublishedPosts } from '../lib/posts';
|
import { getPublishedPosts } from '../lib/posts';
|
||||||
import { site } from '../lib/siteConfig';
|
import { site } from '../lib/siteConfig';
|
||||||
|
import { absoluteUrl } from '../lib/seo';
|
||||||
|
|
||||||
export function GET() {
|
export function GET() {
|
||||||
return rss({
|
return rss({
|
||||||
|
|
@ -10,7 +11,7 @@ export function GET() {
|
||||||
items: getPublishedPosts().map((post) => ({
|
items: getPublishedPosts().map((post) => ({
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.summary ?? '',
|
description: post.summary ?? '',
|
||||||
link: post.url,
|
link: absoluteUrl(post.url),
|
||||||
pubDate: post.date ? new Date(post.date) : undefined,
|
pubDate: post.date ? new Date(post.date) : undefined,
|
||||||
categories: post.tags
|
categories: post.tags
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
150
frontend/site/src/pages/site-map.astro
Normal file
150
frontend/site/src/pages/site-map.astro
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
---
|
||||||
|
import '../styles/normalize.css';
|
||||||
|
import SiteNav from '../components/SiteNav.astro';
|
||||||
|
import SeoHead from '../components/seo/SeoHead.astro';
|
||||||
|
import { site } from '../lib/siteConfig';
|
||||||
|
import { getArchiveYears, getTagSummaries } from '../lib/posts';
|
||||||
|
|
||||||
|
const archiveYears = getArchiveYears();
|
||||||
|
const tags = getTagSummaries();
|
||||||
|
const description = `${site.title} 的文章、归档和标签站点地图`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={site.language}>
|
||||||
|
<head>
|
||||||
|
<SeoHead title="站点地图" description={description} path="/site-map/" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="site-map">
|
||||||
|
<SiteNav />
|
||||||
|
<header>
|
||||||
|
<h1>站点地图</h1>
|
||||||
|
<p>{description}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section aria-labelledby="main-pages">
|
||||||
|
<h2 id="main-pages">主要页面</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">首页</a></li>
|
||||||
|
<li><a href="/archive/">归档</a></li>
|
||||||
|
<li><a href="/tags/">标签</a></li>
|
||||||
|
<li><a href="/rss.xml">RSS</a></li>
|
||||||
|
<li><a href="/sitemap.xml">XML Sitemap</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<section aria-labelledby="tag-pages">
|
||||||
|
<h2 id="tag-pages">标签</h2>
|
||||||
|
<ul class="columns">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<li><a href={`/tags/${tag.slug}/`}>{tag.name}</a> <span>({tag.count})</span></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section aria-labelledby="post-pages">
|
||||||
|
<h2 id="post-pages">文章</h2>
|
||||||
|
{archiveYears.map((year) => (
|
||||||
|
<div class="year-group">
|
||||||
|
<h3>{year.year}</h3>
|
||||||
|
<ul>
|
||||||
|
{year.posts.map((post) => (
|
||||||
|
<li>
|
||||||
|
<a href={post.url}>{post.title}</a>
|
||||||
|
{post.date && <time datetime={post.date}>{new Date(post.date).toLocaleDateString(site.language)}</time>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.site-map {
|
||||||
|
width: min(860px, calc(100% - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 0 80px;
|
||||||
|
color: #20201d;
|
||||||
|
font-family:
|
||||||
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-map :global(.site-nav) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 18px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
color: #625a50;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 1px solid #d8d1c3;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 32px 0 12px;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 20px 0 8px;
|
||||||
|
color: #625a50;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #5b554d;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
time,
|
||||||
|
span {
|
||||||
|
color: #766e63;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-group + .year-group {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
frontend/site/src/pages/sitemap.xml.ts
Normal file
73
frontend/site/src/pages/sitemap.xml.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { getPublishedPosts, getTagSummaries, getTotalPages } from '../lib/posts';
|
||||||
|
import { absoluteUrl, postUpdatedAt, xmlEscape } from '../lib/seo';
|
||||||
|
import { site } from '../lib/siteConfig';
|
||||||
|
|
||||||
|
type SitemapEntry = {
|
||||||
|
path: string;
|
||||||
|
lastmod?: string;
|
||||||
|
changefreq?: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||||
|
priority?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatLastmod(value?: string): string | undefined {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
const posts = getPublishedPosts();
|
||||||
|
const latestPostDate = posts.map(postUpdatedAt).sort().at(-1);
|
||||||
|
const entries: SitemapEntry[] = [
|
||||||
|
{ path: '/', lastmod: latestPostDate, changefreq: 'daily', priority: '1.0' },
|
||||||
|
{ path: '/archive/', lastmod: latestPostDate, changefreq: 'weekly', priority: '0.7' },
|
||||||
|
{ path: '/tags/', lastmod: latestPostDate, changefreq: 'weekly', priority: '0.6' },
|
||||||
|
{ path: '/site-map/', lastmod: latestPostDate, changefreq: 'weekly', priority: '0.5' },
|
||||||
|
...Array.from({ length: Math.max(0, getTotalPages(posts) - 1) }, (_, index) => ({
|
||||||
|
path: `/page/${index + 2}/`,
|
||||||
|
lastmod: latestPostDate,
|
||||||
|
changefreq: 'weekly' as const,
|
||||||
|
priority: '0.6'
|
||||||
|
})),
|
||||||
|
...getTagSummaries().map((tag) => ({
|
||||||
|
path: `/tags/${tag.slug}/`,
|
||||||
|
lastmod: latestPostDate,
|
||||||
|
changefreq: 'weekly' as const,
|
||||||
|
priority: '0.5'
|
||||||
|
})),
|
||||||
|
...posts.map((post) => ({
|
||||||
|
path: post.url,
|
||||||
|
lastmod: postUpdatedAt(post),
|
||||||
|
changefreq: 'monthly' as const,
|
||||||
|
priority: '0.8'
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
${entries
|
||||||
|
.map((entry) => {
|
||||||
|
const lastmod = formatLastmod(entry.lastmod);
|
||||||
|
return ` <url>
|
||||||
|
<loc>${xmlEscape(absoluteUrl(entry.path))}</loc>${lastmod ? `\n <lastmod>${lastmod}</lastmod>` : ''}
|
||||||
|
<changefreq>${entry.changefreq ?? 'weekly'}</changefreq>
|
||||||
|
<priority>${entry.priority ?? '0.5'}</priority>
|
||||||
|
</url>`;
|
||||||
|
})
|
||||||
|
.join('\n')}
|
||||||
|
</urlset>`;
|
||||||
|
|
||||||
|
return new Response(body, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import '../../styles/normalize.css';
|
import '../../styles/normalize.css';
|
||||||
import DefaultTagPosts from '../../components/DefaultTagPosts.astro';
|
import DefaultTagPosts from '../../components/DefaultTagPosts.astro';
|
||||||
import YarTagPosts from '../../components/themes/yar/YarTagPosts.astro';
|
import YarTagPosts from '../../components/themes/yar/YarTagPosts.astro';
|
||||||
|
import SeoHead from '../../components/seo/SeoHead.astro';
|
||||||
import { site } from '../../lib/siteConfig';
|
import { site } from '../../lib/siteConfig';
|
||||||
import { getPostsByTag, getTagSummaries } from '../../lib/posts';
|
import { getPostsByTag, getTagSummaries } from '../../lib/posts';
|
||||||
|
|
||||||
|
|
@ -16,15 +17,13 @@ const { tag } = Astro.props;
|
||||||
const posts = getPostsByTag(tag.slug);
|
const posts = getPostsByTag(tag.slug);
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const TagPostsView = theme === 'yar' ? YarTagPosts : DefaultTagPosts;
|
const TagPostsView = theme === 'yar' ? YarTagPosts : DefaultTagPosts;
|
||||||
|
const description = `${site.title} 中带有「${tag.name}」标签的文章`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead title={tag.name} description={description} path={`/tags/${tag.slug}/`} />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>{tag.name} | {site.title}</title>
|
|
||||||
<meta name="description" content={`Posts tagged ${tag.name}`} />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<TagPostsView tag={tag} posts={posts} />
|
<TagPostsView tag={tag} posts={posts} />
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,20 @@
|
||||||
import '../../styles/normalize.css';
|
import '../../styles/normalize.css';
|
||||||
import DefaultTags from '../../components/DefaultTags.astro';
|
import DefaultTags from '../../components/DefaultTags.astro';
|
||||||
import YarTags from '../../components/themes/yar/YarTags.astro';
|
import YarTags from '../../components/themes/yar/YarTags.astro';
|
||||||
|
import SeoHead from '../../components/seo/SeoHead.astro';
|
||||||
import { site } from '../../lib/siteConfig';
|
import { site } from '../../lib/siteConfig';
|
||||||
import { getTagSummaries } from '../../lib/posts';
|
import { getTagSummaries } from '../../lib/posts';
|
||||||
|
|
||||||
const tags = getTagSummaries();
|
const tags = getTagSummaries();
|
||||||
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
const theme = site.theme?.trim().toLowerCase() ?? 'default';
|
||||||
const TagsView = theme === 'yar' ? YarTags : DefaultTags;
|
const TagsView = theme === 'yar' ? YarTags : DefaultTags;
|
||||||
|
const description = `${site.title} 的全部文章标签`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={site.language}>
|
<html lang={site.language}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<SeoHead title="标签" description={description} path="/tags/" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Tags | {site.title}</title>
|
|
||||||
<meta name="description" content={`Tags on ${site.title}`} />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<TagsView tags={tags} />
|
<TagsView tags={tags} />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue