BlogEngine: Move the /new blog post endpoint to allow pre-rendering
This commit is contained in:
parent
6c48b3f188
commit
57dd0a017e
11 changed files with 127 additions and 81 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, it, beforeEach, beforeAll, expect } from 'vitest';
|
import { describe, it, beforeEach, beforeAll, expect, afterEach } from 'vitest';
|
||||||
import { BlogController } from './BlogController.js';
|
import { BlogController } from './BlogController.js';
|
||||||
|
import { MarkdownRepository } from './markdown-repository.js';
|
||||||
|
|
||||||
describe(`BlogController`, () => {
|
describe(`BlogController`, () => {
|
||||||
let controller: BlogController;
|
let controller: BlogController;
|
||||||
|
|
@ -71,4 +72,18 @@ describe(`BlogController`, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`Creating a new blog post as a file`, () => {
|
||||||
|
let fileName: string;
|
||||||
|
let controller: BlogController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
fileName = 'some-made-up-blog-post.md';
|
||||||
|
controller = await BlogController.singleton();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await controller.markdownRepository.deleteBlogPostFile(fileName);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,17 +28,25 @@ interface BookReviewListItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BlogController {
|
export class BlogController {
|
||||||
|
private readonly _markdownRepository: MarkdownRepository;
|
||||||
|
|
||||||
static async singleton(): Promise<BlogController> {
|
static async singleton(): Promise<BlogController> {
|
||||||
const markdownRepository = await MarkdownRepository.singleton();
|
const markdownRepository = await MarkdownRepository.singleton();
|
||||||
return new BlogController(markdownRepository);
|
return new BlogController(markdownRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly markdownRepository: MarkdownRepository) {}
|
constructor(markdownRepository: MarkdownRepository) {
|
||||||
|
this._markdownRepository = markdownRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
get markdownRepository(): MarkdownRepository {
|
||||||
|
return this._markdownRepository;
|
||||||
|
}
|
||||||
|
|
||||||
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem>> {
|
||||||
const blogPosts = await this.markdownRepository.blogPosts;
|
const blogPosts = await this._markdownRepository.blogPosts;
|
||||||
|
|
||||||
const bookReviews = await this.markdownRepository.bookReviews;
|
const bookReviews = await this._markdownRepository.bookReviews;
|
||||||
|
|
||||||
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
|
const blogPostListItems: BlogPostListItem[] = blogPosts.blogPosts.map((blogPost) => {
|
||||||
return this.blogPostToBlogPostListItem(blogPost);
|
return this.blogPostToBlogPostListItem(blogPost);
|
||||||
|
|
@ -78,12 +86,12 @@ export class BlogController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBlogOrBookReviewBySlug(slug: string): Promise<BookReviewListItem | BlogPostListItem | null> {
|
async getBlogOrBookReviewBySlug(slug: string): Promise<BookReviewListItem | BlogPostListItem | null> {
|
||||||
const blogPost = await this.markdownRepository.getBlogPostBySlug(slug);
|
const blogPost = await this._markdownRepository.getBlogPostBySlug(slug);
|
||||||
if (blogPost) {
|
if (blogPost) {
|
||||||
return this.blogPostToBlogPostListItem(blogPost, true);
|
return this.blogPostToBlogPostListItem(blogPost, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookReview = await this.markdownRepository.getBookReviewBySlug(slug);
|
const bookReview = await this._markdownRepository.getBookReviewBySlug(slug);
|
||||||
if (bookReview) {
|
if (bookReview) {
|
||||||
return this.bookReviewToBookReviewListItem(bookReview, true);
|
return this.bookReviewToBookReviewListItem(bookReview, true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,22 @@ describe(`Blog MarkdownRepository`, () => {
|
||||||
expect(markdownFile).toBeNull();
|
expect(markdownFile).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`Deleting markdown files`, () => {
|
||||||
|
let repository: MarkdownRepository;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
repository = await MarkdownRepository.fromViteGlobImport(blogPostImport, bookReviewImport);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should throw an error if it attempts to delete a blog post file which does not exist`, async () => {
|
||||||
|
// GIVEN
|
||||||
|
const theFileName = 'non-existent-file.md';
|
||||||
|
|
||||||
|
// WHEN/THEN
|
||||||
|
expect(() => repository.deleteBlogPostMarkdownFile(theFileName)).toThrowError(
|
||||||
|
`Cannot delete file ${theFileName} as it does not exist`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { open } from 'fs';
|
||||||
|
|
||||||
import { BlogPost } from './BlogPost.js';
|
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';
|
||||||
import { BookReviewSet } from './BookReviewSet.js';
|
import { BookReviewSet } from './BookReviewSet.js';
|
||||||
import { BookReview } from './BookReview.js';
|
import { BookReview } from './BookReview.js';
|
||||||
|
|
||||||
const blogPostMetaGlobImport = import.meta.glob('../../content/blog/*.md', { as: 'raw' });
|
// We have to duplicate the `../..` here because import.meta must have a static string,
|
||||||
|
// and it (rightfully) cannot have dynamic locations
|
||||||
|
const blogPostMarkdownDirectory = `../../content/blog`;
|
||||||
|
const blogPostMetaGlobImport = import.meta.glob(`../../content/blog/*.md`, { as: 'raw' });
|
||||||
const bookReviewsMetaGlobImport = import.meta.glob('../../content/book-reviews/*.md', { as: 'raw' });
|
const bookReviewsMetaGlobImport = import.meta.glob('../../content/book-reviews/*.md', { as: 'raw' });
|
||||||
|
|
||||||
interface BlogPostFrontmatterValues {
|
interface BlogPostFrontmatterValues {
|
||||||
|
|
@ -119,4 +125,11 @@ export class MarkdownRepository {
|
||||||
getBookReviewBySlug(slug: string): BookReview | null {
|
getBookReviewBySlug(slug: string): BookReview | null {
|
||||||
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
return this.bookReviews.bookReviews.find((bookReview) => bookReview.slug === slug) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteBlogPostMarkdownFile(fileName: string): Promise<void> {
|
||||||
|
const file = this.blogPosts.blogPosts.find((blogPost) => blogPost.fileName === fileName);
|
||||||
|
if (file) {
|
||||||
|
const file = resolve(blogPostMarkdownDirectory, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,61 +4,59 @@ import { FloriferousGame } from './floriferous-game.js';
|
||||||
import { FloriferousPlayer } from './floriferous-player.js';
|
import { FloriferousPlayer } from './floriferous-player.js';
|
||||||
|
|
||||||
describe('FloriferousGame', () => {
|
describe('FloriferousGame', () => {
|
||||||
const alice = new FloriferousPlayer({
|
const alice = new FloriferousPlayer({
|
||||||
name: 'Alice',
|
name: 'Alice',
|
||||||
score: 2,
|
score: 2,
|
||||||
rowAtEndOfGame: 0
|
rowAtEndOfGame: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bob = new FloriferousPlayer({
|
const bob = new FloriferousPlayer({
|
||||||
name: 'Bob',
|
name: 'Bob',
|
||||||
score: 1,
|
score: 1,
|
||||||
rowAtEndOfGame: 1
|
rowAtEndOfGame: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bobWithTwoPoints = new FloriferousPlayer({
|
const bobWithTwoPoints = new FloriferousPlayer({
|
||||||
name: 'Bob',
|
name: 'Bob',
|
||||||
score: 2,
|
score: 2,
|
||||||
rowAtEndOfGame: 1
|
rowAtEndOfGame: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Determines a winner', () => {
|
it('Determines a winner', () => {
|
||||||
const game = new FloriferousGame();
|
const game = new FloriferousGame();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
game.addPlayer(alice);
|
game.addPlayer(alice);
|
||||||
game.addPlayer(bob);
|
game.addPlayer(bob);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(game.winner).toBe('Alice');
|
expect(game.winner).toBe('Alice');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Breaks a tie using the player closest to the top of the board', () => {
|
it('Breaks a tie using the player closest to the top of the board', () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
|
|
||||||
const game = new FloriferousGame();
|
const game = new FloriferousGame();
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
game.addPlayer(alice);
|
game.addPlayer(alice);
|
||||||
game.addPlayer(bobWithTwoPoints);
|
game.addPlayer(bobWithTwoPoints);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(game.winner).toBe('Alice');
|
expect(game.winner).toBe('Alice');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can give a pretty summary', () => {
|
it('Can give a pretty summary', () => {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const game = new FloriferousGame({
|
const game = new FloriferousGame({
|
||||||
playedTs: new Date('2022-08-28T13:12Z'),
|
playedTs: new Date('2022-08-28T13:12Z'),
|
||||||
players: [alice, bob]
|
players: [alice, bob],
|
||||||
});
|
});
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
const prettySummary = game.prettySummary;
|
const prettySummary = game.prettySummary;
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
expect(prettySummary).toBe(
|
expect(prettySummary).toBe('Sunday, 28 August 2022 at 14:12: Alice won with 2 points. Bob: 1 point.');
|
||||||
'Sunday, 28 August 2022, 14:12: Alice won with 2 points. Bob: 1 point.'
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const prerender = true;
|
export const prerender = false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types.js';
|
import { BlogController } from '../../../lib/blog/BlogController.js';
|
||||||
import { BlogController } from '../../../lib/blog/BlogController';
|
|
||||||
|
|
||||||
export const GET = async () => {
|
export const GET = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -19,9 +18,3 @@ export const GET = async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ getClientAddress }) => {
|
|
||||||
const address = await getClientAddress();
|
|
||||||
console.log({ address });
|
|
||||||
return json({ address });
|
|
||||||
};
|
|
||||||
|
|
|
||||||
8
src/routes/api/blog/new.json/+server.ts
Normal file
8
src/routes/api/blog/new.json/+server.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ getClientAddress }) => {
|
||||||
|
const address = await getClientAddress();
|
||||||
|
console.log({ address });
|
||||||
|
return json({ address });
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1 @@
|
||||||
// Nearly four years ago I started writing blog posts in markdown.
|
|
||||||
// Now I've got a bunch of ragtag files, so pre-rendering them
|
|
||||||
// requires some Markdown parsing that I've not done yet.
|
|
||||||
// I'd love to get tis to be `true`
|
|
||||||
// 2023-02-06 Wilson
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import type { LoadEvent } from '@sveltejs/kit';
|
import type { LoadEvent } from '@sveltejs/kit';
|
||||||
import type { Post } from '$lib/Post';
|
import type { Post } from '$lib/Post.js';
|
||||||
|
|
||||||
export async function load({ params, fetch }: LoadEvent): Promise<{ post: Post; date: Date }> {
|
export async function load({ params, fetch }: LoadEvent): Promise<{ post: Post; date: Date }> {
|
||||||
const { slug } = params;
|
const { slug } = params;
|
||||||
const { post } = await fetch(`/api/blog/${slug}.json`)
|
const { post } = await fetch(`/api/blog/${slug}.json`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return { post: null };
|
return { post: null };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
post,
|
post,
|
||||||
date: new Date(post.date)
|
date: new Date(post.date),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import type { LoadEvent } from '@sveltejs/kit';
|
import type { LoadEvent } from '@sveltejs/kit';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load({ route, url }: LoadEvent) {
|
export async function load({ url }: LoadEvent) {
|
||||||
console.log({ route, url });
|
|
||||||
|
|
||||||
if (url.hostname !== 'localhost') {
|
if (url.hostname !== 'localhost') {
|
||||||
return error(404, 'Not found');
|
return error(404, 'Not found');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue