Layout and UI

Build a shared Tailwind layout with Alpine.js menus and HTMX already on the page.

Goal for this part

Give Westloop a shared shell: top nav, main column, and flash messages. Tailwind styles the layout. Alpine.js opens the account menu. HTMX is on the page so later likes and follows do not need a new JavaScript build.

Why Tailwind, HTMX, and Alpine — not React

A social UI is cards, avatars, and a sticky header. Tailwind keeps those decisions in the same files as the HTML. HTMX issues a real POST and swaps a fragment — Django still owns CSRF and authorization. Alpine covers dropdowns and “are you sure?” confirms. A React SPA would add a Node toolchain and an API you then have to host twice on the VPS.

Walkthrough: base template

This snapshot uses the Tailwind CDN for speed. Part 12 switches to a built CSS file so production does not depend on a third-party script. Put HTMX and Alpine on the base template once.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}Westloop{% endblock %}</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/htmx.org@2"></script>
  <script defer src="https://unpkg.com/alpinejs@3"></script>
</head>
<body class="bg-stone-50 text-stone-900" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
  <header class="sticky top-0 border-b bg-white">
    <nav class="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
      <a href="{% url 'posts:feed' %}" class="font-semibold">Westloop</a>
      <div x-data="{ open: false }" class="relative">
        <button type="button" @click="open = !open">Menu</button>
        <div x-show="open" @click.outside="open = false" class="absolute right-0 mt-2 w-44 rounded border bg-white p-2">
          <a href="{% url 'accounts:login' %}">Sign in</a>
        </div>
      </div>
    </nav>
  </header>
  <main class="mx-auto max-w-3xl px-4 py-6">
    {% if messages %}
      {% for message in messages %}
        <p class="mb-3 rounded bg-emerald-50 px-3 py-2">{{ message }}</p>
      {% endfor %}
    {% endif %}
    {% block content %}{% endblock %}
  </main>
</body>
</html>

The hx-headers attribute sends Django’s CSRF token on every HTMX request. Without it, likes and follows will 403 later.

How to run it and what you should see

Open /. You should see a Westloop header, a placeholder feed heading, and a working Menu button. Resize the window: the column should stay readable, not stretch edge to edge.

Common mistakes

Alpine needs defer and an x-data ancestor. HTMX will not see CSRF if you put the token only on classic forms. Do not add a bundler yet — the VPS deploy stays “Python + collected static files.”

Try this

Change the header background to a neighborhood color and add a “Search” link pointing at social:search. Confirm the menu still closes when you click outside it.