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:
		
							
								
								
									
										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
											
										
									
								
							
		Reference in New Issue
	
	Block a user