Bookmarks - load bookmarks page from Outline document instead of file

This commit is contained in:
Garrett Mills 2025-01-07 23:31:55 -05:00
parent 3c70c9a06e
commit 611b73c3c3
5 changed files with 95 additions and 69 deletions

View File

@ -23,6 +23,12 @@ export default {
}, },
}, },
outline: {
apiUrl: env('OUTLINE_API_URL'),
apiKey: env('OUTLINE_API_KEY'),
markmarkDocumentId: env('OUTLINE_MM_DOCUMENT_ID'),
},
session: { session: {
/* The implementation of @extollo/lib.Session that serves as the session backend. */ /* The implementation of @extollo/lib.Session that serves as the session backend. */
driver: ORMSession, driver: ORMSession,

View File

@ -16,49 +16,3 @@
- Homemade Pumpkin Pie Filling - Homemade Pumpkin Pie Filling
- https://unsophisticook.com/homemade-pumpkin-pie-filling/ - https://unsophisticook.com/homemade-pumpkin-pie-filling/
# Tech Projects
Tech projects that I found interesting, funny, or wanted to experiment with later.
- ScratchDB - Open-Source Snowflake on ClickHouse
- https://www.scratchdb.com/
- https://github.com/scratchdata/ScratchDB
- COBOL for GCC Development #lang
- https://cobolworx.com/pages/cobforgcc.html
# Technical Guides
- EV Code Certificates + Automated Builds for Windows
- https://medium.com/@joshualipson/ev-code-certificates-automated-builds-for-windows-6100fb8e8be6
# Blogs & Posts
- Dan Luu (2023-11-20) #dev
- https://danluu.com/
- https://danluu.com/everything-is-broken/
- The Case of a Curious SQL Query - Justing Jaffray #dev
- https://buttondown.email/jaffray/archive/the-case-of-a-curious-sql-query/
- An Aborted Experiment with Server Swift - flak Blog #dev
- https://flak.tedunangst.com/post/an-aborted-experiment-with-server-swift
- The Grug Brained Developer #dev
- https://grugbrain.dev/
- Code Rant - The Configuration Complexity Clock #dev
- https://mikehadlow.blogspot.com/2012/05/configuration-complexity-clock.html
- Mark L. Irons - Patterns for Personal Web Sites #dev #archive
- https://web.archive.org/web/20190904131208/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/index.html
- https://web.archive.org/web/20190826113439/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/standard.header.and.footer.html
- https://web.archive.org/web/20200107155946/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/history.page.html
- https://web.archive.org/web/20200107162931/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/index.pages.html
- https://web.archive.org/web/20230314204244/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/gift.to.the.community.html
- https://web.archive.org/web/20211102185515/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/downloadable.weblet.html
- https://web.archive.org/web/20220120050151/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/site.map.html
# MarkMark Meta
Links, resources, and tools related to MarkMark itself. ([_What's MarkMark?_](https://garrettmills.dev/markmark))
- Standard MarkMark (v1.0)
- https://garrettmills.dev/markmark
- https://garrettmills.dev/markmark/standard
- `mark-mark`: a WIP TypeScript MarkMark parser/renderer collection
- https://code.garrettmills.dev/garrettmills/www/src/branch/master/src/markmark

View File

@ -1,4 +1,4 @@
import {appPath, Singleton} from '@extollo/lib' import {appPath, Cache, Config, fetch, hasOwnProperty, Inject, Singleton} from '@extollo/lib'
import {MarkMark} from '../../markmark/types' import {MarkMark} from '../../markmark/types'
import {Parser} from '../../markmark/parser' import {Parser} from '../../markmark/parser'
import {HtmlRenderer} from '../../markmark/html.renderer' import {HtmlRenderer} from '../../markmark/html.renderer'
@ -6,6 +6,12 @@ import {MarkMarkRenderer} from '../../markmark/markmark.renderer'
@Singleton() @Singleton()
export class MarkMarkService { export class MarkMarkService {
@Inject()
protected readonly config!: Config
@Inject()
protected readonly cache!: Cache
private cachedLinks?: MarkMark private cachedLinks?: MarkMark
private cachedLinksHTML?: string private cachedLinksHTML?: string
private cachedLinksMM?: string private cachedLinksMM?: string
@ -15,11 +21,66 @@ export class MarkMarkService {
return this.cachedLinks return this.cachedLinks
} }
const path = appPath('resources', 'markmark', 'links.mark.md') let content = await this.cache.fetch('www-markmark-content')
const content = await path.read() if ( !content || this.config.get('server.debug') ) {
content = await this.readFromOutline()
const expDateEpoch = (new Date).getTime() + (15 * 60 * 1000)
await this.cache.put('www-markmark-content', content, new Date(expDateEpoch))
}
return (this.cachedLinks = (new Parser()).parse(content)) return (this.cachedLinks = (new Parser()).parse(content))
} }
private async readFromFile(): Promise<string> {
const path = appPath('resources', 'markmark', 'links.mark.md')
return path.read()
}
private async readFromOutline(): Promise<string> {
const apiUrl = this.config.safe('server.outline.apiUrl').string()
const apiKey = this.config.safe('server.outline.apiKey').string()
const docId = this.config.safe('server.outline.markmarkDocumentId').string()
const response = await fetch(`${apiUrl}/documents.info`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
id: docId,
}),
})
if ( !response.ok ) {
throw new Error('Unable to load bookmarks from Outline: response was not okay')
}
const body = await response.json()
if (
typeof body === 'object' && body
&& hasOwnProperty(body, 'data')
&& typeof body.data === 'object' && body.data
&& hasOwnProperty(body.data, 'text')
&& typeof body.data.text === 'string'
) {
// the frontmatter gets escaped by Outline, so fix it
return body.data.text
.replaceAll('\\[//\\]:', '[//]:')
.split('\n')
.map(line => {
if (line.trim()
.startsWith('[//]:')) {
return line.replaceAll('\\n', '\n')
}
return line
})
.join('\n')
}
throw new Error('Unable to load bookmarks from Outline: invalid body content')
}
public async getLinksHTML(): Promise<string> { public async getLinksHTML(): Promise<string> {
if ( this.cachedLinksHTML ) { if ( this.cachedLinksHTML ) {
return this.cachedLinksHTML return this.cachedLinksHTML

View File

@ -29,26 +29,32 @@ export class MarkMarkBlog extends AbstractBlog<BlogPostFrontMatter> {
async getAllPosts(): Promise<Collection<BlogPost>> { async getAllPosts(): Promise<Collection<BlogPost>> {
const mm = await this.mark.getLinks() const mm = await this.mark.getLinks()
const links = mm.sections const posts: Collection<BlogPost> = collect()
.map(s => s.links)
.reduce((l, c): Link[] => [...c, ...l], [])
return collect(links) for ( const section of mm.sections ) {
.filter(l => l.date) for ( const link of section.links ) {
.sort((a, b) => b.date!.getTime() - a.date!.getTime()) if ( !link.date ) {
.map(l => ({ continue
title: l.title, }
slug: l.hash,
date: l.date!, posts.push({
tags: l.tags, title: link.title,
file: l.hash, // not used slug: link.hash,
markdown: (new MarkMarkRenderer()).render({ date: link.date,
...mm, tags: link.tags,
sections: [{ file: link.hash, // not used
links: [l], markdown: (new MarkMarkRenderer()).render({
}], ...mm,
}, false), sections: [{
})) ...section,
links: [link],
}],
}, false),
})
}
}
return posts.sort((a, b) => b.date!.getTime() - a.date!.getTime())
} }
protected createFeed(lastUpdated: Maybe<Date>): Awaitable<RSSFeed.Feed> { protected createFeed(lastUpdated: Maybe<Date>): Awaitable<RSSFeed.Feed> {

View File

@ -105,7 +105,6 @@ export class Parser {
} }
} }
marked.marked.use({ walkTokens }) marked.marked.use({ walkTokens })
marked.marked.parse(content) marked.marked.parse(content)