<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: The prerender job on a COLLAB server: the pages, images, sitemap and /data/ files it writes, when it runs, its user and limits, prices, warnings and failures. -->
# Static pages, data files and prices

The prerender job is the part of a COLLAB server that reads the chain on the server's side. It
writes a static page and a preview image for every DAO, the sitemap, and a cache under `/data/` of
logos, descriptions, the token list and the L1 price. This page is for the operator: what the job
writes, when it runs, what it may and may not do, and how its warnings and failures show.

## Why the site has a prerender job

The site works without it: every page reads the chain in the visitor's browser. The job adds what a
browser cannot do for itself:
- **Search engines and link previews** get a page for every DAO and token, with its name,
  description and image, without running any script.
- **Faster pages.** A DAO's logo and description, the token list and the member network come from
  one file each instead of many reads from the chain.
- **Prices without tracking.** The L1 price is fetched from CoinGecko on the server, so no visitor's
  browser contacts CoinGecko for the default display.
- **The site settings on first paint.** The alpha strip's text, the announcement, the footer's links
  and the home page's search-engine texts are written into the pages' HTML.

## What it writes

The formats of these files are in [Data formats](data-formats.md#files-at-a-glance).

### Pages, in shared/prerender

| File | What it is |
|---|---|
| `index.html`, `daos/index.html`, `votes/index.html`, `create/index.html`, `docs/index.html`, `settings/index.html`, `privacy/index.html`, `swap/index.html`, `tokens/index.html`, and the entry files (`dao.html`, `descifi.html`, `404.html` …) | the release's pages with the site settings written into them; never the admin page |
| `dao/<slug>/index.html` | a page for every DAO: its name with "COLLAB on GenesisL1" as its title, a description of about 155 characters from its on-chain description (checked against its hash), its canonical address `/dao/<slug>`, Open Graph, Twitter and JSON-LD, and a short static summary the app replaces when it starts. A DAO the site hides gets `noindex`. |
| `dao/<address>/index.html` | a page without scripts for a DAO's treasury address, whose canonical link and immediate refresh lead to `/dao/<slug>` |
| `tokens/<address>/index.html` | a page for every token with a DeSciFi pool |
| `pools/index.html`, `locks/index.html` | the DeSciFi listings with heads of their own |
| `og/dao-<address>.png` | a 1200 × 630 preview image: the DAO's molecule glyph or its verified logo, its name, its treasury address and three facts |
| `og/dao-<address>-logo.png` | a 512 × 512 square, the DAO's logo in its JSON-LD |
| `og/network.json` | the member network the home page draws: which DAOs share members, as counts, with no member's address |
| `sitemap.xml` | the indexable pages, the documentation's pages, every DAO the site does not hide, and the token pages, each with its last change |
| `robots.txt` | the release's `robots.txt`, with its `Sitemap:` line on the main host, so a server installed for another name points crawlers at its own sitemap |

### The data cache, in shared/data, served at /data/

| File | What it is |
|---|---|
| `/data/price.json` | the L1 price in US dollars and its 24-hour change, from CoinGecko |
| `/data/tokens.json` | every token with a DeSciFi pool: price and liquidity in L1, market cap, holders, 24-hour change, a 7-day line, its pools and its issuing DAO |
| `/data/logos/<hash>.png` | each DAO's on-chain logo, named by its keccak-256 hash and checked against it |
| `/data/texts/<hash>.txt` | each DAO's on-chain description, named and checked the same way |

JSON and text files of 1 KB or more get a `.gz` copy beside them. Logos and descriptions no DAO
shows any more are removed.

### Before a deployment is published

While `config.js` says the site is not deployed, the job reads no chain. It writes the entry pages
with the site settings, the sitemap of the static and documentation pages, `robots.txt` and the
price, and Status adds the note "not deployed yet: the pages, the sitemap and the price only".

After an unpublish, such a run also removes what the earlier deployment left: its DAO and token
pages, its preview images and `og/network.json`, and its logos, descriptions and
`/data/tokens.json`. The price stays, and so do the job's caches, which are kept outside the
folders nginx serves.

## How nginx serves its files

nginx serves the release, and tries the job's folder first only for the page addresses, the 404
page, the sitemap, `robots.txt` and `/og/`:
- **Never from the job's folder:** scripts, styles, `config.js`, the admin page, the favicon and the
  manifest. They always come from the release, or `config.js` from `shared/`.
- **`/og/`** serves only `og/network.json` and the two image names above, with a DAO's lowercase
  address; every other name is not found.
- **`/data/`** serves only files of plain names ending in `.png`, `.json` or `.txt`. A file the job
  has not written yet answers `204` with `no-store`, and the page reads the chain instead.
- **Caching.** Pages and `/data/*.json` are revalidated on every load; `/og/` and the sitemap are
  kept an hour; files under `/data/` named by their hash are kept a year.

## When it runs

| Trigger | How |
|---|---|
| Every 10 minutes | `collab-prerender.timer`, at minutes 0, 10, 20 and so on |
| On request | `collab-prerender.path` starts a run as soon as `shared/jobs/prerender.request` appears |
| **Run now** | on the Status tab |
| By hand | `sudo collab prerender` (in the foreground) or `sudo collab prerender --request` (by the service) |

The admin service asks for a run after a publish, an unpublish and every settings save; the
installer asks after every run; the content policy step asks after it rewrote the pages. A request
that arrives during a run starts another run when it ends, up to three in a row. Runs never overlap:
one that finds another still working exits and does nothing.

Status shows **Next run** ("in 7 min, every 10 minutes", or "Now" after a request).

## The command it runs

`collab-prerender.service` runs, as user `collab-prerender`:

```text
python3 -I /var/www/collab/current/tools/prerender.py --web /var/www/collab/current/web
  --deployment /var/www/collab/shared/config.js --out /var/www/collab/shared/prerender
  --data /var/www/collab/shared/data --site https://collab.genesisl1.com
  --lock /var/www/collab/shared/jobs/prerender.lock --prune --quiet [--rpc URL]
```

- It reads the chain id, the contracts and the RPC endpoints from `config.js`, trying the endpoints
  in order. When the installer was given `--rpc`, the job reads the chain through that endpoint
  only.
- `--prune` removes the pages and images of DAOs the factory no longer lists, token pages no longer
  written, and unused logos and descriptions.
- The job's status is in `shared/jobs/prerender.json`, and its output in `shared/jobs/prerender.log`
  (the last 256 KB) and in the journal (`journalctl -u collab-prerender`).

## Its user and what it may write

The job reads untrusted data from the chain, logos included, which Pillow decodes. So it runs as a
user of its own, `collab-prerender`, confined by systemd:
- it may write only `shared/prerender/`, `shared/data/` and `shared/jobs/`;
- its settings file, `/etc/collab/prerender.env`, holds no secret, and it cannot read
  `collab.env`, which holds the admin token: it is not in the `collab` group;
- it cannot write `config.js`, the release or nginx's configuration;
- nginx never serves a script, a style or the admin page from its folders, and follows a symbolic
  link in them only when the link and its target have the same owner.

So a flaw in the job, or in Pillow, could deface the prerendered pages, but could not change the
contracts the site uses or run code on the site's origin. [Security](SECURITY.md#the-server-kit) has
the full threat model.

It runs at low priority (nice 10, idle I/O). A run that takes more than 25 minutes is stopped
("error: stopped after 25 minutes").

## The tokens time budget

The token list needs the history of every pool, from the deployment's start block, and the
transfers of every token, to count holders. Reading that history takes the job at most 240 seconds
per run; the next run resumes where it stopped, from a cache beside the lock
(`shared/jobs/prerender.tokens.json`, readable by the job's user only). On a busy chain the first
runs after a deployment may not finish it. Until they do:
- `/data/tokens.json` says `"complete": false`;
- holder counts are lower bounds, shown as "≥ …" with "At least this many accounts hold a balance.
  Older transfers are still being read, so the count may grow.";
- the 24-hour change and the 7-day line may be missing for some tokens.

Prices, liquidity and market caps do not wait for the history: the pages compute them live from the
chain.

## The price snapshot

At each run the job asks CoinGecko's simple price API for the L1 price in US dollars, with its
24-hour change, and writes `/data/price.json`. A failed fetch keeps the previous file, with a
warning. The pages show amounts in US dollars from that file while it is less than a day old; with
an older file, or none, they show amounts in L1 only, unless the visitor chooses a currency in
Settings, which the browser then fetches from CoinGecko itself. Prices are for display only: no
amount sent on chain is ever computed from them.

## Warnings and failures

A run ends in one of three states, which Status and `sudo collab status` show:

| State | Status badge | Means |
|---|---|---|
| `ok` | **Up to date** | everything was written |
| `warn` | **Ran with warnings** | the pages were written, but something extra was not refreshed; the warnings are listed |
| `failed` | **Failed** | nothing was changed; the pages of the last good run are still served |

### Warnings

The job warns and goes on when something extra cannot be read; the file it concerns keeps its last
good version:
- "L1 price not fetched (…); data/price.json stays as it is": CoinGecko could not be reached or gave
  no usable answer;
- "tokens not read (…); data/tokens.json and the token pages stay as they are": MarketLens or the
  token data could not be read this time;
- "member network not read (…); og/network.json and the member cache stay as they are";
- "0x…: description not used (…)" or "0x…: logo not used (…)": a DAO's description or logo could
  not be read, or did not match the hash its profile stores; its page is written without it;
- "og/network.json is … KiB, more than the 512 KiB the landing page reads: it reads the members from
  the chain instead".

Status lists up to 20 warning lines; see [The admin page](admin.md#ran-with-warnings).

### Failures

The job reads every DAO before it writes anything, so a chain problem changes no file:
- "error: no usable RPC endpoint (…); no file was changed": no endpoint answered for chain 29, or the
  one given serves another chain;
- "error: …; no file was changed": an RPC error while reading the DAOs;
- "error: stopped after 25 minutes".

A configuration problem (a missing file, an invalid `config.js`) also fails the run. Status shows
"The last run failed … ago; the pages from … are still served." with the error line, and the warnings
of that run. `sudo collab prerender` runs the job in the foreground and prints everything it says;
see [Troubleshooting](troubleshooting.md#the-prerender-job-failed).

## Outbound HTTPS

The job needs outbound HTTPS to:
- the RPC endpoints: the site's own (from the site settings, or the GenesisL1 RPC and the GL1X
  JSON-RPC by default), or the installer's `--rpc`;
- `api.coingecko.com`, for the price.

When the server reaches the internet only through a proxy, the job passes `http_proxy`,
`https_proxy` and `no_proxy` (in lower or upper case) on to `tools/prerender.py` when its service
has them, and `SSL_CERT_FILE` for a proxy with a certificate authority of its own. Give them to the
service with a systemd drop-in, which the installer leaves alone:

```bash
sudo systemctl edit collab-prerender.service
# then, in the editor:
# [Service]
# Environment=https_proxy=http://proxy.example.org:3128
```

## After an upgrade or a settings change

- **An upgrade** removes the previous release's prerendered pages when `current` switches, so the
  new release's own pages serve until the job has written them again; the installer asks for that
  run at once. Images and the `/data/` cache stay.
- **A settings save** asks for a run, which writes the settings into the pages.
- **A security policy change** asks for a run after the policy step, so the prerendered pages carry
  the new policy. Until it has run, those pages keep the old policy.

## Related

- [The admin page](admin.md#static-pages)
- [Data formats](data-formats.md)
- [Status, logs and the collab command](monitoring.md)
- [Troubleshooting](troubleshooting.md)
- [Deploying COLLAB: reference](DEPLOY.md#3-dao-pages-preview-images-the-sitemap-and-the-member-network)
