Production assets
Build the deployment directory with:
quarto render --profile productionDeploy _site-production/ to the shared origin serving symbolica.io and cdn.symbolica.io. The production profile supplies ASSET_BASE_URL through _environment-production. Ordinary quarto render and quarto preview continue to use local assets in _site/. The separate output directories prevent cached production rewrites from leaking into local previews. Source documents keep their existing relative asset paths.
The final post-render step, scripts/rewrite_assets.py, rewrites image/media attributes, responsive image sources, stylesheets, CSS font/background/import URLs, and social preview images. It also routes the documentation’s Python, Symbolica and ty assets, and the Gallery’s Python runtime, standard library, package wheels and ty assets to the CDN. Both notebook systems share the optimized standard library, Symbolica, ty WASM and type-stub URLs. Query strings, fragments and external URLs survive. Navigation, canonical URLs, notebook HTML, downloadable Python sources, and JavaScript entry points/workers stay on the page’s origin. In particular, moving a module that constructs workers to another origin can break worker creation. The small Gallery lockfile stays local so it is deployed with the worker.
Immutable directories are never rewritten. Changed files with compression sidecars get fresh sidecars; missing optional compressors result in those sidecars being removed, rather than serving stale content. A render needs no additional Python dependencies or network access for the rewrite.
For a different asset origin, override ASSET_BASE_URL in the build environment. The rewrite can also be run explicitly on an already-rendered deployment:
python3 scripts/rewrite_assets.py --output _site-production --base-url https://cdn.symbolica.io
python3 scripts/test_rewrite_assets.pyCDN configuration
The CDN must return Access-Control-Allow-Origin: * on public static assets, including JS modules, fonts, WASM, JSON, wheels and ZIP archives. Add it at the origin (see live-python/Caddyfile.example) or with a Cloudflare response-header Transform Rule. This is required for the cross-origin Python and type-checker fetches. Deploy the same directory to the origin before enabling CDN references; new images and archives must exist under the same paths on both hostnames.
Use one-year immutable caching only for /docs/live-python/immutable/ and /gallery/notebooks/immutable/. Set those paths to Eligible for cache in Cloudflare Cache Rules, so extension defaults do not exclude .mjs, .wasm, or .whl files. For other assets, respect origin revalidation headers or use a finite edge TTL and purge changed files when deploying. Keep old immutable directories for already-open notebooks. Do not cache missing-file responses for a long time: a cached 404 can outlive a deployment.
After deployment, check a successful response with an Origin request header for CORS, and repeated GETs for CF-Cache-Status: HIT. Confirm the Gallery runs and type completion works from symbolica.io; loading a CDN URL directly does not test CORS.
The Caddy example negotiates precompressed zstd, Brotli and gzip for assets with sidecars. HTTP zstd uses an 8 MiB window for browser compatibility. The supplied Symbolica community wheel already uses ZIP-zstd internally and is served as-is, without HTTP compression in documentation snippets; its download is about 19.48 MiB (20.43 MB). NumPy is optional and is not downloaded at startup (saving 2.82 MiB). Other prepared ZIP archives use uncompressed members and HTTP sidecars. Check Content-Encoding when comparing transfer sizes with decoded archive sizes.
Release upload checklist
Build from a clean Git snapshot so untracked notebooks and experimental assets are not published. Use a snapshot directory outside hidden directories (Quarto may skip documents under hidden ancestors). The prepared release is the contents of _site-production/, including all .zst, .br, and .gz sidecars and the restored raw symbolica-core.wasm. Do not upload the source tree, node_modules, or _site/.
Keep both hostnames pointed at the same document root. No DNS changes are needed for the existing
cdn.symbolica.iosetup.Upload new assets before replacing HTML and loader/config files. Prefer uploading a complete release to a new directory, copying the old immutable directories into it without overwriting the new ones, then atomically switching the document-root symlink. With an in-place copy, copy immutable assets first, remaining non-HTML assets second, and HTML last. Do not use
rsync --delete: open notebooks can still request older immutable assets.Merge the caching and precompression settings from
live-python/Caddyfile.exampleinto the server’s existing Caddy configuration. Validate before reloading. Keepfile_server { precompressed zstd br gzip }: copying the compressed files alone does not enable them. Error responses should useCache-Control: no-store.In Cloudflare, scope long caching to the immutable paths. Use this Cache Rule:
(http.host eq "cdn.symbolica.io" and (starts_with(http.request.uri.path, "/docs/live-python/immutable/") or starts_with(http.request.uri.path, "/gallery/notebooks/immutable/")))Select Eligible for cache; for Edge TTL choose Use cache-control header if present, bypass cache if not, and set Browser TTL to Respect origin. Preserve query strings in cache keys. Remove broad rules that override these headers or give errors a positive TTL. If a status-code TTL override is used, set 400–599 to Do not store. Other paths must respect origin headers too.
After upload, purge changed, non-immutable CDN assets. For this first release, a one-time Purge Everything is the simplest way to remove the previous broad-cache entries and any cached 404s. Later immutable releases do not need a full purge; their content hashes create new URLs. Purging the CDN does not invalidate existing browser caches.
Open
https://symbolica.io/gallery/in a fresh browser session, run a notebook, request type completion, switch notebooks, and check for failed network requests. Repeat GETs of a real immutable URL should return 200,Access-Control-Allow-Origin: *, the one-year immutable cache header and eventuallyCF-Cache-Status: HIT. RequestAccept-Encoding: zstd, br, gzipto verifyContent-Encodingon the large runtime and native WASM assets.
Live checks on 2026-10-03
The existing CDN’s Python WASM returned 200, Access-Control-Allow-Origin: *, Brotli encoding, Cache-Control: public, max-age=31536000, immutable, and a subsequent GET returned CF-Cache-Status: HIT. CORS and immutable caching are already functional for documentation assets; extend the same rule to Gallery. An absent runtime path returned 404 with Cache-Control: public, max-age=2592000 (30 days), while the page origin returned public, no-cache. Check Cloudflare TTL/header overrides before release so missing new assets are not cached that long. A normal CDN image also received a four-hour browser TTL, despite the example’s revalidation policy, so non-immutable header overrides merit review.
References: Cloudflare Cache Rule settings, Caddy precompressed files.
The Gallery browser regression accepts a production asset origin:
python scripts/gallery/test_browser.py --browser /usr/bin/brave \
--url https://symbolica.io --cdn-origin https://cdn.symbolica.ioFor pre-upload validation, serve the output on two local origins and use --browser-arg to map cdn.symbolica.io to the local TLS asset server and trust that server’s certificate public key. This tests the real production URLs, compression and CORS without modifying the release or routing browser requests through Playwright interception.