Creating a Dynamic Blog with Flask, HTMX, TailwindCSS, and Authentication (Part 2)
Tags: htmx, flask, tailwindcss, tutorial
Published: 2024-07-02 17:58:32

---
canonical_url: https://devtoys.io/2024/07/02/creating-a-dynamic-blog-with-flask-htmx-tailwindcss-and-authentication-part-2/
---

Building a dynamic blog with 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.

- 1️⃣ If you are brand new to HTMX, check out this article! -> [Comprehensive Guide to HTMX: Building Dynamic Web Applications with Ease](https://devtoys.io/2024/06/22/comprehensive-guide-to-htmx-building-dynamic-web-applications-with-ease/)

- 2️⃣ If you want to focus on barebones without any authentications or adding css framework, check out Part 1 of this tutorial for a simpler version of the app here -> [Building a Dynamic Blog with Flask and HTMX](https://devtoys.io/2024/06/30/building-a-dynamic-blog-with-flask-and-htmx/)

---

## 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

---

## 👽 TL:DR You can find the complete source code here-> [GitHub Repo 🔗 for Part 2 Tutorial](https://github.com/judescripts/flask-htmx/tree/961a5fb62838aa2e0866098d794700ff7629dfea/part-2)

---

## 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 Flask-Login Flask-Bcrypt
```

---

**1.2 Install TailwindCSS**
Next, let’s set up TailwindCSS. You’ll need Node.js and npm installed on your machine.

**Install TailwindCSS:**
```bash
npm install -D tailwindcss
npx tailwindcss init
```
Configure TailwindCSS by editing the tailwind.config.js file:
```javascript
module.exports = {
  content: [
    './templates/**/*.html',
    './static/js/**/*.js',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

**Create a TailwindCSS input file static/css/tailwind.css:**
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

**Add a build script to your package.json:**
```json
"scripts": {
  "build": "tailwindcss -i ./static/css/tailwind.css -o ./static/css/styles.css --watch"
}
```

**Run the build script to generate your CSS:**
```bash
npm run build
```

---

**1.3 Create the Project Structure**
Organize your project directory as follows:
```bash
blog_app/
├── static/
│   ├── css/
│   │   └── tailwind.css
│   └── js/
│       └── scripts.js
├── templates/
│   ├── base.html
│   ├── index.html
│   ├── post.html
│   ├── edit_post.html
│   ├── login.html
│   ├── register.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 and user authentication using SQLAlchemy:
```python
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from flask_bcrypt import Bcrypt

db = SQLAlchemy()
bcrypt = Bcrypt()

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    email = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(150), nullable=False)

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)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    user = db.relationship('User', backref='posts')
```

---

**2.2 Set Up Flask Application**
Next, set up your Flask application in app.py:
```python
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from models import db, User, Post, bcrypt

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'your_secret_key'

db.init_app(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


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


@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'])
@login_required
def create():
    title = request.form['title']
    content = request.form['content']

    if not title or not content:
        flash("Title and content cannot be empty", "danger")
        return redirect(url_for('index'))

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

    return render_template('post_snippet.html', post=new_post)


@app.route('/edit/<int:post_id>', methods=['GET', 'POST'])
@login_required
def edit(post_id):
    post = Post.query.get_or_404(post_id)
    if request.method == 'POST':
        if post.user_id != current_user.id:
            flash("You are not authorized to edit this post", "danger")
            return redirect(url_for('index'))

        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'])
@login_required
def delete(post_id):
    post = Post.query.get_or_404(post_id)
    if post.user_id != current_user.id:
        flash("You are not authorized to delete this post", "danger")
        return redirect(url_for('index'))

    db.session.delete(post)
    db.session.commit()
    return '<script>window.location.href = "{}";</script>'.format(url_for('index'))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form['email']
        password = request.form['password']
        user = User.query.filter_by(email=email).first()
        if user and bcrypt.check_password_hash(user.password, password):
            login_user(user)
            return redirect(url_for('index'))
        flash('Invalid email or password', 'danger')
    return render_template('login.html')


@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        email = request.form['email']
        password = request.form['password']
        hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
        new_user = User(username=username, email=email, password=hashed_password)
        db.session.add(new_user)
        db.session.commit()
        flash('Account created successfully', 'success')
        return redirect(url_for('login'))
    return render_template('register.html')


@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))


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 class="bg-gray-100">
    <nav class="bg-gray-800 p-4 text-white">
        <div class="container mx-auto">
            <a href="{{ url_for('index') }}" class="mr-4">Home</a>
            {% if current_user.is_authenticated %}
                <a href="{{ url_for('logout') }}" class="mr-4">Logout</a>
            {% else %}
                <a href="{{ url_for('login') }}" class="mr-4">Login</a>
                <a href="{{ url_for('register') }}">Register</a>
            {% endif %}
        </div>
    </nav>
    <div class="container mx-auto py-8">
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    <div class="alert alert-{{ category }} mb-4 p-4 border-l-4 border-{{ category }}-400 bg-{{ category }}-100 text-{{ category }}-700">{{ message }}</div>
                {% endfor %}
            {% endif %}
        {% endwith %}
        {% 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 class="text-3xl font-bold text-center mb-8">Blog Posts</h1>
{% if current_user.is_authenticated %}
    <form hx-post="{{ url_for('create') }}" hx-target="#posts" hx-swap="beforeend" method="post" class="mb-8 p-4 bg-white shadow-md rounded">
        <input type="text" name="title" placeholder="Title" required class="w-full p-2 mb-4 border border-gray-300 rounded">
        <textarea name="content" placeholder="Content" required class="w-full p-2 mb-4 border border-gray-300 rounded"></textarea>
        <button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Create</button>
    </form>
{% endif %}
<div id="posts" class="space-y-4">
    {% 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 id="post-{{ post.id }}" class="post bg-white p-8 shadow-md rounded">
    <h1 class="text-2xl font-bold mb-4">{{ post.title }}</h1>
    <p class="mb-4">{{ post.content }}</p>
    {% if current_user.is_authenticated and post.user_id == current_user.id %}
    <div class="post-buttons flex space-x-4">
        <a href="{{ url_for('edit', post_id=post.id) }}"
           class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Edit</a>
        <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 inline-block">
            <input type="hidden" name="_method" value="DELETE">
            <button type="submit" class="bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600">Delete</button>
        </form>
    </div>
    {% endif %}
</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 bg-white p-4 shadow-md rounded" id="post-{{ post.id }}">
    <h2 class="text-xl font-bold"><a href="{{ url_for('post', post_id=post.id) }}" class="hover:underline">{{ post.title }}</a></h2>
    <p class="mb-4">{{ post.content }}</p>
    {% if current_user.is_authenticated and post.user_id == current_user.id %}
        <div class="post-buttons flex space-x-4">
            <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 inline-block">
                <a href="{{ url_for('edit', post_id=post.id) }}" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Edit</a>
                <input type="hidden" name="_method" value="DELETE">
                <button type="submit" class="bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600">Delete</button>
            </form>
        </div>
    {% endif %}
</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 class="text-3xl font-bold text-center mb-8">Edit Post</h1>
<form method="post" class="mb-8 p-4 bg-white shadow-md rounded">
    <input type="text" name="title" value="{{ post.title }}" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <textarea name="content" required class="w-full p-2 mb-4 border border-gray-300 rounded">{{ post.content }}</textarea>
    <button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Save</button>
</form>
{% endblock %}
```

---

**3.6 Login Template**
In templates/login.html, create the template for user login:
```html
{% extends "base.html" %}

{% block content %}
<h1 class="text-3xl font-bold text-center mb-8">Login</h1>
<form method="post" class="mb-8 p-4 bg-white shadow-md rounded">
    <input type="email" name="email" placeholder="Email" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <input type="password" name="password" placeholder="Password" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Login</button>
</form>
{% endblock %}
```

---

**3.7 Register Template**
In templates/register.html, create the template for user registration:
```html
{% extends "base.html" %}

{% block content %}
<h1 class="text-3xl font-bold text-center mb-8">Register</h1>
<form method="post" class="mb-8 p-4 bg-white shadow-md rounded">
    <input type="text" name="username" placeholder="Username" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <input type="email" name="email" placeholder="Email" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <input type="password" name="password" placeholder="Password" required class="w-full p-2 mb-4 border border-gray-300 rounded">
    <button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Register</button>
</form>
{% endblock %}
```

---

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

[![Hypermedia Systems](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o1qm3t19e5sf7mzj8sez.jpg)](https://amzn.to/3XKDBeI)

## [Hypermedia Systems Kindle Edition](https://amzn.to/3XKDBeI)

---

## Step 4: 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 5: 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, HTMX, and TailwindCSS. 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 TailwindCSS
- Adding user authentication and authorization

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 some 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/)
- [TailwindCSS Documentation](https://tailwindcss.com/docs)

Hope you enjoyed a more robust version of the app. The sky is the limit continue to improve and even create a project template that could work for real-world project. Happy coding!

---

## 🔥 If you enjoyed this article, please come join our hacker community at [DevToys.io](https://devtoys.io) and sign up for our newsletter to stay connected and stay with the latest news, trends and gadgets! 👽