diff --git a/package.json b/package.json index ef26d86..f8f0bb7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "lit": "^3.1.4", "marked": "^4.2.12", "marked-footnote": "^1.2.2", + "short-unique-id": "^5.2.0", "ts-expose-internals": "^4.5.4", "ts-node": "^10.9.2", "ts-patch": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33afd1f..87eb1b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: marked-footnote: specifier: ^1.2.2 version: 1.2.2(marked@4.2.12) + short-unique-id: + specifier: ^5.2.0 + version: 5.2.0 ts-expose-internals: specifier: ^4.5.4 version: 4.8.4 @@ -3895,6 +3898,11 @@ packages: interpret: 1.4.0 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: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} diff --git a/src/app/configs/server.config.ts b/src/app/configs/server.config.ts index 8708a73..b94d706 100644 --- a/src/app/configs/server.config.ts +++ b/src/app/configs/server.config.ts @@ -17,6 +17,10 @@ export default { chorus: { baseUrl: env('CHORUS_BASE_URL'), + thread: { + template: env('CHORUS_THREAD_TEMPLATE'), + idPrefix: env('CHORUS_ID_PREFIX', 'c.'), + }, }, session: { diff --git a/src/app/http/controllers/Blog.controller.ts b/src/app/http/controllers/Blog.controller.ts index 172a6d6..adced7b 100644 --- a/src/app/http/controllers/Blog.controller.ts +++ b/src/app/http/controllers/Blog.controller.ts @@ -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 {Blog as BlogService} from '../../services/Blog.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', { ...home.getThemeCSS(), ...this.getBlogData(), + ...this.getChorusData(post), post, title: post.title, 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() { const home = this.make(Home) const postsByYear = await this.blog.getAllPosts() diff --git a/src/app/resources/assets/chorus.js b/src/app/resources/assets/chorus.js index 7ac0bc0..06de587 100644 --- a/src/app/resources/assets/chorus.js +++ b/src/app/resources/assets/chorus.js @@ -39,7 +39,8 @@ Chorus.init = async function(selector) { const response = await fetch(threadDataUrl) if ( response.status === 404 ) { - // fixme + await Chorus.render404(el, baseUrl) + return } if ( !response.ok ) { @@ -61,6 +62,40 @@ Chorus.init = async function(selector) { 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) { const commentDiv = document.createElement('div') commentDiv.classList.add('chorus-comment') diff --git a/src/app/resources/assets/main-70s.css b/src/app/resources/assets/main-70s.css index 65e4d64..b4ca621 100644 --- a/src/app/resources/assets/main-70s.css +++ b/src/app/resources/assets/main-70s.css @@ -908,10 +908,13 @@ section#auth h3 { padding: 0; display: flex; flex-direction: row; + flex-wrap: wrap; } .comments .chorus-summary ul li:first-of-type { flex: 1; + min-width: 200px; + margin-right: 20px; } .comments .chorus-comment { diff --git a/src/app/resources/views/blog/post.pug b/src/app/resources/views/blog/post.pug index cfff423..f9abf82 100644 --- a/src/app/resources/views/blog/post.pug +++ b/src/app/resources/views/blog/post.pug @@ -45,7 +45,7 @@ block blog_content .post-content !{renderedPost} - if chorusUrl && chorusThread + if chorusUrl && chorusThread && chorusAddress .section-border .section-border-inner-1 .section-border-inner-2 @@ -53,7 +53,7 @@ block blog_content .comments-container h1 Comments p Thanks for reading! I'd love to hear your thoughts and questions. - p My blog uses an email-based comments system: Submit a Comment + p My blog uses an email-based comments system: Submit a Comment p You can also email me directly. hr div.comments#chorus-container(data-chorus-url=chorusUrl data-chorus-thread=chorusThread) diff --git a/src/app/services/blog/AbstractBlog.service.ts b/src/app/services/blog/AbstractBlog.service.ts index 40a06d5..07c00b8 100644 --- a/src/app/services/blog/AbstractBlog.service.ts +++ b/src/app/services/blog/AbstractBlog.service.ts @@ -18,6 +18,7 @@ import { export interface BlogPostFrontMatter { title: string slug: string + shortId?: string date: Date tags: string[] } @@ -89,11 +90,37 @@ export abstract class AbstractBlog { } this.posts = this.posts.sortByDesc('date') + .tap(c => this.computeShortIds(c)) } return this.posts } + computeShortIds(posts: Collection>): Collection> { + 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>> { return this.getAllPosts() .then(p => p.firstWhere('slug', '=', slug))