add links rss

This commit is contained in:
浪子
2026-05-17 00:05:36 +08:00
parent 05f522ac86
commit ad758e92ad
10 changed files with 398 additions and 56 deletions
+33 -24
View File
@@ -38,9 +38,31 @@ const config: AskyConfig = {
menu: [ menu: [
{ label: '首页', url: '/' }, { label: '首页', url: '/' },
{ label: '归档', url: '/archive' }, { label: '归档', url: '/archive' },
{ label: '链接', url: '/links' },
{ label: '关于', url: '/about' } { label: '关于', url: '/about' }
] ],
// 主导航菜单(对应原 wp_nav_menu 'primary' 位置) // 主导航菜单(对应原 wp_nav_menu 'primary' 位置)
links: [
{
title: '友情链接',
items: [
{
name: 'Asky',
url: 'https://github.com/saresam/Asky',
description: 'Asky WordPress 主题原版',
image: '/images/none.png'
},
{
name: 'Astro',
url: 'https://astro.build/',
description: '当前站点使用的静态站点框架',
image: '/images/none.png'
}
]
}
]
// 友情链接页面数据(对应原 WordPress 书签/链接分类)
}, },
/* ===== 基本设置 ===== */ /* ===== 基本设置 ===== */
@@ -289,22 +311,6 @@ const config: AskyConfig = {
// 是否显示评论者的 UA 图标(OS / 浏览器) // 是否显示评论者的 UA 图标(OS / 浏览器)
}, },
/* ===== 前台登录 ===== */
login_bg: '',
// 后台登录界面背景图(为空则使用默认)
exlogin_url: '',
// 指定登录地址(强制不使用后台 wp-login.php
exregister_url: '',
// 指定注册地址(作为登录页面的注册入口)
ex_register_open: false,
// 允许用户在前台注册
login_urlskip: false,
// 登录后自动跳转(管理员→后台,用户→主页)
/* ===== 杂七杂八 ===== */ /* ===== 杂七杂八 ===== */
canvas_nest: false, canvas_nest: false,
// 开启蜂窝背景动效(屏幕宽度 >800px 时生效) // 开启蜂窝背景动效(屏幕宽度 >800px 时生效)
@@ -354,6 +360,16 @@ export interface AskyConfig {
language: string; language: string;
charset: string; charset: string;
menu: Array<{ label: string; url: string; children?: Array<{ label: string; url: string }> }>; menu: Array<{ label: string; url: string; children?: Array<{ label: string; url: string }> }>;
links?: Array<{
title: string;
description?: string;
items: Array<{
name: string;
url: string;
description?: string;
image?: string;
}>;
}>;
}; };
/* ===== 基本设置 ===== */ /* ===== 基本设置 ===== */
@@ -451,13 +467,6 @@ export interface AskyConfig {
showUa?: boolean; showUa?: boolean;
}; };
/* ===== 前台登录 ===== */
login_bg: string;
exlogin_url: string;
exregister_url: string;
ex_register_open: boolean;
login_urlskip: boolean;
/* ===== 杂七杂八 ===== */ /* ===== 杂七杂八 ===== */
canvas_nest: boolean; canvas_nest: boolean;
flying_fish: boolean; flying_fish: boolean;
+3
View File
@@ -3,6 +3,9 @@ import { defineConfig } from 'astro/config';
export default defineConfig({ export default defineConfig({
site: 'https://example.com', site: 'https://example.com',
trailingSlash: 'ignore', trailingSlash: 'ignore',
markdown: {
syntaxHighlight: false
},
build: { build: {
format: 'directory', format: 'directory',
assets: 'assets' assets: 'assets'
+71
View File
@@ -6935,3 +6935,74 @@ i.iconfont.icon-people{
} }
} }
/* Mac-style code blocks */
.entry-content pre {
position: relative;
margin: 1.8em 0;
padding: 54px 22px 22px;
border: 1px solid rgba(255, 255, 255, .08);
border-radius: 8px;
background: #1f2329;
color: #d7dde8;
box-shadow: 0 14px 34px rgba(15, 23, 42, .16);
overflow: auto;
}
.entry-content pre:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 38px;
border-bottom: 1px solid rgba(255, 255, 255, .08);
border-radius: 8px 8px 0 0;
background: linear-gradient(#3a3f4b, #2b3039);
}
.entry-content pre:after {
content: "";
position: absolute;
top: 14px;
left: 16px;
width: 11px;
height: 11px;
border-radius: 50%;
background: #ff5f57;
box-shadow: 19px 0 #ffbd2e, 38px 0 #28c840;
}
.entry-content pre code {
display: block;
margin: 0;
padding: 0;
border-radius: 0;
background: transparent;
color: inherit;
font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
font-size: 14px;
line-height: 1.75;
white-space: pre;
}
@media (max-width: 860px) {
.entry-content pre {
padding: 50px 16px 18px;
border-radius: 6px;
}
.entry-content pre:before {
height: 36px;
border-radius: 6px 6px 0 0;
}
.entry-content pre:after {
top: 13px;
left: 14px;
}
.entry-content pre code {
font-size: 13px;
}
}
+48 -2
View File
@@ -8,6 +8,16 @@ export interface Props {
isHome?: boolean; isHome?: boolean;
/** 当前页的特色图(pattern 装饰图) */ /** 当前页的特色图(pattern 装饰图) */
patternImg?: string; patternImg?: string;
/** 装饰图中显示的标题 */
patternTitle?: string;
/** 装饰图中显示的描述 */
patternDescription?: string;
/** 是否使用文章详情页的装饰图布局 */
patternSingle?: boolean;
patternAuthor?: string;
patternAuthorUrl?: string;
patternAvatar?: string;
patternMeta?: string;
/** 自定义 body class */ /** 自定义 body class */
bodyClass?: string; bodyClass?: string;
} }
@@ -17,11 +27,19 @@ const {
description, description,
isHome = false, isHome = false,
patternImg, patternImg,
patternTitle,
patternDescription,
patternSingle = false,
patternAuthor,
patternAuthorUrl = '/',
patternAvatar,
patternMeta,
bodyClass = '' bodyClass = ''
} = Astro.props; } = Astro.props;
const siteName = bloginfo('name'); const siteName = bloginfo('name');
const siteDesc = bloginfo('description'); const siteDesc = bloginfo('description');
const siteUrl = bloginfo('url').replace(/\/$/, '');
const charset = bloginfo('charset'); const charset = bloginfo('charset');
const lang = bloginfo('language'); const lang = bloginfo('language');
@@ -49,6 +67,9 @@ const stats = askyOption('site_statistics');
const statsLink = askyOption('site_statistics_link'); const statsLink = askyOption('site_statistics_link');
const sitemapLink = askyOption('site_map_link'); const sitemapLink = askyOption('site_map_link');
const themeSkin = askyOption('theme_skin'); const themeSkin = askyOption('theme_skin');
const patternClass = `pattern-center${patternSingle ? ' single-center' : ''}`;
const patternHeaderClass = `pattern-header${patternSingle ? ' single-header' : ''}`;
--- ---
<!DOCTYPE html> <!DOCTYPE html>
@@ -67,6 +88,7 @@ const themeSkin = askyOption('theme_skin');
)} )}
<link rel="shortcut icon" href="/images/favicon.ico" /> <link rel="shortcut icon" href="/images/favicon.ico" />
<link rel="alternate" type="application/rss+xml" title={`${siteName} RSS`} href={`${siteUrl}/rss.xml`} />
<link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="/inc/fonts/iconfont.css" /> <link rel="stylesheet" href="/inc/fonts/iconfont.css" />
@@ -100,8 +122,32 @@ const themeSkin = askyOption('theme_skin');
<slot name="header" /> <slot name="header" />
{patternImg && ( {patternImg && (
<div class="pattern-center"> <div class={patternClass}>
<div class="pattern" style={`background-image:url(${patternImg})`}></div> <div class="pattern-attachment-img" style={`background-image: url(${patternImg})`} title={patternTitle ?? pageTitle}></div>
{patternTitle && (
<header class={patternHeaderClass}>
<h1 class={patternSingle ? 'entry-title' : 'cat-title'}>{patternTitle}</h1>
{patternSingle ? (
<>
<span class="toppic-line"></span>
<p class="entry-census">
{patternAuthor && patternAvatar && (
<span><a href={patternAuthorUrl}><img src={patternAvatar} alt={patternAuthor} /></a></span>
)}
{patternAuthor && <span><a href={patternAuthorUrl}>{patternAuthor}</a></span>}
{patternMeta && (
<>
{patternAuthor && <span class="bull">·</span>}
{patternMeta}
</>
)}
</p>
</>
) : (
patternDescription && <span class="cat-des">{patternDescription}</span>
)}
</header>
)}
</div> </div>
)} )}
+34
View File
@@ -0,0 +1,34 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Imgbox from '../components/Imgbox.astro';
import MobileNav from '../components/MobileNav.astro';
import { askyOption, bloginfo, isOptionOn } from '../lib/options';
const showHeader = isOptionOn('patternimg');
const patternImg = showHeader ? undefined : '/images/hd.jpg';
const siteName = bloginfo('name');
const description = askyOption('admin_des', bloginfo('description'));
---
<BaseLayout title="关于" patternImg={patternImg} patternTitle="关于">
<Imgbox slot="imgbox" />
<Header slot="header" />
<MobileNav slot="mo-nav" />
<div id="primary" class="content-area">
<main id="main" class="site-main" role="main">
<article class="page type-page">
{showHeader && (
<header class="entry-header">
<h1 class="entry-title">关于</h1>
</header>
)}
<div class="entry-content">
<h2>{siteName}</h2>
<p>{description}</p>
</div>
</article>
</main>
</div>
</BaseLayout>
+30 -26
View File
@@ -12,45 +12,49 @@ const all = (await getCollection('posts')).sort(
const groups = new Map<string, typeof all>(); const groups = new Map<string, typeof all>();
for (const p of all) { for (const p of all) {
const key = `${p.data.date.getFullYear()}-${String(p.data.date.getMonth() + 1).padStart(2, '0')}`; const key = `${p.data.date.getFullYear()}-${p.data.date.getMonth() + 1}`;
if (!groups.has(key)) groups.set(key, []); if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(p); groups.get(key)!.push(p);
} }
const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a)); const sortedKeys = [...groups.keys()].sort((a, b) => {
const [ay, am] = a.split('-').map(Number);
const [by, bm] = b.split('-').map(Number);
return by === ay ? bm - am : by - ay;
});
const showHeader = isOptionOn('patternimg'); const showHeader = isOptionOn('patternimg');
const patternImg = showHeader ? undefined : '/images/hd.jpg';
--- ---
<BaseLayout title="归档"> <BaseLayout title="归档" patternImg={patternImg} patternTitle="归档" patternDescription={`共 ${all.length} 篇文章`}>
<Imgbox slot="imgbox" /> <Imgbox slot="imgbox" />
<Header slot="header" /> <Header slot="header" />
<MobileNav slot="mo-nav" /> <MobileNav slot="mo-nav" />
<div id="primary" class="content-area"> <article class="post-item">
<main id="main" class="site-main" role="main"> <div id="archives-temp">
{showHeader && ( {showHeader && <h2>归档</h2>}
<header class="page-header"> <div id="archives-content">
<h1 class="cat-title">归档</h1>
<span class="cat-des"><i class="iconfont icon-shuidi"></i>共 {all.length} 篇文章</span>
</header>
)}
<section class="archive-list">
{sortedKeys.map((key) => ( {sortedKeys.map((key) => (
<div class="archive-month"> <div class="archive-title" id={`arti-${key}`}>
<h2><i class="iconfont icon-clock" style="margin-right:5px"></i>{key}</h2> <span class="ar-time"><i class="iconfont icon-clock"></i></span>
<ul> <h3>{key}</h3>
<div class={`archives archives-${key.split('-')[1]}`} id="monlist" data-date={key}>
{groups.get(key)!.map((p) => ( {groups.get(key)!.map((p) => (
<li> <>
<a href={`/posts/${p.slug}`}> <span class="ar-circle"></span>
<span class="archive-date">{p.data.date.toLocaleDateString('zh-CN')}</span> <div class="arrow-left-ar"></div>
<span class="archive-title">{p.data.title}</span> <div class="brick">
</a> <a href={`/posts/${p.slug}`}>
</li> <span class="time"><i class="iconfont icon-clock"></i>{p.data.date.getMonth() + 1}-{p.data.date.getDate()}</span>
{p.data.title}<em>(0)</em>
</a>
</div>
</>
))} ))}
</ul> </div>
</div> </div>
))} ))}
</section> </div>
</main> </div>
</div> </article>
</BaseLayout> </BaseLayout>
+39
View File
@@ -0,0 +1,39 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Imgbox from '../components/Imgbox.astro';
import MobileNav from '../components/MobileNav.astro';
import { getConfig, isOptionOn } from '../lib/options';
const linkGroups = getConfig().site.links ?? [];
const showHeader = isOptionOn('patternimg');
const patternImg = showHeader ? undefined : '/images/hd.jpg';
---
<BaseLayout title="链接" patternImg={patternImg} patternTitle="链接">
<Imgbox slot="imgbox" />
<Header slot="header" />
<MobileNav slot="mo-nav" />
<article class="post-item">
<div class="links">
{linkGroups.map((group) => (
<>
<h3 class="link-title">{group.title}</h3>
{group.description && <div class="link-description">{group.description}</div>}
<ul class="link-items fontSmooth">
{group.items.map((link) => (
<li class="link-item">
<a class="link-item-inner effect-apollo" href={link.url} title={link.description ?? link.name} target="_blank">
<img class="linksimage" src={link.image || `${link.url.replace(/\/$/, '')}/favicon.ico`} alt="" onerror="this.src='/images/none.png'" />
<span class="sitename">{link.name}</span>
<div class="linkdes">{link.description}</div>
</a>
</li>
))}
</ul>
</>
))}
</div>
</article>
</BaseLayout>
+13 -4
View File
@@ -7,7 +7,7 @@ import AuthorProfile from '../../components/AuthorProfile.astro';
import PostNextPrev from '../../components/PostNextPrev.astro'; import PostNextPrev from '../../components/PostNextPrev.astro';
import ShareLike from '../../components/ShareLike.astro'; import ShareLike from '../../components/ShareLike.astro';
import Comments from '../../components/Comments.astro'; import Comments from '../../components/Comments.astro';
import { isOptionOn } from '../../lib/options'; import { askyOption, isOptionOn } from '../../lib/options';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -35,12 +35,21 @@ const showPattern = isOptionOn('patternimg');
const buildTime = post.data.date.toLocaleString('zh-CN'); const buildTime = post.data.date.toLocaleString('zh-CN');
const tags = post.data.tags ?? []; const tags = post.data.tags ?? [];
const fullUrl = new URL(`/posts/${post.slug}`, Astro.site ?? 'https://example.com').toString(); const fullUrl = new URL(`/posts/${post.slug}`, Astro.site ?? 'https://example.com').toString();
const patternImg = showPattern ? undefined : (post.data.thumbnail ?? '/images/hd.jpg');
const authorName = post.data.author ?? 'Admin';
const authorAvatar = askyOption('focus_logo') || '/images/avatar.jpg';
--- ---
<BaseLayout <BaseLayout
title={post.data.title} title={post.data.title}
description={post.data.description} description={post.data.description}
patternImg={post.data.thumbnail} patternImg={patternImg}
patternTitle={post.data.title}
patternSingle
patternAuthor={authorName}
patternAuthorUrl="/"
patternAvatar={authorAvatar}
patternMeta={buildTime}
> >
<Imgbox slot="imgbox" /> <Imgbox slot="imgbox" />
<Header slot="header" /> <Header slot="header" />
@@ -71,7 +80,7 @@ const fullUrl = new URL(`/posts/${post.slug}`, Astro.site ?? 'https://example.co
{tags.length > 0 && ( {tags.length > 0 && (
<div class="post-tags"> <div class="post-tags">
<i class="iconfont icon-tags"></i> <i class="iconfont icon-tags"></i>
{tags.map((t) => <a href={`/tag/${t}`}>{t}</a>)} {tags.map((t) => <a href={`/tag/${encodeURIComponent(t)}`}>{t}</a>)}
</div> </div>
)} )}
<ShareLike postId={post.id} permalink={fullUrl} title={post.data.title} /> <ShareLike postId={post.id} permalink={fullUrl} title={post.data.title} />
@@ -79,7 +88,7 @@ const fullUrl = new URL(`/posts/${post.slug}`, Astro.site ?? 'https://example.co
</article> </article>
<PostNextPrev prev={prev} next={next} /> <PostNextPrev prev={prev} next={next} />
<AuthorProfile authorName={post.data.author ?? 'Admin'} authorUrl="/" /> <AuthorProfile authorName={authorName} authorUrl="/" />
<Comments /> <Comments />
</main> </main>
</div> </div>
+63
View File
@@ -0,0 +1,63 @@
import { getCollection } from 'astro:content';
import { bloginfo } from '../lib/options';
function escapeXml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function stripMarkdown(value: string): string {
return value
.replace(/```[\s\S]*?```/g, '')
.replace(/`([^`]+)`/g, '$1')
.replace(/!\[[^\]]*]\([^)]*\)/g, '')
.replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
.replace(/[#>*_~\-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
export async function GET() {
const siteUrl = bloginfo('url').replace(/\/$/, '');
const siteName = bloginfo('name');
const siteDescription = bloginfo('description');
const posts = (await getCollection('posts')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
const items = posts.map((post) => {
const link = `${siteUrl}/posts/${post.slug}`;
const description = post.data.description ?? stripMarkdown(post.body).slice(0, 280);
return ` <item>
<title>${escapeXml(post.data.title)}</title>
<link>${escapeXml(link)}</link>
<guid isPermaLink="true">${escapeXml(link)}</guid>
<pubDate>${post.data.date.toUTCString()}</pubDate>
<description>${escapeXml(description)}</description>
</item>`;
}).join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(siteName)}</title>
<link>${escapeXml(siteUrl)}</link>
<description>${escapeXml(siteDescription)}</description>
<language>${escapeXml(bloginfo('language'))}</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="${escapeXml(`${siteUrl}/rss.xml`)}" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>`;
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml; charset=utf-8'
}
});
}
+64
View File
@@ -0,0 +1,64 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Header from '../../components/Header.astro';
import Imgbox from '../../components/Imgbox.astro';
import MobileNav from '../../components/MobileNav.astro';
import PostListThumb from '../../components/PostListThumb.astro';
import PostList from '../../components/PostList.astro';
import { askyOption, isOptionOn } from '../../lib/options';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const all = (await getCollection('posts')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
const tags = [...new Set(all.flatMap((post) => post.data.tags ?? []))].sort((a, b) =>
a.localeCompare(b, 'zh-CN')
);
return tags.map((tag) => ({
params: { tag },
props: {
tag,
posts: all.filter((post) => (post.data.tags ?? []).includes(tag))
}
}));
}
const { tag, posts } = Astro.props;
const items = posts.map((p) => ({
id: p.id,
permalink: `/posts/${p.slug}`,
title: p.data.title,
date: p.data.date.toISOString(),
excerpt: p.data.description ?? p.body.replace(/[#*`>_\[\]\(\)]/g, '').slice(0, 280),
thumbnail: p.data.thumbnail,
sticky: p.data.sticky
}));
const showHeader = isOptionOn('patternimg');
const patternImg = showHeader ? undefined : '/images/hd.jpg';
const title = `标签:${tag}`;
const description = `共 ${items.length} 篇文章`;
const style = askyOption('post_list_style');
---
<BaseLayout title={title} patternImg={patternImg} patternTitle={title} patternDescription={description}>
<Imgbox slot="imgbox" />
<Header slot="header" />
<MobileNav slot="mo-nav" />
<div id="primary" class="content-area">
<main id="main" class="site-main" role="main">
{showHeader && (
<header class="page-header">
<h1 class="cat-title">{title}</h1>
<span class="cat-des"><i class="iconfont icon-shuidi"></i>{description}</span>
</header>
)}
{style === 'standard' ? <PostList posts={items} /> : <PostListThumb posts={items} />}
</main>
</div>
</BaseLayout>