feat(blog): add feed xlst options to render beautiful RSS and Atom feeds (#9252)

Co-authored-by: ozakione <29860391+OzakIOne@users.noreply.github.com>
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
Rohan Thakur 2024-08-02 22:20:48 +05:30 committed by GitHub
parent 08a893a2eb
commit 7be1feaa0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 3224 additions and 113 deletions

View file

@ -32,6 +32,8 @@
"website/_dogfooding/_pages tests/diagrams.mdx",
"*.xyz",
"*.docx",
"*.xsl",
"*.xslt",
"*.gitignore",
"versioned_docs",
"*.min.*",

View file

@ -42,6 +42,10 @@ const config: Config = {
},
blog: {
showReadingTime: true,
feedOptions: {
type: ['rss', 'atom'],
xslt: true,
},
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:

View file

@ -48,6 +48,10 @@ const config = {
},
blog: {
showReadingTime: true,
feedOptions: {
type: ['rss', 'atom'],
xslt: true,
},
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:

View file

@ -0,0 +1,75 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
main {
flex: 1 0 auto;
width: 100%;
margin: 2rem auto;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 2rem 0;
padding: 1.6rem 2.4rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #edf5ff;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.4rem;
font-weight: 800;
margin-bottom: 2rem;
display: flex;
align-items: center;
}
h1 .rss-icon {
height: 3.2rem;
width: 3.2rem;
margin-right: 1rem;
}
h2 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.2rem;
}
h3 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.1rem;
}
.blog-description {
font-size: 1.4rem;
margin-bottom: 0.6rem;
}
.blog-post-date {
font-size: 1rem;
line-height: 1.4rem;
font-style: italic;
color: #797b7e;
}
.blog-post-description {
font-size: 1rem;
line-height: 1.4rem;
color: #434349;
}

View file

@ -0,0 +1,92 @@
<?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">
<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>Atom Feed | <xsl:value-of
select="atom:feed/atom:title"
/></title>
<link rel="stylesheet" href="atom.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an Atom feed</strong>. Subscribe by copying the URL
from the address bar into your newsreader. Visit
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free.
</div>
<h1>
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill: #f78422"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill: #ffffff"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill: #ffffff"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill: #ffffff"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
<xsl:value-of select="atom:feed/atom:title" />
</h1>
<p class="blog-description">
<xsl:value-of select="atom:feed/atom:subtitle" />
</p>
</div>
<h2>Recent Posts</h2>
<div class="blog-posts">
<xsl:for-each select="atom:feed/atom:entry">
<div class="blog-post">
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
select="atom:title"
/></a></h3>
<div class="blog-post-date">
Published on <xsl:value-of
select="substring(atom:updated,0,11)"
/>
</div>
<div class="blog-post-description">
<xsl:value-of select="atom:summary" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,75 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
main {
flex: 1 0 auto;
width: 100%;
margin: 2rem auto;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 2rem 0;
padding: 1.6rem 2.4rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #edf5ff;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.4rem;
font-weight: 800;
margin-bottom: 2rem;
display: flex;
align-items: center;
}
h1 .rss-icon {
height: 3.2rem;
width: 3.2rem;
margin-right: 1rem;
}
h2 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.2rem;
}
h3 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.1rem;
}
.blog-description {
font-size: 1.4rem;
margin-bottom: 0.6rem;
}
.blog-post-date {
font-size: 1rem;
line-height: 1.4rem;
font-style: italic;
color: #797b7e;
}
.blog-post-description {
font-size: 1rem;
line-height: 1.4rem;
color: #434349;
}

View file

@ -0,0 +1,86 @@
<?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">
<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>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
<link rel="stylesheet" href="rss.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an RSS feed</strong>. Subscribe by copying the URL
from the address bar into your newsreader. Visit
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free.
</div>
<h1>
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill: #f78422"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill: #ffffff"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill: #ffffff"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill: #ffffff"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
<xsl:value-of select="rss/channel/title" />
</h1>
<p class="blog-description">
<xsl:value-of select="rss/channel/description" />
</p>
</div>
<h2>Recent Posts</h2>
<div class="blog-posts">
<xsl:for-each select="rss/channel/item">
<div class="blog-post">
<h3><a href="{link}"><xsl:value-of select="title" /></a></h3>
<div class="blog-post-date">
Published on <xsl:value-of select="substring(pubDate,0,17)" />
</div>
<div class="blog-post-description">
<xsl:value-of select="description" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -59,6 +59,7 @@
"node": ">=18.0"
},
"devDependencies": {
"@total-typescript/shoehorn": "^0.1.2"
"@total-typescript/shoehorn": "^0.1.2",
"tree-node-cli": "^1.6.0"
}
}

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,65 @@
<?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">
<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>Atom Feed | <xsl:value-of select="atom:feed/atom:title" /></title>
<link rel="stylesheet" href="custom-atom.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an Atom feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731" xml:space="preserve">
<g>
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
<g>
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
</g>
</g>
</svg>
</div>
Custom Atom Feed Preview </h1>
<h2>
<xsl:value-of select="atom:feed/atom:title" />
</h2>
<p>Description: <xsl:value-of select="atom:feed/atom:subtitle" /></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="atom:feed/atom:entry">
<div class="pb-7">
<a href="{atom:link[@rel='alternate']/@href}">
<xsl:value-of select="atom:title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(atom:updated, 0, 17)" />
</div>
<div class="text-2 text-offset italic">
<xsl:value-of select="atom:summary" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,66 @@
<?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">
<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>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
<link rel="stylesheet" href="custom-rss.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an RSS feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731" xml:space="preserve">
<g>
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
<g>
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
</g>
</g>
</svg>
</div>
Custom RSS Feed Preview </h1>
<h2>
<xsl:value-of select="rss/channel/title" />
</h2>
<p>Description: <xsl:value-of select="rss/channel/description" /></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="rss/channel/item">
<div class="pb-7">
<a href="{link}">
<xsl:value-of select="title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(pubDate,0,17)" />
</div>
<div class="text-2 text-offset italic">
<xsl:value-of
select="description" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,75 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
main {
flex: 1 0 auto;
width: 100%;
margin: 2rem auto;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 2rem 0;
padding: 1.6rem 2.4rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #edf5ff;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.4rem;
font-weight: 800;
margin-bottom: 2rem;
display: flex;
align-items: center;
}
h1 .rss-icon {
height: 3.2rem;
width: 3.2rem;
margin-right: 1rem;
}
h2 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.2rem;
}
h3 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.1rem;
}
.blog-description {
font-size: 1.4rem;
margin-bottom: 0.6rem;
}
.blog-post-date {
font-size: 1rem;
line-height: 1.4rem;
font-style: italic;
color: #797b7e;
}
.blog-post-description {
font-size: 1rem;
line-height: 1.4rem;
color: #434349;
}

View file

@ -0,0 +1,92 @@
<?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">
<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>Atom Feed | <xsl:value-of
select="atom:feed/atom:title"
/></title>
<link rel="stylesheet" href="atom.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an Atom feed</strong>. Subscribe by copying the URL
from the address bar into your newsreader. Visit
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free.
</div>
<h1>
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill: #f78422"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill: #ffffff"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill: #ffffff"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill: #ffffff"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
<xsl:value-of select="atom:feed/atom:title" />
</h1>
<p class="blog-description">
<xsl:value-of select="atom:feed/atom:subtitle" />
</p>
</div>
<h2>Recent Posts</h2>
<div class="blog-posts">
<xsl:for-each select="atom:feed/atom:entry">
<div class="blog-post">
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
select="atom:title"
/></a></h3>
<div class="blog-post-date">
Published on <xsl:value-of
select="substring(atom:updated,0,11)"
/>
</div>
<div class="blog-post-description">
<xsl:value-of select="atom:summary" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,65 @@
<?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">
<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>Atom Feed | <xsl:value-of select="atom:feed/atom:title" /></title>
<link rel="stylesheet" href="custom-atom.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an Atom feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731" xml:space="preserve">
<g>
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
<g>
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
</g>
</g>
</svg>
</div>
Custom Atom Feed Preview </h1>
<h2>
<xsl:value-of select="atom:feed/atom:title" />
</h2>
<p>Description: <xsl:value-of select="atom:feed/atom:subtitle" /></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="atom:feed/atom:entry">
<div class="pb-7">
<a href="{atom:link[@rel='alternate']/@href}">
<xsl:value-of select="atom:title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(atom:updated, 0, 17)" />
</div>
<div class="text-2 text-offset italic">
<xsl:value-of select="atom:summary" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,66 @@
<?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">
<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>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
<link rel="stylesheet" href="custom-rss.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an RSS feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731" xml:space="preserve">
<g>
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
<g>
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
</g>
</g>
</svg>
</div>
Custom RSS Feed Preview </h1>
<h2>
<xsl:value-of select="rss/channel/title" />
</h2>
<p>Description: <xsl:value-of select="rss/channel/description" /></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="rss/channel/item">
<div class="pb-7">
<a href="{link}">
<xsl:value-of select="title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(pubDate,0,17)" />
</div>
<div class="text-2 text-offset italic">
<xsl:value-of
select="description" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,75 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
main {
flex: 1 0 auto;
width: 100%;
margin: 2rem auto;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 2rem 0;
padding: 1.6rem 2.4rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #edf5ff;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.4rem;
font-weight: 800;
margin-bottom: 2rem;
display: flex;
align-items: center;
}
h1 .rss-icon {
height: 3.2rem;
width: 3.2rem;
margin-right: 1rem;
}
h2 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.2rem;
}
h3 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.1rem;
}
.blog-description {
font-size: 1.4rem;
margin-bottom: 0.6rem;
}
.blog-post-date {
font-size: 1rem;
line-height: 1.4rem;
font-style: italic;
color: #797b7e;
}
.blog-post-description {
font-size: 1rem;
line-height: 1.4rem;
color: #434349;
}

View file

@ -0,0 +1,86 @@
<?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">
<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>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
<link rel="stylesheet" href="rss.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an RSS feed</strong>. Subscribe by copying the URL
from the address bar into your newsreader. Visit
<a href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free.
</div>
<h1>
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill: #f78422"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill: #ffffff"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill: #ffffff"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill: #ffffff"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
<xsl:value-of select="rss/channel/title" />
</h1>
<p class="blog-description">
<xsl:value-of select="rss/channel/description" />
</p>
</div>
<h2>Recent Posts</h2>
<div class="blog-posts">
<xsl:for-each select="rss/channel/item">
<div class="blog-post">
<h3><a href="{link}"><xsl:value-of select="title" /></a></h3>
<div class="blog-post-date">
Published on <xsl:value-of select="substring(pubDate,0,17)" />
</div>
<div class="blog-post-description">
<xsl:value-of select="description" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateOptions throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`;
exports[`validateOptions feed throws Error in case of invalid feed type 1`] = `""feedOptions.type" does not match any of the allowed types"`;
exports[`validateOptions throws Error in case of invalid options 1`] = `""postsPerPage" must be greater than or equal to 1"`;

View file

@ -10,13 +10,15 @@ import path from 'path';
import fs from 'fs-extra';
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils';
import {fromPartial} from '@total-typescript/shoehorn';
import {DEFAULT_OPTIONS} from '../options';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import tree from 'tree-node-cli';
import {DEFAULT_OPTIONS, validateOptions} from '../options';
import {generateBlogPosts} from '../blogUtils';
import {createBlogFeedFiles} from '../feed';
import {getAuthorsMap} from '../authorsMap';
import type {LoadContext, I18n} from '@docusaurus/types';
import type {LoadContext, I18n, Validate} from '@docusaurus/types';
import type {BlogContentPaths} from '../types';
import type {PluginOptions} from '@docusaurus/plugin-content-blog';
import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog';
const DefaultI18N: I18n = {
currentLocale: 'en',
@ -50,8 +52,16 @@ function getBlogContentPaths(siteDir: string): BlogContentPaths {
async function testGenerateFeeds(
context: LoadContext,
options: PluginOptions,
optionsInput: Options,
): Promise<void> {
const options = validateOptions({
validate: normalizePluginOptions as Validate<
Options | undefined,
PluginOptions
>,
options: optionsInput,
});
const contentPaths = getBlogContentPaths(context.siteDir);
const authorsMap = await getAuthorsMap({
contentPaths,
@ -72,10 +82,11 @@ async function testGenerateFeeds(
siteConfig: context.siteConfig,
outDir: context.outDir,
locale: 'en',
contentPaths,
});
}
describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
describe.each(['atom', 'rss', 'json'] as const)('%s', (feedType) => {
const fsMock = jest.spyOn(fs, 'outputFile').mockImplementation(() => {});
it('does not get generated without posts', async () => {
@ -105,13 +116,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
feedOptions: {
type: [feedType],
copyright: 'Copyright',
xslt: {atom: null, rss: null},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
} as PluginOptions,
},
);
expect(fsMock).toHaveBeenCalledTimes(0);
@ -148,13 +160,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
feedOptions: {
type: [feedType],
copyright: 'Copyright',
xslt: {atom: null, rss: null},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
} as PluginOptions,
},
);
expect(
@ -203,13 +216,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
...rest,
});
},
xslt: {atom: null, rss: null},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
} as PluginOptions,
},
);
expect(
@ -249,13 +263,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
type: [feedType],
copyright: 'Copyright',
limit: 2,
xslt: {atom: null, rss: null},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
} as PluginOptions,
},
);
expect(
@ -295,13 +310,14 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
feedOptions: {
type: [feedType],
copyright: 'Copyright',
xslt: {atom: null, rss: null},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
} as PluginOptions,
},
);
expect(
@ -309,4 +325,99 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
).toMatchSnapshot();
fsMock.mockClear();
});
it('has xslt files for feed', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
}),
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
xslt: true,
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
},
);
expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree');
expect(fsMock.mock.calls).toMatchSnapshot();
fsMock.mockClear();
});
it('has custom xslt files for feed', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
markdown,
};
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
fromPartial({
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
}),
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
xslt: {
rss: 'custom-rss.xsl',
atom: 'custom-atom.xsl',
},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
onInlineTags: 'ignore',
onInlineAuthors: 'ignore',
},
);
expect(tree(path.join(outDir, 'blog'))).toMatchSnapshot('blog tree');
expect(fsMock.mock.calls).toMatchSnapshot();
fsMock.mockClear();
});
});

View file

@ -6,8 +6,12 @@
*/
import {normalizePluginOptions} from '@docusaurus/utils-validation';
import {validateOptions, DEFAULT_OPTIONS} from '../options';
import type {Options, PluginOptions} from '@docusaurus/plugin-content-blog';
import {validateOptions, DEFAULT_OPTIONS, XSLTBuiltInPaths} from '../options';
import type {
Options,
PluginOptions,
UserFeedOptions,
} from '@docusaurus/plugin-content-blog';
import type {Validate} from '@docusaurus/types';
function testValidate(options?: Options) {
@ -38,7 +42,10 @@ describe('validateOptions', () => {
it('accepts correctly defined user options', () => {
const userOptions: Options = {
...defaultOptions,
feedOptions: {type: 'rss' as const, title: 'myTitle'},
feedOptions: {
type: 'rss' as const,
title: 'myTitle',
},
path: 'not_blog',
routeBasePath: '/myBlog',
postsPerPage: 5,
@ -48,7 +55,13 @@ describe('validateOptions', () => {
};
expect(testValidate(userOptions)).toEqual({
...userOptions,
feedOptions: {type: ['rss'], title: 'myTitle', copyright: '', limit: 20},
feedOptions: {
type: ['rss'],
title: 'myTitle',
copyright: '',
limit: 20,
xslt: {rss: null, atom: null},
},
});
});
@ -79,60 +92,182 @@ describe('validateOptions', () => {
).toThrowErrorMatchingSnapshot();
});
it('throws Error in case of invalid feed type', () => {
expect(() =>
testValidate({
describe('feed', () => {
it('throws Error in case of invalid feed type', () => {
expect(() =>
testValidate({
feedOptions: {
// @ts-expect-error: test
type: 'none',
},
}),
).toThrowErrorMatchingSnapshot();
});
it('converts all feed type to array with other feed type', () => {
expect(
testValidate({
feedOptions: {type: 'all'},
}),
).toEqual({
...defaultOptions,
feedOptions: {
// @ts-expect-error: test
type: 'none',
type: ['rss', 'atom', 'json'],
copyright: '',
limit: 20,
xslt: {rss: null, atom: null},
},
}),
).toThrowErrorMatchingSnapshot();
});
it('converts all feed type to array with other feed type', () => {
expect(
testValidate({
feedOptions: {type: 'all'},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: ['rss', 'atom', 'json'], copyright: '', limit: 20},
});
});
});
it('accepts null type and return same', () => {
expect(
testValidate({
feedOptions: {type: null},
}),
).toEqual({
...defaultOptions,
feedOptions: {type: null, limit: 20},
it('accepts null feed type and return same', () => {
expect(
testValidate({
feedOptions: {type: null},
}),
).toEqual({
...defaultOptions,
feedOptions: {
type: null,
limit: 20,
xslt: {rss: null, atom: null},
},
});
});
});
it('contains array with rss + atom for missing feed type', () => {
expect(
testValidate({
feedOptions: {},
}),
).toEqual(defaultOptions);
});
it('contains array with rss + atom for missing feed type', () => {
expect(
testValidate({
feedOptions: {},
}),
).toEqual(defaultOptions);
});
it('has array with rss + atom, title for missing feed type', () => {
expect(
testValidate({
feedOptions: {title: 'title'},
}),
).toEqual({
...defaultOptions,
feedOptions: {
type: ['rss', 'atom'],
title: 'title',
copyright: '',
limit: 20,
},
it('has array with rss + atom, title for missing feed type', () => {
expect(
testValidate({
feedOptions: {title: 'title'},
}),
).toEqual({
...defaultOptions,
feedOptions: {
type: ['rss', 'atom'],
title: 'title',
copyright: '',
limit: 20,
xslt: {rss: null, atom: null},
},
});
});
describe('feed xslt', () => {
function testXSLT(xslt: UserFeedOptions['xslt']) {
return testValidate({feedOptions: {xslt}}).feedOptions.xslt;
}
it('accepts xslt: true', () => {
expect(testXSLT(true)).toEqual({
rss: XSLTBuiltInPaths.rss,
atom: XSLTBuiltInPaths.atom,
});
});
it('accepts xslt: false', () => {
expect(testXSLT(false)).toEqual({
rss: null,
atom: null,
});
});
it('accepts xslt: null', () => {
expect(testXSLT(null)).toEqual({
rss: null,
atom: null,
});
});
it('accepts xslt: undefined', () => {
expect(testXSLT(undefined)).toEqual({
rss: null,
atom: null,
});
});
it('accepts xslt: {rss: true}', () => {
expect(testXSLT({rss: true})).toEqual({
rss: XSLTBuiltInPaths.rss,
atom: null,
});
});
it('accepts xslt: {atom: true}', () => {
expect(testXSLT({atom: true})).toEqual({
rss: null,
atom: XSLTBuiltInPaths.atom,
});
});
it('accepts xslt: {rss: true, atom: true}', () => {
expect(testXSLT({rss: true, atom: true})).toEqual({
rss: XSLTBuiltInPaths.rss,
atom: XSLTBuiltInPaths.atom,
});
});
it('accepts xslt: {rss: "custom-path"}', () => {
expect(testXSLT({rss: 'custom-path'})).toEqual({
rss: 'custom-path',
atom: null,
});
});
it('accepts xslt: {rss: true, atom: "custom-path"}', () => {
expect(testXSLT({rss: true, atom: 'custom-path'})).toEqual({
rss: XSLTBuiltInPaths.rss,
atom: 'custom-path',
});
});
it('accepts xslt: {rss: null, atom: true}', () => {
expect(testXSLT({rss: null, atom: true})).toEqual({
rss: null,
atom: XSLTBuiltInPaths.atom,
});
});
it('accepts xslt: {rss: false, atom: null}', () => {
expect(testXSLT({rss: false, atom: null})).toEqual({
rss: null,
atom: null,
});
});
it('rejects xslt: 42', () => {
// @ts-expect-error: bad type
expect(() => testXSLT(42)).toThrowErrorMatchingInlineSnapshot(
`""feedOptions.xslt" must be one of [object, boolean]"`,
);
});
it('rejects xslt: []', () => {
// @ts-expect-error: bad type
expect(() => testXSLT([])).toThrowErrorMatchingInlineSnapshot(
`""feedOptions.xslt" must be one of [object, boolean]"`,
);
});
it('rejects xslt: {rss: 42}', () => {
// @ts-expect-error: bad type
expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot(
`""feedOptions.xslt.rss" must be one of [string, boolean]"`,
);
});
it('rejects xslt: {rss: []}', () => {
// @ts-expect-error: bad type
expect(() => testXSLT({rss: 42})).toThrowErrorMatchingInlineSnapshot(
`""feedOptions.xslt.rss" must be one of [string, boolean]"`,
);
});
});
});

View file

@ -7,15 +7,20 @@
import path from 'path';
import fs from 'fs-extra';
import logger from '@docusaurus/logger';
import {Feed, type Author as FeedAuthor} from 'feed';
import * as srcset from 'srcset';
import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
import {
getDataFilePath,
normalizeUrl,
readOutputHTMLFile,
} from '@docusaurus/utils';
import {
blogPostContainerID,
applyTrailingSlash,
} from '@docusaurus/utils-common';
import {load as cheerioLoad} from 'cheerio';
import logger from '@docusaurus/logger';
import type {BlogContentPaths} from './types';
import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
import type {
FeedType,
@ -23,6 +28,8 @@ import type {
Author,
BlogPost,
BlogFeedItem,
FeedOptions,
FeedXSLTOptions,
} from '@docusaurus/plugin-content-blog';
async function generateBlogFeed({
@ -180,32 +187,144 @@ async function defaultCreateFeedItems({
);
}
async function resolveXsltFilePaths({
xsltFilePath,
contentPaths,
}: {
xsltFilePath: string;
contentPaths: BlogContentPaths;
}) {
const xsltAbsolutePath: string = path.isAbsolute(xsltFilePath)
? xsltFilePath
: (await getDataFilePath({filePath: xsltFilePath, contentPaths})) ??
path.resolve(contentPaths.contentPath, xsltFilePath);
if (!(await fs.pathExists(xsltAbsolutePath))) {
throw new Error(
logger.interpolate`Blog feed XSLT file not found at path=${path.relative(
process.cwd(),
xsltAbsolutePath,
)}`,
);
}
const parsedPath = path.parse(xsltAbsolutePath);
const cssAbsolutePath = path.resolve(
parsedPath.dir,
`${parsedPath.name}.css`,
);
if (!(await fs.pathExists(xsltAbsolutePath))) {
throw new Error(
logger.interpolate`Blog feed XSLT file was found at path=${path.relative(
process.cwd(),
xsltAbsolutePath,
)}
But its expected co-located CSS file could not be found at path=${path.relative(
process.cwd(),
cssAbsolutePath,
)}
If you want to provide a custom XSLT file, you must provide a CSS file with the exact same name.`,
);
}
return {xsltAbsolutePath, cssAbsolutePath};
}
async function generateXsltFiles({
xsltFilePath,
generatePath,
contentPaths,
}: {
xsltFilePath: string;
generatePath: string;
contentPaths: BlogContentPaths;
}) {
const {xsltAbsolutePath, cssAbsolutePath} = await resolveXsltFilePaths({
xsltFilePath,
contentPaths,
});
const xsltOutputPath = path.join(
generatePath,
path.basename(xsltAbsolutePath),
);
const cssOutputPath = path.join(generatePath, path.basename(cssAbsolutePath));
await fs.copy(xsltAbsolutePath, xsltOutputPath);
await fs.copy(cssAbsolutePath, cssOutputPath);
}
// This modifies the XML feed content to add a relative href to the XSLT file
// Good enough for now: we probably don't need a full XML parser just for that
// See also https://darekkay.com/blog/rss-styling/
function injectXslt({
feedContent,
xsltFilePath,
}: {
feedContent: string;
xsltFilePath: string;
}) {
return feedContent.replace(
'<?xml version="1.0" encoding="utf-8"?>',
`<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="${path.basename(
xsltFilePath,
)}"?>`,
);
}
const FeedConfigs: Record<
FeedType,
{
outputFileName: string;
getContent: (feed: Feed) => string;
getXsltFilePath: (xslt: FeedXSLTOptions) => string | null;
}
> = {
rss: {
outputFileName: 'rss.xml',
getContent: (feed) => feed.rss2(),
getXsltFilePath: (xslt) => xslt.rss,
},
atom: {
outputFileName: 'atom.xml',
getContent: (feed) => feed.atom1(),
getXsltFilePath: (xslt) => xslt.atom,
},
json: {
outputFileName: 'feed.json',
getContent: (feed) => feed.json1(),
getXsltFilePath: () => null,
},
};
async function createBlogFeedFile({
feed,
feedType,
generatePath,
feedOptions,
contentPaths,
}: {
feed: Feed;
feedType: FeedType;
generatePath: string;
feedOptions: FeedOptions;
contentPaths: BlogContentPaths;
}) {
const [feedContent, feedPath] = (() => {
switch (feedType) {
case 'rss':
return [feed.rss2(), 'rss.xml'];
case 'json':
return [feed.json1(), 'feed.json'];
case 'atom':
return [feed.atom1(), 'atom.xml'];
default:
throw new Error(`Feed type ${feedType} not supported.`);
}
})();
try {
await fs.outputFile(path.join(generatePath, feedPath), feedContent);
const feedConfig = FeedConfigs[feedType];
let feedContent = feedConfig.getContent(feed);
const xsltFilePath = feedConfig.getXsltFilePath(feedOptions.xslt);
if (xsltFilePath) {
await generateXsltFiles({xsltFilePath, contentPaths, generatePath});
feedContent = injectXslt({feedContent, xsltFilePath});
}
const outputPath = path.join(generatePath, feedConfig.outputFileName);
await fs.outputFile(outputPath, feedContent);
} catch (err) {
logger.error(`Generating ${feedType} feed failed.`);
throw err;
throw new Error(`Generating ${feedType} feed failed.`, {
cause: err as Error,
});
}
}
@ -222,12 +341,14 @@ export async function createBlogFeedFiles({
siteConfig,
outDir,
locale,
contentPaths,
}: {
blogPosts: BlogPost[];
options: PluginOptions;
siteConfig: DocusaurusConfig;
outDir: string;
locale: string;
contentPaths: BlogContentPaths;
}): Promise<void> {
const blogPosts = allBlogPosts.filter(shouldBeInFeed);
@ -250,6 +371,8 @@ export async function createBlogFeedFiles({
feed,
feedType,
generatePath: path.join(outDir, options.routeBasePath),
feedOptions: options.feedOptions,
contentPaths,
}),
),
);

View file

@ -388,6 +388,7 @@ export default async function pluginContentBlog(
outDir,
siteConfig,
locale: currentLocale,
contentPaths,
});
},

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import {
Joi,
RemarkPluginsSchema,
@ -19,11 +20,20 @@ import type {
PluginOptions,
Options,
FeedType,
FeedXSLTOptions,
} from '@docusaurus/plugin-content-blog';
import type {OptionValidationContext} from '@docusaurus/types';
export const DEFAULT_OPTIONS: PluginOptions = {
feedOptions: {type: ['rss', 'atom'], copyright: '', limit: 20},
feedOptions: {
type: ['rss', 'atom'],
copyright: '',
limit: 20,
xslt: {
rss: null,
atom: null,
},
},
beforeDefaultRehypePlugins: [],
beforeDefaultRemarkPlugins: [],
admonitions: true,
@ -64,6 +74,94 @@ export const DEFAULT_OPTIONS: PluginOptions = {
onInlineAuthors: 'warn',
};
export const XSLTBuiltInPaths = {
rss: path.resolve(__dirname, '..', 'assets', 'rss.xsl'),
atom: path.resolve(__dirname, '..', 'assets', 'atom.xsl'),
};
function normalizeXsltOption(
option: string | null | boolean,
type: 'rss' | 'atom',
): string | null {
if (typeof option === 'string') {
return option;
}
if (option === true) {
return XSLTBuiltInPaths[type];
}
return null;
}
function createXSLTFilePathSchema(type: 'atom' | 'rss') {
return Joi.alternatives()
.try(
Joi.string().required(),
Joi.boolean()
.allow(null, () => undefined)
.custom((val) => normalizeXsltOption(val, type)),
)
.optional()
.default(null);
}
const FeedXSLTOptionsSchema = Joi.alternatives()
.try(
Joi.object<FeedXSLTOptions>({
rss: createXSLTFilePathSchema('rss'),
atom: createXSLTFilePathSchema('atom'),
}).required(),
Joi.boolean()
.allow(null, () => undefined)
.custom((val) => ({
rss: normalizeXsltOption(val, 'rss'),
atom: normalizeXsltOption(val, 'atom'),
})),
)
.optional()
.custom((val) => {
if (val === null) {
return {
rss: null,
atom: null,
};
}
return val;
})
.default(DEFAULT_OPTIONS.feedOptions.xslt);
const FeedOptionsSchema = Joi.object({
type: Joi.alternatives()
.try(
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
Joi.alternatives().conditional(
Joi.string().equal('all', 'rss', 'atom', 'json'),
{
then: Joi.custom((val: FeedType | 'all') =>
val === 'all' ? ['rss', 'atom', 'json'] : [val],
),
},
),
)
.allow(null)
.default(DEFAULT_OPTIONS.feedOptions.type),
xslt: FeedXSLTOptionsSchema,
title: Joi.string().allow(''),
description: Joi.string().allow(''),
// Only add default value when user actually wants a feed (type is not null)
copyright: Joi.when('type', {
is: Joi.any().valid(null),
then: Joi.string().optional(),
otherwise: Joi.string()
.allow('')
.default(DEFAULT_OPTIONS.feedOptions.copyright),
}),
language: Joi.string(),
createFeedItems: Joi.function(),
limit: Joi.alternatives()
.try(Joi.number(), Joi.valid(null), Joi.valid(false))
.default(DEFAULT_OPTIONS.feedOptions.limit),
}).default(DEFAULT_OPTIONS.feedOptions);
const PluginOptionSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
archiveBasePath: Joi.string()
@ -116,37 +214,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
beforeDefaultRehypePlugins: RehypePluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
),
feedOptions: Joi.object({
type: Joi.alternatives()
.try(
Joi.array().items(Joi.string().equal('rss', 'atom', 'json')),
Joi.alternatives().conditional(
Joi.string().equal('all', 'rss', 'atom', 'json'),
{
then: Joi.custom((val: FeedType | 'all') =>
val === 'all' ? ['rss', 'atom', 'json'] : [val],
),
},
),
)
.allow(null)
.default(DEFAULT_OPTIONS.feedOptions.type),
title: Joi.string().allow(''),
description: Joi.string().allow(''),
// Only add default value when user actually wants a feed (type is not null)
copyright: Joi.when('type', {
is: Joi.any().valid(null),
then: Joi.string().optional(),
otherwise: Joi.string()
.allow('')
.default(DEFAULT_OPTIONS.feedOptions.copyright),
}),
language: Joi.string(),
createFeedItems: Joi.function(),
limit: Joi.alternatives()
.try(Joi.number(), Joi.valid(null), Joi.valid(false))
.default(DEFAULT_OPTIONS.feedOptions.limit),
}).default(DEFAULT_OPTIONS.feedOptions),
feedOptions: FeedOptionsSchema,
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),
sortPosts: Joi.string()

View file

@ -315,10 +315,26 @@ declare module '@docusaurus/plugin-content-blog' {
}) => string | undefined;
export type FeedType = 'rss' | 'atom' | 'json';
export type FeedXSLTOptions = {
/**
* RSS XSLT file path, relative to the blog content folder.
* If null, no XSLT file is used and the feed will be displayed as raw XML.
*/
rss: string | null;
/**
* Atom XSLT file path, relative to the blog content folder.
* If null, no XSLT file is used and the feed will be displayed as raw XML.
*/
atom: string | null;
};
/**
* Normalized feed options used within code.
*/
export type FeedOptions = {
/** Enable feeds xslt stylesheets */
xslt: FeedXSLTOptions;
/** If `null`, no feed is generated. */
type?: FeedType[] | null;
/** Title of generated feed. */
@ -507,6 +523,14 @@ declare module '@docusaurus/plugin-content-blog' {
onInlineAuthors: 'ignore' | 'log' | 'warn' | 'throw';
};
export type UserFeedXSLTOptions =
| boolean
| null
| {
rss?: string | boolean | null;
atom?: string | boolean | null;
};
/**
* Feed options, as provided by user config. `type` accepts `all` as shortcut
*/
@ -515,6 +539,8 @@ declare module '@docusaurus/plugin-content-blog' {
{
/** Type of feed to be generated. Use `null` to disable generation. */
type?: FeedOptions['type'] | 'all' | FeedType;
/** User-provided XSLT config for feeds, un-normalized */
xslt?: UserFeedXSLTOptions;
}
>;
/**

View file

@ -412,6 +412,8 @@ webpackbar
webstorm
Wolcott
Xplorer
xslt
XSLT
XSOAR
Yacop
yangshun

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,94 @@
<?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">
<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>Atom Feed | <xsl:value-of
select="atom:feed/atom:title"
/></title>
<link rel="stylesheet" href="custom-atom.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an Atom feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a
href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill:#F78422;"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill:#FFFFFF;"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill:#FFFFFF;"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill:#FFFFFF;"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
Custom Atom Feed Preview </h1>
<h2>
<xsl:value-of select="atom:feed/atom:title" />
</h2>
<p>Description: <xsl:value-of
select="atom:feed/atom:subtitle"
/></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="atom:feed/atom:entry">
<div class="pb-7">
<a href="{atom:link[@rel='alternate']/@href}">
<xsl:value-of select="atom:title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(atom:updated, 0, 17)"
/>
</div>
<div class="text-2 text-offset italic">
<xsl:value-of select="atom:summary" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
* {
color: #0d1137;
}
main {
flex: 1 0 auto;
width: 100%;
margin: 4rem auto;
padding: 1.5 rem;
max-width: 800px;
/* stylelint-disable-next-line font-family-name-quotes */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.info {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid dodgerblue;
border-left-width: 0.5rem;
border-radius: 0.4rem;
background-color: #e52165;
}
.rss-icon {
height: 3.8rem;
width: 3.8rem;
margin-right: 1rem;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.pb-7 {
padding-bottom: 3rem;
}
a {
color: #005aff;
text-decoration: none;
}
h1 {
text-wrap: balance;
font-size: 3.8rem;
line-height: 1;
font-weight: 800;
margin-bottom: 4rem;
}
h2 {
font-size: 3rem;
line-height: 1.2;
font-weight: 700;
margin-bottom: 3rem;
}
h2:not(:first-child) {
margin-top: 5.8rem;
}
.italic {
font-style: italic;
}

View file

@ -0,0 +1,92 @@
<?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">
<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>RSS Feed | <xsl:value-of select="rss/channel/title" /></title>
<link rel="stylesheet" href="custom-rss.css" />
</head>
<body>
<main>
<div class="description">
<div class="info">
<strong>This is an RSS feed</strong>. Subscribe by copying the URL from the address
bar into your newsreader. Visit <a
href="https://aboutfeeds.com/">About Feeds</a> to learn more
and get started. Its free. </div>
<h1 class="flex items-start">
<div class="rss-icon">
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731"
xml:space="preserve">
<g>
<rect
x="0"
y="0"
style="fill:#F78422;"
width="455.731"
height="455.731"
/>
<g>
<path
style="fill:#FFFFFF;"
d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"
/>
<path
style="fill:#FFFFFF;"
d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"
/>
<circle
style="fill:#FFFFFF;"
cx="109.833"
cy="346.26"
r="46.088"
/>
</g>
</g>
</svg>
</div>
Custom RSS Feed Preview </h1>
<h2>
<xsl:value-of select="rss/channel/title" />
</h2>
<p>Description: <xsl:value-of
select="rss/channel/description"
/></p>
</div>
<h2>Recent Posts</h2>
<div class="postsList">
<xsl:for-each select="rss/channel/item">
<div class="pb-7">
<a href="{link}">
<xsl:value-of select="title" />
</a>
<div class="text-2 text-offset"> Published on <xsl:value-of
select="substring(pubDate,0,17)"
/>
</div>
<div class="text-2 text-offset italic">
<xsl:value-of select="description" />
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -88,6 +88,10 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
type: 'all',
title: 'Docusaurus Tests Blog',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
xslt: {
rss: 'custom-rss.xsl',
atom: 'custom-atom.xsl',
},
},
readingTime: ({content, frontMatter, defaultReadingTime}) =>
frontMatter.hide_reading_time

View file

@ -77,6 +77,7 @@ Accepted fields:
| `feedOptions.title` | `string` | `siteConfig.title` | Title of the feed. |
| `feedOptions.description` | `string` | <code>\`$\{siteConfig.title} Blog\`</code> | Description of the feed. |
| `feedOptions.copyright` | `string` | `undefined` | Copyright message. |
| `feedOptions.xslt` | <code>boolean \| [FeedXSLTOptions](#FeedXSLTOptions)</code> | `undefined` | Copyright message. |
| `feedOptions.language` | `string` (See [documentation](http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes) for possible values) | `undefined` | Language metadata of the feed. |
| `sortPosts` | <code>'descending' \| 'ascending' </code> | `'descending'` | Governs the direction of blog post sorting. |
| `processBlogPosts` | <code>[ProcessBlogPostsFn](#ProcessBlogPostsFn)</code> | `undefined` | An optional function which can be used to transform blog posts (filter, modify, delete, etc...). |
@ -129,6 +130,25 @@ type ReadingTimeFn = (params: {
type FeedType = 'rss' | 'atom' | 'json';
```
#### `FeedXSLTOptions` {#FeedXSLTOptions}
Permits to style the blog XML feeds so that browsers render them nicely with [XSLT](https://developer.mozilla.org/en-US/docs/Web/XSLT).
- Use `true` to let the blog use its built-in `.xsl` and `.css` files to style the blog feed
- Use a falsy value (`undefined | null | false`) to disable the feature
- Use a `string` to provide a file path to a custom `.xsl` file relative to the blog content folder. By convention, you must provide a `.css` file with the exact same name.
```ts
type FeedXSLTOptions =
| boolean
| undefined
| null
| {
rss?: string | boolean | null | undefined;
atom?: string | boolean | null | undefined;
};
```
#### `CreateFeedItemsFn` {#CreateFeedItemsFn}
```ts

View file

@ -602,9 +602,18 @@ type BlogOptions = {
title?: string;
description?: string;
copyright: string;
language?: string; // possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
limit?: number | false | null; // defaults to 20
/** Allow control over the construction of BlogFeedItems */
// XSLT permits browsers to style and render nicely the feed XML files
xslt?:
| boolean
| {
//
rss?: string | boolean;
atom?: string | boolean;
};
// Allow control over the construction of BlogFeedItems
createFeedItems?: (params: {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;

View file

@ -487,7 +487,10 @@ export default async function createConfigAsync() {
postsPerPage: 5,
feedOptions: {
type: 'all',
description:
'Keep up to date with upcoming Docusaurus releases and articles by following our feed!',
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
xslt: true,
},
blogTitle: 'Docusaurus blog',
blogDescription: 'Read blog posts about Docusaurus from the team',