No description
Find a file
2025-12-21 20:37:35 +01:00
coffeegit_pages_server feat: Honor _redirects file 2025-12-21 20:31:55 +01:00
examples chore: Add some docs 2025-12-21 20:37:35 +01:00
.gitignore feat: Initial version of Forgejo pages server 2025-12-21 17:43:20 +01:00
LICENSE chore: Add some docs 2025-12-21 20:37:35 +01:00
pyproject.toml feat: Initial version of Forgejo pages server 2025-12-21 17:43:20 +01:00
README.md chore: Add some docs 2025-12-21 20:37:35 +01:00

coffeegit-pages-server

A small FastAPI/uvicorn service that serves “Pages”-style static sites from a Forgejo instance.

It supports:

  • Pages subdomains: https://{owner}.<PAGES_DOMAIN>/...
  • Repo subdomains: https://{repo}.{owner}.<PAGES_DOMAIN>/...
  • Branch+repo subdomains: https://{branch}.{repo}.{owner}.<PAGES_DOMAIN>/...
  • Custom domains mapped via DNS to your pages domain (CNAME or TXT)
  • A raw domain for directly fetching files via a URL path scheme
  • _redirects (Netlify-style)
  • _headers (Netlify-style)
  • Optional/unstable support for:
    • symlinks (via Forgejo contents API)
    • Git LFS pointer resolution

This project is intended to be deployed behind a reverse proxy (tested with Caddy) that terminates TLS and forwards requests to the service.

Installation

python3 -m venv /opt/coffeegit-pages/venv
/opt/coffeegit-pages/venv/bin/pip install --upgrade pip
/opt/coffeegit-pages/venv/bin/pip install git+https://git.private.coffee/privatecoffee/coffeegit-pages-server.git

Or install from a local checkout:

pip install .

Running

CLI entrypoint

The package installs a console script:

coffeegit-pages-server

It runs uvicorn on 0.0.0.0:${PORT} (default 8131).

Configuration

Configuration is via environment variables:

Variable Default Meaning
FORGE_ROOT https://git.private.coffee Forgejo root URL (no trailing slash)
FORGE_API_TOKEN empty Optional API token for Forgejo (recommended if you have rate limits / private repos)
PAGES_DOMAIN coffeegit.page Base pages domain suffix used for subdomain routing
RAW_DOMAIN raw.coffeegit.page Domain that enables raw routing mode
PAGES_BRANCHES pages Space/comma-separated list of candidate branches to try (in order)
ALLOWED_CORS_DOMAINS empty If request Host matches one of these, Access-Control-Allow-Origin: * is returned
BLOCKED_PATH_PREFIXES /.well-known/acme-challenge/ Deny requests for these path prefixes
ENABLE_SYMLINK_SUPPORT false Resolve Forgejo symlinks to their targets
ENABLE_LFS_SUPPORT false Detect Git LFS pointer files and serve the actual LFS object
DEFAULT_MIME_TYPE application/octet-stream Fallback content type
FORBIDDEN_MIME_TYPES empty If guessed type is in this list, fallback to DEFAULT_MIME_TYPE
ASK_STRICT_DOMAINS_FILE true /ask uses .domains validation instead of only “owner exists”
ENABLE_FILE_CACHE true Enable in-memory body cache
FILE_CACHE_TTL 300 Cache TTL seconds
FILE_CACHE_MAX_BYTES 2097152 Max size to buffer/cache per response

Routing behavior

1) Pages subdomain mode (*.PAGES_DOMAIN)

Assuming PAGES_DOMAIN=coffeegit.page:

  • https://alice.coffeegit.page/
    Serves owner=alice, repo=pages, branch chosen from PAGES_BRANCHES, path /

  • https://docs.alice.coffeegit.page/
    Serves owner=alice, repo=docs, branch chosen from PAGES_BRANCHES

  • https://dev.docs.alice.coffeegit.page/
    Serves owner=alice, repo=docs, branch=dev

Branch selection rules:

  • If branch is explicitly encoded in the hostname ({branch}.{repo}.{owner}) it is used.
  • Otherwise the server tries branches from PAGES_BRANCHES and picks the first existing one.

2) Custom domain mode

For example.org, configure DNS so the server can map it back to a hosted pages target.

Supported DNS mapping methods:

  • CNAME to a hostname under the pages domain suffix, e.g.

    • example.org CNAME alice.coffeegit.page.
    • example.org CNAME docs.alice.coffeegit.page.
  • TXT containing such a hostname (useful when CNAME isnt possible), e.g.

    • example.org TXT "alice.coffeegit.page"
    • example.org TXT "docs.alice.coffeegit.page"

Custom domains must also be allowed by a .domains file in the repo (see below), otherwise the request is rejected with 421.

3) Raw domain mode (RAW_DOMAIN)

Requests to RAW_DOMAIN use a path format:

https://RAW_DOMAIN/{owner}/{repo}[/@{branch}]/{path}

Example:

  • https://raw.coffeegit.page/alice/pages/@main/index.html

Raw mode forces Content-Type: text/plain; charset=utf-8.

Repository conventions

index.html and directory paths

  • If the request is “directory-ish” (/ suffix), the server tries {path}/index.html.
  • If index.html exists and the URL didnt end in /, it redirects to the / form.
  • If a file isnt found, it falls back to 404.html if present.

Also:

  • /something may redirect to /something.html if that exists (GitHub Pages-like behavior).
  • /foo/index.html redirects to /foo/ (non-raw mode only).

.domains (custom domain allowlist + canonical choice)

If a repo contains a .domains file at the repository root on the served branch, its contents define which custom domains are valid.

  • One domain per line
  • Lines may include http(s):// prefixes (they will be stripped)
  • Blank lines and comments (# ...) are ignored

The first valid domain is treated as the canonical domain.

If .domains is missing or does not list the requested domain, the request is rejected with 421.

A fallback canonical is always appended: {owner}.<PAGES_DOMAIN> (and /{repo} for non-pages repos).

_headers (optional)

If _headers exists at repo root, it is parsed in a Netlify-like format:

/*.css
  Cache-Control: public, max-age=86400
  X-Test: hello

_redirects

_redirects at repo root is parsed in a Netlify-like format:

/blog    /blog/    301
/docs/*  /documentation/:splat  302

If ENABLE_SYMLINK_SUPPORT is true, the server will resolve symlinks in the repo.

Git LFS support (optional)

If ENABLE_LFS_SUPPORT is true, the server will detect Git LFS pointer files and fetch the actual LFS object from the Forgejo LFS endpoint.

Endpoints

  • GET /ask?domain=example.org
    Returns 200 ok or 403 forbidden depending on whether a domain would be considered authorized. This is used by Caddys ask directive for automatic TLS.

  • GET /__inspect?url=https://...
    Returns JSON describing how the service would route a given public URL. Add &probe_upstream=true to also request the upstream raw file and report status/headers.

Running behind a reverse proxy

Run the service on localhost (for example 127.0.0.1:8131) and have your proxy forward requests to it. Ensure the proxy preserves the Host header correctly.

Your proxy should also handle TLS termination. For an example, see the Caddyfile example.

License

This project is licensed under the MIT License. See the LICENSE file for details.