Skip to the content.

Case Calendar can render a static index.html alongside your ICS files — a single landing page that lists every calendar, with one-click subscribe buttons, the cases on each calendar, and (when AI summaries are enabled) a short prose description for each. Drop it behind any HTTP server and you’ve got a shareable URL for the whole project.

A live deployment is at casecalendar.net — look there for a sense of what the rendered page actually looks like with real cases, real summaries, and real subscribe buttons.

This page describes how to enable it and how to host it.

← Back to docs

What you get

When index_path is set in config.yaml, every sync / serve / emit writes an index.html to that path. The page is fully self-contained — inline CSS and JS, no external requests, no CDN — so it works behind any static HTTP server (or just opening the file directly in a browser).

Features:

Enabling the page

Three config keys, all optional, control the page:

index_path:        out/index.html
public_base_url:   https://calendars.example.com
site_title:        "Federal court calendar feeds"
site_description:  "Subscribable feeds for cybercrime and AI-litigation hearings, sourced from CourtListener / RECAP."
Key Purpose
index_path Where to write the HTML file. Setting this turns the feature on.
public_base_url The URL where the out/ directory is hosted. When set, subscribe buttons emit absolute URLs; otherwise they fall back to relative filenames (which still let you open the page locally but break one-click subscribe).
site_title The page <h1> and <title>.
site_description The <meta name="description"> content (used by search engines and link-preview cards). Keep under 160 characters.

The page is rendered once at the end of every sync / serve / emit — even when only one calendar’s content actually changed — because the page is a cross-calendar view and sibling calendars can move in the sort even when their own contents didn’t.

Hosting with Caddy

Caddy is the simplest option for serving the out/ directory over HTTPS with Let’s Encrypt. Create /etc/caddy/Caddyfile with:

calendars.example.com {
    root * /opt/case-calendar/out
    file_server {
        index index.html
    }

    # .ics files should be served with the calendar MIME type so clients
    # treat the download as a subscription rather than plain text.
    @ics path *.ics
    header @ics Content-Type "text/calendar; charset=utf-8"

    # Subscribe URLs change rarely; short-circuit refreshes.
    header Cache-Control "public, max-age=300"
}

Replace calendars.example.com and /opt/case-calendar/out with your own values. Caddy needs inbound TCP/80 + TCP/443 reachable from the public internet for the ACME HTTP-01 challenge; if you’re behind a firewall, use the DNS-01 plugin instead.

Save it and reload Caddy:

sudo systemctl reload caddy

Subscribers now point their calendar app at:

https://calendars.example.com/cybercrime.ics

…and the landing page is at:

https://calendars.example.com/

Combining with the webhook receiver

The same Caddy install can also reverse-proxy the webhook receiver on a sibling subdomain, keeping the public calendar feeds and the (sensitive) webhook endpoint on separate hostnames — Caddy gets one TLS certificate per hostname automatically, and your access logs separate the two surfaces cleanly. The receiver’s own site block (and the rate-limit caveat that goes with it) lives in real-time webhooks → Put it behind HTTPS with Caddy; add it to the same Caddyfile as the calendars.example.com block above.

Other static servers

The page is plain HTML — anything that serves files works:

The page is fully self-contained, so the only server-side concern is the MIME type for .ics files.

Cron note

If you’re running case-calendar sync on a cron and serving via Caddy, nothing else is needed — every sync writes a fresh index.html. If you’re running case-calendar serve for real-time webhooks, the index is re-rendered on every delivery that changes a row plus a debounced re-emit when AI summaries finish refreshing.

Next steps