Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6a9fca5
fix(site[notice]) Describe the 0.1.N releases instead of an alpha
tony Oct 25, 2026
56aa472
fix(site[install]) Drop alpha pins and prerelease flags from 0.1.0 in…
tony Oct 25, 2026
7388268
feat(site[powershell]) Navigate PowerShell's .NET dependency at 0.1.0
tony Oct 26, 2026
1462bbd
fix(site[tmux-usage]) Record the C# and PowerShell senders at 0.1.0
tony Oct 26, 2026
d655f8e
fix(site[manifest-packages]) Read the .NET and PowerShell packages at…
tony Oct 26, 2026
0badaca
test(site[csharp]) Refresh the C# example receipt for 0.1.0
tony Oct 26, 2026
d1a3009
feat(site[tmux-manual]) Badge the C implementation entries by kind
tony Oct 26, 2026
fbda0f1
feat(site[api-reference]) Promote tmux and cross-port links to sections
tony Oct 26, 2026
b4f587f
docs(ports[install]) Say release, not prerelease, on the 0.1.0 packag…
tony Oct 25, 2026
d6cc42b
test(site[product-docs]) Expect "published" in the workspace CLI summary
tony Oct 25, 2026
422b42f
fix(api-model[swift]) Depend on a final Swift release with from:
tony Oct 25, 2026
85e8e9f
fix(api-model[resolver]) Treat a release-numbering label as no symbol
tony Oct 26, 2026
e12a37b
fix(api-model[builtins]) Link InnerException and NotImplemented for P…
tony Oct 26, 2026
fbfa796
fix(api-model[mentions]) Resolve the C# workspace names PowerShell gu…
tony Oct 26, 2026
078aef4
fix(site[api-reference]) Style the tmux lists and read the paths in o…
tony Oct 26, 2026
fd2cc5f
fix(site[api-reference]) Keep scope detail and scope degree 1 to what…
tony Oct 26, 2026
f1858c1
test(site[api-reference]) Accept a direct send's own source line as t…
tony Oct 26, 2026
77a607f
fix(site[api-reference]) Label path steps by edge kind and load path …
tony Oct 26, 2026
d1ee4d8
test(site[api-reference]) Count path steps on a rendered page that ha…
tony Oct 26, 2026
50f4a17
fix(site[registry]) Record the 0.1.0 releases by their series
tony Oct 26, 2026
ace43ed
docs(ports[powershell]) Install LibTmux 0.1.0 without -Prerelease
tony Oct 26, 2026
1dd4bf6
test(site[browser]) Point the browser checks at the In other ports an…
tony Oct 26, 2026
f7dab7b
fix(api-model) Record the right-hand side of TypeScript aliases
tony Oct 26, 2026
ea12f4d
feat(site) Declare type aliases and list the members they name
tony Oct 26, 2026
b37a5ac
fix(site) List alias members under "Members of" the target
tony Oct 26, 2026
4030205
test(site[browser]) Cover the Swift member-body inset with a paramete…
tony Oct 26, 2026
e40d5f9
fix(api-model) Expand alias members only on the alias page
tony Oct 26, 2026
61ebae9
fix(site[powershell]) Rebind the PowerShell guides to libtmux-powersh…
tony Oct 26, 2026
cb9f00b
fix(site[tmux-usage]) Record the PowerShell senders at fddd1180
tony Oct 26, 2026
1223ba7
fix(api-model[builtins]) Land NotImplemented on its ErrorCategory member
tony Oct 26, 2026
d8771e7
fix(scripts[compose-inventory]) Skip ports without a published catalog
tony Oct 26, 2026
ff77f2d
fix(scripts[compose-inventory]) Fall back to latest catalogs
tony Oct 26, 2026
98405a8
fix(versions) Publish a release tag at its own prefix only
tony Oct 26, 2026
cffc239
fix(versions) Stop consumers and checks from assuming a stable channel
tony Oct 26, 2026
645c1fe
docs(versions) Drop the stable channel from the edge and CI guides
tony Oct 26, 2026
068630f
test(versions) Use release slugs where fixtures and notes said stable
tony Oct 26, 2026
1710d97
fix(api-tree) Draw the nested guide line in Grouped and Focus
tony Oct 26, 2026
80301c7
feat(native-shell) Capture the Sphinx global toctree into the native …
tony Oct 26, 2026
d8f5726
refactor(api-tree) Make the glossary the sidebar's last row
tony Oct 26, 2026
983b5dd
feat(native-shell) Show the Sphinx toctree in the native sidebar
tony Oct 26, 2026
dacce1d
fix(native-shell) Keep page ids out of native search labels
tony Oct 26, 2026
44bbf6b
test(api-tree) Prove the sidebar row rules fail when broken
tony Oct 26, 2026
a3c851a
test(native-shell) Check the nested toctree in the native browser checks
tony Oct 26, 2026
d4aeaa3
test(native-shell) Give native shell fixtures an empty toctree
tony Oct 26, 2026
1459541
fix(native-shell) Use the shared external-link mark in the native toc…
tony Oct 26, 2026
4acf632
fix(api-model) Give TypeScript type syntax a grammar role
tony Oct 26, 2026
479400f
Publish the root site when a port's default has no catalog yet (#206)
tony Oct 26, 2026
8095a8b
Show tmux links as sections on API reference pages (#202)
tony Oct 26, 2026
35791fd
Declare TypeScript type aliases and list the members they name (#205)
tony Oct 26, 2026
d43bd3b
Navigate PowerShell's .NET dependency at 0.1.0 (#204)
tony Oct 26, 2026
21eb9d2
Document the 0.1.0 releases and drop stable from the site (#200)
tony Oct 26, 2026
897bf24
Remove the stable docs channel (#201)
tony Oct 26, 2026
ea48904
Draw sidebar guide lines in every layout and put the glossary last (#…
tony Oct 26, 2026
eba5a78
Show the Sphinx toctree in native documentation sidebars (#209)
tony Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ A link the page marks for a preview card must have a card to show. The preview
check hovers the C function link of a method page, then asks for the payload of
every marked link on four pages. It asks only for destinations the dev build
serves: one version of each port's reference, as its build target says, so the
Python reference that the other ports link to at `stable` is not asked for. What
Python reference that the other ports link to at its default version is not asked for. What
it leaves out is bounded: only reference trees whose publisher owns the version
tree (`publishesOwnTree`), at versions the dev build does not serve. A payload
missing on a port the build does serve is reported, and the check fails if it
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: deploy-shell
# and historical versions remain owned by their port publishers.
#
# The shell has no versioned URL space of its own (notes/research/06: root
# is always "whatever's on trunk," never /latest/ or /stable/ like a port).
# is always "whatever's on trunk," never /latest/ like a port).
# A tag here is a release marker for this repo's own history; it deploys to
# the same root a trunk push does, not a separate prefix. Release branches
# are a per-port version concept (versions.ts VersionKind 'branch', e.g.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: publish
# One dispatch publishes any set of ports at a ref (scripts/publish-plan.mjs):
#
# ports: all ref: latest every default branch as latest
# ports: all ref: release every newest release, + alias
# ports: all ref: release every newest release, + next if prerelease
# ports: rs ref: <ref> one exact ref, as the inputs name
#
# The plan runs everywhere and needs no credentials, so a dry run (the
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/reusable-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ on:
inputs:
path-prefix:
description: >
S3 key prefix this call may write, e.g. 'py/v0.46.2', 'py/stable',
S3 key prefix this call may write, e.g. 'py/v0.46.2', 'py/next',
'pr-42'. Validated below, not just documented — this is the one
thing standing between a caller and every other prefix in the
bucket.
Expand Down Expand Up @@ -57,7 +57,7 @@ on:
type: string
default: ''
version:
description: Version slug for the manifest entry (VersionEntry.slug), e.g. 'v0.46.2', 'stable'. Required when `port` is set.
description: Version slug for the manifest entry (VersionEntry.slug), e.g. 'v0.46.2', 'next'. Required when `port` is set.
required: false
type: string
default: ''
Expand Down Expand Up @@ -147,7 +147,7 @@ jobs:
reserved=(py ruby lua powershell julia elixir ts rs go java kotlin scala csharp fsharp cxx swift manifest _shell)
for r in "${reserved[@]}"; do
if [[ "$p" == "$r" ]]; then
echo "::error::path-prefix '$p' is a bare reserved top-level name — qualify it, e.g. '$r/stable'" >&2
echo "::error::path-prefix '$p' is a bare reserved top-level name — qualify it, e.g. '$r/latest'" >&2
exit 1
fi
done
Expand Down
37 changes: 18 additions & 19 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ caller cannot hand an immutable tag a five-minute TTL by copy-paste:
| `tag` | `py/v0.46.2` | `public, max-age=31536000, immutable` |
| `trunk` | `py/latest` | `public, max-age=0, s-maxage=300` |
| `branch` | `py/v0.x` | `public, max-age=0, s-maxage=300` |
| `alias` | `py/stable` | `public, max-age=0, s-maxage=300` |
| `alias` | `py/next` | `public, max-age=0, s-maxage=300` |
| `pr` | `ruby/pr-42` | `public, max-age=0, s-maxage=300` |

No column for invalidation, because this workflow issues none. Every mutable
Expand All @@ -63,28 +63,27 @@ per request, so CloudFront never caches it — it returns
`x-cache: FunctionGeneratedResponse` every time — and invalidating a URL that
is generated rather than stored clears nothing.

A tag push on a port repo normally calls `reusable-deploy.yml` twice — once
with `version-kind: tag` at the immutable prefix, once more with
`version-kind: alias` at `stable` if that tag becomes the new default — since
`/stable/` and `/latest/` are separate builds with their own base path and
canonical URL, never edge rewrites of the tag's bytes. The second call adds
`resolves-to` so the manifest entry carries `VersionEntry.resolvesTo`:
A tag push on a port repo calls `reusable-deploy.yml` once with
`version-kind: tag` at the immutable prefix. A final release that is newer than
every other final release tag also passes `is-default: true`, so the manifest's
`defaultVersion` names it; an older backport tag never takes the default. A
prerelease adds a second call with `version-kind: alias` at `next`, which
carries `resolves-to` so the manifest entry has `VersionEntry.resolvesTo`:

```yaml
with:
path-prefix: py/stable
artifact: docs-html-stable
path-prefix: py/next
artifact: docs-html-next
version-kind: alias
port: py
version: stable
resolves-to: v0.46.2
is-default: true
version: next
resolves-to: v0.47.0a1
environment: docs
```

The prefix a caller passes is its own — `py/stable`, `py/v0.46.2` — and the
The prefix a caller passes is its own — `py/next`, `py/v0.46.2` — and the
workflow prepends the locale before writing, so those objects land at
`en/py/stable` and `en/py/v0.46.2`. That is applied on this side rather than
`en/py/next` and `en/py/v0.46.2`. That is applied on this side rather than
asked of the caller because the caller is each port's own repository, and
moving a segment would otherwise be a coordinated edit across ten of them. A
pull-request preview keeps its own shape: it owns its whole prefix and nests
Expand Down Expand Up @@ -264,9 +263,9 @@ The events it answers (`scripts/port-docs-identity.sh`):

- A pull request builds `latest` at its merge commit and publishes nothing.
- A push to the default branch publishes `latest`. It is the default only
until the port's first stable release tag.
- A release tag publishes its version plus `next` (prerelease) or `stable`
(release, the default).
until the port's first final release tag.
- A release tag publishes its version, plus `next` for a prerelease. A final
release is the default only when no newer final release tag exists.
- A dispatch publishes exactly what it names, at any ref. From the port's
checkout:

Expand Down Expand Up @@ -299,7 +298,7 @@ The dispatcher plans first and stops there by default:
| `ports` | `ref` | Publishes |
|---|---|---|
| `all` or a list | `latest` | each default branch as `latest` |
| `all` or a list | `release` | each newest release tag, plus `next` or `stable` |
| `all` or a list | `release` | each newest release tag, the default when final, plus `next` when a prerelease |
| one port | an exact ref | that ref, as `version` and `version-kind` name it |

```console
Expand Down Expand Up @@ -378,7 +377,7 @@ A branch or tag containing the publisher is insufficient for role assumption.
## This repo's own deploy (`deploy-shell.yml`)

The shell has no versioned URL space: the bucket root is always "whatever's
on trunk," never `/latest/` or `/stable/` the way a port's prefix is. A tag
on trunk," never `/latest/` the way a port's prefix is. A tag
on this repo is a release marker for this repo's own history and deploys to
the same root a trunk push does — not a second prefix. Release branches are
a per-port version concept (`versions.ts`'s `'branch'` kind, e.g. `v0.x`)
Expand Down
2 changes: 1 addition & 1 deletion docs/superpowers/handoffs/lua-docs-site-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Docs(feat): Export versioned Lua documentation
### Documentation workflow

- **Revision binding**: Verify the requested source ref resolves to the checked-out commit before generating or publishing documentation.
- **Release handling**: Publish prerelease tags under immutable version prefixes and update `next` without treating an alpha as stable.
- **Release handling**: Publish prerelease tags under immutable version prefixes and update `next` without treating an alpha as a final release.
- **Credential boundaries**: Use the preview role for `pr-*` publication, the production role for release and branch trees, and a separate cleanup role for closed previews.

## Integrated version
Expand Down
2 changes: 1 addition & 1 deletion docs/superpowers/handoffs/ruby-docs-site-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Docs(feat): Export versioned Ruby documentation
### Documentation workflow

- **Revision binding**: Verify the requested source ref resolves to the checked-out commit before generating or publishing documentation.
- **Release handling**: Publish prerelease tags under immutable version prefixes and update `next` without treating an alpha as stable.
- **Release handling**: Publish prerelease tags under immutable version prefixes and update `next` without treating an alpha as a final release.
- **Credential boundaries**: Use the preview role for `pr-*` publication, the production role for release and branch trees, and a separate cleanup role for closed previews.

## Integrated version
Expand Down
2 changes: 1 addition & 1 deletion docs/superpowers/handoffs/ruby-lua-site-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Add first-class Ruby and Lua documentation
### Versioning and publication

- **Revision identity**: Require the requested source ref to resolve to the checked-out commit and carry that identity through generated models and pages.
- **Release policy**: Keep prereleases out of stable defaults, protect immutable release prefixes, and distinguish trunk, maintenance branch, pull-request, tag, and alias builds.
- **Release policy**: Keep prereleases out of default versions, protect immutable release prefixes, and distinguish trunk, maintenance branch, pull-request, tag, and alias builds.
- **Scoped deployment**: Publish port-owned version trees and manifests through the reusable workflow, merge them in the shell job, and remove only the closed preview's prefix.

## Integrated versions
Expand Down
4 changes: 2 additions & 2 deletions docs/superpowers/plans/2026-09-20-ruby-lua-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ and API maturity are separate facts.
- [x] Add Ruby tag parsing and Lua tag parsing to `versions.ts`. Normalize
LuaRocks rock revisions separately: `0.1.0alpha1-1` installs the rock,
while `v0.1.0alpha1` identifies its source. Avoid relabeling these as SemVer.
- [x] Exercise alpha.2 versus alpha.10, alpha versus stable, invalid tags,
- [x] Exercise alpha.2 versus alpha.10, alpha versus final release, invalid tags,
and different rock revisions. Preserve all existing grammar tests.
- [x] Keep `latest` as the initial default for these prerelease-only ports.
Expose `next` or immutable release URLs only after building their exact
source; do not synthesize a `stable` release.
source; do not synthesize a final release.
- [x] Replace all-port MCP assumptions with capability checks. Set Ruby's
workspace command to `libtmux-workspace load`; Lua exposes status only.

Expand Down
29 changes: 14 additions & 15 deletions infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,22 @@ distribution is out of scope for the three files above, which configure

## The bare-language-root redirect and its KeyValueStore

`/en/py` and `/en/py/` 302 to that port's current default version (normally
`/en/py/stable/`). The destination comes from one CloudFront KeyValueStore,
`/en/py` and `/en/py/` 302 to that port's current default version (the newest
final release tag, such as `/en/py/v0.46.2/`, or `latest` while the port has none). The destination comes from one CloudFront KeyValueStore,
associated with `cloudfront-function.js`, holding one key per port that
publishes a version prefix:

```
py:default -> "stable"
py:default -> "v0.46.2"
ruby:default -> "latest"
lua:default -> "latest"
ts:default -> "stable"
ts:default -> "latest"
rs:default -> "latest"
go:default -> "latest"
java:default -> "latest"
csharp:default -> "stable"
cxx:default -> "stable"
swift:default -> "stable"
csharp:default -> "latest"
cxx:default -> "latest"
swift:default -> "latest"
```

Every port gets a row: every port publishes a versioned prose tree, including
Expand Down Expand Up @@ -139,7 +139,7 @@ The KVS holds **only** these bare-root pointers, never a rewrite target for
an already-qualified `/<port>/<version>/...` path. DocC bakes an absolute
`--hosting-base-path` into every page it renders; if the edge silently
swapped the version under an already-loaded DocC page, the page's own router
would desync from `location.pathname`. `/py/v0.46.2` and `/py/stable` (note:
would desync from `location.pathname`. `/py/v0.46.2` and `/py/latest` (note:
no trailing slash) are handled by the *generic* extensionless-redirect rule,
not this one — see the self-check table inside `cloudfront-function.js` for
the full trace, including why the bare-root check has to run before the two
Expand Down Expand Up @@ -175,16 +175,15 @@ them and trusts the KeyValueStore lookup to miss for anything that is not one.
| --- | --- | --- |
| `/` | 0 (origin root) | 302 to `/en/`, the one redirect off an unprefixed path |
| `/en/` | 2 (directory index) | rewrite to `/en/index.html` |
| `/en/py` | 1 (KVS, `parts[3]` undefined) | 302 to `/en/py/stable/`, or whatever `py:default` holds |
| `/en/py/` | 1 (KVS, `parts[3]` empty) | 302 to `/en/py/stable/` |
| `/en/py` | 1 (KVS, `parts[3]` undefined) | 302 to `/en/py/v0.46.2/`, or whatever `py:default` holds |
| `/en/py/` | 1 (KVS, `parts[3]` empty) | 302 to `/en/py/v0.46.2/` |
| `/en/rs` | 1 (KVS) | 302 to `/en/rs/latest/`, or whatever `rs:default` holds. When the row is missing, rule 3 301s to `/en/rs/` and rule 2 serves its redirect page |
| `/en/py/stable` | 3 (extensionless) | 301 to `/en/py/stable/` |
| `/en/py/latest` | 3 | 301 to `/en/py/latest/`, never the KVS default — `parts[3]` is truthy |
| `/en/py/v0.46.2` | 3 (`2` is not an asset extension) | 301 to `/en/py/v0.46.2/` |
| `/en/dotnet/stable/api/libtmux.client` | documentation rename | 301 to `/en/csharp/stable/api/libtmux.client/`, preserving query parameters |
| `/en/csharp/stable/api/libtmux.client` | 3 (`client` is not an asset extension) | 301 with a trailing slash |
| `/en/py/stable/reference/objects.inv` | none (`inv` is an asset extension) | passes through — this is how an external Sphinx project resolves intersphinx into this site |
| `/en/py/stable/api/.buildinfo` | none (`buildinfo` is an asset extension) | passes through; a leading dot is a separator like any other |
| `/en/dotnet/latest/api/libtmux.client` | documentation rename | 301 to `/en/csharp/latest/api/libtmux.client/`, preserving query parameters |
| `/en/csharp/latest/api/libtmux.client` | 3 (`client` is not an asset extension) | 301 with a trailing slash |
| `/en/py/latest/reference/objects.inv` | none (`inv` is an asset extension) | passes through — this is how an external Sphinx project resolves intersphinx into this site |
| `/en/py/latest/api/.buildinfo` | none (`buildinfo` is an asset extension) | passes through; a leading dot is a separator like any other |
| `/en/pagefind/pagefind.js` | none (`js` is an asset extension) | passes through |
| `/en/versions.json` | none (dot excludes rule 1, `json` is an asset extension) | passes through |
| `/robots.txt` | none | passes through; it sits above every locale |
Expand Down
12 changes: 6 additions & 6 deletions infra/cache-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ fixed-TTL policy that ignores the header outright.
| --- | --- | --- | --- | --- | --- |
| Immutable tag | `/en/py/v0.46.2/` | `tag` | `public, max-age=31536000, immutable` | once, at release, never rewritten | never — the bytes never change |
| Rebuilt version prefix | `/en/py/v0.x/` | `branch` | `public, max-age=0, s-maxage=300` | every push to that branch | no — expires in 5 min |
| Mutable alias | `/en/py/stable/`, `/en/py/latest/` | `alias`, `trunk` | `public, max-age=0, s-maxage=300` | every release / every push to trunk | no — expires in 5 min |
| Mutable alias | `/en/py/next/`, `/en/py/latest/` | `alias`, `trunk` | `public, max-age=0, s-maxage=300` | every prerelease / every push to trunk | no — expires in 5 min |
| PR preview | `/pr-123/en/` | `pr` | `public, max-age=0, s-maxage=300` | every push to the PR | no — expires in 5 min, and is deleted when the PR closes |
| Version manifest | `/en/versions.json` | — | `public, max-age=0, s-maxage=60` (or `no-cache`) | every publish, any port | no — its own TTL is already a minute |
| Locale landing page | `/en/`, `/ja/` | — | `public, max-age=0, s-maxage=300` | every shell push | **yes** — the one path still spent |
| Origin root | `/` | — | `public, max-age=0, s-maxage=300` | never written; the edge function answers it | cannot be — generated per request, never stored |
| Shell prose | `/en/concepts/…` | — | `public, max-age=0, s-maxage=300` | every shell push | no — expires in 5 min |
| Error page | `/en/404.html` | — | `public, max-age=0, s-maxage=300` | every shell push | no — expires in 5 min |
| Content-hashed assets (optional 3rd pass) | `/py/stable/_astro/*.css` | — | `public, max-age=31536000, immutable` | every push that changes the hash | never (name changes instead) |
| Content-hashed assets (optional 3rd pass) | `/py/latest/_astro/*.css` | — | `public, max-age=31536000, immutable` | every push that changes the hash | never (name changes instead) |

Only a `tag` prefix is immutable. A `branch` version (`v0.x`) *looks* like a
version prefix the same shape as a tag, but `versions.ts` documents it as
Expand Down Expand Up @@ -67,8 +67,8 @@ $ aws s3 sync ./dist-v0.46.2/ \
```

```console
$ aws s3 sync ./dist-stable/ \
s3://libtmux-docs/py/stable/ \
$ aws s3 sync ./dist-latest/ \
s3://libtmux-docs/py/latest/ \
--delete \
--cache-control "public, max-age=0, s-maxage=300"
```
Expand All @@ -92,8 +92,8 @@ before this policy existed has no `Cache-Control` at all and needs one
backfill pass to pick up its class's header without re-uploading content:

```console
$ aws s3 cp s3://libtmux-docs/py/stable/ \
s3://libtmux-docs/py/stable/ \
$ aws s3 cp s3://libtmux-docs/py/latest/ \
s3://libtmux-docs/py/latest/ \
--recursive \
--metadata-directive REPLACE \
--cache-control "public, max-age=0, s-maxage=300"
Expand Down
10 changes: 5 additions & 5 deletions infra/cloudfront-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const DEFAULT_LOCALE = 'en'
async function handler(event) {
const request = event.request
const uri = request.uri
const parts = uri.split('/') // '/py/stable/' -> ['', 'py', 'stable', '']
const parts = uri.split('/') // '/py/latest/' -> ['', 'py', 'latest', '']

// --- 0. The origin root. Every page is published under a locale, so the
// bare root is the one URL with nothing behind it.
Expand Down Expand Up @@ -91,7 +91,7 @@ async function handler(event) {
// - !parts[2].includes('.'): excludes files that sit directly under
// the locale — "/en/versions.json", "/en/404.html" — each of which
// would otherwise pay for a lookup that can only miss.
// - !parts[3]: nothing follows the port. "/en/py/stable"
// - !parts[3]: nothing follows the port. "/en/py/latest"
// already names a version and must get its own trailing slash from
// rule 3, not be redirected to a different one.
if (parts[1] === DEFAULT_LOCALE && parts[2] && !parts[2].includes('.') && !parts[3]) {
Expand Down Expand Up @@ -132,14 +132,14 @@ async function handler(event) {
}

// --- 3. Page path without its trailing slash: redirect, don't rewrite.
// A rewrite would serve the same bytes at two URLs ("/py/stable" and
// "/py/stable/"); a redirect keeps exactly one canonical URL per page,
// A rewrite would serve the same bytes at two URLs ("/py/latest" and
// "/py/latest/"); a redirect keeps exactly one canonical URL per page,
// which is what Seo.astro's canonical tag and the sitemap both assume.
//
// "Is this a file?" is decided by the extension, not by "contains a
// dot". Testing for a dot looks equivalent and is not: three kinds of
// real page carry one in their last segment — a version tag
// ("/py/v0.46.2"), a .NET type ("/csharp/stable/api/libtmux.client",
// ("/py/v0.46.2"), a .NET type ("/csharp/latest/api/libtmux.client",
// 215 of them), and a locale-tagged path — and each was silently
// treated as a file, passed through, and 403'd at the origin. The
// allowlist below is every extension this build actually emits.
Expand Down
Loading
Loading