Creating My Website: A Blog with Markdown Functionality (Django)

November/December 2018

Wanting a place to showcase my different projects and write posts about them, I decided to create a blog. Although I had already created a blog in Django before, I also wanted to add new features in both the front-end and the back-end of the website. Most importantly, was incorporating Markdown, a markup language with plain text formatting syntax, into my website. By adding Markdown, it allowed me to take control of the formatting and display content in a better way.

Table of Contents

Rationale and Specification

I wanted a website where I could post about projects and other things I was working on. Creating a blog for the Edmonds-Woodway Key Club website already, I had an idea of what I could do. However, I also wanted to add additional features:

  • Markdown
    • Markup language with plain text formatting syntax
  • URL slugs
    • URL based on the title of the post
  • Heading anchors for table of contents

Django

For this project, I used Django as my framework to create the backend of my website. I decided on using Django mainly because I had already used it to create a previous project and wanted more in-depth experience with it.

Post Model

First, I had to figure out what models I needed in my blog application. For a blog, the only form of data I had to store was the blog's posts. I had to structure this Post model so that my posts would fit what I wanted.

The basics of a post includes:

  • An ID (the post's primary key)
    • To directly reference a specific post.
  • A title
  • A date
  • A description
  • Content

Additionally, I wanted to have:

  • the ID be a randomly generated string of numbers and letters instead of a number that increments each post
  • a URL slug created from the title of the post
    • URL slug: the exact address of a specific page on a site and usually refers to a specific page or post
    • Having a URL slug allows my URLS to be cleaner and easier to understand.
  • Markdown support for the content of my posts

Heres the code for my Post model:

# models.py
def generate_id():
    return get_random_string(6)

class Post(models.Model):
    id = models.CharField(primary_key=True, unique=True, max_length=6, default=generate_id)
    title = models.CharField(max_length=100)
    slug = models.SlugField(unique=False, max_length=100)
    date = models.CharField(max_length=25)
    publishDate = models.DateField(default=datetime.date.today)
    isPublished = models.BooleanField(default=False)
    timestamp = models.DateTimeField(auto_now=True)
    description = models.CharField(max_length=500)
    content = models.TextField()
    renderedContent = models.TextField()

    class Meta:
        ordering = ['-publishDate']

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('post', kwargs={'slug': self.slug, 'pk': self.id})

Post ID

For the ID, I didn't want my posts to be an incrementing number (1, 2, 3...etc). Instead, I wanted a randomly generated string consisting of numbers and letters like "Wa4VTZ", so that the URL of that specific post would be:

https://ivanxtan.com/posts/Wa4VTZ/...

To do this, I used get_random_string() because it is included in Django already. I created a function named generate_id before the model and used it as a callable in the default value of the id field.

Post Title and URL Slug

After, I added the title and slug fields into the model. Then, I created the title and slug fields so that both would have a max length of 100 characters. Next, I changed the save method so that it would call slugify with its title and save the result into the slug field. Lastly, I changed the get_absolute_url method so it would use my URLs and create a working link within the Django admin page.

Publishing Information

I wanted a field where I could write anything in my dates, so I made my post's date field a CharField. This allowed me to put a specific time period or even a date. This is the "date" shown on each of my posts. However, I also have an actual DateField where I can put a date in the Django admin. This publishDate is how I sort how my posts are ordered. I also wanted to have a timestamp where it would mark when my post was last updated. For this, I made a DateTimeField which would be automatically set to whenever I created or edited a post. Lastly, I made a BooleanField to set whether or not I wanted the post to be published and available to see.

Content of Post

Next, I added a field where I could write a 500 character description of the post. This was just a simple CharField. For the content, I wanted two TextFields. One would contain the bare Markdown text and then the other field would be generated automatically into HTML from the Markdown.

Views

For my views, they simply query the database for posts and then use the templates I had created.

Heres the code for my views:

# views.py
class PostListView(generic.ListView):
    model = Post
    context_object_name = 'post_list'
    queryset = Post.objects.filter(isPublished=True)
    template_name = 'work.html'
    paginate_by = 3

class PostDetailView(generic.DetailView):
    model = Post
    queryset = Post.objects.filter()
    template_name = 'post.html'

The only main difference is that for my list view, I paginate by three and filter my posts to check if I set the BooleanField to true.

Admin

Markdown Renderer

To add Markdown functionality, I used Mistune as my Markdown renderer and Pygments to highlight the code sections of my text.

In my code, I added the code highlighting snippet from Mistune:

# admin.py
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import html

class HighlightRenderer(mistune.Renderer):
    def block_code(self, code, lang):
        if not lang:
            return '\n<pre><code>%s</code></pre>\n' % \
                mistune.escape(code)
        lexer = get_lexer_by_name(lang, stripall=True)
        formatter = html.HtmlFormatter()
        return highlight(code, lexer, formatter)

renderer = HighlightRenderer()
markdown = mistune.Markdown(renderer=renderer)

PostAdmin

Also, I changed the save_model method of my PostAdmin.

# admins.py
class PostAdmin(admin.ModelAdmin):
    ...
    def save_model(self, request, obj, form, change):
        obj.renderedContent = markdown(obj.content)
        super().save_model(request, obj, form, change)

This was the most important part to make it so that my Markdown content could be rendered into HTML. I changed the save_model method so that it would have Mistune render the Markdown from the Post's content field and save it into the Post's renderedContent field.

Templates

In my templates, it was pretty simple. I just had to mark my renderedContent as safe so that Django wouldn't automatically escape HTML tags it found. So for an article, the template would look like this:

<article>
    <div class="article-info">
        <h1>{{ post.title }}</h1>
        <h2>{{ post.date }}</h2>
        <h3>Last updated: {{ post.timestamp|date:"F j, Y" }}</h3>
        <p>{{ post.description }}</p>
    </div>
    <div class="article-content">{{ post.renderedContent|safe }}</div>
</article>

URLs

Specifically for my URLs, I made it so that my website would only care about the ID of the post. This way, if I changed the title of a post, users wouldn't be sent a 404 error since the post's ID wouldn't have changed.

Heres my code for my URLs:

# urls.py
path('posts/', blogViews.PostListView.as_view(), name='posts'),
# Matches primary key (id) format (letters & numbers) and excludes everything after / for the slug
re_path(r'^posts/(?P<pk>[a-zA-Z0-9]+)/(?P<slug>[-\w\d]+)/$', blogViews.PostDetailView.as_view(), name='post'),
re_path(r'^posts/(?P<pk>[a-zA-Z0-9]+)/$', blogViews.PostDetailView.as_view(), name='postOnlyID'),

JavaScript

I also wanted to make the reading experience better by adding client-side features. This included heading anchors to create a working table of contents.

Heading Anchors

To create a working table of contents, I had to create links to each heading dynamically using JavaScript and jQuery.

$(document).ready(function() {
    $(".article-content h1, .article-content h2, .article-content h3, .article-content h4, .article-content h5, .article-content h6").each(function(i) {
        var heading = $(this);
        var headingString = heading.text().toLowerCase().replace(/([~!@#$%^&*()_+=`{}\[\]\|\\:;'<>,.\/? ])+/g, '-').replace(/^(-)+|(-)+$/g,'');
        var headingId = headingString;
        heading.attr("id", headingId);
        heading.append("<a class='heading-link' href='#" + headingId + "'><i class='fas fa-link'></i></a>");
    });
})

Conclusion

Overall, this project was great because it allowed me to go deeper into the Django framework and learn things that I hadn't learned before. Additionally, I learned how to deploy a Django app and manage a domain.

Resources

Django

Django Framework

Django Guide/Tutorial from Mozilla/MDN

Alphanumeric IDs

Generate random alphanumeric string in Django

Setting a default value to a function call / callable

Slugs in Django

Create Human Readable URLs in Django That Don't Break

Readable URLs

Markdown and Syntax Highlighting

Mistune

Pygments

Heading Anchors

Create Dynamic Heading Anchor Links