This is a draft post
I've been hosting WordPress blogs for me and my wife on AWS EC2 for quite some time. While it isn't horribly expensive, it costs more than I really want to pay for two blogs with pretty limited audiences. At first, I started with a serverless blog engine based purely on AWS S3 and markdown with the assumption that this would "work" while using basically zero compute resources since everything would be stored as HTML in S3. Pretty quickly I switched from HTML to JSON and then realized just how much metadata you needed for navigating posts in a blog so I threw out the "zero compute" idea. Finally, I wanted to be able to leverage themes built on static HTML/CSS so I decided handlebars was a great starting point for theming.
This blog is currently using 4 projects:
- streetlight-tech/aws: CloudFormation templates for configuring AWS resources to support GitHub Actions pipelines.
- handleblog: Node.js/TypeScript core module providing object types, interfaces, and orchestrations.
- handleblog-aws: Node.js/TypeScript implementation of administration and rendering APIs hosted on AWS Lambda/API Gateway, content hosting in S3/CloudFront, and Cognito IDP.
- handleblog-react: React.js/TypeScript front end.
Simplifying Image Syntax
As I said in a previous post, optimizing code requires knowing what key variables you want to optimize. In the case of this project, I am looking for high WAF (Wife Acceptance Factor). I would like my wife to be able to use this platform and, while she is pretty tech-savvy, she has little patience when it comes to unnecessarily difficult UX. Even though I do plan on supporting WYSIWYG editing eventually, I certainly don't need this so keeping the markdown simple as possible is a great way to maximize WAF. One problem area I saw here was images. It's particularly problematic because in the short term, I'm not configuring a custom domain for the CloudFront CDN used to host content so every image needs to be fully-qualified with a cryptic domain generated by AWS. Custom rendering to the rescue! Looking in the docs for the marked
package, I found the following example.
// Create reference instance
import { marked } from 'marked';
// Override function
const renderer = {
heading(text, level) {
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
return `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
}
};
marked.use({ renderer });
// Run marked
console.log(marked.parse('# heading+'));
Combining this with some more documentation of which functions you can specify in the renderer, I found I could quickly get the desired effect with something like this:
const renderer = {
image(href: string, title: string, text: string) {
return `<img src="${options.pageConfig.contentRoot}/${href}" alt="${text}" />`;
},
};
Excerpt Logic and Markdown Parsing Fail
I have talked about the Law of Least Astonishmen many times in this blog and probably even more often IRL. A blog behavior that meets this law is to automatically create excerpts for blog posts when listing posts so I set out to write a function to render an excerpt based on the following logic:
- Excerpt must be text only
- Excerpt length must be less than or equal to a specified value (500 characters by default)
- Excerpt should break at the end of a sentence (at least following an instance of
.
) - Excerpt should be 80% of max length. If less than that, try breaking on a space instead of a period
- If we still can't get to 80%, just break regardless of the character
Since the posts are stored as markdown, converting to text seemed pretty simple. At first, I was going to try to find a utility to strip the markdown from the post but then I realized I was already using a custom renderer to handle custom roots for images as I described above. I just needed to write a custom renderer to return unformatted version of all of the various markdown elements. Easy peasy lemon squeezy! Until it wasn't.
Everything worked great at first. I was able to convert my markdown to plain text and then trim it down to the desired length. The list of posts rendered just how I wanted, but then when I rendered a post, it was rendered without any formatting like it was an excerpt. So I found this section of the documentation for marked:
All options will overwrite those previously set, except for the following options which will be merged with the existing framework and can be used to change or extend the > functionality of Marked: renderer, tokenizer, walkTokens, and extensions.
The renderer and tokenizer options are objects with functions that will be merged into the built-in renderer and tokenizer respectively.
Aha! So I just need to make sure I merge my custom renderers with a default renderer and then everything should work perfectly, right? Wrong.
Now the first time I render the post after rendering the list of posts, it renders as an excerpt but then will render correctly when I refresh. Strange. I'm still looking for the smoking gun on this issue. I believe what is going on, but have yet to confirm, is the renderers are being called in sequence so the excerpt renderer is being called first which is stripping all markdown so the second renderer has nothing to do. The fix ended up looking like this:
marked.defaults.renderer = this.bodyRenderer;