import datetime
current_time = datetime.datetime.now()
print(f"The current time is {current_time} UTC")
The current time is 2025-03-04 11:04:43.182013 UTC
Static site generators are not a new thing. You can write a blog post in markdown, and then use a static site generator to convert it to html. The issue is that the site is...static. Nothing on the site can change after the moment that html was generated.
FastHTML and MonsterUI make it easy to process those same markdown files at the time they are being served, making it possible to add dynamic content to them. Still, there isn't an easy way to truly make those markdown files dynamic.
That's why I've created fh-posts. It is a library that makes it easy to write blog posts in markdown and .ipynb files that can be processed at the time they are being served to generate dynamic content from python code blocks in the markdown and from any code cell in the .ipynb files.
Tagged code blocks in the markdown are processed in order and the namespace is preserved from one code block to the next allowing you to write markdown files that function like a jupyter notebook.
It is even easier in a notebook. All python cells are run but only the tagged cells are output. This means you can write content in markdown cells, experiment with code in the code cells, and only output the interesting results to the blog post.
Tags
In a markdown file when you add a code block with ```python (triple backticks) you can append additional colon seperated tags to control how the code is run and rendered.
python
(default) - output the code but don’t run itpython:run
- run and show the code and the outputpython:run:hide
- run the code but don’t show the code or outputpython:run:hide-in
- run the code but don’t show the code block, only the outputpython:run:hide-out
- run the code and show the output but don’t show the code blockpython:run:hide-call
- run the code and show the output and the code block but don’t show the call to the function (last line of code)
In a notebook file all code cells are run by default. Add a #|python
tag to the first line of any python code cell to also have it appear as a code block in the post. All of the other tags for markdown posts apply to notebook posts as well.
A Simple Example
In a markdown file to add a code block you would normally write:
```python
print("Hello, world!")
```
With fh-posts this will still work as expected and the code block will just be displayed as a code block. However, if you want to run the code and display both the code block and the output you would update the code block to:
```python:run
print("Hello, world!")
```
This would produce the following output:
print("Hello, world!")
Hello, world!
I am actually writing this blog post in a Jupyter Notebook which you can see here. So, instead of writing a markdown code block I am just putting the code in a normal python code cell and adding #|python:run
to the first line of the cell to dispaly the example above.
To only show the output of the code without the code block you can use the python:run:hide-in
tag. Here is the output of me printing the current time variable we created above:
The current time is 2025-03-04 11:04:43.182013 UTC
Notice the time is the same as when I first printed it. This works the same way in a markdown file where previously run code blocks are available to the next code block. All code cells are run in a notebook but if you didn't add run
to the tag in a markdown code block then that code is never run and not available to the next code block.
Both fasthtml and monsterui are imported by default so you can use them in your code blocks without any imports. Here is an example card from my last blog post on FastHTML and MonsterUI:
def TeamCard(name, role, location="Remote"):
icons = ("mail", "linkedin", "github")
return Card(
DivLAligned(
DiceBearAvatar(name, h=24, w=24),
Div(H3(name), P(role))),
footer=DivFullySpaced(
DivHStacked(UkIcon("map-pin", height=16), P(location)),
DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))),
cls="max-w-sm mx-auto"
)
TeamCard("James Wilson", "Senior Developer", "New York")
If I want to run the code and show the code block and output but omit the function call (last line) I can use the tag: python:run:hide-call
to get the following output:
def TeamCard(name, role, location="Remote"):
icons = ("mail", "linkedin", "github")
return Card(
DivLAligned(
DiceBearAvatar(name, h=24, w=24),
Div(H3(name), P(role))),
footer=DivFullySpaced(
DivHStacked(UkIcon("map-pin", height=16), P(location)),
DivHStacked(*(UkIconLink(icon, height=16) for icon in icons))),
cls="max-w-sm mx-auto"
)
James Wilson
Senior Developer
YAML Frontmatter
load_posts
from fh-posts
will automatically load the YAML frontmatter from the markdown or notebook file. It uses fastcore's AttrDict
to make it easy to access the frontmatter in the post. You can access the frontmatter in the post by calling post.title
or by calling post['title']
. This gives you frontmatter code completion in your IDE.
Loading Posts
Here is an example of how to use fh-posts
to load all the posts in the posts
directory and render a single post:
from fh_posts.all import *
# Or import only the core functionality
from fh_posts.core import Post, load_posts
from pathlib import Path
# Load posts from the 'posts' directory
posts = load_posts(Path('posts'))
# Access metadata
for post in posts:
print(post.title, post.date)
# Render a post by its slug
post = next(p for p in posts if p.slug == 'hello')
html_output = post.render(open_links_new_window=True)
print(html_output)
Other Features
The 'Live rendered output' labels can be turned off by calling post.render(live_label=False)
when you render the post. Additionally, if you prefer all links in a post to open in a new window you can call post.render(open_links_new_window=True)
. You can also check out the documentation for more information.
Why fh-posts?
I want to write an upcoming blog post about converting websites to markdown for LLMs to better process. I want to blog about my experiences testing Jina.ai, Markitdown, and Docling. If the blog post is dynamic then I can blog about my current findings with a mixture of markdown and python but have it also be displaying the current results of those sites/libraries at the time the post is rendered. This means that the posts I write will function like a Jupyter notebook and make it easy for me to see if either of those sites or libraries have improved.
You could imagine this with testing different prompts over time with different LLM APIs or any other research you need to conduct periodically. Instead of it being in a notebook that is hard to find and share you can create a blog post.
If you want to include posts in a public blog that you don't want others to be able to run because it uses your API key, just add a front matter value of private: true
and validate your identity via the session in your code before the server is allowed to run that code using your API key from the .env file. It feels like this adds just one more reason to start a blog.