mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-15 19:22:52 +02:00
refactor: separate rss and atom generation, optimize back button logic (#22)
* refactor: separate rss and atom generation, optimize back button logic, remove redundant whitespace in component styles * fix: add missing <published> tag in atom feed
This commit is contained in:
parent
4b05ba9caf
commit
fc1cc050bc
14 changed files with 576 additions and 418 deletions
14
package.json
14
package.json
|
@ -15,13 +15,13 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.2.6",
|
"@astrojs/mdx": "^4.2.6",
|
||||||
"@astrojs/partytown": "^2.1.4",
|
"@astrojs/partytown": "^2.1.4",
|
||||||
"@astrojs/rss": "^4.0.11",
|
|
||||||
"@astrojs/sitemap": "^3.3.1",
|
"@astrojs/sitemap": "^3.3.1",
|
||||||
"@waline/client": "^3.5.7",
|
"@waline/client": "^3.5.7",
|
||||||
"astro": "^5.7.11",
|
"astro": "^5.7.12",
|
||||||
"astro-og-canvas": "^0.7.0",
|
"astro-og-canvas": "^0.7.0",
|
||||||
"astro-robots-txt": "^1.0.0",
|
"astro-robots-txt": "^1.0.0",
|
||||||
"canvaskit-wasm": "^0.40.0",
|
"canvaskit-wasm": "^0.40.0",
|
||||||
|
"feed": "^5.0.0",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
@ -40,18 +40,18 @@
|
||||||
"@antfu/eslint-config": "^4.13.0",
|
"@antfu/eslint-config": "^4.13.0",
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.15.15",
|
"@types/node": "^22.15.17",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@unocss/eslint-plugin": "66.1.0",
|
"@unocss/eslint-plugin": "66.1.1",
|
||||||
"@unocss/preset-attributify": "66.1.0",
|
"@unocss/preset-attributify": "66.1.1",
|
||||||
"@unocss/reset": "66.1.0",
|
"@unocss/reset": "66.1.1",
|
||||||
"astro-eslint-parser": "^1.2.2",
|
"astro-eslint-parser": "^1.2.2",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
"lint-staged": "^15.5.2",
|
"lint-staged": "^15.5.2",
|
||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"unocss": "66.1.0",
|
"unocss": "66.1.1",
|
||||||
"unocss-preset-theme": "^0.14.1"
|
"unocss-preset-theme": "^0.14.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|
521
pnpm-lock.yaml
generated
521
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
105
public/feeds/atom-style.xsl
Normal file
105
public/feeds/atom-style.xsl
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
<head>
|
||||||
|
<title><xsl:value-of select="/atom:feed/atom:title"/> Web Feed</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||||
|
<style type="text/css">
|
||||||
|
/* Basic styles */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
||||||
|
body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';font-size:16px;line-height:1.5;word-wrap:break-word;color:oklch(25% 0.005 298)}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
p{margin-top:0;margin-bottom:16px}
|
||||||
|
strong{font-weight:600}
|
||||||
|
small{font-size:90%}
|
||||||
|
h1,h2,h3,h4,h5,h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}
|
||||||
|
h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #E0DFE3}
|
||||||
|
h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #E0DFE3}
|
||||||
|
h3{font-size:1.25em;margin-top:0;margin-bottom:0}
|
||||||
|
a{color:#0366d6;text-decoration:none}
|
||||||
|
a:hover{text-decoration:underline}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.bg-white{background-color:oklch(0.96 0.005 298)!important}
|
||||||
|
.px-4{padding-right:24px!important;padding-left:24px!important}
|
||||||
|
.py-2{padding-top:8px!important;padding-bottom:8px!important}
|
||||||
|
.py-5{padding-top:32px!important;padding-bottom:32px!important}
|
||||||
|
.pb-3{padding-bottom:16px!important}
|
||||||
|
.pb-5{padding-bottom:32px!important}
|
||||||
|
.mt-3{margin-top:16px!important}
|
||||||
|
.mb-1{margin-bottom:4px!important}
|
||||||
|
.pr-1{padding-right:4px!important}
|
||||||
|
.border-0{border:0!important}
|
||||||
|
.text-gray{color:oklch(0.25 0.005 298 / 75%)!important}
|
||||||
|
@media (min-width:768px){
|
||||||
|
.mt-md-5{margin-top:32px!important}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Component styles */
|
||||||
|
.markdown{max-width:768px;margin-right:auto;margin-left:auto}
|
||||||
|
.markdown>*:first-child{margin-top:0!important}
|
||||||
|
.markdown>*:last-child{margin-bottom:0!important}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-white">
|
||||||
|
<nav class="px-4 py-2 mt-3 mt-md-5 markdown">
|
||||||
|
<p>
|
||||||
|
<strong>This is a web feed,</strong> also known as an RSS feed. <strong>Subscribe</strong> by copying the URL from the address bar into your newsreader.
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
<div class="px-4 pb-3 markdown">
|
||||||
|
<header class="py-5">
|
||||||
|
<h1 class="border-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="vertical-align: text-bottom; width: 1.2em; height: 1.2em;" class="pr-1" id="RSSicon" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
|
||||||
|
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
|
||||||
|
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
|
||||||
|
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
|
||||||
|
<stop offset="1.0" stop-color="#D95B29"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
|
||||||
|
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
|
||||||
|
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
|
||||||
|
<circle cx="68" cy="189" r="24" fill="#FFF"/>
|
||||||
|
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
|
||||||
|
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
|
||||||
|
</svg>
|
||||||
|
Web Feed Preview
|
||||||
|
</h1>
|
||||||
|
<h2><xsl:value-of select="/atom:feed/atom:title"/></h2>
|
||||||
|
<p><xsl:value-of select="/atom:feed/atom:subtitle"/></p>
|
||||||
|
<a target="_blank">
|
||||||
|
<xsl:attribute name="href">
|
||||||
|
<xsl:value-of select="/atom:feed/atom:link[@rel='alternate']/@href"/>
|
||||||
|
</xsl:attribute>
|
||||||
|
Visit Website →
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<h2>Recent Items</h2>
|
||||||
|
<xsl:for-each select="/atom:feed/atom:entry">
|
||||||
|
<div class="pb-5">
|
||||||
|
<h3 class="mb-1">
|
||||||
|
<a target="_blank">
|
||||||
|
<xsl:attribute name="href">
|
||||||
|
<xsl:value-of select="atom:link[@rel='alternate']/@href"/>
|
||||||
|
</xsl:attribute>
|
||||||
|
<xsl:value-of select="atom:title"/>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<small class="text-gray">
|
||||||
|
Published: <xsl:value-of select="substring(atom:published, 1, 10)" />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</xsl:for-each>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
|
@ -69,128 +69,98 @@ document.addEventListener('astro:page-load', initWaline)
|
||||||
#waline .wl-login-info {
|
#waline .wl-login-info {
|
||||||
--at-apply: 'mt-0 mr-3'
|
--at-apply: 'mt-0 mr-3'
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-avatar {
|
#waline .wl-avatar {
|
||||||
--at-apply: 'border-none'
|
--at-apply: 'border-none'
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-logout-btn {
|
#waline .wl-logout-btn {
|
||||||
--at-apply: 'z-99'
|
--at-apply: 'z-99'
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-login-nick:not(:has(img)) {
|
#waline .wl-login-nick:not(:has(img)) {
|
||||||
--at-apply: 'leading-3.6 mt-1.4';
|
--at-apply: 'leading-3.6 mt-1.4';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-panel {
|
#waline .wl-panel {
|
||||||
--at-apply: 'm-0 rounded border-secondary/25'
|
--at-apply: 'm-0 rounded border-secondary/25'
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-header {
|
#waline .wl-header {
|
||||||
--at-apply: 'p-0';
|
--at-apply: 'p-0';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-header-item {
|
#waline .wl-header-item {
|
||||||
border-bottom: 1px solid var(--waline-border-color);
|
border-bottom: 1px solid var(--waline-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-header label {
|
#waline .wl-header label {
|
||||||
--at-apply: 'text-3';
|
--at-apply: 'text-3';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-header input {
|
#waline .wl-header input {
|
||||||
--at-apply: 'text-2.8';
|
--at-apply: 'text-2.8';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-card,
|
#waline .wl-card,
|
||||||
#waline .wl-header.item3 {
|
#waline .wl-header.item3 {
|
||||||
--at-apply: 'border-b-0';
|
--at-apply: 'border-b-0';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-card .wl-quote {
|
#waline .wl-card .wl-quote {
|
||||||
--at-apply: 'border-is-none mt-4';
|
--at-apply: 'border-is-none mt-4';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-editor {
|
#waline .wl-editor {
|
||||||
--at-apply: 'min-h-24';
|
--at-apply: 'min-h-24';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-editor::placeholder {
|
#waline .wl-editor::placeholder {
|
||||||
color: var(--waline-light-grey);
|
color: var(--waline-light-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-footer {
|
#waline .wl-footer {
|
||||||
--at-apply: 'm-2';
|
--at-apply: 'm-2';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-info .wl-btn {
|
#waline .wl-info .wl-btn {
|
||||||
--at-apply: 'rounded';
|
--at-apply: 'rounded';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-text-number,
|
#waline .wl-text-number,
|
||||||
#waline .wl-action[title="Markdown Guide"],
|
#waline .wl-action[title="Markdown Guide"],
|
||||||
#waline .wl-sort,
|
#waline .wl-sort,
|
||||||
#waline .wl-gallery::-webkit-scrollbar {
|
#waline .wl-gallery::-webkit-scrollbar {
|
||||||
--at-apply: 'hidden';
|
--at-apply: 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-emoji-popup {
|
#waline .wl-emoji-popup {
|
||||||
--at-apply: 'start-0 rounded border-secondary/25';
|
--at-apply: 'start-0 rounded border-secondary/25';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar {
|
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar {
|
||||||
--at-apply: 'w-1.2';
|
--at-apply: 'w-1.2';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-thumb {
|
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-thumb {
|
||||||
background: oklch(var(--un-preset-theme-colors-secondary) / 0.25);
|
background: oklch(var(--un-preset-theme-colors-secondary) / 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-track-piece {
|
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar-track-piece {
|
||||||
--at-apply: 'bg-transparent';
|
--at-apply: 'bg-transparent';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-gif-popup {
|
#waline .wl-gif-popup {
|
||||||
--at-apply: 'border-secondary/25';
|
--at-apply: 'border-secondary/25';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-gif-popup input {
|
#waline .wl-gif-popup input {
|
||||||
--at-apply: 'bg-background border-secondary/25';
|
--at-apply: 'bg-background border-secondary/25';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-gif-popup input::placeholder {
|
#waline .wl-gif-popup input::placeholder {
|
||||||
--at-apply: 'c-secondary/30 text-3.5';
|
--at-apply: 'c-secondary/30 text-3.5';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-gallery {
|
#waline .wl-gallery {
|
||||||
--at-apply: 'scrollbar-hidden';
|
--at-apply: 'scrollbar-hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-meta-head {
|
#waline .wl-meta-head {
|
||||||
--at-apply: 'pt-3 pb-2 px-0';
|
--at-apply: 'pt-3 pb-2 px-0';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-card-item {
|
#waline .wl-card-item {
|
||||||
--at-apply: 'px-0';
|
--at-apply: 'px-0';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-user-avatar {
|
#waline .wl-user-avatar {
|
||||||
--at-apply: 'mt-1';
|
--at-apply: 'mt-1';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-content p {
|
#waline .wl-content p {
|
||||||
--at-apply: 'leading-6 text-3.5';
|
--at-apply: 'leading-6 text-3.5';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-time {
|
#waline .wl-time {
|
||||||
color: oklch(var(--un-preset-theme-colors-primary) / 0.75);
|
color: oklch(var(--un-preset-theme-colors-primary) / 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-edit,
|
#waline .wl-edit,
|
||||||
#waline .wl-delete {
|
#waline .wl-delete {
|
||||||
--at-apply: 'mr-0.4';
|
--at-apply: 'mr-0.4';
|
||||||
}
|
}
|
||||||
|
|
||||||
#waline .wl-like {
|
#waline .wl-like {
|
||||||
--at-apply: 'mr-1.2';
|
--at-apply: 'mr-1.2';
|
||||||
}
|
}
|
||||||
|
@ -199,33 +169,33 @@ document.addEventListener('astro:page-load', initWaline)
|
||||||
<!-- Official CSS Variables >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
<!-- Official CSS Variables >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
||||||
<!-- https://waline.js.org/reference/client/style.html -->
|
<!-- https://waline.js.org/reference/client/style.html -->
|
||||||
<style>
|
<style>
|
||||||
#waline {
|
#waline {
|
||||||
/* Regular Colors */
|
/* Regular Colors */
|
||||||
--waline-white: var(--uno-colors-background);
|
--waline-white: var(--uno-colors-background);
|
||||||
--waline-light-grey: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
--waline-light-grey: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
||||||
--waline-dark-grey: var(--uno-colors-secondary);
|
--waline-dark-grey: var(--uno-colors-secondary);
|
||||||
|
|
||||||
/* Theme Colors */
|
/* Theme Colors */
|
||||||
--waline-theme-color: var(--uno-colors-primary);
|
--waline-theme-color: var(--uno-colors-primary);
|
||||||
--waline-active-color: var(--uno-colors-primary);
|
--waline-active-color: var(--uno-colors-primary);
|
||||||
|
|
||||||
/* Layout Colors */
|
/* Layout Colors */
|
||||||
--waline-color: var(--uno-colors-secondary);
|
--waline-color: var(--uno-colors-secondary);
|
||||||
--waline-bg-color: var(--uno-colors-background);
|
--waline-bg-color: var(--uno-colors-background);
|
||||||
--waline-bg-color-light: var(--uno-colors-background);
|
--waline-bg-color-light: var(--uno-colors-background);
|
||||||
--waline-bg-color-hover: var(--uno-colors-background);
|
--waline-bg-color-hover: var(--uno-colors-background);
|
||||||
--waline-border-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
--waline-border-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
||||||
--waline-disable-bg-color: oklch(var(--un-preset-theme-colors-secondary) / 0.05);
|
--waline-disable-bg-color: oklch(var(--un-preset-theme-colors-secondary) / 0.05);
|
||||||
--waline-disable-color: var(--uno-colors-primary);
|
--waline-disable-color: var(--uno-colors-primary);
|
||||||
|
|
||||||
/* Special Colors */
|
/* Special Colors */
|
||||||
--waline-bq-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
--waline-bq-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
||||||
|
|
||||||
/* Information */
|
/* Information */
|
||||||
--waline-info-bg-color: var(--uno-colors-background);
|
--waline-info-bg-color: var(--uno-colors-background);
|
||||||
--waline-info-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
--waline-info-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
|
||||||
|
|
||||||
/* Rendering Options */
|
/* Rendering Options */
|
||||||
--waline-avatar-radius: 0.5rem;
|
--waline-avatar-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import GoBackIcon from '@/assets/icons/go-back.svg';
|
||||||
id="back-button"
|
id="back-button"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
lg="block absolute c-secondary/40 left--10 top-1/2 aspect-square w-4.5 translate-y--1/2 transition-colors ease-out c-secondary active:scale-90 hover:c-primary/80"
|
lg="block absolute c-secondary/40 left--10 top-1/2 aspect-square w-4.5 translate-y--1/2 transition-colors ease-out c-secondary active:scale-90 hover:c-primary/80"
|
||||||
aria-label="Back to home"
|
aria-label="Go back"
|
||||||
>
|
>
|
||||||
<GoBackIcon
|
<GoBackIcon
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -21,16 +21,9 @@ function setupBackButton() {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Click site title link to trigger view transition when no history
|
|
||||||
const titleLink = document.getElementById('site-title-link') as HTMLAnchorElement
|
|
||||||
if (titleLink) {
|
|
||||||
titleLink.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setupBackButton()
|
setupBackButton()
|
||||||
document.addEventListener('astro:page-load', setupBackButton)
|
document.addEventListener('astro:after-swap', setupBackButton)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -82,11 +82,9 @@ const filteredHeadings = headings.filter(heading =>
|
||||||
.toc-title {
|
.toc-title {
|
||||||
--at-apply: 'font-semibold ml-4 select-none 2xl:hidden';
|
--at-apply: 'font-semibold ml-4 select-none 2xl:hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-list {
|
.toc-list {
|
||||||
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4 2xl:space-y-1.2';
|
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4 2xl:space-y-1.2';
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-link-h2, .toc-link-h3, .toc-link-h4 {
|
.toc-link-h2, .toc-link-h3, .toc-link-h4 {
|
||||||
--at-apply: 'text-sm no-underline font-normal text-balance select-none 2xl:(text-3.2 c-secondary/60 transition-colors ease-in hover:(c-secondary font-medium))';
|
--at-apply: 'text-sm no-underline font-normal text-balance select-none 2xl:(text-3.2 c-secondary/60 transition-colors ease-in hover:(c-secondary font-medium))';
|
||||||
}
|
}
|
||||||
|
@ -95,7 +93,6 @@ const filteredHeadings = headings.filter(heading =>
|
||||||
.accordion-wrapper {
|
.accordion-wrapper {
|
||||||
--at-apply: 'grid rows-[0fr] duration-300 ease-in-out';
|
--at-apply: 'grid rows-[0fr] duration-300 ease-in-out';
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
--at-apply: 'overflow-hidden max-h-66 2xl:(max-h-[calc(100vh-21.5rem)]) pl-4 pr-6';
|
--at-apply: 'overflow-hidden max-h-66 2xl:(max-h-[calc(100vh-21.5rem)]) pl-4 pr-6';
|
||||||
}
|
}
|
||||||
|
@ -104,7 +101,6 @@ const filteredHeadings = headings.filter(heading =>
|
||||||
.accordion-toggle:checked ~ .accordion-wrapper {
|
.accordion-toggle:checked ~ .accordion-wrapper {
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
||||||
--at-apply: 'overflow-y-auto';
|
--at-apply: 'overflow-y-auto';
|
||||||
}
|
}
|
||||||
|
@ -113,19 +109,15 @@ const filteredHeadings = headings.filter(heading =>
|
||||||
.accordion-wrapper {
|
.accordion-wrapper {
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-toggle:checked ~ .accordion-wrapper {
|
.accordion-toggle:checked ~ .accordion-wrapper {
|
||||||
grid-template-rows: 0fr;
|
grid-template-rows: 0fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-content {
|
.accordion-content {
|
||||||
--at-apply: 'overflow-y-auto';
|
--at-apply: 'overflow-y-auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
||||||
--at-apply: 'overflow-hidden';
|
--at-apply: 'overflow-hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-link-active {
|
.toc-link-active {
|
||||||
--at-apply: 'c-secondary font-medium';
|
--at-apply: 'c-secondary font-medium';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { APIContext } from 'astro'
|
import type { APIContext } from 'astro'
|
||||||
import { moreLocales } from '@/config'
|
import { moreLocales } from '@/config'
|
||||||
import { generateRSS } from '@/utils/rss'
|
import { generateAtom } from '@/utils/feed'
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return moreLocales.map(lang => ({
|
return moreLocales.map(lang => ({
|
||||||
|
@ -8,7 +8,6 @@ export function getStaticPaths() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET({ params }: APIContext) {
|
export async function GET(context: APIContext) {
|
||||||
const lang = params.lang as typeof moreLocales[number]
|
return generateAtom(context)
|
||||||
return generateRSS({ lang })
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { APIContext } from 'astro'
|
import type { APIContext } from 'astro'
|
||||||
import { moreLocales } from '@/config'
|
import { moreLocales } from '@/config'
|
||||||
import { generateRSS } from '@/utils/rss'
|
import { generateRSS } from '@/utils/feed'
|
||||||
|
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return moreLocales.map(lang => ({
|
return moreLocales.map(lang => ({
|
||||||
|
@ -8,7 +8,6 @@ export function getStaticPaths() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET({ params }: APIContext) {
|
export async function GET(context: APIContext) {
|
||||||
const lang = params.lang as typeof moreLocales[number]
|
return generateRSS(context)
|
||||||
return generateRSS({ lang })
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { generateRSS } from '@/utils/rss'
|
import type { APIContext } from 'astro'
|
||||||
|
import { generateAtom } from '@/utils/feed'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(context: APIContext) {
|
||||||
return generateRSS()
|
return generateAtom(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { generateRSS } from '@/utils/rss'
|
import type { APIContext } from 'astro'
|
||||||
|
import { generateRSS } from '@/utils/feed'
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(context: APIContext) {
|
||||||
return generateRSS()
|
return generateRSS(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { CollectionEntry } from 'astro:content'
|
||||||
import { defaultLocale } from '@/config'
|
import { defaultLocale } from '@/config'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
type ExcerptScene = 'list' | 'meta' | 'og' | 'rss'
|
type ExcerptScene = 'list' | 'meta' | 'og' | 'feed'
|
||||||
|
|
||||||
const parser = new MarkdownIt()
|
const parser = new MarkdownIt()
|
||||||
const isCJKLang = (lang: string) => ['zh', 'zh-tw', 'ja'].includes(lang)
|
const isCJKLang = (lang: string) => ['zh', 'zh-tw', 'ja'].includes(lang)
|
||||||
|
@ -24,7 +24,7 @@ const EXCERPT_LENGTHS: Record<ExcerptScene, {
|
||||||
cjk: 70,
|
cjk: 70,
|
||||||
other: 140,
|
other: 140,
|
||||||
},
|
},
|
||||||
rss: {
|
feed: {
|
||||||
cjk: 70,
|
cjk: 70,
|
||||||
other: 140,
|
other: 140,
|
||||||
},
|
},
|
||||||
|
|
155
src/utils/feed.ts
Normal file
155
src/utils/feed.ts
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import type { APIContext } from 'astro'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import type { Author } from 'feed'
|
||||||
|
import { defaultLocale, themeConfig } from '@/config'
|
||||||
|
import { ui } from '@/i18n/ui'
|
||||||
|
import { generateDescription } from '@/utils/description'
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
import { Feed } from 'feed'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
const markdownParser = new MarkdownIt()
|
||||||
|
const { title, description, url, author: siteAuthor } = themeConfig.site
|
||||||
|
const followConfig = themeConfig.seo?.follow
|
||||||
|
|
||||||
|
interface GenerateFeedOptions {
|
||||||
|
lang?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate post URL with language prefix and abbrlink/slug
|
||||||
|
*/
|
||||||
|
function generatePostUrl(post: CollectionEntry<'posts'>, baseUrl: string): string {
|
||||||
|
const needsLangPrefix = post.data.lang !== defaultLocale && post.data.lang !== ''
|
||||||
|
const langPrefix = needsLangPrefix ? `${post.data.lang}/` : ''
|
||||||
|
const postSlug = post.data.abbrlink || post.id
|
||||||
|
|
||||||
|
return new URL(`${langPrefix}posts/${postSlug}/`, baseUrl).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a feed object supporting both RSS and Atom formats
|
||||||
|
*/
|
||||||
|
export async function generateFeed({ lang }: GenerateFeedOptions = {}) {
|
||||||
|
const currentUI = ui[lang as keyof typeof ui] || ui[defaultLocale as keyof typeof ui]
|
||||||
|
const useI18nTitle = themeConfig.site.i18nTitle
|
||||||
|
const siteTitle = useI18nTitle ? currentUI.title : title
|
||||||
|
const siteDescription = useI18nTitle ? currentUI.description : description
|
||||||
|
const siteURL = lang ? `${url}/${lang}` : url
|
||||||
|
const author: Author = {
|
||||||
|
name: siteAuthor,
|
||||||
|
link: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Feed instance
|
||||||
|
const feed = new Feed({
|
||||||
|
title: siteTitle,
|
||||||
|
description: siteDescription,
|
||||||
|
id: siteURL,
|
||||||
|
link: siteURL,
|
||||||
|
language: lang || themeConfig.global.locale,
|
||||||
|
copyright: `Copyright © ${new Date().getFullYear()} ${siteAuthor}`,
|
||||||
|
updated: new Date(),
|
||||||
|
generator: 'Astro-Theme-Retypeset with Feed for Node.js',
|
||||||
|
feedLinks: {
|
||||||
|
rss: new URL(lang ? `/${lang}/rss.xml` : '/rss.xml', url).toString(),
|
||||||
|
atom: new URL(lang ? `/${lang}/atom.xml` : '/atom.xml', url).toString(),
|
||||||
|
},
|
||||||
|
author,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Filter posts by language and exclude drafts
|
||||||
|
const posts = await getCollection(
|
||||||
|
'posts',
|
||||||
|
({ data }: { data: CollectionEntry<'posts'>['data'] }) =>
|
||||||
|
(!data.draft && (data.lang === lang || data.lang === '' || (lang === undefined && data.lang === defaultLocale))),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort posts by published date in descending order
|
||||||
|
const sortedPosts = [...posts].sort((a, b) =>
|
||||||
|
new Date(b.data.published).getTime() - new Date(a.data.published).getTime(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limit to the latest 25 posts
|
||||||
|
const limitedPosts = sortedPosts.slice(0, 25)
|
||||||
|
|
||||||
|
// Add posts to feed
|
||||||
|
for (const post of limitedPosts) {
|
||||||
|
const postLink = generatePostUrl(post, url)
|
||||||
|
|
||||||
|
const postContent = post.body
|
||||||
|
? sanitizeHtml(markdownParser.render(post.body), {
|
||||||
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
|
||||||
|
feed.addItem({
|
||||||
|
title: post.data.title,
|
||||||
|
id: postLink,
|
||||||
|
link: postLink,
|
||||||
|
description: generateDescription(post, 'feed'),
|
||||||
|
content: postContent,
|
||||||
|
author: [author],
|
||||||
|
// published -> Atom:<published>, RSS:<pubDate>
|
||||||
|
published: new Date(post.data.published),
|
||||||
|
// date -> Atom:<updated>, RSS has no update tag
|
||||||
|
date: post.data.updated ? new Date(post.data.updated) : new Date(post.data.published),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add follow verification if available
|
||||||
|
if (followConfig?.feedID && followConfig?.userID) {
|
||||||
|
feed.addExtension({
|
||||||
|
name: 'follow_challenge',
|
||||||
|
objects: {
|
||||||
|
feedId: followConfig.feedID,
|
||||||
|
userId: followConfig.userID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate RSS 2.0 format feed
|
||||||
|
*/
|
||||||
|
export async function generateRSS(context: APIContext) {
|
||||||
|
const feed = await generateFeed({
|
||||||
|
lang: context.params?.lang as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
let rssXml = feed.rss2()
|
||||||
|
rssXml = rssXml.replace(
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet href="/feeds/rss-style.xsl" type="text/xsl"?>',
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Response(rssXml, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Atom 1.0 format feed
|
||||||
|
*/
|
||||||
|
export async function generateAtom(context: APIContext) {
|
||||||
|
const feed = await generateFeed({
|
||||||
|
lang: context.params?.lang as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
let atomXml = feed.atom1()
|
||||||
|
atomXml = atomXml.replace(
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>',
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>',
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Response(atomXml, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/atom+xml; charset=utf-8',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
import type { CollectionEntry } from 'astro:content'
|
|
||||||
import { defaultLocale, themeConfig } from '@/config'
|
|
||||||
import { ui } from '@/i18n/ui'
|
|
||||||
import { generateDescription } from '@/utils/description'
|
|
||||||
import rss from '@astrojs/rss'
|
|
||||||
import { getCollection } from 'astro:content'
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import sanitizeHtml from 'sanitize-html'
|
|
||||||
|
|
||||||
const parser = new MarkdownIt()
|
|
||||||
const { title, description, url } = themeConfig.site
|
|
||||||
const followConfig = themeConfig.seo?.follow
|
|
||||||
|
|
||||||
interface GenerateRSSOptions {
|
|
||||||
lang?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateRSS({ lang }: GenerateRSSOptions = {}) {
|
|
||||||
const currentUI = ui[lang as keyof typeof ui] || ui[defaultLocale as keyof typeof ui]
|
|
||||||
const siteTitle = themeConfig.site.i18nTitle ? currentUI.title : title
|
|
||||||
const siteDescription = themeConfig.site.i18nTitle ? currentUI.description : description
|
|
||||||
|
|
||||||
// Get posts for specific language (including universal posts and default language when lang is undefined)
|
|
||||||
const posts = await getCollection(
|
|
||||||
'posts',
|
|
||||||
({ data }: { data: CollectionEntry<'posts'>['data'] }) =>
|
|
||||||
(!data.draft && (data.lang === lang || data.lang === '' || (lang === undefined && data.lang === defaultLocale))),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sort posts by published date in descending order
|
|
||||||
const sortedPosts = [...posts].sort((a, b) =>
|
|
||||||
new Date(b.data.published).getTime() - new Date(a.data.published).getTime(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return rss({
|
|
||||||
title: siteTitle,
|
|
||||||
site: lang ? `${url}/${lang}` : url,
|
|
||||||
description: siteDescription,
|
|
||||||
stylesheet: '/rss-style.xsl',
|
|
||||||
customData: `
|
|
||||||
<copyright>Copyright © ${new Date().getFullYear()} ${themeConfig.site.author}</copyright>
|
|
||||||
<language>${lang || themeConfig.global.locale}</language>
|
|
||||||
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
|
||||||
${followConfig?.feedID && followConfig?.userID
|
|
||||||
? `<follow_challenge>
|
|
||||||
<feedId>${followConfig.feedID}</feedId>
|
|
||||||
<userId>${followConfig.userID}</userId>
|
|
||||||
</follow_challenge>`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
`.trim(),
|
|
||||||
items: sortedPosts.map((post: CollectionEntry<'posts'>) => ({
|
|
||||||
title: post.data.title,
|
|
||||||
// Generate URL with language prefix and abbrlink/slug
|
|
||||||
link: new URL(
|
|
||||||
`${post.data.lang !== defaultLocale && post.data.lang !== '' ? `${post.data.lang}/` : ''}posts/${post.data.abbrlink || post.id}/`,
|
|
||||||
url,
|
|
||||||
).toString(),
|
|
||||||
description: generateDescription(post, 'rss'),
|
|
||||||
pubDate: post.data.published,
|
|
||||||
content: post.body
|
|
||||||
? sanitizeHtml(parser.render(post.body), {
|
|
||||||
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
|
|
||||||
})
|
|
||||||
: '',
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue