Implement deterministic ID generation for blog posts, wire up Chorus thread ID and address logic, Chorus 404 handling

This commit is contained in:
Garrett Mills 2025-01-05 17:04:49 -05:00
parent d6ada9a222
commit 92018efa5c
8 changed files with 104 additions and 5 deletions

View File

@ -25,6 +25,7 @@
"lit": "^3.1.4", "lit": "^3.1.4",
"marked": "^4.2.12", "marked": "^4.2.12",
"marked-footnote": "^1.2.2", "marked-footnote": "^1.2.2",
"short-unique-id": "^5.2.0",
"ts-expose-internals": "^4.5.4", "ts-expose-internals": "^4.5.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"ts-patch": "^2.0.1", "ts-patch": "^2.0.1",

View File

@ -56,6 +56,9 @@ dependencies:
marked-footnote: marked-footnote:
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2(marked@4.2.12) version: 1.2.2(marked@4.2.12)
short-unique-id:
specifier: ^5.2.0
version: 5.2.0
ts-expose-internals: ts-expose-internals:
specifier: ^4.5.4 specifier: ^4.5.4
version: 4.8.4 version: 4.8.4
@ -3895,6 +3898,11 @@ packages:
interpret: 1.4.0 interpret: 1.4.0
rechoir: 0.6.2 rechoir: 0.6.2
/short-unique-id@5.2.0:
resolution: {integrity: sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==}
hasBin: true
dev: false
/signal-exit@3.0.7: /signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}

View File

@ -17,6 +17,10 @@ export default {
chorus: { chorus: {
baseUrl: env('CHORUS_BASE_URL'), baseUrl: env('CHORUS_BASE_URL'),
thread: {
template: env('CHORUS_THREAD_TEMPLATE'),
idPrefix: env('CHORUS_ID_PREFIX', 'c.'),
},
}, },
session: { session: {

View File

@ -1,4 +1,4 @@
import {Controller, view, Inject, Injectable, collect, plaintext, Config} from '@extollo/lib' import {Controller, view, Inject, Injectable, collect, plaintext, Config, Maybe} from '@extollo/lib'
import {Home} from './Home.controller' import {Home} from './Home.controller'
import {Blog as BlogService} from '../../services/Blog.service' import {Blog as BlogService} from '../../services/Blog.service'
import {BlogPost} from '../../services/blog/AbstractBlog.service' import {BlogPost} from '../../services/blog/AbstractBlog.service'
@ -40,16 +40,37 @@ export class Blog extends Controller {
}) })
} }
// FIXME: set chorusThread here
return view('blog:post', { return view('blog:post', {
...home.getThemeCSS(), ...home.getThemeCSS(),
...this.getBlogData(), ...this.getBlogData(),
...this.getChorusData(post),
post, post,
title: post.title, title: post.title,
renderedPost: await this.blog.renderPost(post.slug), renderedPost: await this.blog.renderPost(post.slug),
}) })
} }
private getChorusData(post: BlogPost): {}|{ chorusThread: string, chorusAddress: string } {
if ( !post.shortId ) {
return {}
}
if ( !this.config.get('server.chorus.baseUrl') ) {
return {}
}
const template = this.config.get('server.chorus.thread.template')
const idPrefix = this.config.get('server.chorus.thread.idPrefix')
if ( !template || !idPrefix ) {
return {}
}
const chorusThread = `${idPrefix}blog${post.shortId}`
const chorusAddress = String(template).replaceAll('%ID%', chorusThread)
return { chorusThread, chorusAddress }
}
public async archive() { public async archive() {
const home = <Home> this.make(Home) const home = <Home> this.make(Home)
const postsByYear = await this.blog.getAllPosts() const postsByYear = await this.blog.getAllPosts()

View File

@ -39,7 +39,8 @@ Chorus.init = async function(selector) {
const response = await fetch(threadDataUrl) const response = await fetch(threadDataUrl)
if ( response.status === 404 ) { if ( response.status === 404 ) {
// fixme await Chorus.render404(el, baseUrl)
return
} }
if ( !response.ok ) { if ( !response.ok ) {
@ -61,6 +62,40 @@ Chorus.init = async function(selector) {
console.log(threadData) console.log(threadData)
}; };
Chorus.render404 = async function(el, baseUrl) {
let refreshDate = undefined
try {
const rootDataUrl = `${baseUrl}root.json`
const response = await fetch(rootDataUrl)
if ( response.ok ) {
const rootData = await response.json()
if ( rootData && rootData.refresh && rootData.refresh.date ) {
const date = new Date(rootData.refresh.date)
if ( !isNaN(date.getTime()) ) {
refreshDate = date
}
}
}
} catch (e) {
console.error('[Chorus] Error when loading refresh date from root data', e)
}
const summaryDiv = document.createElement('div')
summaryDiv.classList.add('chorus-summary')
el.appendChild(summaryDiv)
const summaryUl = document.createElement('ul')
summaryDiv.appendChild(summaryUl)
const countLi = document.createElement('li')
countLi.innerText = `There are no comments yet.`
summaryUl.appendChild(countLi)
const lastRefreshLi = document.createElement('li')
lastRefreshLi.innerText = `Last refreshed: ${refreshDate.toLocaleString()}`
summaryUl.appendChild(lastRefreshLi)
};
Chorus.renderComment = function(commentsDiv, comment) { Chorus.renderComment = function(commentsDiv, comment) {
const commentDiv = document.createElement('div') const commentDiv = document.createElement('div')
commentDiv.classList.add('chorus-comment') commentDiv.classList.add('chorus-comment')

View File

@ -908,10 +908,13 @@ section#auth h3 {
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap;
} }
.comments .chorus-summary ul li:first-of-type { .comments .chorus-summary ul li:first-of-type {
flex: 1; flex: 1;
min-width: 200px;
margin-right: 20px;
} }
.comments .chorus-comment { .comments .chorus-comment {

View File

@ -45,7 +45,7 @@ block blog_content
.post-content !{renderedPost} .post-content !{renderedPost}
if chorusUrl && chorusThread if chorusUrl && chorusThread && chorusAddress
.section-border .section-border
.section-border-inner-1 .section-border-inner-1
.section-border-inner-2 .section-border-inner-2
@ -53,7 +53,7 @@ block blog_content
.comments-container .comments-container
h1 Comments h1 Comments
p Thanks for reading! I'd love to hear your thoughts and questions. p Thanks for reading! I'd love to hear your thoughts and questions.
p My blog uses an email-based comments system: <a href="#">Submit a Comment</a> p My blog uses an email-based comments system: <a href="mailto:#{chorusAddress}">Submit a Comment</a>
p You can also <a href="mailto:shout@garrettmills.dev">email me</a> directly. p You can also <a href="mailto:shout@garrettmills.dev">email me</a> directly.
hr hr
div.comments#chorus-container(data-chorus-url=chorusUrl data-chorus-thread=chorusThread) div.comments#chorus-container(data-chorus-url=chorusUrl data-chorus-thread=chorusThread)

View File

@ -18,6 +18,7 @@ import {
export interface BlogPostFrontMatter { export interface BlogPostFrontMatter {
title: string title: string
slug: string slug: string
shortId?: string
date: Date date: Date
tags: string[] tags: string[]
} }
@ -89,11 +90,37 @@ export abstract class AbstractBlog<TFrontMatter extends BlogPostFrontMatter> {
} }
this.posts = this.posts.sortByDesc('date') this.posts = this.posts.sortByDesc('date')
.tap(c => this.computeShortIds(c))
} }
return this.posts return this.posts
} }
computeShortIds(posts: Collection<BlogPost<TFrontMatter>>): Collection<BlogPost<TFrontMatter>> {
const ShortUniqueId = require('short-unique-id')
const uid = new ShortUniqueId({ length: 6, counter: 0, shuffle: false })
const usedIds: {[id: string]: boolean} = {}
posts.sortBy('date')
.each(post => {
let shortId = uid.sequentialUUID()
let tries = 10
while ( usedIds[shortId] && tries > 0 ) {
shortId = uid.sequentialUUID()
tries -= 1
}
if ( usedIds[shortId] ) {
throw new Error('Could not recover from collision when generating blog post short ID')
}
usedIds[shortId] = true
post.shortId = shortId
})
return posts
}
getPost(slug: string): Promise<Maybe<BlogPost<TFrontMatter>>> { getPost(slug: string): Promise<Maybe<BlogPost<TFrontMatter>>> {
return this.getAllPosts() return this.getAllPosts()
.then(p => p.firstWhere('slug', '=', slug)) .then(p => p.firstWhere('slug', '=', slug))