DEV Community

Cover image for Creating a Dynamic Blog with Flask, HTMX, TailwindCSS, and Authentication (Part 2)
3a5abi πŸ₯·
3a5abi πŸ₯·

Posted on • Originally published at devtoys.io

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.


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


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

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

Install TailwindCSS:

npm install -D tailwindcss
npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

Configure TailwindCSS by editing the tailwind.config.js file:

module.exports = {
  content: [
    './templates/**/*.html',
    './static/js/**/*.js',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Create a TailwindCSS input file static/css/tailwind.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Add a build script to your package.json:

"scripts": {
  "build": "tailwindcss -i ./static/css/tailwind.css -o ./static/css/styles.css --watch"
}
Enter fullscreen mode Exit fullscreen mode

Run the build script to generate your CSS:

npm run build
Enter fullscreen mode Exit fullscreen mode

1.3 Create the Project Structure
Organize your project directory as follows:

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
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 and user authentication using SQLAlchemy:

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

2.2 Set Up Flask Application
Next, set up your Flask application in app.py:

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

3.6 Login Template
In templates/login.html, create the template for user login:

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

3.7 Register Template
In templates/register.html, create the template for user registration:

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

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

Hypermedia Systems

Hypermedia Systems Kindle Edition


Step 4: 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 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:

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, 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:

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 and sign up for our newsletter to stay connected and stay with the latest news, trends and gadgets! πŸ‘½

Top comments (2)

Collapse
 
jgdevelopments profile image
Julian Gaston

Great article. Thanks for the read!

Collapse
 
3a5abi profile image
3a5abi πŸ₯·

Thanks appreciate it!