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(), svelte(),
sitemap(), sitemap({
filter: (page) => !page.includes("/archive/"),
}),
], ],
markdown: { markdown: {
remarkPlugins: [ remarkPlugins: [
+21 -3
View File
@@ -30,6 +30,21 @@ interface Props {
let { title, banner, description, lang, setOGTypeArticle } = Astro.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 // 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 // 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 // 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 charset="UTF-8" />
<meta name="description" content={description || pageTitle}> <meta name="description" content={description || pageTitle}>
<meta name="author" content={profileConfig.name}> <meta name="author" content={profileConfig.name}>
<link rel="canonical" href={canonicalUrl}>
<meta property="og:site_name" content={siteConfig.title}> <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:title" content={pageTitle}>
<meta property="og:description" content={description || pageTitle}> <meta property="og:description" content={description || pageTitle}>
<meta property="og:image" content={ogImageUrl}>
{setOGTypeArticle ? ( {setOGTypeArticle ? (
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />
) : ( ) : (
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
)} )}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content={twitterCard}>
<meta property="twitter:url" content={Astro.url}> <meta property="twitter:url" content={canonicalUrl}>
<meta name="twitter:title" content={pageTitle}> <meta name="twitter:title" content={pageTitle}>
<meta name="twitter:description" content={description || pageTitle}> <meta name="twitter:description" content={description || pageTitle}>
<meta name="twitter:image" content={ogImageUrl}>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} /> <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 I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation"; import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro"; import MainGridLayout from "@layouts/MainGridLayout.astro";
import { siteConfig } from "@/config";
import { getSortedPostsList } from "../utils/content-utils"; import { getSortedPostsList } from "../utils/content-utils";
const sortedPostsList = await getSortedPostsList(); const sortedPostsList = await getSortedPostsList();
--- ---
<MainGridLayout title={i18n(I18nKey.archive)}> <MainGridLayout title={i18n(I18nKey.archive)} description={`${siteConfig.title}的全部文章归档,按时间浏览所有博客文章。`}>
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel> <ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
</MainGridLayout> </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="card-base p-6 md:p-8">
<div class="flex items-center gap-2 mb-6"> <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="flex items-center gap-4 mb-8 mt-4 px-2">
<div class="avatar"> <div class="avatar">
+13 -1
View File
@@ -28,6 +28,11 @@ const { Content, headings } = await entry.render();
const { remarkPluginFrontmatter } = 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 = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "BlogPosting", "@type": "BlogPosting",
@@ -40,10 +45,17 @@ const jsonLd = {
url: Astro.site, url: Astro.site,
}, },
datePublished: formatDateToYYYYMMDD(entry.data.published), 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 inLanguage: entry.data.lang
? entry.data.lang.replace("_", "-") ? entry.data.lang.replace("_", "-")
: siteConfig.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}> <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' import I18nKey from '@i18n/i18nKey'
--- ---
<MainGridLayout title={i18n(I18nKey.series)}> <MainGridLayout title={i18n(I18nKey.series)} description="按系列浏览博客文章,查看同一主题下的连载内容。">
<SeriesPanel></SeriesPanel> <SeriesPanel></SeriesPanel>
</MainGridLayout> </MainGridLayout>