BlogEngine: Update the BlogIndex page to use the new BlogController. Remove reliance on the old Pythonlist
This commit is contained in:
parent
2d16ce03df
commit
dbc368fc3c
14 changed files with 289 additions and 51 deletions
|
|
@ -46,11 +46,13 @@
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"rehype-stringify": "^9.0.3",
|
"rehype-stringify": "^9.0.3",
|
||||||
|
"remark": "^14.0.2",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-parse": "^10.0.1",
|
"remark-parse": "^10.0.1",
|
||||||
"remark-rehype": "^10.1.0",
|
"remark-rehype": "^10.1.0",
|
||||||
"remark-stringify": "^10.0.2",
|
"remark-stringify": "^10.0.2",
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
|
"strip-markdown": "^5.0.0",
|
||||||
"to-vfile": "^7.2.3",
|
"to-vfile": "^7.2.3",
|
||||||
"unified": "^10.1.2",
|
"unified": "^10.1.2",
|
||||||
"zod": "^3.18.0"
|
"zod": "^3.18.0"
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,92 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { BlogPost } from './BlogPost.js';
|
import { BlogPost } from './BlogPost.js';
|
||||||
|
import { aBlogPost } from './test-builders/blog-post-builder.js';
|
||||||
|
|
||||||
const exampleMarkdownWithFrontMatter = `---
|
const exampleMarkdownWithFrontMatter = `---
|
||||||
title: "Test Blog Post"
|
title: "Test Blog Post"
|
||||||
date: 2023-02-01T08:00:00Z
|
date: 2023-02-01T08:00:00Z
|
||||||
slug: "2023-02-01-test"
|
slug: "2023-02-01-test"
|
||||||
|
author: Thomas Wilson
|
||||||
---
|
---
|
||||||
|
|
||||||
This is the content of the blog post.
|
This is the content of the blog post.
|
||||||
|
|
||||||
This is a [link](http://www.bbc.co.uk)
|
This is a [link](http://www.bbc.co.uk).
|
||||||
|
|
||||||
- This is a list item
|
- This is a list item
|
||||||
- This is another list item
|
- This is another list item
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe('BlogPost', () => {
|
describe('BlogPost', () => {
|
||||||
it(`should construct`, () => {
|
describe(`Constructing`, () => {
|
||||||
// GIVEN
|
it(`should construct`, async () => {
|
||||||
const blogPost = new BlogPost({ title: 'Test Title', markdownContent: 'Test Content' });
|
// GIVEN
|
||||||
|
const blogPost = new BlogPost({
|
||||||
|
title: 'Test Title',
|
||||||
|
author: 'Test Author',
|
||||||
|
date: new Date('2022-01-01T00:00Z'),
|
||||||
|
slug: 'test-slug',
|
||||||
|
markdownContent: 'Test Content',
|
||||||
|
});
|
||||||
|
|
||||||
// THEN
|
// WHEN
|
||||||
expect(blogPost.title).toBe('Test Title');
|
await blogPost.build();
|
||||||
expect(blogPost.markdownContent).toBe('Test Content');
|
|
||||||
|
// THEN
|
||||||
|
const expectedBlogPost = await aBlogPost()
|
||||||
|
.withTitle('Test Title')
|
||||||
|
.withAuthor('Test Author')
|
||||||
|
.withDate(new Date('2022-01-01T00:00Z'))
|
||||||
|
.withSlug('test-slug')
|
||||||
|
.withMarkdownContent('Test Content')
|
||||||
|
.constructAndThenBuild();
|
||||||
|
|
||||||
|
expect(blogPost).toStrictEqual(expectedBlogPost);
|
||||||
|
expect(blogPost.html).toBeDefined();
|
||||||
|
expect(blogPost.excerpt).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`Building the blog post`, () => {
|
||||||
|
it(`should know if a blog post has been built`, () => {
|
||||||
|
// GIVEN
|
||||||
|
const blogPost = aBlogPost().build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const hasBeenBuilt = blogPost.hasBeenBuilt;
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
expect(hasBeenBuilt).toBe(false);
|
||||||
|
expect(blogPost.html).toBeNull();
|
||||||
|
expect(blogPost.excerpt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should know if a blog post has been built`, async () => {
|
||||||
|
// GIVEN
|
||||||
|
const blogPost = aBlogPost().build();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await blogPost.build();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
expect(blogPost.hasBeenBuilt).toBe(true);
|
||||||
|
expect(blogPost.html).toBeDefined();
|
||||||
|
expect(blogPost.excerpt).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`Should parse markdown to HTML`, async () => {
|
it(`Should parse markdown to HTML`, async () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const blogPost = new BlogPost({ title: 'Test Title', markdownContent: exampleMarkdownWithFrontMatter });
|
const blogPost = await aBlogPost().withMarkdownContent(exampleMarkdownWithFrontMatter).constructAndThenBuild();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const html = await blogPost.getHtml();
|
const html = blogPost.html;
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(html).toStrictEqual(
|
expect(html).toStrictEqual(
|
||||||
[
|
[
|
||||||
`<p>This is the content of the blog post.</p>`,
|
`<p>This is the content of the blog post.</p>`,
|
||||||
`<p>This is a <a href="http://www.bbc.co.uk">link</a></p>`,
|
`<p>This is a <a href="http://www.bbc.co.uk">link</a>.</p>`,
|
||||||
`<ul>`,
|
`<ul>`,
|
||||||
`<li>This is a list item</li>`,
|
`<li>This is a list item</li>`,
|
||||||
`<li>This is another list item</li>`,
|
`<li>This is another list item</li>`,
|
||||||
|
|
@ -44,4 +94,15 @@ describe('BlogPost', () => {
|
||||||
].join(`\n`)
|
].join(`\n`)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`should have a plain-text excerpt`, async () => {
|
||||||
|
// GIVEN
|
||||||
|
const blogPost = await aBlogPost().withMarkdownContent(exampleMarkdownWithFrontMatter).constructAndThenBuild();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const excerpt = await blogPost.getExcerpt();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
expect(excerpt).toBe('This is the content of the blog post. This is a link.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,86 @@
|
||||||
import type { Processor } from 'unified';
|
import type { Processor } from 'unified';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
import { remark } from 'remark';
|
||||||
import markdown from 'remark-parse';
|
import markdown from 'remark-parse';
|
||||||
import markdownFrontmatter from 'remark-frontmatter';
|
import markdownFrontmatter from 'remark-frontmatter';
|
||||||
import remarkStringify from 'remark-stringify';
|
import remarkStringify from 'remark-stringify';
|
||||||
import remarkRehype from 'remark-rehype';
|
import remarkRehype from 'remark-rehype';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
|
import stripMarkdown from 'strip-markdown';
|
||||||
|
import remarkFrontmatter from 'remark-frontmatter';
|
||||||
|
|
||||||
interface BlogPostParams {
|
interface BlogPostParams {
|
||||||
title: string;
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
author: string;
|
||||||
|
slug: string;
|
||||||
markdownContent: string;
|
markdownContent: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlogPost {
|
export class BlogPost {
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
|
readonly date: Date;
|
||||||
|
readonly author: string;
|
||||||
|
readonly slug: string;
|
||||||
readonly markdownContent: string;
|
readonly markdownContent: string;
|
||||||
|
|
||||||
|
private _html: string | null = null;
|
||||||
|
private _excerpt: string | null = null;
|
||||||
|
|
||||||
constructor(params: BlogPostParams) {
|
constructor(params: BlogPostParams) {
|
||||||
this.title = params.title;
|
this.title = params.title;
|
||||||
|
this.date = params.date;
|
||||||
|
this.author = params.author;
|
||||||
|
this.slug = params.slug;
|
||||||
this.markdownContent = params.markdownContent;
|
this.markdownContent = params.markdownContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get html(): string | null {
|
||||||
|
return this._html;
|
||||||
|
}
|
||||||
|
|
||||||
|
get excerpt(): string | null {
|
||||||
|
return this._excerpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasBeenBuilt(): boolean {
|
||||||
|
return this._html !== null && this._excerpt !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async build(): Promise<void> {
|
||||||
|
await this.getHtml();
|
||||||
|
await this.getExcerpt();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExcerpt(wordLength = 50): Promise<string> {
|
||||||
|
const processor = this.markdownToExcerptProcessorFactory();
|
||||||
|
const value = await processor.process(this.markdownContent);
|
||||||
|
|
||||||
|
const textValueWithNoLinebreaks = value.toString();
|
||||||
|
|
||||||
|
// A regex that looks for any character, followed by `.`, and then another character.
|
||||||
|
// e.g. "This is a sentence.This is another sentence."
|
||||||
|
// becomes "This is a sentence. This is another sentence."
|
||||||
|
const reg = /([a-zA-Z0-9])\.([a-zA-Z0-9])/g;
|
||||||
|
|
||||||
|
const textWithSpacesBetweenSentences = textValueWithNoLinebreaks
|
||||||
|
.replaceAll('\r', ' ')
|
||||||
|
.replaceAll('\n', ' ')
|
||||||
|
.replaceAll(reg, '$1. $2')
|
||||||
|
.split(' ')
|
||||||
|
.filter((word) => word !== ' ' && word !== '')
|
||||||
|
.slice(0, wordLength)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
this._excerpt = textWithSpacesBetweenSentences;
|
||||||
|
return this._excerpt;
|
||||||
|
}
|
||||||
|
|
||||||
async getHtml(): Promise<string> {
|
async getHtml(): Promise<string> {
|
||||||
const processor = this.markdownToHtmlProcessorFactory();
|
const processor = this.markdownToHtmlProcessorFactory();
|
||||||
const html = await processor.process(this.markdownContent);
|
const html = await processor.process(this.markdownContent);
|
||||||
return html.toString();
|
this._html = html.toString();
|
||||||
|
return this._html;
|
||||||
}
|
}
|
||||||
|
|
||||||
private markdownToHtmlProcessorFactory(): Processor {
|
private markdownToHtmlProcessorFactory(): Processor {
|
||||||
|
|
@ -34,4 +91,11 @@ export class BlogPost {
|
||||||
.use(remarkRehype)
|
.use(remarkRehype)
|
||||||
.use(rehypeStringify);
|
.use(rehypeStringify);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private markdownToExcerptProcessorFactory(): Processor {
|
||||||
|
return remark()
|
||||||
|
.use(markdown)
|
||||||
|
.use(remarkFrontmatter)
|
||||||
|
.use(stripMarkdown, { remove: ['list'] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,19 @@ describe(`BlogPostSet`, () => {
|
||||||
expect(blogPostSet.blogPosts).toStrictEqual([blogPostOne, blogPostTwo]);
|
expect(blogPostSet.blogPosts).toStrictEqual([blogPostOne, blogPostTwo]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`Should be able to build all the blog posts`, async () => {
|
||||||
|
// GIVEN
|
||||||
|
const blogPostOne = aBlogPost().withTitle('Blog Post One').build();
|
||||||
|
const blogPostTwo = aBlogPost().withTitle('Blog Post Two').build();
|
||||||
|
const blogPostSet = new BlogPostSet([blogPostOne, blogPostTwo]);
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
await blogPostSet.buildAllBlogPosts();
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
expect(blogPostSet.blogPosts.every((post) => post.hasBeenBuilt)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
describe(`Finding a blog post by title`, () => {
|
describe(`Finding a blog post by title`, () => {
|
||||||
const blogPostOne = aBlogPost().withTitle('Blog Post One').build();
|
const blogPostOne = aBlogPost().withTitle('Blog Post One').build();
|
||||||
const blogPostTwo = aBlogPost().withTitle('Blog Post Two').build();
|
const blogPostTwo = aBlogPost().withTitle('Blog Post Two').build();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
import type { BlogPost } from './BlogPost.js';
|
import type { BlogPost } from './BlogPost.js';
|
||||||
|
|
||||||
export class BlogPostSet {
|
export class BlogPostSet {
|
||||||
constructor(readonly blogPosts: BlogPost[]) {}
|
private _blogPosts: BlogPost[] = [];
|
||||||
|
|
||||||
|
constructor(blogPosts: BlogPost[]) {
|
||||||
|
this._blogPosts = blogPosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
get blogPosts(): BlogPost[] {
|
||||||
|
return this._blogPosts;
|
||||||
|
}
|
||||||
|
|
||||||
getBlogPostWithTitle(title: string): BlogPost | null {
|
getBlogPostWithTitle(title: string): BlogPost | null {
|
||||||
return this.blogPosts.find((post) => post.title === title) ?? null;
|
return this._blogPosts.find((post) => post.title === title) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildAllBlogPosts(): Promise<void> {
|
||||||
|
await Promise.all(this.blogPosts.map((post) => post.build()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedBlogPost = aBlogPost()
|
const expectedBlogPost = aBlogPost()
|
||||||
|
.withAuthor('Thomas Wilson')
|
||||||
|
.withDate(new Date('2023-02-01T08:00:00Z'))
|
||||||
|
.withSlug('2023-02-01-test')
|
||||||
.withTitle('Test Blog Post')
|
.withTitle('Test Blog Post')
|
||||||
.withMarkdownContent(testMarkdownContent)
|
.withMarkdownContent(testMarkdownContent)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@ import { BlogPost } from './BlogPost.js';
|
||||||
import { MarkdownFile } from './MarkdownFile.js';
|
import { MarkdownFile } from './MarkdownFile.js';
|
||||||
import { BlogPostSet } from './BlogPostSet.js';
|
import { BlogPostSet } from './BlogPostSet.js';
|
||||||
|
|
||||||
|
interface FrontmatterValues {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
date: Date;
|
||||||
|
author: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class MarkdownRepository {
|
export class MarkdownRepository {
|
||||||
readonly markdownFiles: MarkdownFile[];
|
readonly markdownFiles: MarkdownFile[];
|
||||||
readonly blogPosts: BlogPostSet;
|
readonly blogPosts: BlogPostSet;
|
||||||
|
|
@ -13,22 +20,32 @@ export class MarkdownRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromViteGlobImport(globImport): Promise<MarkdownRepository> {
|
public static async fromViteGlobImport(globImport): Promise<MarkdownRepository> {
|
||||||
let fileImports: MarkdownFile[] = [];
|
let fileImports: MarkdownFile<FrontmatterValues>[] = [];
|
||||||
let blogPosts: BlogPost[] = [];
|
let blogPosts: BlogPost[] = [];
|
||||||
const allFiles = Object.entries(globImport);
|
const allFiles = Object.entries(globImport);
|
||||||
|
|
||||||
for (const entry of allFiles) {
|
for (const entry of allFiles) {
|
||||||
const [filename, module] = entry as [string, () => Promise<string>];
|
const [filename, module] = entry as [string, () => Promise<string>];
|
||||||
const fileContent = await module();
|
try {
|
||||||
|
const fileContent = await module();
|
||||||
|
|
||||||
const markdownFile = new MarkdownFile<{ title: string }>({ fileName: filename, content: fileContent });
|
const markdownFile = new MarkdownFile<FrontmatterValues>({ fileName: filename, content: fileContent });
|
||||||
const blogPost = new BlogPost({
|
const blogPost = new BlogPost({
|
||||||
markdownContent: markdownFile.content,
|
markdownContent: markdownFile.content,
|
||||||
title: markdownFile.frontmatter.title,
|
title: markdownFile.frontmatter.title,
|
||||||
});
|
slug: markdownFile.frontmatter.slug,
|
||||||
|
author: markdownFile.frontmatter.author,
|
||||||
|
date: markdownFile.frontmatter.date,
|
||||||
|
});
|
||||||
|
|
||||||
fileImports = [...fileImports, markdownFile];
|
fileImports = [...fileImports, markdownFile];
|
||||||
blogPosts = [...blogPosts, blogPost];
|
blogPosts = [...blogPosts, blogPost];
|
||||||
|
} catch (e) {
|
||||||
|
console.error({
|
||||||
|
message: `[MarkdownRespository::fromViteGlobImport] Error loading file ${filename}`,
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MarkdownRepository(fileImports, blogPosts);
|
return new MarkdownRepository(fileImports, blogPosts);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { BlogPost } from '../BlogPost.js';
|
||||||
|
|
||||||
class BlogPostBuilder {
|
class BlogPostBuilder {
|
||||||
private _title = 'default title';
|
private _title = 'default title';
|
||||||
|
private _author = 'default author';
|
||||||
|
private _date = new Date('2022-01-01T00:00Z');
|
||||||
|
private _slug = 'default-slug';
|
||||||
|
|
||||||
private _markdownContent = 'default markdown content';
|
private _markdownContent = 'default markdown content';
|
||||||
|
|
||||||
withTitle(title: string): BlogPostBuilder {
|
withTitle(title: string): BlogPostBuilder {
|
||||||
|
|
@ -14,8 +18,35 @@ class BlogPostBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withAuthor(author: string): BlogPostBuilder {
|
||||||
|
this._author = author;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDate(date: Date): BlogPostBuilder {
|
||||||
|
this._date = date;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withSlug(slug: string): BlogPostBuilder {
|
||||||
|
this._slug = slug;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async constructAndThenBuild(): Promise<BlogPost> {
|
||||||
|
const blogPost = this.build();
|
||||||
|
await blogPost.build();
|
||||||
|
return blogPost;
|
||||||
|
}
|
||||||
|
|
||||||
build(): BlogPost {
|
build(): BlogPost {
|
||||||
return new BlogPost({ title: this._title, markdownContent: this._markdownContent });
|
return new BlogPost({
|
||||||
|
title: this._title,
|
||||||
|
markdownContent: this._markdownContent,
|
||||||
|
author: this._author,
|
||||||
|
date: this._date,
|
||||||
|
slug: this._slug,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,11 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { BlogController } from './BlogController.js';
|
||||||
|
|
||||||
import allPosts from '../../../content/posts.json';
|
export const GET = async () => {
|
||||||
|
|
||||||
export const GET = async ({ url }) => {
|
|
||||||
try {
|
try {
|
||||||
const posts = Object.entries(allPosts).map(([key, value]) => ({
|
const controller = await BlogController.singleton();
|
||||||
...value,
|
const blogPosts = await controller.getAllBlogPosts();
|
||||||
}));
|
return json({ posts: blogPosts });
|
||||||
|
|
||||||
const sortedBlogPosts = posts.sort((a, b) => {
|
|
||||||
if (a.date > b.date) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.date < b.date) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return json({
|
|
||||||
posts: sortedBlogPosts,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error({ error: JSON.stringify(error) });
|
console.error({ error: JSON.stringify(error) });
|
||||||
return json(
|
return json(
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,13 @@ describe(`BlogController`, () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const blogPosts = await controller.getAllBlogPosts();
|
const blogPosts = await controller.getAllBlogPosts();
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
const aKnownBlogPost = blogPosts.find((post) => post.title === 'Vibe Check #10');
|
||||||
|
const aMadeUpBlogPost = blogPosts.find((post) => post.title === 'Some made up blog post');
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(blogPosts.getBlogPostWithTitle('Vibe Check #10')).toBeDefined();
|
expect(aMadeUpBlogPost).toBeNull();
|
||||||
|
expect(aKnownBlogPost).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,42 @@
|
||||||
import type { BlogPostSet } from '../../../lib/blog/BlogPostSet.js';
|
import type { BlogPostSet } from '../../../lib/blog/BlogPostSet.js';
|
||||||
import { MarkdownRepository } from '../../../lib/blog/markdown-repository.js';
|
import { MarkdownRepository } from '../../../lib/blog/markdown-repository.js';
|
||||||
|
|
||||||
const blogPosts = import.meta.glob('../../content/blog/*.md', { as: 'raw' });
|
const blogPostMetaGlobImport = import.meta.glob('../../../content/blog/*.md', { as: 'raw' });
|
||||||
|
|
||||||
|
interface BlogPostListItem {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
book_review: boolean;
|
||||||
|
preview: string;
|
||||||
|
content: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class BlogController {
|
export class BlogController {
|
||||||
static async singleton(): Promise<BlogController> {
|
static async singleton(): Promise<BlogController> {
|
||||||
const markdownRepository = await MarkdownRepository.fromViteGlobImport(blogPosts);
|
const markdownRepository = await MarkdownRepository.fromViteGlobImport(blogPostMetaGlobImport);
|
||||||
return new BlogController(markdownRepository);
|
return new BlogController(markdownRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly markdownRepository: MarkdownRepository) {}
|
constructor(private readonly markdownRepository: MarkdownRepository) {}
|
||||||
|
|
||||||
async getAllBlogPosts(): Promise<BlogPostSet> {
|
async getAllBlogPosts(): Promise<BlogPostListItem[]> {
|
||||||
const blogPosts = await this.markdownRepository.blogPosts;
|
const blogPosts = await this.markdownRepository.blogPosts;
|
||||||
return blogPosts;
|
await blogPosts.buildAllBlogPosts();
|
||||||
|
|
||||||
|
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
|
||||||
|
return {
|
||||||
|
title: blogPost.title,
|
||||||
|
author: blogPost.author,
|
||||||
|
book_review: false,
|
||||||
|
content: blogPost.html,
|
||||||
|
date: blogPost.date.toISOString(),
|
||||||
|
preview: blogPost.excerpt,
|
||||||
|
slug: blogPost.slug,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return blogPostListItems.sort((a, b) => (a.date > b.date ? -1 : 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
import { intlFormat } from "date-fns";
|
import { intlFormat } from "date-fns";
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: ({
|
$: ({
|
||||||
posts,
|
posts,
|
||||||
firstPost,
|
firstPost,
|
||||||
|
|
|
||||||
25
yarn.lock
25
yarn.lock
|
|
@ -357,7 +357,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
|
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3", "@types/unist@^2.0.6":
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||||
|
|
@ -2255,7 +2255,7 @@ remark-frontmatter@^4.0.1:
|
||||||
micromark-extension-frontmatter "^1.0.0"
|
micromark-extension-frontmatter "^1.0.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
remark-parse@^10.0.1:
|
remark-parse@^10.0.0, remark-parse@^10.0.1:
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
|
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
|
||||||
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
|
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
|
||||||
|
|
@ -2274,7 +2274,7 @@ remark-rehype@^10.1.0:
|
||||||
mdast-util-to-hast "^12.1.0"
|
mdast-util-to-hast "^12.1.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
remark-stringify@^10.0.2:
|
remark-stringify@^10.0.0, remark-stringify@^10.0.2:
|
||||||
version "10.0.2"
|
version "10.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-10.0.2.tgz#50414a6983f5008eb9e72eed05f980582d1f69d7"
|
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-10.0.2.tgz#50414a6983f5008eb9e72eed05f980582d1f69d7"
|
||||||
integrity sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==
|
integrity sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==
|
||||||
|
|
@ -2283,6 +2283,16 @@ remark-stringify@^10.0.2:
|
||||||
mdast-util-to-markdown "^1.0.0"
|
mdast-util-to-markdown "^1.0.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
|
remark@^14.0.2:
|
||||||
|
version "14.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/remark/-/remark-14.0.2.tgz#4a1833f7441a5c29e44b37bb1843fb820797b40f"
|
||||||
|
integrity sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==
|
||||||
|
dependencies:
|
||||||
|
"@types/mdast" "^3.0.0"
|
||||||
|
remark-parse "^10.0.0"
|
||||||
|
remark-stringify "^10.0.0"
|
||||||
|
unified "^10.0.0"
|
||||||
|
|
||||||
require-from-string@^2.0.2:
|
require-from-string@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||||
|
|
@ -2579,6 +2589,15 @@ strip-literal@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.8.1"
|
acorn "^8.8.1"
|
||||||
|
|
||||||
|
strip-markdown@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-markdown/-/strip-markdown-5.0.0.tgz#222b864ecce6cf2ef87b2bb1e6466464e8127081"
|
||||||
|
integrity sha512-PXSts6Ta9A/TwGxVVSRlQs1ukJTAwwtbip2OheJEjPyfykaQ4sJSTnQWjLTI2vYWNts/R/91/csagp15W8n9gA==
|
||||||
|
dependencies:
|
||||||
|
"@types/mdast" "^3.0.0"
|
||||||
|
"@types/unist" "^2.0.6"
|
||||||
|
unified "^10.0.0"
|
||||||
|
|
||||||
supports-color@^5.3.0:
|
supports-color@^5.3.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue