Capstone: Integrating the Catalogue Into the Existing Astro Site, and Planning Barcode Scanning as a Later Phase

Personal Catalogue: Django & PostgreSQL

Chapter 10 (Capstone) · Integrating the Catalogue Into the Existing Astro Site, and Planning Barcode Scanning as a Later Phase

Nine chapters have built a genuinely working catalogue — a real normalized schema, the admin as a fast entry tool, real public views, a type-aware add form, tag filtering, search (including a real PostgreSQL-specific full-text upgrade), styling, and a real deployment plan. This closing chapter covers the actual point of building it: getting it in front of the user, mounted onto their real, existing Astro-based site, then reviews the whole course chapter by chapter.

Three Real Ways to Mount a Django App Onto an Astro Site

Astro's own live site is a static build served by a web server — it has no Python runtime of its own, and doesn't need one for its own pages. Integrating this project isn't a matter of "adding it into Astro" the way a new Astro page would be added — it's a matter of getting both apps served correctly, side by side, behind the same web server:

OptionWhat It Looks LikeReal Effort
Subdomain catalogue.example.com, a fully separate vhost Lowest — no code changes at all, just DNS + a new nginx server block
Path-mounted reverse proxy example.com/catalogue/, same domain as the Astro site Low — one nginx location block plus one real Django setting
JSON API + Astro frontend Django returns data only; an Astro page/island renders it Highest — every page effectively rebuilt in Astro/React

The Realistic Choice: Path-Mounted Reverse Proxy

For a personal project like this, a subdomain works but feels like more infrastructure than the project needs, and a full JSON-API rewrite defeats much of the point of having already built a working Django app across nine chapters. A path-mounted reverse proxy is the real middle ground: one domain, the existing Astro site untouched, and the catalogue reachable at its own clean path.

Assuming nginx is already serving the Astro site's static build (per this site's own Setting Up a Web Server on Debian and Nginx In Depth courses), and Gunicorn (Chapter 9) is running the catalogue on a local port, the real config addition is one location block using proxy_pass rather than PHP's own fastcgi_pass:

/etc/nginx/sites-available/example.com (excerpt)
server { listen 443 ssl; server_name example.com; # The existing Astro static site root /var/www/example.com/dist; index index.html; location / { try_files $uri $uri/ =404; } # The catalogue, mounted at /catalogue/, proxied to Gunicorn location /catalogue/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
Django Needs to Know It's Mounted Under a Sub-Path
Unlike a plain PHP app, where absolute links break unless manually prefixed, Django's own {% url %} tags and reverse() calls — used throughout every template built since Chapter 4 — generate URLs based on what Django itself believes its own root path is. Proxied at /catalogue/ with no further configuration, Django still generates links starting from /, which would resolve to example.com/item/1/ instead of the real, correctly mounted example.com/catalogue/item/1/. The fix is Django's own real FORCE_SCRIPT_NAME setting:
# settings.py FORCE_SCRIPT_NAME = config('FORCE_SCRIPT_NAME', default=None)

Setting FORCE_SCRIPT_NAME=/catalogue in production's own .env tells Django, at the framework level, that every URL it generates needs that prefix — every {% url 'item_detail' item.pk %} call across every template built since Chapter 4 then correctly produces /catalogue/item/1/ with zero changes to any individual template. This is the real Django-specific counterpart to the PHP variant's own absolute-vs-relative-redirect problem — a genuinely different mechanism, but solving the identical underlying "the app doesn't know it's not at the web root" issue.

Making It Feel Like Part of the Same Site

A reverse proxy solves reachability, not visual consistency. Chapter 8's own style.css already gives the catalogue a real dark theme — matching its own CSS custom properties (background/accent colors) to whatever the Astro site's own build already uses is a small, one-time styling pass rather than a full visual redesign, and the base.html nav bar built in Chapter 8 is the natural place to add a real "← Back to Main Site" link.

A Real Alternative for Deeper Integration: A JSON Endpoint

If, later, the catalogue's own search results need to appear directly on an Astro page rather than linking out to a separate Django page, the reverse-proxy setup already built supports that too — a small Django view can return JSON instead of HTML, and an Astro island can fetch() it client-side:

catalogue/views.py
from django.http import JsonResponse from django.db.models import Q def api_search(request): query = request.GET.get('q', '').strip() items = Item.objects.filter( Q(title__icontains=query) | Q(creator__icontains=query) ).values('id', 'title', 'creator')[:20] return JsonResponse(list(items), safe=False)

This isn't built out further in this course — it's flagged here honestly as the real next step if the project's own scope ever grows toward a tighter Astro-embedded experience, reusing exactly the same ORM querying discipline Chapter 7 already established.

Capstone: What Each Chapter Actually Built

The finished project is a genuine sum of nine real prior chapters, not a single new piece written for this capstone:

ChapterWhat It Contributed
1The real Django project, its catalogue app, and a working PostgreSQL connection
2The shared Item model, the ItemType lookup, and the Tag many-to-many
3A real, customized Django admin as the fastest working data-entry tool
4The real public list and detail views, plus the N+1 query bug found and fixed with prefetch_related
5A public add-item form with type-aware labels via json_script, and the real server-vs-client validation distinction
6Tag filtering, a real pagination-drops-the-filter bug found and fixed, and an aggregated tag index
7Basic search, chained with the tag filter, plus a real PostgreSQL-specific full-text search upgrade
8Real static-file setup, a reusable _item_card.html partial, and dark-theme styling
9Environment-based settings, a least-privilege database role, collectstatic, Gunicorn, and admin hardening
10Mounting the finished app behind the same domain as the real, existing Astro site

Where Barcode Scanning Fits Now

Chapter 1's own real deadline pushed barcode scanning out of scope for this course entirely — items get added manually, through the admin (Chapter 3) or the public form (Chapter 5). With the core catalogue now genuinely working and deployed, barcode scanning becomes a real, concrete next feature rather than an abstract "someday": a barcode's ISBN or UPC number looked up against a free API (Open Library for books is a natural real fit, matching this site's own established Food Tracker courses' use of Open Food Facts for a very similar lookup-by-code pattern), pre-filling ItemForm's own fields instead of requiring every field typed by hand.

Hands-On Exercises

Exercise 1

Write the nginx location /catalogue/ block from this chapter, set FORCE_SCRIPT_NAME=/catalogue in a local test .env, and confirm a rendered {% url 'item_detail' item.pk %} link now correctly includes the /catalogue prefix.

📄 View solution
Exercise 2

Explain, in your own words, why FORCE_SCRIPT_NAME is the real Django-specific counterpart to the PHP variant's own absolute-vs-relative-redirect problem — what underlying issue do both actually solve?

📄 View solution
Exercise 3

Build the api_search JSON view from this chapter, confirm it returns real JSON via a browser or curl request, and explain why it deliberately reuses the exact same Q-object query already built in Chapter 7 rather than writing new search logic.

📄 View solution

Chapter 10 Quick Reference

  • Path-mounted reverse proxy — one nginx location /catalogue/ block using proxy_pass to Gunicorn
  • FORCE_SCRIPT_NAME — the real Django setting that keeps every {% url %}-generated link correct once the app is mounted at a sub-path
  • Visual integration — matching Chapter 8's own CSS custom properties to the Astro site's own theme
  • JSON endpoint — a real, honest next step for tighter Astro-embedded integration, reusing Chapter 7's own query logic unchanged
  • Barcode scanning — a real, concrete next phase now that the core catalogue is live, looked up via a free API and pre-filling ItemForm
Course Complete — Personal Catalogue: Django & PostgreSQL (10/10 chapters)