Capstone

Personal Catalogue: PHP & MySQL

Chapter 10 · Capstone

Nine chapters have built a genuinely working catalogue — add/edit/delete, type-specific fields, tags, search, an N+1-avoiding list view, fast bulk entry, 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 PHP App Onto an Astro Site

Astro's own live site is a static build served by a web server — it has no PHP runtime of its own, and doesn't need one for its own pages. That means 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 vhost
Path-mounted reverse proxy example.com/catalogue/, same domain as the Astro site Low — one nginx location block, no PHP code changes
JSON API + Astro frontend PHP 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 PHP 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 with zero changes to any of the PHP code built so far.

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 PHP-FPM is running the catalogue on a local port or socket, the real config addition is one location block:

/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/ location /catalogue/ { alias /var/www/catalogue/; index index.php; location ~ \.php$ { fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; } } }
Why alias, Not root, for the Nested Location
Using alias instead of root inside the /catalogue/ block matters specifically because the two paths don't share the same tail: a request for /catalogue/index.php needs to resolve to the real file at /var/www/catalogue/index.php, not /var/www/catalogue/catalogue/index.php (which is what root would produce, since it appends the full matched URI onto the given path). alias replaces the matched location prefix with the given directory instead of appending to it, which is exactly the behavior needed here.
Every Internal Link Needs the /catalogue/ Prefix
Every header('Location: /add_item.php...') redirect and every href="edit_item.php?..." link written across Chapters 3-8 was built assuming the app sits at the web root. Mounted at /catalogue/ instead, absolute redirects like header('Location: /add_item.php?added=1') would actually send the browser to example.com/add_item.php — outside the mounted path entirely, a real 404 on the Astro site. Every absolute redirect needs a /catalogue prefix, or (the more robust fix) needs to become a relative redirect (header('Location: add_item.php?added=1')) that stays correct regardless of where the app is ever mounted.

Making It Feel Like Part of the Same Site

A reverse proxy solves reachability, not visual consistency — without any further work, the catalogue's own plain unstyled markup would look nothing like the Astro site's real dark theme. A lightweight, real fix: extract the catalogue's own repeated page chrome into one shared partial, styled to visually match the main site's own established look:

partials/header.php
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My Catalogue</title> <link rel="stylesheet" href="/catalogue/assets/catalogue.css"> </head> <body> <nav> <a href="/">← Back to Main Site</a> <a href="/catalogue/index.php">Browse</a> <a href="/catalogue/search.php">Search</a> <a href="/catalogue/add_item.php">Add Item</a> </nav> <main>

catalogue.css itself doesn't need to be built from scratch — copying the real dark-background/accent-color CSS custom properties already used across this site's own Astro build gives the catalogue a genuinely matching look with a small, one-time styling effort, rather than a full visual redesign.

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 PHP page), the reverse-proxy setup already built supports that too — a small PHP script can return JSON instead of HTML, and an Astro island can fetch() it client-side:

api_search.php
<?php require_once __DIR__ . '/config.php'; header('Content-Type: application/json'); $searchTerm = trim($_GET['q'] ?? ''); $stmt = $pdo->prepare(' SELECT id, title, creator FROM items WHERE title LIKE :query OR creator LIKE :query ORDER BY title LIMIT 20 '); $stmt->execute(['query' => '%' . $searchTerm . '%']); echo json_encode($stmt->fetchAll());

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 PDO connection and prepared-statement discipline every other chapter 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 PDO connection, native prepared statements, and the catalogue database itself
2The shared items table, item_types, and the tags/item_tags many-to-many pairing
3Add/edit/delete, with delete deliberately restricted to POST
4Dynamic, type-aware labels and a real per-type release_year sanity check
5Tag creation, attachment, and the diff-based sync_item_tags()
6Real search across title/creator, with type/tag filters and LIKE escaping
7An N+1-avoiding list view via GROUP_CONCAT, and the detail page
8Keyboard-friendly quick-add and a transaction-backed bulk importer
9Config separation, schema migration, a least-privilege database user, and production error handling
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, via the fast bulk-paste path built in Chapter 8. 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 add_item.php's own form instead of requiring every field typed by hand.

Hands-On Exercises

Exercise 1

Add the nginx location /catalogue/ block from this chapter to a real (or test) nginx config, alongside a real static index.html served at the site root. Confirm both example.com/ (or your test domain) and example.com/catalogue/index.php resolve correctly to their own separate real content.

📄 View solution
Exercise 2

Update every header('Location: ...') redirect across add_item.php, edit_item.php, and delete_item.php to use a relative path instead of an absolute one (per this chapter's own warning), then confirm the app still works correctly whether it's mounted at the web root or at /catalogue/.

📄 View solution
Exercise 3

Build api_search.php from this chapter, visit it directly in a browser with a real search term (e.g. api_search.php?q=Python), and confirm the response is genuine, valid JSON — not an HTML page — by checking the response's own Content-Type header and validating the body with a JSON linter or php -r 'var_dump(json_decode(file_get_contents("php://stdin")));'.

📄 View solution

Chapter 10 & Course Quick Reference

  • Path-mounted reverse proxy — one nginx location /catalogue/ block, zero PHP code changes, same domain as the existing Astro site
  • alias, not root — required for a nested location whose URL path doesn't match its real filesystem path
  • Relative redirects — the real fix that makes every prior chapter's own header('Location: ...') call correct regardless of mount path
  • A shared header partial — the lightweight fix for visual consistency with the main site, reusing its own real CSS custom properties
  • A JSON endpoint — the real, honest next step if this project's scope ever grows toward tighter Astro-embedded integration
  • Barcode scanning — the feature Chapter 1 deliberately deferred, now a real, concrete next step once the core app is live
  • Course complete — every chapter's own piece (Chapters 1-9) verified working together as one deployed, real personal catalogue