SEO 优化:补全 canonical、og:image、JSON-LD 及页面描述

Layout.astro:
- 新增 <link rel="canonical">,og:url / twitter:url 统一使用规范链接
- 新增 og:image / twitter:image:文章封面为绝对或 public URL 时使用封面,
  否则回退站点 favicon-192 并将 twitter:card 从 summary_large_image
  降级为 summary,避免声明大图卡片却无图
- banner 判断改用 typeof 收窄,兼容 404 页传入 banner={false} 的情况

posts/[...slug].astro (JSON-LD BlogPosting):
- 新增 dateModified(读取 frontmatter 的 updated 字段)
- 新增 image(仅绝对 URL 封面)和 mainEntityOfPage
- 移除遗留的 TODO 注释

页面 meta description:
- archive / friends / expenses / series 四个页面补充独立描述,
  不再退化为用页面标题填充

astro.config.mjs:
- sitemap 增加 filter 排除 /archive/,与 robots.txt 的
  Disallow: /archive/ 保持一致,消除收录信号矛盾
This commit is contained in:
mckero committed 2026-07-26 07:56:47 +00:00
1 parent b35a071c6b
commit 6105577add
7 files changed
+42 -9

No files matched your search

+3 -1
View File
@@ -102,7 +102,9 @@ export default defineConfig({
}
}),
svelte(),
sitemap(),
sitemap({
filter: (page) => !page.includes("/archive/"),
}),
],
markdown: {
remarkPlugins: [
+21 -3
View File
@@ -30,6 +30,21 @@ interface Props {
let { title, banner, description, lang, setOGTypeArticle } = Astro.props;
// canonical URL
const canonicalUrl = new URL(Astro.url.pathname, Astro.site).href;
// social share image: use the page's own image if it's an absolute or public URL,
// otherwise fall back to the site favicon with a smaller "summary" card
let ogImage: string | undefined;
if (typeof banner === "string" && /^https?:\/\//.test(banner)) {
ogImage = banner;
} else if (typeof banner === "string" && banner.startsWith("/")) {
ogImage = new URL(url(banner), Astro.site).href;
}
const twitterCard = ogImage ? "summary_large_image" : "summary";
const ogImageUrl =
ogImage ?? new URL(url("/favicon/favicon-light-192.png"), Astro.site).href;
// apply a class to the body element to decide the height of the banner, only used for initial page load
// Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change
// so use Swup hooks instead to change the height immediately when a link is clicked
@@ -83,21 +98,24 @@ const bannerOffset =
<meta charset="UTF-8" />
<meta name="description" content={description || pageTitle}>
<meta name="author" content={profileConfig.name}>
<link rel="canonical" href={canonicalUrl}>
<meta property="og:site_name" content={siteConfig.title}>
<meta property="og:url" content={Astro.url}>
<meta property="og:url" content={canonicalUrl}>
<meta property="og:title" content={pageTitle}>
<meta property="og:description" content={description || pageTitle}>
<meta property="og:image" content={ogImageUrl}>
{setOGTypeArticle ? (
<meta property="og:type" content="article" />
) : (
<meta property="og:type" content="website" />
)}
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:url" content={Astro.url}>
<meta name="twitter:card" content={twitterCard}>
<meta property="twitter:url" content={canonicalUrl}>
<meta name="twitter:title" content={pageTitle}>
<meta name="twitter:description" content={description || pageTitle}>
<meta name="twitter:image" content={ogImageUrl}>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
+2 -1
View File
@@ -3,12 +3,13 @@ import ArchivePanel from "@components/ArchivePanel.svelte";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { siteConfig } from "@/config";
import { getSortedPostsList } from "../utils/content-utils";
const sortedPostsList = await getSortedPostsList();
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<MainGridLayout title={i18n(I18nKey.archive)} description={`${siteConfig.title}的全部文章归档,按时间浏览所有博客文章。`}>
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
</MainGridLayout>
+1 -1
View File
@@ -56,7 +56,7 @@ const historyData = [
];
---
<MainGridLayout title="运行成本">
<MainGridLayout title="运行成本" description="本博客的运行成本明细,包括域名、CDN 等固定支出记录。">
<div class="card-base p-6 md:p-8">
<div class="flex items-center gap-2 mb-6">
+1 -1
View File
@@ -66,7 +66,7 @@ const friends = [
];
---
<MainGridLayout title="友链">
<MainGridLayout title="友链" description="友情链接页面,收录了一些朋友的博客与网站。">
<!-- 头部 -->
<div class="flex items-center gap-4 mb-8 mt-4 px-2">
<div class="avatar">
+13 -1
View File
@@ -28,6 +28,11 @@ const { Content, headings } = await entry.render();
const { remarkPluginFrontmatter } = await entry.render();
const postUrl = new URL(getPostUrlBySlug(entry.slug), Astro.site).href;
const coverImage = /^https?:\/\//.test(entry.data.image)
? entry.data.image
: undefined;
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
@@ -40,10 +45,17 @@ const jsonLd = {
url: Astro.site,
},
datePublished: formatDateToYYYYMMDD(entry.data.published),
...(entry.data.updated && {
dateModified: formatDateToYYYYMMDD(entry.data.updated),
}),
...(coverImage && { image: coverImage }),
mainEntityOfPage: {
"@type": "WebPage",
"@id": postUrl,
},
inLanguage: entry.data.lang
? entry.data.lang.replace("_", "-")
: siteConfig.lang.replace("_", "-"),
// TODO include cover image here
};
---
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings} series={entry.data.series}>
+1 -1
View File
@@ -5,6 +5,6 @@ import { i18n } from '@i18n/translation'
import I18nKey from '@i18n/i18nKey'
---
<MainGridLayout title={i18n(I18nKey.series)}>
<MainGridLayout title={i18n(I18nKey.series)} description="按系列浏览博客文章,查看同一主题下的连载内容。">
<SeriesPanel></SeriesPanel>
</MainGridLayout>