Skip to content

thenexustv/nexus-archive

Repository files navigation

nexus-archive

A static archive of thenexus.tv, a podcast network that produced 1,384+ episodes across 13 series between 2011 and 2022. Built with Astro, TailwindCSS, and React.

The original site ran on WordPress with the Nexus Core plugin and Coprime theme. This project replaces it with a fully static site generated from a JSON data export.

Getting Started

pnpm install
pnpm dev        # Start dev server
pnpm build      # Build static site to dist/
pnpm preview    # Preview the built site
pnpm test       # Run tests

Hosting

The site runs as a standalone Node server using the @astrojs/node adapter, deployed in a Podman container.

Why output: 'server' instead of output: 'static'

Astro is configured with output: 'server' even though every page is prerendered as static HTML (export const prerender = true in each page file). This is intentional.

With output: 'static', the node adapter acts as a plain static file server. Requests that don't match a prebuilt file (e.g., /episode/ted20/) are 404'd immediately — Astro middleware never runs at request time in static mode, and neither do server-rendered routes. This is a significant and poorly documented limitation of the Astro node standalone adapter.

By using output: 'server' with prerendered pages, we get the best of both worlds:

  • Pages are still prebuilt as static HTML at build time (same performance as static mode)
  • Server-rendered route files can handle requests at runtime for paths that don't match a static file

This matters because the original WordPress site used /episode/ted20 (singular) while the archive uses /episodes/ted20/ (plural). Server-rendered redirect routes handle these so old links, bookmarks, and search engine results continue to work.

Important caveat: Even with output: 'server', Astro middleware does not reliably run at request time for paths without a matching route. Redirects are therefore implemented as server-rendered route files (e.g., src/pages/episode/[slug].ts, src/pages/[slug].ts) rather than in middleware. The middleware in src/middleware.ts exists as a secondary layer but should not be relied upon as the sole redirect mechanism.

Redirect routes

Old URL Redirects to
/episode/{slug} /episodes/{slug}/ (with slug normalization)
/episode/ /episodes/
/{slug} (e.g., /ted20) /episodes/{slug}/
/category/{series}/feed /series/{series}/feed.xml

Slug normalization converts dashed formats (atn-1) to the canonical dashless format (atn1).

Running locally

pnpm build
node dist/server/entry.mjs

Deploying with Podman

The production server (grail, Ubuntu 24.04) runs the site as a rootless Podman container managed by systemd via Quadlet. The full setup story — including Podman installation, rootless containers, lingering, and networking — is documented in .dev/plans/plan13.md.

Internet → Apache (TLS termination) → 127.0.0.1:4321 (Podman container)

The Quadlet unit file is at deploy/nexus-archive.container and gets installed to ~/.config/containers/systemd/ on the server. It tells systemd how to run the container: which image, port binding, environment variables, healthcheck, and restart policy.

Initial setup (one-time, on the server):

# Build the image from docker-compose.yml
podman compose build

# Install the Quadlet file and start the service
cp deploy/nexus-archive.container ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user start nexus-archive

Do not run systemctl --user enable — Quadlet-generated services handle auto-start via the [Install] section automatically. It will fail with "unit is transient or generated," which is expected.

Deploying updates:

git pull
podman compose build
systemctl --user restart nexus-archive

Managing the service:

systemctl --user start nexus-archive    # Start
systemctl --user stop nexus-archive     # Stop
systemctl --user restart nexus-archive  # Restart (after rebuilding image)
systemctl --user status nexus-archive   # Check status + health
journalctl --user -u nexus-archive -f   # Tail logs

The container exposes port 4321 on localhost only (127.0.0.1:4321). Apache reverse-proxies to it (see .dev/plans/plan12.md). A healthcheck pings /api/health every 30 seconds.

Architecture

Data Layer

All data comes from a single JSON export (export/nexus-export-1770519097.json, ~4.8 MB). There are no content collections or database connections — src/data/index.ts loads the JSON at build time, resolves all relationships between series, episodes, people, and media, then exports typed query functions like getAllEpisodes(), getSeriesBySlug(), and getEpisodesByPerson().

Episode slugs follow the format {series_slug}{number} (e.g., atn1, tf130). Person slugs are derived from names (e.g., ryan-rampersad). Episodes can have fringe (spin-off) and parent (back-link) relationships to other episodes.

Pages

Route Description
/ Homepage with recent episodes, series grid, and network stats
/episodes/ Paginated episode listing (50 per page)
/episodes/[slug]/ Episode detail with audio player, people, and related episodes
/series/ All series directory
/series/[slug]/ Series detail with paginated episodes
/people/ People directory, separated into hosts and guests
/people/[slug]/ Person profile with episode history
/about/ Network history and archive information
/contact/ Contact page
/licenses/ Creative Commons license details

See Hosting for details on how server-rendered redirect routes handle old WordPress-style URLs.

Components

  • AudioPlayer.tsx — React component with HTML5 audio playback, Web Audio API frequency visualization (with synthetic fallback for CORS-blocked sources), keyboard shortcuts, and a native player toggle. Client-side hydrated.
  • EpisodeCard.astro — Episode card with series tag, date, duration, and description.
  • Pagination.astro — Page navigation with smart range display and ellipsis.
  • PersonCard.astro — Person card with Gravatar, role badge, and episode count.

Styling

TailwindCSS v4 with the typography plugin. Full dark mode support. Custom League Gothic font for headings. The site uses a neutral gray/stone palette with blue accents.

Testing

pnpm test           # Run once
pnpm test:watch     # Watch mode

Tests use Vitest and cover the data layer query functions and utility functions (slugifyName, makeEpisodeSlug, gravatarUrl, processContent, parseDuration, formatTime). Tests load the real JSON export — no mocks needed.

Project Structure

src/
  components/     # Astro and React UI components
  data/           # Data loading, types, and query functions
  layouts/        # Base HTML layout
  pages/          # All route pages
  styles/         # Global CSS
export/           # Source JSON data export
public/           # Static assets (images, fonts, favicons)
.dev/             # Development plans and knowledge base

About

thenexus.tv archive astro site

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors