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. The repo ships a working Caddyfile you can adapt:

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.

Install it as the system Caddyfile and reload:

sudo ln -s /opt/case-calendar/Caddyfile /etc/caddy/Caddyfile
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. The shipped Caddyfile has a commented-out template:

webhook.example.com {
    reverse_proxy 127.0.0.1:8000
}

That keeps 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. See real-time webhooks for the rest of the webhook setup.

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