Python Exceptions
Handle errors gracefully with Python's try and except statements.
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.
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.
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
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
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.
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.
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.
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.
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?
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.
It works! But now what if you navigate to a page that doesn’t exist?
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.
We now have our pages functioning, along with a 404 page, but wouldn’t be nice to generate a basic menu too?
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.
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.
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.
Look at that! It’s pretty nifty how all this works.
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!