Writing environment-specific documentation#
There are multiple science platforms deployed in different locations for different purposes. Each science platform instance is a separate Phalanx environment. A separate version of this documentation is built for each science platform environment and deployed using LSST the Docs’s editions feature. The documentation corresponding to the primary, public-facing science platform is always deployed as the main edition at the root URL (https://rsp.lsst.io). Other editions are available from https://rsp.lsst.io/v and the Science Platform homepages of each environment also link directly to these editions.
Information about the RSP environments is fetched at build time from the per-environment Repertoire service-discovery data that Phalanx publishes at https://phalanx.lsst.io/discovery/environments/{env}.json.
A small supplementary shim in the documentation repository (src/rspdocs/discovery/environments.json) provides the handful of things discovery doesn’t cover, such as human-readable environment titles and the roster of environments to build.
When the network is unavailable, the build falls back to a local cache (in _build/discovery/) and announces the fallback in the build output.
Because discovery data is fetched fresh at build time, the published sites only pick up environment changes when a build runs.
Merges to main trigger a build and publish, and a weekly scheduled rebuild (Thursday 23:15 UTC, after the Patch Thursday update window ends at 3:15 pm Pacific) republishes every environment so fresh discovery data flows out without needing a commit.
You can also trigger an on-demand rebuild by running the CI GitHub Actions workflow manually (its workflow_dispatch trigger); when dispatched from main it publishes all environments.
This page describes the supported approaches for writing documentation that differs between environments. Reach for them in this order, from the most targeted to the most general:
Linking to RSP services with roles for inline URLs and links to RSP services.
Linking to per-dataset services with roles for inline URLs, links, and docs links tied to a specific dataset (for example a dataset’s TAP endpoint).
Tabulating dataset services with the rsp-data-table directive for a table of which datasets expose which data-access services, rendered from discovery data.
Using reStructuredText substitutions for env-specific words and short phrases that a role can’t produce.
Substitutions inside code samples for environment URLs inside code samples (
code-blockandliteralinclude).Conditional content with the rsp-only directive to include or exclude whole blocks of content per environment.
Using Jinja templating when a branch’s text must be computed from environment data (interpolation, loops, or expressions).
Using source file includes (*.in.rst) with rsp-only and jinja to swap large included files.
Leaving a whole page out of some environments to leave a whole page out of some environments: conditionalize its toctree entry and exclude its source so it isn’t flagged as an orphan.
The roles and the rsp-only directive are provided by the in-repo src/rspdocs/sphinxext Sphinx extension.
The homepage (docs/index.rst) and log-in (docs/guides/getting-started/get-an-account.rst) pages demonstrate these techniques.
Linking to RSP services with roles#
For an inline link or URL to an RSP service, use one of these two roles. Each takes a service name — listed in the table below — and resolves it to that service’s URL for the environment being built.
rsp-url:rsp-url:`service`renders the service’s URL as a code literal. For example,:rsp-url:`rsp`renders ashttps://data-dev.lsst.cloud/.rsp-link:rsp-link:`service`renders a hyperlink to the service whose link text is the URL itself. For example,:rsp-link:`rsp`renders as https://data-dev.lsst.cloud/.Give an explicit link title with the familiar
title <target>syntax::rsp-link:`Rubin Science Platform <rsp>`renders as Rubin Science Platform.
Both roles also accept a path after the service name, appended to the service’s URL for the environment being built.
For example, :rsp-url:`rsp/settings/quotas` renders as https://data-dev.lsst.cloud/settings/quotas, and :rsp-link:`Quotas page <rsp/settings/quotas>` renders as Quotas page.
The path is always joined relative to the service’s full URL (which may itself include a path — portal/onlinehelp/ resolves under /portal/app/), and slashes at the seam are normalized, so you don’t need to worry about whether the path takes a leading slash: rsp/settings and rsp//settings are equivalent.
The trailing slash of the result follows the path exactly as you write it, since some endpoints are sensitive to it.
Note that only the service token is validated — a typo in the path builds without complaint, so check that the resulting URL exists.
Some services aren’t deployed in every environment; targeting a service that is absent in the environment being built raises a warning (fatal under -W), which usually means the reference should be wrapped in a matching rsp-only block.
Name |
Service |
In every environment? |
|---|---|---|
|
RSP homepage |
yes |
|
Portal Aspect |
no |
|
Notebook Aspect |
no |
|
VO API root |
no |
|
TAP service |
no |
|
WebDAV service |
no |
|
Times Square |
no |
|
Access-token page |
yes |
|
Phalanx environment docs |
yes |
Linking to per-dataset services with roles#
The roles above resolve an environment-wide service — the environment’s single TAP endpoint, for example.
Discovery also describes each service per dataset: which datasets (dp1, dp02, dp03, and so on) expose which data-access services, and at what URL.
Three roles reach that dataset dimension so you don’t hard-code a dataset’s endpoint.
rsp-data-url:rsp-data-url:`service dataset`renders a dataset’s service URL as a code literal. The target is exactly two tokens — a data-access service name and a dataset name — separated by a space. For example, in the primary environment:rsp-data-url:`tap dp1`renders ashttps://data.lsst.cloud/api/tap.rsp-data-link:rsp-data-link:`service dataset`renders a hyperlink to that dataset’s service, defaulting to the URL as its text, and accepts the familiartitle <target>syntax. For example,:rsp-data-link:`DP1 catalogs <tap dp1>`renders as a link titled “DP1 catalogs” pointing at the dataset’s TAP endpoint for the environment being built.rsp-dataset-docs:rsp-dataset-docs:`dataset`renders a hyperlink to a dataset’s own documentation site (from discovery’sdocs_url), also accepting atitle <dataset>. For example,:rsp-dataset-docs:`Data Preview 1 <dp1>`renders as a link titled “Data Preview 1” pointing athttps://dp1.lsst.io/.
(The examples above show their primary-environment output as static text rather than rendering live, because no dataset is served in every environment — a live render would fail the builds for dataset-less environments, as described next.)
The data-access service names are tap, sia, cutout, datalink, and hips.
Both URL roles accept a path after the dataset name — :rsp-data-url:`tap dp1/tables` appends tables to the dataset’s TAP URL — joined with the same slash-normalizing rules as the plain roles.
A dataset can expose a service in one environment but not another (the same dp1 offers SIA on the primary environment but not on every internal one), and some environments serve no datasets at all.
Referencing a dataset or service that’s absent in the environment being built — or a dataset with no docs_url — raises a warning (fatal under -W), just like the plain roles.
Wrap such references in a matching rsp-only block.
Note that the per-dataset service names are not rsp-only condition tokens: only tap and api double as both.
Gate on api (true wherever the environment serves datasets), on tap, or on explicit environment names — and because the environment-level condition doesn’t guarantee any particular dataset or per-dataset service, build the affected environments to confirm the guarded content resolves everywhere the condition holds.
Tabulating dataset services with the rsp-data-table directive#
To show which datasets expose which services — instead of hand-maintaining such a list — use the rsp-data-table directive, which renders a table from discovery data at build time:
.. rsp-data-table::
By default it produces one row per dataset served in the environment being built and one column per data-access service (TAP, SIA, cutout, datalink, HiPS), with each cell either a check mark linking to that dataset’s service endpoint or an em dash where the service is absent.
Datasets that expose none of the data-access services are omitted, and when no rows remain — notably in an environment that serves no datasets — the directive emits nothing at all.
Even so, wrap it in a .. rsp-only:: api block so any surrounding lead-in prose is also dropped where the environment serves no datasets (see the API aspect page, which does exactly this).
The directive takes a few options:
:services:A comma- or space-separated subset of the service tokens to use as columns (default: all five). An unknown token warns (fatal under
-W).:datasets:A subset of dataset names to use as rows (default: every dataset served in the environment). An explicitly requested dataset is kept even when it exposes no data-access services, but a name that isn’t served — in the environment being built (
envscope) or anywhere (environmentsscope) — warns.:scope:env(the default) tabulates datasets against services for the current environment, as above.environmentsinstead tabulates datasets against every environment that serves datasets (dataset-less environments get no column), marking where a single chosen service — set with:service:(defaulttap) — is available for each dataset. Use this cross-environment matrix to show, for example, which environments offer a dataset’s TAP endpoint.:service:The service the
environmentsscope tabulates (defaulttap); ignored in the defaultenvscope.:title:An optional table caption.
Using reStructuredText substitutions#
For env-specific prose — a word, a name, or a phrase that a role can’t produce — use reStructuredText substitutions.
These substitutions are defined in rst_prolog.rst.jinja, which is itself templated with Jinja so the replacement text can vary by environment.
(The definitions are rendered into Sphinx’s rst_prolog, not rst_epilog, so they precede every page’s content and are therefore also available to code samples.)
Syntax |
Example |
Description |
|---|---|---|
|
An inline link to this environment’s homepage; outside the primary environment it also names the environment. |
|
|
Rubin Interim Data Facility (Dev) |
The environment’s full display name, as plain text. |
|
The environment’s full display name, linked to its homepage. |
|
|
|
The TAP service’s URL without a trailing slash, for code samples. Defined only in environments with a TAP service. |
|
|
The WebDAV server address with a username placeholder, for code samples. Defined only in environments with a WebDAV service. |
Prefer a role whenever you only need a service’s URL, a link to it, or a URL derived by appending a path — the roles replaced the per-service URL, link, and derived-path substitutions the docs used to define.
Substitutions inside code samples#
Roles and ordinary substitution references don’t work inside literal blocks, so environment URLs in code samples need one more ingredient: the sphinx-substitution-extensions package, which is enabled in this build.
Add the :substitutions: option to a code-block (or :content-substitutions: to a literalinclude) and any |substitution| in its content is replaced with the plain text of its definition:
.. code-block:: bash
:substitutions:
export EXTERNAL_TAP_URL="|rsp-tap-url|"
|rsp-tap-url| is the TAP service’s URL without a trailing slash, the form client configuration expects (whereas :rsp-url:`tap` renders discovery’s canonical trailing-slash URL).
It is defined only in environments that have a TAP service, so wrap its uses in a matching .. rsp-only:: tap block — including any lead-in prose that would be left dangling in environments where the block is dropped.
In this environment, the example above renders as:
export EXTERNAL_TAP_URL="https://data-dev.lsst.cloud/api/tap"
Because the substitution machinery matches text at parse time, only substitutions defined in rst_prolog.rst.jinja (or earlier in the same page) are available inside code samples.
Conditional content with the rsp-only directive#
To include a block of content only in certain environments, wrap it in the rsp-only directive:
.. rsp-only:: primary
This content appears only in the primary (public) build.
The directive takes one or more bare condition tokens, which may be:
a service name (from the table above) — true where that service is deployed;
an environment name (
base,idfdev,idfint,idfprod,summit,tucson-teststand,usdfdev,usdfprod) — true only in that environment; orthe keyword
primary— synonymous withidfprod, the primary environment, whose documentation is the default edition.
By default every token must hold (logical AND):
.. rsp-only:: portal nublado
This content appears only where both the Portal and Notebook aspects exist.
Use the :any: option for a logical OR, and the :not: option to negate the result:
.. rsp-only:: summit base
:any:
This content appears in the summit or base environments.
.. rsp-only:: primary
:not:
This content appears in every environment except the primary one.
For an “AND of an OR” condition, nest rsp-only directives.
Unlike Sphinx’s built-in only directive, rsp-only excludes non-matching content at parse time: excluded content never enters the doctree, so it can’t leak into the table of contents, the index, or search results.
This is safe here because each environment is built separately (one sphinx-build per environment), so rsp-only is the right tool for conditional content — there’s no need to avoid it the way you would avoid only.
Using Jinja templating#
The rsp-only directive only includes or excludes static reStructuredText.
Reach for the jinja directive — available through sphinx-jinja — when a branch’s text has to be computed rather than merely shown or hidden: interpolating an expression like {{ env.title }}, running a loop, or testing an env attribute that isn’t a service name, an environment name, or primary.
The deciding question is not how many branches you have, but whether the prose inside a branch depends on a value — and one that no rsp-url/rsp-link role or substitution already provides.
If every branch is self-contained rST, prefer stacking rsp-only blocks; use jinja only when a branch needs to say something built from the environment’s data.
For example, a three-way switch whose branches are worded differently and each interpolates an environment value that has no substitution of its own (here the short title env.title and the env.domain host):
.. jinja:: rsp
{% if env.is_primary %}
The public Science Platform is served at {{ env.domain }}.
{% elif env.name in ("idfint", "idfdev") %}
{{ env.title }} is a staff integration environment.
{% else %}
{{ env.title }} runs at {{ env.domain }} for internal use.
{% endif %}
The argument to the jinja directive is always rsp.
Inside it, env is the environment being built (an instance of rspdocs.discovery.models.PhalanxEnv, so any of its attributes are available), and all_envs maps environment names to their PhalanxEnv.
As with any Sphinx directive, indent the content consistently with respect to the directive’s scope, as shown above.
Two things to keep in mind:
For a simple two-way include/exclude, prefer Conditional content with the rsp-only directive: it reads more clearly and keeps excluded content out of search and the table of contents.
Each build fetches every roster environment, so
all_envsholds all of them and a{% for %}loop over it enumerates the whole fleet.envis still just the one environment being built, andall_envs.primaryis the primary edition’s environment.
Using source file includes (*.in.rst) with rsp-only and jinja#
Both the rsp-only and jinja approaches work well for tailoring specific paragraphs for different environments, but writing a large amount of content inside a directive is inconvenient.
To customize large portions of text, you can combine the rsp-only directive (or a jinja directive for multi-way switches) with the include directive:
.. rsp-only:: primary
.. include:: the-page.primary.in.rst
.. rsp-only:: primary
:not:
.. include:: the-page.notprimary.in.rst
This inserts content from the included source files, either the-page.primary.in.rst or the-page.notprimary.in.rst.
Those included files are in the familiar reStructuredText syntax (you shouldn’t need further Jinja syntax within them, though you can certainly use roles and substitutions).
The included files must have a .in.rst suffix so that the Sphinx build won’t incorporate those files as separate pages.
Our further convention is to prefix the name with the root name of the page, followed by a description of the environment or context where the content applies.
Leaving a whole page out of some environments#
Some pages document a single service end-to-end — for example, the Times Square guides or the “using TOPCAT outside the RSP” page — and shouldn’t appear at all in an environment where that service isn’t deployed.
Leaving such a page out of a build is a two-part change: conditionalize its toctree entry, and exclude its source file.
Both parts are necessary, and they must agree on the gating service.
Conditionalize the toctree entry#
A page appears in a build only if a toctree links to it, so the first step is to drop the page’s toctree entry in the environments that shouldn’t have it.
Wrap the entry in a jinja conditional on the gating service’s URL attribute.
For instance, docs/guides/auth/index.rst lists the TOPCAT page only where the TAP service exists:
.. jinja:: rsp
.. toctree::
:titlesonly:
creating-user-tokens
token-scopes
{% if env.api_tap_url %}using-topcat-outside-rsp{% endif %}
The same pattern gates an entire subtree by its index page — docs/guides/index.rst includes times-square/index (and everything under it) only when env.times_square_url is set.
Exclude the now-unreferenced source#
Conditionalizing the toctree stops linking the page, but its source file still sits in docs/.
Sphinx flags any document that no toctree reaches as an orphan (document isn't included in any toctree), which is a warning — and the build runs under -W, so a stray orphan fails the build.
The fix is to exclude the page’s source in exactly the environments where its toctree entry disappears, so Sphinx never parses it and never sees an orphan.
These exclusions live in src/rspdocs/sphinxext/page_excludes.yaml.
The file maps each service token to a list of glob patterns, relative to the docs/ source directory, for the pages that document that service:
times-square:
- guides/times-square/*.rst
- guides/times-square/**/*.rst
tap:
- guides/auth/using-topcat-outside-rsp.rst
When an environment is built, the patterns for every service absent from that environment are added to Sphinx’s exclude_patterns.
A service counts as absent when discovery reports no URL for it (for example, TAP in an environment that serves no datasets) or when it’s listed in that environment’s hidden_services shim (see src/rspdocs/discovery/environments.json) — the same condition the toctree’s {% if env.… %} tests, which is why the two stay in sync.
To leave a page out where a service is missing:
gate its
toctreeentry on the service’s URL attribute, as above; andadd its source path (or a glob for its subtree) under the matching service token in
page_excludes.yaml.
When excluding an index page that heads a subtree, exclude the subtree too (the ** glob above), or its child pages become orphans in turn.
The YAML keys must be recognized service tokens from the service table; a typo fails the build rather than silently excluding nothing.
This and the other hand-edited bundled config (src/rspdocs/discovery/environments.json) are checked by the rspdocs-validate-config command, run automatically by pre-commit and the test suite, so a mistake is caught at commit time rather than only during a build.