Create and link RSS feed
This commit is contained in:
parent
4595ab7a9f
commit
f26bd2fe9c
16
package-lock.json
generated
16
package-lock.json
generated
@ -1932,6 +1932,14 @@
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"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": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
|
||||
@ -4037,6 +4045,14 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"compression": "^1.7.4",
|
||||
"date-fns": "^2.11.1",
|
||||
"feed": "^4.2.0",
|
||||
"front-matter": "^3.1.0",
|
||||
"marked": "^0.8.2",
|
||||
"polka": "^0.5.2",
|
||||
|
@ -5,6 +5,8 @@
|
||||
import twitchLogo from '../svg/iconfinder_twitch_306173.svg'
|
||||
import instagramLogo from '../svg/iconfinder_38-instagram_1161953.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
|
||||
</script>
|
||||
@ -65,7 +67,7 @@
|
||||
|
||||
.twitter :global(svg path) {
|
||||
/* fill: rgb(29, 161, 242); */
|
||||
stroke: #fff;
|
||||
stroke: #eae9be;
|
||||
stroke-width: 2px;
|
||||
fill: #eae9be;
|
||||
}
|
||||
@ -85,6 +87,14 @@
|
||||
fill: #eae9be;
|
||||
}
|
||||
|
||||
.rss :global(svg) {
|
||||
fill: #eae9be;
|
||||
}
|
||||
|
||||
.json-feed :global(svg) {
|
||||
fill: #eae9be;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
@ -110,6 +120,15 @@
|
||||
color: #a7a574;
|
||||
}
|
||||
|
||||
.subscribe {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: #86856f;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 900px) {
|
||||
.site-footer {
|
||||
font-size: 0.8em;
|
||||
@ -182,6 +201,13 @@
|
||||
</li>
|
||||
{/each}
|
||||
</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 class="socials">
|
||||
<h3>Contact</h3>
|
||||
|
55
src/routes/blog/_content.js
Normal file
55
src/routes/blog/_content.js
Normal 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
|
||||
}
|
||||
|
@ -1,63 +1,10 @@
|
||||
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
|
||||
import { getBlogListing } from './_content'
|
||||
|
||||
export async function get(req, res) {
|
||||
const { tag } = req.query
|
||||
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 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)
|
||||
|
||||
const filteredContents = await getBlogListing(tag)
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
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
39
src/routes/feed/_feed.js
Normal 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
|
||||
}
|
11
src/routes/feed/index.json.js
Normal file
11
src/routes/feed/index.json.js
Normal 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())
|
||||
}
|
||||
|
11
src/routes/feed/index.xml.js
Normal file
11
src/routes/feed/index.xml.js
Normal 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())
|
||||
}
|
||||
|
@ -33,7 +33,11 @@
|
||||
</style>
|
||||
|
||||
<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>
|
||||
|
||||
<header class="index-header">
|
||||
|
20
src/svg/icon.svg
Normal file
20
src/svg/icon.svg
Normal 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 |
1
src/svg/iconfinder_icon-social-rss_211914.svg
Normal file
1
src/svg/iconfinder_icon-social-rss_211914.svg
Normal 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 |
75
src/svg/json_feed_icon.svg
Normal file
75
src/svg/json_feed_icon.svg
Normal 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
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
BIN
static/images/rss-feed-logo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user