Create and link RSS feed

This commit is contained in:
Michal Vanko 2020-06-14 11:33:34 +02:00
parent 4595ab7a9f
commit f26bd2fe9c
14 changed files with 269 additions and 57 deletions

16
package-lock.json generated
View File

@ -1932,6 +1932,14 @@
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
}, },
"feed": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/feed/-/feed-4.2.0.tgz",
"integrity": "sha512-nLU4Fn+5TCJ1Zu9kBDqXPxsaTXaL/hZgZ3pmT87TUzS1kfaL91iIKJ+DFWygL8CrOeYw80z7QWxabkMV/x+g2g==",
"requires": {
"xml-js": "^1.6.11"
}
},
"file-type": { "file-type": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
@ -4037,6 +4045,14 @@
"xtend": "^4.0.0" "xtend": "^4.0.0"
} }
}, },
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"requires": {
"sax": "^1.2.4"
}
},
"xml-parse-from-string": { "xml-parse-from-string": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",

View File

@ -15,6 +15,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"compression": "^1.7.4", "compression": "^1.7.4",
"date-fns": "^2.11.1", "date-fns": "^2.11.1",
"feed": "^4.2.0",
"front-matter": "^3.1.0", "front-matter": "^3.1.0",
"marked": "^0.8.2", "marked": "^0.8.2",
"polka": "^0.5.2", "polka": "^0.5.2",

View File

@ -5,6 +5,8 @@
import twitchLogo from '../svg/iconfinder_twitch_306173.svg' import twitchLogo from '../svg/iconfinder_twitch_306173.svg'
import instagramLogo from '../svg/iconfinder_38-instagram_1161953.svg' import instagramLogo from '../svg/iconfinder_38-instagram_1161953.svg'
import emailIcon from '../svg/iconfinder_mail_5474819.svg' import emailIcon from '../svg/iconfinder_mail_5474819.svg'
import rssIcon from '../svg/iconfinder_icon-social-rss_211914.svg'
import jsonFeedIcon from '../svg/json_feed_icon.svg'
export let latestPosts export let latestPosts
</script> </script>
@ -65,7 +67,7 @@
.twitter :global(svg path) { .twitter :global(svg path) {
/* fill: rgb(29, 161, 242); */ /* fill: rgb(29, 161, 242); */
stroke: #fff; stroke: #eae9be;
stroke-width: 2px; stroke-width: 2px;
fill: #eae9be; fill: #eae9be;
} }
@ -85,6 +87,14 @@
fill: #eae9be; fill: #eae9be;
} }
.rss :global(svg) {
fill: #eae9be;
}
.json-feed :global(svg) {
fill: #eae9be;
}
:global(svg) { :global(svg) {
height: 1em; height: 1em;
width: 1em; width: 1em;
@ -110,6 +120,15 @@
color: #a7a574; color: #a7a574;
} }
.subscribe {
font-weight: bold;
}
hr {
color: #86856f;
margin: 0.2em 0;
}
@media only screen and (min-width: 900px) { @media only screen and (min-width: 900px) {
.site-footer { .site-footer {
font-size: 0.8em; font-size: 0.8em;
@ -182,6 +201,13 @@
</li> </li>
{/each} {/each}
</ul> </ul>
<hr />
<section class="subscribe">
<a href="/feed.xml" title="RSS feed" class="rss">
Subscribe {@html rssIcon}
</a>
<a href="/feed.json" title="JSON feed" class="json-feed">{@html jsonFeedIcon}</a>
</section>
</section> </section>
<section class="socials"> <section class="socials">
<h3>Contact</h3> <h3>Contact</h3>

View File

@ -0,0 +1,55 @@
import { readdir, readFile } from 'fs'
import { promisify } from 'util'
import { basename } from 'path'
import { pipe, partial, prop, sortBy, reverse, filter } from 'ramda'
import fm from 'front-matter'
import marked from 'marked'
const { NODE_ENV } = process.env
export async function getBlogListing(tag) {
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
const filteredFiles = filterDevelopmentFiles(files)
const contents = await Promise.all(
filteredFiles.map(async file => {
const fileContent = await promisify(readFile)(
`_posts/blog/${file}`,
'utf-8'
)
const parsedAttributes = fm(fileContent)
const lineOfTextRegExp = /^(?:\w|\[).+/gm
const lines = parsedAttributes.body
.match(lineOfTextRegExp)
.slice(0, 2)
.join('\n')
const preview = marked(lines)
return {
...parsedAttributes.attributes,
preview,
slug: basename(file, '.md'),
}
})
)
const filteredContents = pipe(
sortBy(prop('date')),
reverse,
filter(article => article.published),
partial(filterByTag, [tag])
)(contents)
return filteredContents
}
function filterDevelopmentFiles(files) {
return NODE_ENV !== 'production'
? files
: files.filter(file => !file.startsWith('dev-'))
}
function filterByTag(tag, contents) {
return tag ? contents.filter(content => content.tags.includes(tag)) : contents
}

View File

@ -1,63 +1,10 @@
import { readdir, readFile } from 'fs' import { getBlogListing } from './_content'
import { promisify } from 'util'
import { basename } from 'path'
import { pipe, partial, prop, sortBy, reverse, filter } from 'ramda'
import fm from 'front-matter'
import marked from 'marked'
const { NODE_ENV } = process.env
export async function get(req, res) { export async function get(req, res) {
const { tag } = req.query const { tag } = req.query
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8') const filteredContents = await getBlogListing(tag)
const filteredFiles = filterDevelopmentFiles(files)
const contents = await Promise.all(
filteredFiles.map(async file => {
const fileContent = await promisify(readFile)(
`_posts/blog/${file}`,
'utf-8'
)
const parsedAttributes = fm(fileContent)
const lineOfTextRegExp = /^(?:\w|\[).+/gm
const sentenceRegExp = /(?:\w|\[).[.?!]/
const lines = parsedAttributes.body
.match(lineOfTextRegExp)
.slice(0, 2)
.join('\n')
const preview = marked(lines)
return {
...parsedAttributes.attributes,
preview,
slug: basename(file, '.md'),
}
})
)
const filteredContents = pipe(
sortBy(prop('date')),
reverse,
filter(article => article.published),
partial(filterByTag, [tag])
)(contents)
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}) })
res.end(JSON.stringify(filteredContents)) res.end(JSON.stringify(filteredContents))
} }
function filterDevelopmentFiles(files) {
return NODE_ENV !== 'production'
? files
: files.filter(file => !file.startsWith('dev-'))
}
function filterByTag(tag, contents) {
return tag ? contents.filter(content => content.tags.includes(tag)) : contents
}
function filterPublished(article) {
return article.published
}

39
src/routes/feed/_feed.js Normal file
View File

@ -0,0 +1,39 @@
import { Feed } from 'feed'
import { getBlogListing } from '../blog/_content'
export async function getFeed() {
const feed = new Feed({
title: 'michalvanko.dev latest posts',
id: 'https://michalvanko.dev',
link: 'https://michalvanko.dev',
description: 'Latest posts published on michalvanko.dev',
copyright: 'All rights reserved 2020, Michal Vanko',
generator: 'sapper with Feed for node.js',
updated: new Date(),
image: 'https://michalvanko.dev/eye.png',
favicon: 'https://michalvanko.dev/m-favicon-192x192.png',
language: 'en',
author: {
name: 'Michal Vanko',
email: 'michalvankosk@gmail.com',
link: 'https://michalvanko.dev',
},
feedLinks: {
json: 'https://michalvanko.dev/feed.json',
rss: 'https://michalvanko.dev/feed.xml',
},
})
const blogListing = await getBlogListing()
blogListing.forEach(post => {
feed.addItem({
title: post.title,
id: `https://michalvanko.dev/blog/${post.slug}`,
link: `https://michalvanko.dev/blog/${post.slug}`,
description: post.preview,
date: post.date,
image: post.thumbnail ? `https://michalvanko.dev/${post.thumbnail}` : undefined,
})
})
return feed
}

View File

@ -0,0 +1,11 @@
import { getFeed } from './_feed'
export async function get(req, res) {
const feed = await getFeed()
res.writeHead(200, {
'Content-Type': 'application/json',
})
res.end(feed.json1())
}

View File

@ -0,0 +1,11 @@
import { getFeed } from './_feed'
export async function get(req, res) {
const feed = await getFeed()
res.writeHead(200, {
'Content-Type': 'application/xml',
})
res.end(feed.rss2())
}

View File

@ -33,7 +33,11 @@
</style> </style>
<svelte:head> <svelte:head>
<title>michalvanko.dev index page</title> <title>Introduction @michalvankodev</title>
<link rel="alternate" type="application/rss+xml" title="RSS feed for latest posts" href="https://michalvanko.dev/feed.xml" />
<link rel="alternate" title="JSON feed for latest posts" type="application/json" href="https://michalvanko.dev/feed.json" />
</svelte:head> </svelte:head>
<header class="index-header"> <header class="index-header">

20
src/svg/icon.svg Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="140px" height="140px" viewBox="0 0 140 140" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>icon</title>
<desc>Created with Sketch.</desc>
<defs/>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon" transform="translate(-4.000000, -14.000000)">
<rect id="background" fill="#8BE028" x="4" y="14" width="140" height="140"/>
<g id="elements" transform="translate(79.549513, 79.549513) rotate(-45.000000) translate(-79.549513, -79.549513) translate(26.049513, 20.549513)">
<path d="M7,64.9835977 L7,65.1904297 C15.2519531,65.1904297 17.9179687,68.4912109 17.9179687,78.4570312 L17.9179687,94.1992187 C17.9179687,110.576172 25.9794922,117.8125 44.2607422,117.8125 L49.8466797,117.8125 L49.8466797,105.942383 L46.9902344,105.942383 C36.6435547,105.942383 32.9619141,102.387695 32.9619141,92.4853516 L32.9619141,74.0136719 C32.9619141,64.4921875 28.7089844,59.2871094 20.3935547,58.5253906 L20.3935547,56.7480469 C28.9628906,55.9228516 32.9619141,51.1621094 32.9619141,41.9580078 L32.9619141,25.390625 C32.9619141,15.2978516 36.5166016,11.8066406 46.9902344,11.8066406 L49.8466797,11.8066406 L49.8466797,0 L44.2607422,0 C25.9160156,0 17.9179687,6.98242187 17.9179687,22.9785156 L17.9179687,36.6259766 C17.9179687,46.5917969 15.1884766,49.9560547 7,49.9560547 L7,50.0164023 C3.09098409,50.2736953 -9.23705556e-14,53.5258787 -9.23705556e-14,57.5 C-9.23705556e-14,61.4741213 3.09098409,64.7263047 7,64.9835977 Z" id="transmitter" fill="#497614"/>
<g id="dots-@-15pt" transform="translate(42.000000, 50.000000)" fill="#FFFFFF">
<circle id="Oval-1" cx="7.5" cy="7.5" r="7.5"/>
<circle id="Oval-2" cx="32.5" cy="7.5" r="7.5"/>
<circle id="Oval-3" cx="57.5" cy="7.5" r="7.5"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M119.9,336.1c-30.8,0-55.9,25.1-55.9,55.8c0,30.8,25.1,55.6,55.9,55.6c30.9,0,55.9-24.9,55.9-55.6 C175.8,361.2,150.8,336.1,119.9,336.1z"/><path d="M64,192v79.9c48,0,94.1,14.2,128,48.1c33.9,33.9,48,79.9,48,128h80C320,308.1,204,192,64,192z"/><path d="M64,64v79.9c171,0,303.9,133,303.9,304.1H448C448,236.3,276,64,64,64z"/></g></svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="json_feed_icon.svg"
id="svg17"
version="1.1"
viewBox="0 0 140 140"
height="140px"
width="140px">
<sodipodi:namedview
inkscape:current-layer="icon"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="80.375472"
inkscape:cx="41.171285"
inkscape:zoom="5.6714286"
showgrid="false"
id="namedview19"
inkscape:window-height="1016"
inkscape:window-width="1920"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<defs
id="defs6" />
<g
id="Page-1">
<g
transform="translate(-4,-14)"
id="icon">
<rect
style="fill:none;stroke-width:0"
height="0"
width="140"
y="1000014"
x="-74"
id="background" />
<g
transform="rotate(-45,91.330213,37.830213)"
id="elements">
<path
d="m 7,64.983598 v 0.206832 c 8.251953,0 10.917969,3.300781 10.917969,13.266601 v 15.742188 c 0,16.376951 8.061523,23.613281 26.342773,23.613281 h 5.585938 v -11.87012 h -2.856446 c -10.346679,0 -14.02832,-3.55469 -14.02832,-13.457028 v -18.47168 c 0,-9.521485 -4.25293,-14.726563 -12.568359,-15.488281 V 56.748047 C 28.962891,55.922852 32.961914,51.162109 32.961914,41.958008 V 25.390625 c 0,-10.092773 3.554688,-13.583984 14.02832,-13.583984 H 49.84668 V 0 H 44.260742 C 25.916016,0 17.917969,6.9824219 17.917969,22.978516 v 13.647461 c 0,9.96582 -2.729492,13.330078 -10.917969,13.330078 v 0.06035 C 3.0909841,50.273695 0,53.525879 0,57.5 c 0,3.974121 3.0909841,7.226305 7,7.483598 z" />
<g
transform="translate(42,50)"
id="dots-@-15pt">
<circle
r="7.5"
cy="7.5"
cx="7.5"
id="Oval-1" />
<circle
r="7.5"
cy="7.5"
cx="32.5"
id="Oval-2" />
<circle
r="7.5"
cy="7.5"
cx="57.5"
id="Oval-3" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/images/json-feed-logo.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
static/images/rss-feed-logo.png (Stored with Git LFS) Normal file

Binary file not shown.