feat: add new XSL template for the RSS feed

This commit is contained in:
Stefan Imhoff
2026-01-26 14:10:03 +01:00
committed by Stefan Imhoff
parent 3c354d937d
commit 02ba52cefc
2 changed files with 185 additions and 104 deletions

View File

@@ -1,111 +1,161 @@
<?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:stylesheet version="1.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"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<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="/rss/channel/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">
/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}[hidden],template{display:none!important}a{background-color:transparent}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*{box-sizing:border-box}
<html lang="en">
<head>
<title>
<xsl:value-of select="/rss/channel/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">
:root {
--accent: rgb(230, 5, 16);
--backgroundDark: rgb(27, 25, 23);
--backgroundLight: rgb(230, 230, 227);
--linkDark: rgba(230, 230, 227, 0.2);
--linkLight: rgba(27, 25, 23, 0.2);
--textDark: rgba(205, 204, 199, 0.87);
--textLight: rgb(14, 13, 12);
/* Base Styles */
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:14px;line-height:1.5;color:#24292e;background-color:#fff}
a{color:#0366d6;text-decoration:none}
a:hover{text-decoration:underline}
b,strong{font-weight:600}
h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0;font-weight:600}
h1{font-size:32px}h2{font-size:24px}h3{font-size:20px}
p{margin-top:0;margin-bottom:10px}
color-scheme: light dark;
}
/* Utility Classes */
.bg-white{background-color:#fff!important}
.text-gray{color:#586069!important}
.border-0{border:0!important}
.mb-0{margin-bottom:0!important}
.pr-1{padding-right:4px!important}
.px-3{padding-right:16px!important;padding-left:16px!important}
.py-3{padding-top:16px!important;padding-bottom:16px!important}
.py-5{padding-top:32px!important;padding-bottom:32px!important}
.pb-5{padding-bottom:32px!important}
body {
background-color: light-dark(var(--backgroundLight), var(--backgroundDark));
color: light-dark(var(--textLight), var(--textDark));
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
}
/* Container */
.container-md{max-width:768px;margin-right:auto;margin-left:auto}
.container-md::before{display:table;content:""}
.container-md::after{display:table;clear:both;content:""}
section {
margin: 0 auto;
width: clamp(300px, 50vw, 768px);
}
/* Markdown Body */
.markdown-body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5;word-wrap:break-word}
.markdown-body::before{display:table;content:""}
.markdown-body::after{display:table;clear:both;content:""}
.markdown-body>*:first-child{margin-top:0!important}
.markdown-body>*:last-child{margin-bottom:0!important}
.markdown-body a:not([href]){color:inherit;text-decoration:none}
.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}
.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}
.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}
.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}
.markdown-body h1{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #eaecef}
.markdown-body h2{padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #eaecef}
.markdown-body ol,.markdown-body ul{padding-left:2em}
.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}
</style>
</head>
<body class="bg-white">
<div class="container-md px-3 py-3 markdown-body">
<header class="py-5">
<h1 class="border-0">
<!-- https://commons.wikimedia.org/wiki/File:Feed-icon.svg -->
<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>
article {
border-block-start: 1px solid light-dark(var(--linkLight), var(--linkDark));
clear: both;
padding-block: 1em;
}
Web Feed Preview
</h1>
<h2><xsl:value-of select="/rss/channel/title"/></h2>
<p><xsl:value-of select="/rss/channel/description"/></p>
<a class="head_link" target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/link"/>
</xsl:attribute>
Visit Website &#x2192;
</a>
</header>
<h2>Recent Items</h2>
<xsl:for-each select="/rss/channel/item">
<div class="pb-5">
<h3 class="mb-0">
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="link"/>
</xsl:attribute>
<xsl:value-of select="title"/>
</a>
</h3>
<small class="text-gray">
Published: <xsl:value-of select="pubDate" />
</small>
h1 {
font-size: clamp(20px, 2vw, 50px);
}
h2 {
font-size: clamp(15px, 1.2vw, 40px);
margin-block-end: 0.5em;
}
h3 {
font-size: clamp(12px, 1vw, 30px);
line-height: 1;
margin-block-end: 0.5em;
margin-block-start: 0;
}
img {
float: inline-start;
margin-inline-end: 1em;
margin-block-end: 1.5em;
}
small,
.timestamp {
font-size: clamp(10px, 0.8vw, 15px);
margin-block: 0;
}
a {
color: light-dark(var(--textLight), var(--textDark));
text-decoration-line: none;
text-size-adjust: 100%;
text-underline-offset: auto;
}
a:hover,
a:active,
a:focus {
text-decoration-color: var(--accent);
text-decoration-line: underline;
text-decoration-style: solid;
text-decoration-thickness: 4px;
}
.timestamp {
color: light-dark(var(--linkLight), var(--linkDark));
font-style: italic;
}
.title {
margin-block-end: 0;
}
.description {
margin-block-start: 0;
}
</style>
</head>
<body>
<section>
<header>
<h1>
<svg xmlns="http://www.w3.org/2000/svg" style="vertical-align: text-bottom; width: 1.2em; height: 1.2em;" class="pr-1" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 3C12.9411 3 21 11.0589 21 21H18C18 12.7157 11.2843 6 3 6V3ZM3 10C9.07513 10 14 14.9249 14 21H11C11 16.5817 7.41828 13 3 13V10ZM3 17C5.20914 17 7 18.7909 7 21H3V17Z"></path>
</svg>
Web Feed Preview
</h1>
<h2 class="title">
<xsl:value-of select="/rss/channel/title" />
</h2>
<p class="description">
<xsl:value-of select="/rss/channel/description" />
</p>
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/link" />
</xsl:attribute>
Visit Website
</a>
</header>
<h2>Recent Items</h2>
<xsl:for-each select="/rss/channel/item">
<article>
<xsl:if test="media:thumbnail/@url">
<img src="{media:thumbnail/@url}" alt="{title}" width="100" height="100" />
</xsl:if>
<div class="timestamp">
<time>
<xsl:value-of select="pubDate" />
</time>
</div>
</xsl:for-each>
</div>
</body>
<h3>
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="link" />
</xsl:attribute>
<xsl:value-of select="title" />
</a>
</h3>
<small>
<xsl:value-of select="description" disable-output-escaping="yes" />
</small>
</article>
</xsl:for-each>
</section>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View File

@@ -21,6 +21,9 @@ export async function GET(context) {
title: site.title,
description: site.description,
site: context.site,
xmlns: {
media: 'http://search.yahoo.com/mrss/',
},
items: [
...journal.map((post) => {
// Filter out import statements from content
@@ -48,18 +51,37 @@ export async function GET(context) {
},
});
// Logic to determine image URL
const isWebp =
post.data.cover.startsWith('/assets/images/cover/') &&
post.data.cover.endsWith('.webp');
const imgUrl = isWebp
? post.data.cover
.replace('/assets/images/cover/', '/assets/images/thumbnail/')
.replace(/\.webp$/, '.jpg')
: '/assets/images/thumbnail/bonsai.jpg';
return {
title: post.data.title,
pubDate: post.data.date,
description: `<![CDATA[${post.data.description}]]>`,
description: post.data.description,
link: `/${post.slug}/`,
content: `<![CDATA[${sanitizedContent}]]>`,
content: sanitizedContent,
enclosure: {
url: `${site.url}${post.data.cover.startsWith('/assets/images/cover/') && post.data.cover.endsWith('.webp') ? post.data.cover.replace('/assets/images/cover/', '/assets/images/og/').replace(/\.webp$/, '.jpg') : '/assets/images/og/bonsai.jpg'}`,
url:
site.url +
(isWebp
? post.data.cover
.replace('/assets/images/cover/', '/assets/images/og/')
.replace(/\.webp$/, '.jpg')
: '/assets/images/og/bonsai.jpg'),
length: 0,
type: 'image/jpeg',
},
customData: '<language>en-us</language>',
customData: `
<language>en-us</language>
<media:thumbnail url="${site.url}${imgUrl}" width="100" height="100" />
`,
};
}),
...haiku.map((item) => {
@@ -69,6 +91,15 @@ export async function GET(context) {
customData: '<language>en-us</language>',
link: `/haiku/${item.slug}/`,
content: `<blockquote><p>${item.data.de}</p><hr /><p>${item.data.en}</p></blockquote>`,
enclosure: {
url: `${site.url}'/assets/images/og/bonsai.jpg`,
length: 0,
type: 'image/jpeg',
},
customData: `
<language>en-us</language>
<media:thumbnail url="${site.url}/assets/images/thumbnail/bonsai.jpg" width="100" height="100" />
`,
};
}),
].sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf()),