Bookmarks - load bookmarks page from Outline document instead of file
This commit is contained in:
parent
3c70c9a06e
commit
611b73c3c3
@ -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,
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
date: link.date,
|
||||||
|
tags: link.tags,
|
||||||
|
file: link.hash, // not used
|
||||||
markdown: (new MarkMarkRenderer()).render({
|
markdown: (new MarkMarkRenderer()).render({
|
||||||
...mm,
|
...mm,
|
||||||
sections: [{
|
sections: [{
|
||||||
links: [l],
|
...section,
|
||||||
|
links: [link],
|
||||||
}],
|
}],
|
||||||
}, false),
|
}, 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> {
|
||||||
|
@ -105,7 +105,6 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
marked.marked.use({ walkTokens })
|
marked.marked.use({ walkTokens })
|
||||||
marked.marked.parse(content)
|
marked.marked.parse(content)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user