initial feed implementation
This commit is contained in:
parent
a28ee8b2f0
commit
7fb1f05a1e
@ -13,6 +13,9 @@
|
|||||||
export const description = '';
|
export const description = '';
|
||||||
export const draft = false;
|
export const draft = false;
|
||||||
export let toc = null;
|
export let toc = null;
|
||||||
|
|
||||||
|
export let prev = null;
|
||||||
|
export let next = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -46,7 +49,6 @@
|
|||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@ -101,18 +103,23 @@
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="#">
|
{#if prev}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<a href="/{prev}" data-sveltekit-preload-data="hover">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||||
Previous
|
</svg>
|
||||||
</a>
|
Previous
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<a href="#">
|
{#if next}
|
||||||
Next
|
<!-- we use margin-left rather than justify-content so it works regardless of whether the "previous" link exists -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<a href="/{next}" style="margin-left: auto;" data-sveltekit-preload-data="hover">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
Next
|
||||||
</svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
</a>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidenote:before {
|
.sidenote:before {
|
||||||
content: counter(sidenote) " ";
|
content: var(--sidenote-index, counter(sidenote)) " ";
|
||||||
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
|
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
|
||||||
(because the sidenote is floated it counts as a positioned parent, I think) */
|
(because the sidenote is floated it counts as a positioned parent, I think) */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
.sidenote {
|
.sidenote {
|
||||||
--gap: 2rem;
|
--gap: 2rem;
|
||||||
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
|
--sidenote-width: min(16rem, calc(50vw - var(--gap) - 1rem - var(--content-width) / 2));
|
||||||
width: var(--sidenote-width);
|
width: var(--sidenote-width);
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
let currentSubheadingSlug = null;
|
let currentSubheadingSlug = null;
|
||||||
|
|
||||||
function setCurrentHeading() {
|
function setCurrentHeading() {
|
||||||
const start = performance.now();
|
|
||||||
for (const h of headings) {
|
for (const h of headings) {
|
||||||
const yPos = h.getBoundingClientRect().y;
|
const yPos = h.getBoundingClientRect().y;
|
||||||
if (yPos > (window.innerHeight / 3)) {
|
if (yPos > (window.innerHeight / 3)) {
|
||||||
@ -25,8 +24,6 @@
|
|||||||
currentSubheadingSlug = h.id
|
currentSubheadingSlug = h.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const end = performance.now();
|
|
||||||
console.log(`Elapsed: ${end - start}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount (() => {
|
onMount (() => {
|
||||||
|
94
src/lib/xml.js
Normal file
94
src/lib/xml.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// const Node = {
|
||||||
|
// addChild(child) {
|
||||||
|
// this.children.push(child);
|
||||||
|
// return child;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
export function tag(name, attrs, children) {
|
||||||
|
return {
|
||||||
|
type: 'tag',
|
||||||
|
tag: name,
|
||||||
|
attrs: attrs || {},
|
||||||
|
children: children || [],
|
||||||
|
|
||||||
|
addTag(name, attrs, children) {
|
||||||
|
const child = tag(name, attrs, children);
|
||||||
|
this.children.push(child);
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function text(content) {
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
text: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function serialize(node, depth) {
|
||||||
|
if (!depth) {
|
||||||
|
depth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indent = ' '.repeat(depth * 4);
|
||||||
|
let fragments = [];
|
||||||
|
|
||||||
|
// version tag, if this is the top level
|
||||||
|
if (depth === 0) {
|
||||||
|
fragments.push('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
fragments.push(`${indent}<${node.tag}`);
|
||||||
|
|
||||||
|
// this happens if there are multiple text nodes within the same parent
|
||||||
|
if (node.type === 'text') {
|
||||||
|
return `${indent}${escape(node.text)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children === undefined) {
|
||||||
|
console.log(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// opening tag <element attr="value">
|
||||||
|
for (const attr in node.attrs) {
|
||||||
|
fragments.push(` ${attr}="${node.attrs[attr]}"`);
|
||||||
|
}
|
||||||
|
if (node.children.length === 0) {
|
||||||
|
fragments.push(' />');
|
||||||
|
return fragments.join('');
|
||||||
|
}
|
||||||
|
fragments.push('>');
|
||||||
|
|
||||||
|
// if the only child is a single text node, skip recursion and just dump contents directly
|
||||||
|
if (node.children.length === 1 && node.children[0].type === 'text') {
|
||||||
|
const text = escape(node.children[0].text);
|
||||||
|
fragments.push(text);
|
||||||
|
}
|
||||||
|
// otherwise, start a new line for each child node, then recurse
|
||||||
|
else {
|
||||||
|
for (const child of node.children) {
|
||||||
|
fragments.push('\n');
|
||||||
|
fragments.push(serialize(child, depth + 1));
|
||||||
|
}
|
||||||
|
// no need to verify that there were children, we already did that
|
||||||
|
fragments.push(`\n${indent}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragments.push(`</${node.tag}>`);
|
||||||
|
|
||||||
|
return fragments.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function escape(text) {
|
||||||
|
// we aren't going to bother with escaping attributes, so we won't worry about quotes
|
||||||
|
return text
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '>')
|
||||||
|
.replaceAll('>', '<');
|
||||||
|
}
|
@ -1,19 +1,48 @@
|
|||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
import { toString } from 'mdast-util-to-string';
|
import { toString } from 'mdast-util-to-string';
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
// build table of contents and inject into frontmatter
|
// build table of contents and inject into frontmatter
|
||||||
export function localRemark() {
|
export function localRemark() {
|
||||||
return (tree, vfile) => {
|
return (tree, vfile) => {
|
||||||
let toc = [];
|
let toc = [];
|
||||||
|
let description = null;
|
||||||
|
|
||||||
visit(tree, 'heading', node => {
|
visit(tree, ['heading', 'paragraph'], node => {
|
||||||
toc.push({
|
// build table of contents and inject into frontmatter
|
||||||
text: toString(node),
|
if (node.type === 'heading') {
|
||||||
depth: node.depth,
|
toc.push({
|
||||||
});
|
text: toString(node),
|
||||||
|
depth: node.depth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject description (first 25 words of the first paragraph)
|
||||||
|
if (node.type === 'paragraph' && description === null) {
|
||||||
|
description = summarize(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
vfile.data.fm.toc = toc;
|
vfile.data.fm.toc = toc;
|
||||||
|
vfile.data.fm.description = description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// convert paragraph to single string after stripping everything between html tags
|
||||||
|
function summarize(par) {
|
||||||
|
let newChildren = [];
|
||||||
|
let push = true;
|
||||||
|
for (const child of par.children) {
|
||||||
|
if (child.type === 'html') {
|
||||||
|
push = !push;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (push) {
|
||||||
|
newChildren.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toString({type: 'paragraph', children: newChildren});
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ export async function load({ url, params, data }) {
|
|||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// throw error(404, `Not found: ${url.pathname}`);
|
// throw error(404, `Not found: ${url.pathname}`);
|
||||||
|
console.log(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
const posts = import.meta.globEager('./*.svx');
|
const posts = import.meta.globEager('./*.svx');
|
||||||
|
|
||||||
@ -16,6 +15,19 @@ for (const path in posts) {
|
|||||||
postData.push(posts[path].metadata);
|
postData.push(posts[path].metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let ids = new Set();
|
||||||
|
for (const postMeta of postData) {
|
||||||
|
if (postMeta.uuid === undefined) {
|
||||||
|
throw(`Missing UUID for post: ${postMeta.title}`);
|
||||||
|
}
|
||||||
|
if (ids.has(postMeta.uuid)) {
|
||||||
|
throw(`Duplicate UUID in post: ${postMeta.title}`);
|
||||||
|
}
|
||||||
|
ids.add(postMeta.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
postData.sort((a, b) => {
|
postData.sort((a, b) => {
|
||||||
// sorting in reverse, so we flip the intuitive order
|
// sorting in reverse, so we flip the intuitive order
|
||||||
if (a.date > b.date) return -1;
|
if (a.date > b.date) return -1;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: Exposing Docker Containers to your LAN
|
title: Exposing Docker Containers to your LAN
|
||||||
description: If, for some strange reason, you should want to do such a thing.
|
description: If, for some strange reason, you should want to do such a thing.
|
||||||
date: 2022-03-21
|
date: 2022-03-21
|
||||||
|
uuid: 81715fb3-990e-487e-9662-fed7b7d02943
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
import Sidenote from '$lib/Sidenote.svelte';
|
import Sidenote from '$lib/Sidenote.svelte';
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: The Hitchiker's Guide to Mesh VPNs
|
title: The Hitchiker's Guide to Mesh VPNs
|
||||||
description: The golden age of VPNery is upon us.
|
description: The golden age of VPNery is upon us.
|
||||||
date: 2022-03-17
|
date: 2022-03-17
|
||||||
|
uuid: fc6930ef-979c-4851-bc5a-c0e1b1698061
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
import Sidenote from '$lib/Sidenote.svelte';
|
import Sidenote from '$lib/Sidenote.svelte';
|
||||||
|
@ -4,6 +4,7 @@ description: Can we replace passwords with something more user-friendly?
|
|||||||
date: 2021-04-30
|
date: 2021-04-30
|
||||||
draft: true
|
draft: true
|
||||||
dropcap: false
|
dropcap: false
|
||||||
|
uuid: 696020b3-1513-42a8-b346-634d40f0e9d9
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
import Sidenote from '$lib/Sidenote.svelte';
|
import Sidenote from '$lib/Sidenote.svelte';
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: 'Languages: High and Low'
|
title: 'Languages: High and Low'
|
||||||
description: How high is up?
|
description: How high is up?
|
||||||
date: 2022-08-19
|
date: 2022-08-19
|
||||||
|
uuid: 89ae4194-7785-4fac-a841-8bcf5a5a3a2e
|
||||||
draft: true
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: Sidenotes
|
title: Sidenotes
|
||||||
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
|
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
|
||||||
date: 2023-08-14
|
date: 2023-08-14
|
||||||
|
uuid: c514c46e-92f3-4078-a76b-e1dafd5f7e07
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
import Sidenote from '$lib/Sidenote.svelte';
|
import Sidenote from '$lib/Sidenote.svelte';
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: Let's Design A Simpler SocketIO
|
title: Let's Design A Simpler SocketIO
|
||||||
date: 2021-10-16
|
date: 2021-10-16
|
||||||
description: SocketIO is packed with features. But do we really need all of them all the time?
|
description: SocketIO is packed with features. But do we really need all of them all the time?
|
||||||
|
uuid: 95cde7e7-9293-4fab-a0b4-fc6ab7da08c8
|
||||||
draft: true
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: Sufficiently Advanced Technology Is Often Distinguishable From Magic
|
title: Sufficiently Advanced Technology Is Often Distinguishable From Magic
|
||||||
description: I see what Arthur C. Clarke was getting at, but I don't think I agree.
|
description: I see what Arthur C. Clarke was getting at, but I don't think I agree.
|
||||||
date: 2022-05-14
|
date: 2022-05-14
|
||||||
|
uuid: 84636766-eb78-4060-a98d-593d8d5b55c9
|
||||||
draft: true
|
draft: true
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
title: Thoughts on Vue vs Svelte
|
title: Thoughts on Vue vs Svelte
|
||||||
description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences.
|
description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences.
|
||||||
date: 2023-06-29
|
date: 2023-06-29
|
||||||
|
uuid: 8280f0e0-6bf5-43a2-9eac-b8c2508cca29
|
||||||
---
|
---
|
||||||
<script>
|
<script>
|
||||||
import Sidenote from '$lib/Sidenote.svelte';
|
import Sidenote from '$lib/Sidenote.svelte';
|
||||||
|
49
src/routes/feed/+server.js
Normal file
49
src/routes/feed/+server.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { tag, text, serialize } from '$lib/xml.js';
|
||||||
|
import { postData } from '../_posts/all.js';
|
||||||
|
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
return new Response(renderFeed(), {
|
||||||
|
headers: {'Content-Type': 'text/xml'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderFeed() {
|
||||||
|
const feed = tag('feed', {xmlns: 'http://www.w3.org/2005/Atom'});
|
||||||
|
|
||||||
|
feed.addTag('id', {}, [text('https://blog.jfmonty2.com')])
|
||||||
|
feed.addTag('title', {}, [text("Joe's Blog")]);
|
||||||
|
feed.addTag('link', {href: 'https://blog.jfmonty2.com/'});
|
||||||
|
|
||||||
|
const lastUpdate = iso(postData[0].updated || postData[0].date);
|
||||||
|
feed.addTag('updated', {}, [text(lastUpdate)]);
|
||||||
|
|
||||||
|
const author = feed.addTag('author');
|
||||||
|
author.addTag('name', {}, [text('Joseph Montanaro')]);
|
||||||
|
|
||||||
|
for (const post of postData) {
|
||||||
|
const entry = feed.addTag('entry');
|
||||||
|
entry.addTag('title', {}, [text(post.title)]);
|
||||||
|
entry.addTag('link', {rel: 'alternate', href: `https://blog.jfmonty2.com/${post.slug}`});
|
||||||
|
entry.addTag('id', {}, [text(post.uuid)]);
|
||||||
|
|
||||||
|
const publishedDate = iso(post.date);
|
||||||
|
entry.addTag('published', {}, [text(publishedDate)])
|
||||||
|
const updatedDate = iso(post.updated || post.date);
|
||||||
|
entry.addTag('updated', {}, [text(updatedDate)]);
|
||||||
|
|
||||||
|
entry.addTag('content', {type: 'html'}, [text(renderDescription(post))]);
|
||||||
|
}
|
||||||
|
return serialize(feed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderDescription(post) {
|
||||||
|
return `<p>${post.description} <a href="https://blog.jfmonty2.com/${post.slug}">Read more</a></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function iso(datetimeStr) {
|
||||||
|
return new Date(datetimeStr).toISOString();
|
||||||
|
}
|
@ -6,14 +6,13 @@
|
|||||||
<style>
|
<style>
|
||||||
#posts {
|
#posts {
|
||||||
/*text-align: center;*/
|
/*text-align: center;*/
|
||||||
max-width: 24rem;
|
max-width: var(--content-width);
|
||||||
margin-left: auto;
|
margin: 0 auto;
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
hr {
|
||||||
border-bottom: 2px solid #eee;
|
margin: 2rem 0;
|
||||||
margin-top: 1rem;
|
border-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-date {
|
.post-date {
|
||||||
@ -38,9 +37,14 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h2 {
|
||||||
display: inline;
|
font-size: 1.25rem;
|
||||||
margin: 0;
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 a {
|
||||||
|
color: currentcolor;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -50,18 +54,22 @@
|
|||||||
|
|
||||||
<div id="posts">
|
<div id="posts">
|
||||||
<h1 style:text-align="center">All Posts</h1>
|
<h1 style:text-align="center">All Posts</h1>
|
||||||
{#each postData as post}
|
{#each postData as post, idx}
|
||||||
<div class="post">
|
<div class="post">
|
||||||
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
|
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
|
||||||
<div>
|
<h2>
|
||||||
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
|
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
|
||||||
<h3>{post.title}<h3>
|
{post.title}
|
||||||
</a>
|
</a>
|
||||||
{#if post.draft}
|
{#if post.draft}
|
||||||
<span class="draft-notice">Draft</span>
|
<span class="draft-notice">Draft</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</h2>
|
||||||
<p>{post.description}</p>
|
<p>{post.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if idx < postData.length - 1}
|
||||||
|
<hr>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user