2151 字
11 分钟
Fuwari改造计划之赞助页面

成果展示#

修改过程#

src\i18n\i18nKey.ts
32 collapsed lines
enum I18nKey {
home = "home",
about = "about",
archive = "archive",
search = "search",
tags = "tags",
categories = "categories",
recentPosts = "recentPosts",
comments = "comments",
untitled = "untitled",
uncategorized = "uncategorized",
noTags = "noTags",
wordCount = "wordCount",
wordsCount = "wordsCount",
minuteCount = "minuteCount",
minutesCount = "minutesCount",
postCount = "postCount",
postsCount = "postsCount",
themeColor = "themeColor",
lightMode = "lightMode",
darkMode = "darkMode",
systemMode = "systemMode",
more = "more",
author = "author",
publishedAt = "publishedAt",
license = "license",
sponsor = "sponsor",
}
export default I18nKey;
35 collapsed lines
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const zh_CN: Translation = {
[Key.home]: "主页",
[Key.about]: "关于",
[Key.archive]: "归档",
[Key.search]: "搜索",
[Key.tags]: "标签",
[Key.categories]: "分类",
[Key.recentPosts]: "最新文章",
[Key.comments]: "评论",
[Key.untitled]: "无标题",
[Key.uncategorized]: "未分类",
[Key.noTags]: "无标签",
[Key.wordCount]: "字",
[Key.wordsCount]: "字",
[Key.minuteCount]: "分钟",
[Key.minutesCount]: "分钟",
[Key.postCount]: "篇文章",
[Key.postsCount]: "篇文章",
[Key.themeColor]: "主题色",
[Key.lightMode]: "亮色",
[Key.darkMode]: "暗色",
[Key.systemMode]: "跟随系统",
[Key.more]: "更多",
[Key.author]: "作者",
[Key.publishedAt]: "发布于",
[Key.license]: "许可协议",
[Key.sponsor]: "赞助",
};
src\pages\sponsor.astro
---
import { getEntry } from "astro:content";
import Markdown from "@components/misc/Markdown.astro";
import SponsorCard from "@components/SponsorCard.astro";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import MainGridLayout from "../layouts/MainGridLayout.astro";
import SupportAndShare from "@components/misc/SupportAndShare.astro";
import MarkdownIt from 'markdown-it';
const sponsorPost = await getEntry("spec", "sponsor");
if (!sponsorPost) {
throw new Error("sponsor page content not found");
}
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true
});
interface SponsorBlock {
name: string;
img: string;
icon: string;
color: string;
description: string;
index: number; // 在内容中的位置
}
interface ContentSegment {
type: 'markdown' | 'sponsors';
content?: string;
htmlContent?: string;
sponsors?: SponsorBlock[];
}
function parseContentWithSponsors(content: string): { segments: ContentSegment[], sponsorBlocks: SponsorBlock[] } {
const segments: ContentSegment[] = [];
const sponsorBlocks: SponsorBlock[] = [];
const regex = /:::sponsor\s*([\s\S]*?):::/g;
let lastIndex = 0;
let match;
let sponsorIndex = 0;
let consecutiveSponsors: SponsorBlock[] = [];
const renderMd = (text: string) => {
const processed = text.replace(/:::(tip|note|important|caution|warning)\s*([\s\S]*?):::/g, (_, type, inner) => {
const renderedInner = md.render(inner.trim());
return `<blockquote class="admonition bdm-${type}"><div class="bdm-title">${type.toUpperCase()}</div>${renderedInner}</blockquote>`;
});
return md.render(processed);
};
while ((match = regex.exec(content)) !== null) {
const markdownBetween = content.substring(lastIndex, match.index).trim();
if (markdownBetween) {
if (consecutiveSponsors.length > 0) {
segments.push({ type: 'sponsors', sponsors: [...consecutiveSponsors] });
consecutiveSponsors = [];
}
segments.push({
type: 'markdown',
content: markdownBetween,
htmlContent: renderMd(markdownBetween)
});
}
const blockContent = match[1];
const block: Record<string, string> = {};
blockContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (!trimmed) return;
const colonIdx = trimmed.indexOf(':');
if (colonIdx === -1) return;
const key = trimmed.substring(0, colonIdx).trim();
let value = trimmed.substring(colonIdx + 1).trim();
value = value.replace(/,\s*$/, '');
block[key] = value;
});
const sponsor: SponsorBlock = {
name: block.name || '',
img: block.img || '',
icon: block.icon || '',
color: block.color || '',
description: block.description || '',
index: sponsorIndex++
};
sponsorBlocks.push(sponsor);
consecutiveSponsors.push(sponsor);
lastIndex = match.index + match[0].length;
}
if (consecutiveSponsors.length > 0) {
segments.push({ type: 'sponsors', sponsors: [...consecutiveSponsors] });
}
if (lastIndex < content.length) {
const markdownContent = content.substring(lastIndex).trim();
if (markdownContent) {
segments.push({
type: 'markdown',
content: markdownContent,
htmlContent: renderMd(markdownContent)
});
}
}
return { segments, sponsorBlocks };
}
const { segments, sponsorBlocks } = parseContentWithSponsors(sponsorPost.body);
---
<MainGridLayout title={i18n(I18nKey.sponsor)} description={i18n(I18nKey.sponsor)}>
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32">
<div class="card-base z-10 px-9 py-6 relative w-full ">
<!-- Header section with Flutter-like M3 style -->
<div class="flex flex-col mb-8 mt-4">
<h1 class="text-3xl font-bold text-neutral-900 dark:text-neutral-100 mb-2">{i18n(I18nKey.sponsor)}</h1>
</div>
<!-- 渲染分段内容:Markdown 和 Sponsor Cards 交替出现 -->
{segments.map((segment, idx) => {
if (segment.type === 'markdown' && segment.htmlContent) {
return (
<Markdown class="mt-2 text-75 leading-relaxed max-w-2xl mx-auto">
<div set:html={segment.htmlContent} />
</Markdown>
);
} else if (segment.type === 'sponsors' && segment.sponsors) {
const sponsors = segment.sponsors;
const isSingleCard = sponsors.length === 1;
return isSingleCard ? (
<div class="flex justify-center mt-12 mb-12 px-2 sm:px-10">
<SponsorCard
name={sponsors[0].name}
img={sponsors[0].img}
icon={sponsors[0].icon}
color={sponsors[0].color}
description={sponsors[0].description}
layout="horizontal"
isSingleCard={true}
/>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mt-12 mb-12 px-2 sm:px-10 max-w-4xl mx-auto">
{sponsors.map((sponsor) => (
<SponsorCard
name={sponsor.name}
img={sponsor.img}
icon={sponsor.icon}
color={sponsor.color}
description={sponsor.description}
layout="vertical"
/>
))}
</div>
);
}
})}
</div>
</div>
</MainGridLayout>
src\components\SponsorCard.astro
---
import { Icon } from "astro-icon/components";
interface Props {
name: string;
img: string;
icon: string;
color: string;
description: string;
layout?: "vertical" | "horizontal";
isSingleCard?: boolean;
}
const { name, img, icon, color, description, layout = "vertical", isSingleCard = false } = Astro.props;
const imagePath = img.startsWith("./")
? `/src/content/spec/${img.slice(2)}`
: img;
---
<div class="block md:hidden">
<div class="group relative flex flex-col items-center p-8 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800">
<div class="absolute top-6 left-8 opacity-20 group-hover:opacity-100 transition-opacity duration-300">
<Icon name={icon} class="text-3xl" style={`color: ${color}`} />
</div>
<div class="relative w-48 h-48 mb-6 p-3 bg-white rounded-[var(--radius-large)]">
<img
src={imagePath}
alt={name}
class="w-full h-full object-cover rounded-[var(--radius-large)]"
/>
<!-- Decorative corners -->
<div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div>
</div>
<h3 class="text-xl font-bold text-90 mb-1">{name}</h3>
<p class="text-sm text-50 font-normal">{description}</p>
</div>
</div>
<div class="hidden md:block">
{layout === "vertical" ? (
<div class="group relative flex flex-col items-center p-8 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800">
<div class="absolute top-6 left-8 opacity-20 group-hover:opacity-100 transition-opacity duration-300">
<Icon name={icon} class="text-3xl" style={`color: ${color}`} />
</div>
<div class="relative w-48 h-48 mb-6 p-3 bg-white rounded-[var(--radius-large)]">
<img
src={imagePath}
alt={name}
class="w-full h-full object-cover rounded-[var(--radius-large)]"
/>
<!-- Decorative corners -->
<div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div>
</div>
<h3 class="text-xl font-bold text-90 mb-1">{name}</h3>
<p class="text-sm text-50 font-normal">{description}</p>
</div>
) : (
<div class="group relative flex flex-row items-center p-10 rounded-[var(--radius-large)] bg-neutral-50/50 dark:bg-neutral-900/50 border border-neutral-200/50 dark:border-neutral-800/50 bg-white dark:bg-neutral-800 gap-10 max-w-3xl">
<div class="relative w-64 h-64 p-4 bg-white rounded-[var(--radius-large)] flex-shrink-0">
{!isSingleCard && (
<div class="absolute -top-6 -left-6 opacity-20 group-hover:opacity-100 transition-opacity duration-300 z-10">
<Icon name={icon} class="text-4xl" style={`color: ${color}`} />
</div>
)}
<img
src={imagePath}
alt={name}
class="w-full h-full object-cover rounded-[calc(var(--radius-large)-0.25rem)]"
/>
<!-- Decorative corners -->
<div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 rounded-tl-lg" style={`border-color: ${color}`}></div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 rounded-br-lg" style={`border-color: ${color}`}></div>
</div>
<div class="flex flex-col justify-center items-start flex-1">
<h3 class="text-2xl font-bold text-90 mb-3">{name}</h3>
<p class="text-base text-50 font-normal leading-relaxed mb-6">{description}</p>
</div>
{isSingleCard && (
<div class="absolute bottom-6 right-6 opacity-20 group-+hover:opacity-100 transition-opacity duration-300 z-10">
<Icon name={icon} class="text-8xl" style={`color: +${color}`} />
</div>
)}
</div>
)}
</div>
src\config.ts
42 collapsed lines
import type {
ExpressiveCodeConfig,
LicenseConfig,
NavBarConfig,
ProfileConfig,
SiteConfig,
} from "./types/config";
import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = {
title: "樱花庄驿",
subtitle: "SakuraVillage Post",
lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
themeColor: {
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
fixed: false, // Hide the theme color picker for visitors
},
banner: {
enable: true,
src: "content/assets/images/banner.jpg", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
credit: {
enable: false, // Display the credit text of the banner image
text: "", // Credit text to be displayed
url: "", // (Optional) URL link to the original artwork or artist's page
},
},
toc: {
enable: true, // Display the table of contents on the right side of the post
depth: 2, // Maximum heading depth to show in the table, from 1 to 3
},
favicon: [
// Leave this array empty to use the default favicon
// {
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
// theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
// }
],
};
export const navBarConfig: NavBarConfig = {
links: [
LinkPreset.Home,
LinkPreset.Archive,
LinkPreset.About,
LinkPreset.Sponsor,
{
name: "GitHub",
url: "https://github.com/SakuraVillager/", // Internal links should not include the base path, as it is automatically added
external: true, // Show an external link icon and will open in a new tab
},
],
33 collapsed lines
};
export const profileConfig: ProfileConfig = {
avatar: "/src/content/assets/images/avatar.gif", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: "SakuraVillager",
bio: "一个普通的樱花庄的居民。",
links: [
{
name: "BiliBili",
icon: "fa6-brands:bilibili", // Visit https://icones.js.org/ for icon codes
// You will need to install the corresponding icon set if it's not already included
// `pnpm add @iconify-json/<icon-set-name>`
url: "https://space.bilibili.com/646905644",
},
{
name: "GitHub",
icon: "fa6-brands:github",
url: "https://github.com/SakuraVillager/",
},
],
};
export const licenseConfig: LicenseConfig = {
enable: true,
name: "CC BY-NC-SA 4.0",
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
};
export const expressiveCodeConfig: ExpressiveCodeConfig = {
// Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.
// Please select a dark theme, as this blog theme currently only supports dark background color
theme: "github-dark",
};
src\constants\link-presets.ts
14 collapsed lines
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { LinkPreset, type NavBarLink } from "@/types/config";
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
[LinkPreset.Home]: {
name: i18n(I18nKey.home),
url: "/",
},
[LinkPreset.About]: {
name: i18n(I18nKey.about),
url: "/about/",
},
[LinkPreset.Archive]: {
name: i18n(I18nKey.archive),
url: "/archive/",
},
[LinkPreset.Sponsor]: {
name: i18n(I18nKey.sponsor),
url: "/sponsor/",
},
};
src\types\config.ts
47 collapsed lines
import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants";
export type SiteConfig = {
title: string;
subtitle: string;
lang:
| "en"
| "zh_CN"
| "zh_TW"
| "ja"
| "ko"
| "es"
| "th"
| "vi"
| "tr"
| "id";
themeColor: {
hue: number;
fixed: boolean;
};
banner: {
enable: boolean;
src: string;
position?: "top" | "center" | "bottom";
credit: {
enable: boolean;
text: string;
url?: string;
};
};
toc: {
enable: boolean;
depth: 1 | 2 | 3;
};
favicon: Favicon[];
};
export type Favicon = {
src: string;
theme?: "light" | "dark";
sizes?: string;
};
export enum LinkPreset {
Home = 0,
Archive = 1,
About = 3,
Sponsor = 4,
}
export type NavBarLink = {
name: string;
50 collapsed lines
url: string;
external?: boolean;
};
export type NavBarConfig = {
links: (NavBarLink | LinkPreset)[];
};
export type ProfileConfig = {
avatar?: string;
name: string;
bio?: string;
links: {
name: string;
url: string;
icon: string;
}[];
};
export type LicenseConfig = {
enable: boolean;
name: string;
url: string;
};
export type LIGHT_DARK_MODE =
| typeof LIGHT_MODE
| typeof DARK_MODE
| typeof AUTO_MODE;
export type BlogPostData = {
body: string;
title: string;
published: Date;
description: string;
tags: string[];
draft?: boolean;
image?: string;
category?: string;
series?: string;
pinned?: boolean;
prevTitle?: string;
prevSlug?: string;
nextTitle?: string;
nextSlug?: string;
};
export type ExpressiveCodeConfig = {
theme: string;
};

使用示例#

spec 文件夹下新建一个文件叫做 sponsor.md,随后新建一个文件夹叫做 assets,在 assets 下新建一个文件夹叫做 sponsor,随后把支付宝、微信的二维码保存在这个文件夹下。

可以在markdown文件中这样写:

# 赞助
通过支付宝或者微信给博主打赏吧!
:::sponsor
name: 支付宝,
img: ./assets/sponsor/你的支付码名字.jpg,
icon: fa6-brands:alipay,
color: #1677FF,
description: 请作者喝一杯咖啡吧,
:::
:::sponsor
name: 微信支付,
img: ./assets/sponsor/你的支付码名字.png,
icon: fa6-brands:weixin,
color: #07C160,
description: 感谢您对我的支持,
:::
# 赞助列表
其他内容
Fuwari改造计划之赞助页面
https://testblog.jijiz.cn/posts/fuwari改造计划/2026-02-13-fuwari改造计划之赞助页面/
作者
SakuraVillager
发布于
2026-02-13
许可协议
CC BY-NC-SA 4.0

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助