DEV Community

Cover image for Building a Dynamic Blog with Flask and HTMX
3a5abi πŸ₯·
3a5abi πŸ₯·

Posted on • Originally published at devtoys.io

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:

python -m venv venv
source venv/bin/activate  # On Windows, use `venv\Scripts\activate`
pip install Flask Flask-SQLAlchemy
Enter fullscreen mode Exit fullscreen mode

1.2 Create the Project Structure

Organize your project directory as follows:

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Flask Backend

2.1 Define Models

In models.py, define a simple data model for blog posts using SQLAlchemy:

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)
Enter fullscreen mode Exit fullscreen mode

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.

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)
Enter fullscreen mode Exit fullscreen mode

Step 3: Create HTML Templates

3.1 Base Template

In templates/base.html, define the base HTML structure:

<!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>
Enter fullscreen mode Exit fullscreen mode

3.2 Index Template

In templates/index.html, create the index page to list all posts:

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

3.3 Post Template

In templates/post.html, create the template for displaying a single post:

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

3.4 Post Snippet Template

In templates/post_snippet.html, create a snippet for individual posts to be used for dynamic updates:

<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>
Enter fullscreen mode Exit fullscreen mode

3.5 Edit Post Template

In templates/edit_post.html, create the template for editing a post:

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

πŸ”₯ Fired up to learn HTMX in more depth? This is a MUST read for leveling up. πŸ†™

Hypermedia SystemsΒ Kindle Edition

Hypermedia SystemsΒ Kindle Edition


Step 4: Styling the Application

Create a simple CSS file (styles.css) to style your blog:

/* 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 */
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Add Enhanced Debugging for HTMX

Create a simple JavaScript file (scripts.js) to handle HTMX events for better debugging:

/* 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);
});
Enter fullscreen mode Exit fullscreen mode

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:

flask --debug run
Enter fullscreen mode Exit fullscreen mode

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:

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 πŸ‘½

Top comments (0)