Generating Blog Frontmatter
I recently began the process of migrating my small blog from Ghost to a static site build with Nuxt. As apart of the process I wanted to create a small script that would automate some of the basic metadata creation the Nuxt can use to generate cards and sort content. Luckily, frontmatter in markdown is just yaml, making it easy to parse and manipulate.
The Goal
On the initial pass of what I’m calling the “Article Processor” I set out to accomplish a few things to bridge the gap of difficulty for moving a post from draft state to published. This is what I tried to keep in mind while I was working.
- Easily draft posts with images in markdown
- Automatically generate specific metadata such as
- reading time
- slugs
- url-paths
- Move, rename, and organize all files related to a post
- Convert image paths in the markdown to what is expected by Nuxt
- Create something flexible enough to expand upon.
Defining the Data
As with most things these days I started by installing Pydantic, because why not! Pydantic is a great library that aids tremendously in data validation. It also comes with a few handy methods like .dict()
that easily can convert nested classes into a friendly python dictionary that can be used directly in Jinja2 Templates. Here’s the Pydantic class I came up with to support in the creation of the frontmatter.
import datetime
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
from frontmatter import Frontmatter
class MetaData(BaseModel):
publish: bool
title: str
slug: Optional[str]
path: Optional[str]
date: Optional[datetime.date]
summary: str
reading_time: Optional[str]
tags: list
image: str
class Article(BaseModel):
metadata: MetaData
content: str
dest: Optional[Path]
@classmethod
def from_file(cls, markdown_file: Path):
raw_data = Frontmatter.read_file(markdown_file)
frontmatter = raw_data.get("attributes")
body = raw_data.get("body")
return cls(metadata=MetaData(**frontmatter), content=body)
The Article class houses the entire markdown files contents where:
- metadata is the frontmatter content defined in the draft
- content is the markdown content of the file
- dest is the path that will be set as the destination in Nuxt
Note that the Optional
parameters are ones that are set in the program while being processed. The from_file
method uses the frontmatters python library to get the data necessary from the draft posts markdown file. I didn’t actually install the package, but did copy the code as it’s a fairly small file.
Working with the Data
Now that the data has been defined, there are a few actions that need to be performed to get it ready to be published. The first of which is to determine if the post is to be published. I created an attribute in the frontmatter published: bool
where true sets a file to be published and false is to do nothing. A simple for loop through the files will determine what gets processed.
Once an article to be published is found, the script will do the following
Set Frontmatter
- slug: Uses the python-slugify package to create a slug for the post with
slugify(title)
- path: defines the path URL for next as
/{slug}
- Date: Adds a published date the file with
datetime.now()
- reading_time: Creates a basic read time from the following equation
total_words += len(content_text) / 5
- image: transforms
./image.png
->/{slug}/image.png
for Nuxt to know where to find the images in the post
Additional Processing
- Sets Image URLs in markdown from
./image.png
->/{slug}/image.png
- Sets markdown file name to
YYYY-MM-DD-{slug}.md
- Copies markdown file to
site/content
- Copies non-markdown files to
site/static/{slug}/
After settings the attributes on the Article and MetaData classes I can now pass that through a jinja2 template to render my markdown files. Which is where Pydantic comes in with a .dict()
method that I can use with the jinja2 Template.
Function to create and write the processed markdown file
def write_article(article: Article):
with open(POST_TEMPLATE, "r") as f:
template = Template(f.read())
content = template.render(article=article.dict())
with open(article.dest, "w") as f:
f.write(content)
The Jinja2 Template used to render the final file
---
title: {{ article.metadata.title }}
path: {{ article.metadata.slug }}
date: {{ article.metadata.date }}
summary: {{ article.metadata.summary }}
reading_time: {{ article.metadata.reading_time }}
tags: {{ article.metadata.tags }}
image: {{ article.metadata.image }}
---
{{ article.content }}
With that what may have taken me a few minutes per post now happens instantly! You can find the full source-code for this project and my whole website on github.