Markdown, Python, and Flask

Markdown is a simple markup language used to write content for the web. It uses a plain text format where symbols like hashtags, asterisks, and underscores convert the text to HTML elements when parsed by the appropriate software.

In this post we introduce a Python module called Python-Markdown and the micro-framework Flask to see how Markdown (.md) files can be rendered into web pages.

Project Setup

Create the Virtual Environment

Create a new project and virtual environment. Virtual environments can be created using the Python venv module that comes as part of the Python standard library.

Inside your project folder, execute the following command in a Terminal window to create your virtual environment.

python3 -m venv venv

You can call your virtual environment anything you choose. One common naming convention developers use is either venv or .venv. The dot-notation would keep the directory hidden in Finder. The name helps identify why the directory exists.

Next, you must activate the virtual environment to use it. Execute the following command to activate your environment.

. venv/bin/activate

Once the virtual environment is activated, we can install Flask.

Install Flask

Flask is a popular micro-framework for building web applications in Python. Flask comes with a few essential components such as Wrkzeug, used to create a web server, and Jinja, a web templating engine.

Run the following command to install Flask and its dependencies.

pip3 install flask

Install Markdown

Now we can install the Python-Markdown module. This module contains a function that converts our .md files into HTML, which we can output to our web pages.

Run the following command to install markdown.

pip3 install markdown

Hello World

Let’s create a minimal web application to get something up and running.

Create a main.py file in your project directory with the following code:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def root():
	return "<h1>Hello, World!</h1>"

The app variable is created with an instance of Flask. We define a simple root path with the function decorator @app.route("/"). The path function root() returns an H1 header with a bit of text.

Run the command below to start the web server.

flask --app main.py --debug run

This command can be broken down into:

Part Description
flask The Flask CLI tool
--app The instance of Flask in our application
main.py The name of our application file
--debug Turns on debug mode and auto-reloads the server when code changes are made
run The CLI command to start everything

Navigate to http://localhost:5000 to see the Hello, World! message displayed in your browser.

Hello world message

HTML Templates

With the web server working, we can start with HTML templating.

Create a folder in the root of your project directory and name it templates, and inside that folder, create an index.html file.

Add the following code to the index.html file.

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>Home</title>
  </head>
  <body>
	<h1>Home Page</h1>
  </body>
</html>

Include render_template to your list of flask imports. This lets us serve HTML files rather than a string of HTML elements.

from flask import Flask, render_template

Next, update the root path in your main.py file to be:

@app.route("/")
def root():
	return render_template('index.html')

When we call the render_template() method, we pass in the name of our HTML file. Flask will automatically look in the templates folder for this file and then serve it whenever the client accesses the root path of the web server.

Refresh your browser to see the following.

Home page screenshot

Everything still works! Now we can start passing data into the web page.

Update the root path again in your main.py to match the following:

@app.route("/")
def root():
	data = {}
	data["page_title"] = "Home Page"
	return render_template('index.html', data=data)

Here, we create a data dictionary and set the key “page_title” to be “Home”. Inside the render_template() function, we pass the data dictionary to a data variable that can be read in our HTML template.

A dictionary data type is often preferred here because we can create as many key-value pairs as we wish and only need to pass in the data once to the render_template() function.

Now in your index.html file, update the page title as shown below.

<title>{{ data.page_title }}</title>

The page title will update with the “page_title” value in our data dictionary.

Page title updated

Markdown

With a basic page template working we can start importing Markdown content.

Create a new folder called content and a new home.md file in it. Then add the following snippet to the markdown file.

# Welcome

**Lorem ipsum** dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

1. list item 1
1. list item 2
1. list item 3

In your main.py file, add the following import.

import markdown

Update the root path as follows.

@app.route("/")
def root():
	data = {}
	data["page_title"] = "Home Page"

	with open('content/home.md', 'r') as f:
		text = f.read()
		data["html"] = markdown.markdown(text)

	return render_template('index.html', data=data)

We open the markdown file in r read-only mode. Because we use the with keyword, files will automatically be closed once this code block finishes executing.

The read() method is used to assign the content of the file to a text variable. The text variable is then passed into a markdown function inside the markdown module that returns the HTML we assign to an “html” key in our data dictionary.

After all that, our markdown content is now available in our HTML template.

Open index.html and update the <body> tags:

<body>
	{{ data.html|safe }}
</body>

We pass in the content of the “html” key. We also need to use the “safe” filter to mark the content as safe and prevent Jinja from outputting HTML entities rather than HTML tags.

Refresh your browser to see the contents of the markdown file as a web page.

Home markdown

That looks great! But what if we had more Markdown files in our content directory and wanted to dynamically generate links to all the them?

Automatically Generate Pages

In your content folder, create two new files: about.md and contact.md.

Add the following to the about.md file.

# About

The about page.

Add the following to the contact.md file.

# Contact

The contact page.

Feel free to create more pages and include more content in the Markdown files if you want.

Next, we need to use a function in Python’s os library that lets us check whether a file exists at a specified path.

Add the following import to main.py.

from os.path import exists

Then add the new path as follows.

@app.route("/<page>")
def get_page(page):
	if exists("content/"+page+".md"):
		data = {}
		data["page_title"] = page.title()
		with open('content/'+page+'.md', 'r') as f:
			text = f.read()
			data["html"] = markdown.markdown(text)
		return render_template('index.html', data=data)

This route will grab all GET requests that match the http://localhost:8000/<page-name> format and then immediately test whether the specified page exists as a corresponding .md markdown file inside the content folder.

If the Markdown file exists, we create our data dictionary again and set the “page_title” key to be the value of the page parameter but with the first letter of the word capitalized.

Then we open the markdown file, parse it, assign the content to the “html” key in our data dictionary, and pass it into the render_template() function like we did before.

Navigate to http://localhost:5000/about and http://localhost:5000/contact to see the content of your markdown files rendered out to the page.

About page

It works! But now what if you navigate to a page that doesn’t exist?

404 Page

Try accessing http://localhost:5000/blog. A TypeError is generated by the Werkzeug web server. This error is caused by our web application failing to return a valid response.

We could update our if-statement with an else clause like below.

@app.route("/<page>")
def get_page(page):
	if exists("content/"+page+".md"):
		data = {}
		data["page_title"] = page.title()
		with open('content/'+page+'.md', 'r') as f:
			text = f.read()
			data["html"] = markdown.markdown(text)
		return render_template('index.html', data=data)
	else:
		return ''

This works and serves a blank page to the user, but it’s not very informative and it doesn’t send the correct HTTP status code either (404 Not Found).

Create a new file 404.html inside your templates folder and add the following code. This will be our Page Not Found page.

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>404 - Not Found</title>
  </head>
  <body>
	<h1>Page Not Found</h1>
  </body>
</html>

Next, add abort to your list of flask imports.

from flask import Flask, render_template, abort

Then update the page route to call abort() if the markdown file is not found.

@app.route("/<page>")
def get_page(page):
	if exists("content/"+page+".md"):
		data = {}
		data["page_title"] = page.title()
		with open('content/'+page+'.md', 'r') as f:
			text = f.read()
			data["html"] = markdown.markdown(text)
		return render_template('index.html', data=data)
	else:
		abort(404)

Finally, add the @app.errorhandler() decorator and path function to the bottom of your main.py file.

@app.errorhandler(404)
def page_not_found(e):
	return render_template('404.html'), 404

When abort(404) is called, Python finds this 404 errorhandler and returns the the 404.html file and the correct HTTP status code to the user.

Refresh the page for http://localhost:5000/blog and you see the 404.html page being served.

Page not found

We now have our pages functioning, along with a 404 page, but wouldn’t be nice to generate a basic menu too?

Create a Basic Menu

The os library provides us with useful functions for when working with the file system.

Add the following import to your main.py file.

import os

Create a new function get_menu() at the bottom of the file and add the following.

def get_menu():
	pages = os.listdir("content")
	pages = list(map(lambda page:page.replace(".md", ""), pages))
	return pages

We pass the name of our content folder to os.listdir(), which returns a list of all the files in that directory, along with their file extensions.

We then use a lambda function to replace the .md file extensions with an empty string, effectively removing the file extension.

Then at last, we return the list of pages.

Update Routes

We can call the get_menu() function in each of our routes and pass the list of pages into our data dictionary.

Update the root and page paths in your main.py file so the data dictionary includes a new key “pages” that is assigned the result of the get_menu() function.

data["pages"] = get_menu()

We can include the navigation in a separate file to make our code more modular. This is nice when we wish to update the navigation in the future but don’t want to browse each individual HTML template to do so.

Create a new file called nav.html in your templates folder and add the following code to it.

<nav>
	{% for page in data.pages %}
		<a href="/{{ page }}">{{ page }}</a>
	{% endfor %}
</nav>

The curly brackets and perfect symbols are part of the Jinja templating engine, which offers the ability to execute code. In our case, we loop over the pages in the data dictionary variable and output each element as a link.

The above code will generate the following:

<nav>
	<a href="/contact">contact</a>
	<a href="/home">home</a>
	<a href="/about">about</a>
</nav>

There are different ways to modify the order. For example, we could give our Markdown files different names, like 1.home.md, 2.about.md, and so on. Then process the names accordingly so the page names and routes still worked. We could also organize our Markdown into subfolders to represent a hierarchical-system of pages. These techniques won’t be covered in this post, but will likely appear in future posts.

HTML Template

Next, update the body of the index.html file to include an import statement for the nav.html file.

<body>
	{% include 'nav.html' %}
	{{ data.html|safe }}
</body>

The include statement takes the content in the specified HTML file and inserts it into the current page template.

Refresh one of your pages to see the menu now appears at the top.

Contact page with menu

Look at that! It’s pretty nifty how all this works.

Summary

In this post, we explored how to use Python to read Markdown files and convert their content into HTML. We also created a basic web application that serves the user a directory of Markdown files as HTML pages.

The website is simple, for sure. But imagine how this could be built out into a larger web application. You may even have a use for it when creating simple APIs and other web applications where you just want to have an About page or a few pages that are easy to update.

You could even implement something like SimpleMDE Markdown Editor, have user login in Flask and then edit Markdown content right there in your Flask app. The possibilities are endless!