forked from cory/tildefriends
Add the blog app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4669 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
parent
023731fc3f
commit
ae2015a604
5
apps/blog.json
Normal file
5
apps/blog.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🪵",
|
||||||
|
"previous": "&CMKodqxRSDNxt/MMpYRysfN+6ArnEWaHUiE27J6BOIA=.sha256"
|
||||||
|
}
|
8
apps/blog/app.js
Normal file
8
apps/blog/app.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
await app.setDocument(blog.render_html(blogs));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
149
apps/blog/blog.js
Normal file
149
apps/blog/blog.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
|
function escape(text) {
|
||||||
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(text) {
|
||||||
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdown(md) {
|
||||||
|
let reader = new commonmark.Parser({safe: true});
|
||||||
|
let writer = new commonmark.HtmlRenderer();
|
||||||
|
let parsed = reader.parse(md || '');
|
||||||
|
let walker = parsed.walker();
|
||||||
|
let event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.type == 'image') {
|
||||||
|
if (node.destination.startsWith('&')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function render_blog_post_html(blog_post) {
|
||||||
|
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blob)}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post(blog_post) {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<h2><a href="../ssb/#${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blog_post.summary)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_html(blogs) {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Blog</title>
|
||||||
|
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<base target="_blank">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||||
|
<h1>🪵Tilde Blog</h1>
|
||||||
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
|
</div>
|
||||||
|
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post_atom(blog_post) {
|
||||||
|
return `<entry>
|
||||||
|
<title>${escape(blog_post.title)}</title>
|
||||||
|
<link href="https://tildefriends.net/~cory/ssb/#${blog_post.id}" />
|
||||||
|
<id>${blog_post.id}</id>
|
||||||
|
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
||||||
|
<summary>${escape(blog_post.summary)}</summary>
|
||||||
|
<author>
|
||||||
|
<name>${escape(blog_post.name)}</name>
|
||||||
|
<feed>${escape(blog_post.author)}</feed>
|
||||||
|
</author>
|
||||||
|
</entry>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_atom(blogs) {
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>🪵Tilde Blog</title>
|
||||||
|
<subtitle>A subtitle.</subtitle>
|
||||||
|
<link href="https://tildefriends.net/~cory/blog/atom" rel="self"/>
|
||||||
|
<link href="https://tildefriends.net/~cory/blog/"/>
|
||||||
|
<id>https://www.tildefriends.net/~cory/blog/</id>
|
||||||
|
<updated>${new Date().toString()}</updated>
|
||||||
|
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
|
</feed>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_posts() {
|
||||||
|
let blogs = [];
|
||||||
|
await ssb.sqlAsync(`
|
||||||
|
WITH
|
||||||
|
blogs AS (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
messages.id,
|
||||||
|
json_extract(messages.content, '$.title') AS title,
|
||||||
|
json_extract(messages.content, '$.summary') AS summary,
|
||||||
|
json_extract(messages.content, '$.blog') AS blog,
|
||||||
|
messages.timestamp
|
||||||
|
FROM messages_fts('blog')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'blog'),
|
||||||
|
public AS (
|
||||||
|
SELECT author FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.publicWebHosting') AS is_public
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
|
||||||
|
WHERE author_rank = 1 AND is_public),
|
||||||
|
names AS (
|
||||||
|
SELECT author, name FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.name') AS name
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
json_extract(messages.content, '$.about') = messages.author AND
|
||||||
|
name IS NOT NULL)
|
||||||
|
WHERE author_rank = 1)
|
||||||
|
SELECT blogs.*, names.name FROM blogs
|
||||||
|
JOIN public ON public.author = blogs.author
|
||||||
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
|
`, [], function(row) {
|
||||||
|
blogs.push(row);
|
||||||
|
});
|
||||||
|
return blogs;
|
||||||
|
}
|
1
apps/blog/commonmark.min.js
vendored
Normal file
1
apps/blog/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22
apps/blog/handler.js
Normal file
22
apps/blog/handler.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
for (let blog_post of blogs) {
|
||||||
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
|
if (request.path === title) {
|
||||||
|
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.path == 'atom') {
|
||||||
|
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
||||||
|
} else {
|
||||||
|
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(function(error) {
|
||||||
|
respond({data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
||||||
|
});
|
126
apps/blog/lit-all.min.js
vendored
Normal file
126
apps/blog/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/blog/lit-all.min.js.map
Normal file
1
apps/blog/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user