Building a Dynamic Blog with Flask and HTMX
Tags: htmx, python, webdev, devtoys
Published: 2024-07-01 05:33:14

---
canonical_url: https://devtoys.io/2024/06/30/building-a-dynamic-blog-with-flask-and-htmx/
---

Creating a dynamic blog using Flask and HTMX can be both fun and rewarding. This guide will take you through the entire process, focusing on making your blog interactive without the need for a complex single-page application (SPA) framework. By the end, you'll have a fully functional blog where users can create, read, update, and delete posts seamlessly.

---

## What You'll Need

- Basic knowledge of HTML, CSS, and JavaScript
- Basic understanding of Python and Flask (or your preferred backend framework)
- Python and pip installed on your machine

---

## Step 1: Setting Up Your Environment

**1.1 Install Flask**

First things first, let's set up our Flask environment. Open your terminal and create a virtual environment, then install Flask:
```bash
python -m venv venv
source venv/bin/activate  # On Windows, use `venv\Scripts\activate`
pip install Flask Flask-SQLAlchemy
```

## 1.2 Create the Project Structure

Organize your project directory as follows:
```bash
blog_app/
├── static/
│   ├── css/
│   │   └── styles.css
│   └── js/
│       └── scripts.js
├── templates/
│   ├── base.html
│   ├── index.html
│   ├── post.html
│   ├── edit_post.html
│   └── post_snippet.html
├── app.py
└── models.py
```

---

## Step 2: Create the Flask Backend

**2.1 Define Models**

In models.py, define a simple data model for blog posts using SQLAlchemy:
```python
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
```

**2.2 Set Up Flask Application**

Next, set up your Flask application in app.py:

*---Note---: SQLite is included with Python as a built-in library, which means you don't need to install it separately. SQLite is a lightweight, disk-based database that doesn’t require a separate server process and allows access to the database using a nonstandard variant of the SQL query language.*

```python
from flask import Flask, render_template, request, redirect, url_for
from models import db, Post
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db.init_app(app)

with app.app_context():
    db.create_all()  # Create database tables

@app.before_request
def method_override():
    if request.method == 'POST' and '_method' in request.form:
        method = request.form['_method'].upper()
        if method in ['PUT', 'DELETE', 'PATCH']:
            request.environ['REQUEST_METHOD'] = method

@app.route('/')
def index():
    posts = Post.query.all()
    return render_template('index.html', posts=posts)

@app.route('/post/<int:post_id>')
def post(post_id):
    post = Post.query.get_or_404(post_id)
    return render_template('post.html', post=post)

@app.route('/create', methods=['POST'])
def create():
    try:
        title = request.form['title']
        content = request.form['content']

        if not title or not content:
            raise ValueError("Title and content cannot be empty")

        new_post = Post(title=title, content=content)
        db.session.add(new_post)
        db.session.commit()

        # Render the new post as HTML
        return render_template('post_snippet.html', post=new_post)
    except Exception as e:
        print(f"Error occurred: {e}")
        db.session.rollback()
        return '', 500  # Return an error response

@app.route('/edit/<int:post_id>', methods=['GET', 'POST'])
def edit(post_id):
    post = Post.query.get_or_404(post_id)
    if request.method == 'POST':
        post.title = request.form['title']
        post.content = request.form['content']
        db.session.commit()
        return redirect(url_for('post', post_id=post.id))
    return render_template('edit_post.html', post=post)

@app.route('/delete/<int:post_id>', methods=['POST', 'DELETE'])
def delete(post_id):
    post = Post.query.get_or_404(post_id)
    db.session.delete(post)
    db.session.commit()
    return '<div id="post-{}"></div>'.format(post_id) # Return an empty div to swap the deleted post

if __name__ == '__main__':
    app.run(debug=True)
```

---


## Step 3: Create HTML Templates

**3.1 Base Template**

In templates/base.html, define the base HTML structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Blog App</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
    <script src="https://unpkg.com/htmx.org@2.0.0"></script>
    <script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
</head>
<body>
    <nav class="navbar">
        <a href="{{ url_for('index') }}">Home</a>
    </nav>
    <div class="container">
        {% block content %}{% endblock %}
    </div>
</body>
</html>
```
**3.2 Index Template**

In templates/index.html, create the index page to list all posts:
```html
{% extends "base.html" %}

{% block content %}
<h1>Blog Posts</h1>
<form hx-post="{{ url_for('create') }}" hx-target="#posts" hx-swap="beforeend" method="post">
    <input type="text" name="title" placeholder="Title" required>
    <textarea name="content" placeholder="Content" required></textarea>
    <button type="submit" class="btn btn-primary">Create</button>
</form>
<div id="posts">
    {% for post in posts %}
        {% include 'post_snippet.html' %}
    {% endfor %}
</div>
{% endblock %}
```

**3.3 Post Template**

In templates/post.html, create the template for displaying a single post:
```html
{% extends "base.html" %}

{% block content %}
    <div class="post">
        <h1>{{ post.title }}</h1>
        <p>{{ post.content }}</p>
        <div class="post-buttons">
                <a href="{{ url_for('edit', post_id=post.id) }}" class="btn btn-primary">Edit</a>
        </div>
    </div>
{% endblock %}
```
**3.4 Post Snippet Template**

In templates/post_snippet.html, create a snippet for individual posts to be used for dynamic updates:
```html
<div class="post" id="post-{{ post.id }}">
    <h2><a href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>
    <p>{{ post.content }}</p>
    <div class="post-buttons">
        <form action="{{ url_for('delete', post_id=post.id) }}" hx-delete="{{ url_for('delete', post_id=post.id) }}" hx-target="#post-{{ post.id }}" hx-swap="outerHTML" method="post" class="delete-form">
            <a href="{{ url_for('edit', post_id=post.id) }}" class="btn btn-primary">Edit</a>
            <input type="hidden" name="_method" value="DELETE">
            <button type="submit" class="btn btn-danger">Delete</button>
        </form>
    </div>
</div>
```

**3.5 Edit Post Template**

In templates/edit_post.html, create the template for editing a post:
```html
{% extends "base.html" %}

{% block content %}
<h1>Edit Post</h1>
<form method="post">
    <input type="text" name="title" value="{{ post.title }}" required>
    <textarea name="content" required>{{ post.content }}</textarea>
    <button type="submit" class="btn btn-primary">Save</button>
</form>
{% endblock %}
```

🔥 Fired up to learn HTMX in more depth? This is a MUST read for leveling up. 🆙

[![Hypermedia Systems Kindle Edition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqcb9sa06357ri79yxw0.jpg)](https://amzn.to/4eCUL3M)

## [Hypermedia Systems Kindle Edition](https://amzn.to/4eCUL3M)

---

## Step 4: Styling the Application

Create a simple CSS file (styles.css) to style your blog:
```css
/* General Styles */
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f4f4f4;
}

.container {
    width: 80%;
    margin: 0 auto;
    padding: 20px;
    background-color: #fff;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.navbar {
    background-color: #343a40;
    color: #fff;
    padding: 10px;
    text-align: center;
}

.navbar a {
    color: #fff;
    margin-right: 10px;
    text-decoration: none;
}

h1 {
    color: #343a40;
    text-align: center;
    margin-bottom: 20px;
}

/* Form Styles */
form {
    margin-bottom: 20px;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 5px;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}

form input, form textarea {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ccc;
    border-radius: 5px;
}

form button {
    font-size: 0.8rem;
    background-color: #007bff;
    color: #fff;
    border: none;
    padding: 10px 20px;
    cursor: pointer;
    border-radius: 5px;
}

form button:hover {
    background-color: #0056b3;
}

/* Post Styles */
.post {
    padding: 20px;
    background: #fff;
    margin-bottom: 20px;
    border-radius: 5px;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
    transition: transform 0.2s;
}

.post:hover {
    transform: scale(1.02);
}

.post h2 {
    margin-top: 0;
    color: #6c757d;
}

.post p {
    margin: 10px 0;
    color: #6c757d;
}

/* Post Buttons Styles */
.post-buttons {
    display: flex;
    gap: 10px;
    margin-top: 10px;
}

.post-buttons .btn {
    padding: 8px 16px;
    border-radius: 5px;
    font-size: 0.8rem;
    border: none;
    cursor: pointer;
    text-align: center;
    transition: background-color 0.3s, color 0.3s;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none; /* Remove underline for anchor tags */
}

.post-buttons .edit-btn, .post-buttons .delete-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.post-buttons .btn-primary {
    background-color: #007bff;
    color: #fff;
}

.post-buttons .btn-primary:hover {
    background-color: #0056b3;
}

.post-buttons .btn-danger {
    background-color: #dc3545;
    color: #fff;
}

.post-buttons .btn-danger:hover {
    background-color: #c82333;
}

.delete-form {
    display: flex;
    align-items: center;
    gap: 10px; /* Ensure space between the buttons within the form */
}
```

---

## Step 5: Add Enhanced Debugging for HTMX

Create a simple JavaScript file (scripts.js) to handle HTMX events for better debugging:
```javascript
/* static/js/scripts.js */
document.addEventListener('htmx:afterRequest', (event) => {
    console.log('HTMX request completed:', event.detail);
});

document.addEventListener('htmx:error', (event) => {
    console.error('HTMX request error:', event.detail);
});
```

## Step 6: Testing Your Application

Now that you have set up the backend, created the HTML templates, and added HTMX for interactivity, it’s time to test your application. Make sure your Flask server is running by using the command:
```bash
flask --debug run
```
*Open your web browser and navigate to http://127.0.0.1:5000/. You should see your blog's home page, where you can create, view, edit, and delete blog posts.*

---

**Create a Post**

Enter a title and content in the form at the top of the page.

Click the "Create" button. The new post should appear instantly on the page without a full page reload.

---

**View a Post**

Click on the title of a post to view its full content on a separate page.

---

**Edit a Post**

Click the "Edit" link next to a post.

Modify the title or content and click "Save". You should be redirected to the updated post's page.

Click home on top to go back to home page.

---

**Delete a Post**

Click the "Delete" button next to a post. The post should be removed instantly without a full page reload.

---

## Conclusion

In this comprehensive tutorial, you have learned how to create a dynamic blog application using Flask and HTMX. Here's a quick recap of what we've covered:

- Setting up a Flask environment and project structure
- Creating and configuring a Flask application
- Defining models with SQLAlchemy
- Creating HTML templates for your blog
- Adding HTMX attributes for dynamic form submission and deletion
- Styling your application with CSS

By following these steps, you can build modern web applications with enhanced interactivity without the need for complex single-page application frameworks. HTMX allows you to keep your workflow simple and productive while providing a smooth user experience.

## Further Reading and Resources

To deepen your understanding and keep up with the latest trends and best practices in web development, here are som👽e resources you might find helpful:

- [HTMX Documentation](https://htmx.org/)
- [Flask Documentation](https://flask.palletsprojects.com/en/3.0.x/)
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/en/20/)

By leveraging these resources, you can continue to enhance your skills and stay updated with the latest trends and best practices in web development. Happy coding!

This guide provides a complete walkthrough for creating a dynamic blog using Flask and HTMX, focusing on interactivity and simplicity. By following these steps, you'll have a modern, interactive blog application that can easily be expanded and customized to meet your needs.

## 🔥 If you enjoyed this article come visit our hacker community to connect and find more! [DevToys.io](https://devtoys.io) 👽