Put a docs site inside your app
You'll mount a whole second FSR application inside the first one, at a prefix, sharing its navbar and its theme. The two applications can live in separate repositories with separate owners and release schedules. One binary still serves both on one domain. The page you're reading is a mounted site.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Create the site#
A mount is written in two places. The site's config names the site and the shell's config says where it mounts. fsr sites link writes both for you.
The site's own config gains a [site] section naming itself, its prefix and the shell it renders inside. The shell's config gains a [sites.docs] row naming the artifact directory.
There is no app/vendor/ and the import map is empty. A mounted site takes its frameworks from the shell, so ignore that fsr use docs/app react line unless you intend to run this site standalone as well.
What a mount does#
At boot the shell reads the artifact, prefixes every id in it with the site's name and grafts the whole route subtree into the shell's root layout. So the site's pages arrive inside your navbar, your theme class and your footer, with the shell's store seeds already in place. The site supplies the pages. The navbar, theme and footer come from the shell.
Nesting goes one level deep. A site can't mount other sites and the host refuses an artifact that tries.
What the shell ignores#
The shell drops some of the site's config and the report lists what it dropped.
The site's static roots are taken only when they sit under its prefix. A root it configured elsewhere is dropped, so it can't claim /static/js/vendor and shadow the shell's. The roots the host infers are already under the prefix, so a site that vendors a package of its own gets it at /fsr/docs/static/js/vendor, its stylesheets and icons the same way. Its [session], [auth], [locales] and [cache] are ignored, because a visitor has one session and one identity across the whole binary.
Its import map entries are merged in where the shell lacks them, which is why the site should use the same URLs the shell does. When the URLs are identical the merge changes nothing.
Frameworks in a mounted site#
A mounted site renders under the shell's React, because the shell's import map overrides the site's on every shared specifier. The browser never loads a copy the site vendors itself.
The shell's generated/shell.json records the exact version of each framework it vendors and the site's build reads it out of there, so the two stay in step.
If you vendor React in the site anyway at a different version, the build names both copies and tells you which one wins:
A site can still vendor something of its own, a chart library say, which gets served under the site's prefix like everything else. That means the import map has to name the prefixed URL. Copy an entry over from the shell and forget to change it and the build tells you what to write:
Pinning a framework to a version the shell doesn't serve gets refused for the same reason: the shell's map overrides the site's at mount, so the pin would never take effect.
See what's mounted#
path means the mount names a working tree rather than a pinned release. The hash is the content hash of everything the artifact ships:
The running host will tell you the same thing over HTTP:
Swap a site without restarting#
This lets a docs team ship docs without anyone restarting the product.
The reload route needs the reload_route feature and the mounter. Without either one the route does not exist. Both live in the shell's own binary, so the shell needs a Cargo project beside its app/ and config/. Turn on the feature in its dependencies:
[]
= "shell"
= "0.1.0"
= "2024"
[]
= "0"
= { = "0", = ["reload_route"] }
= { = "1", = ["macros", "rt-multi-thread"] }and install the mounter in main, which tells the host how to mount the sites a second time:
use Arc;
use Host;
async
mount_allon its own mounts the sites at boot and leaves the process with no way to do it again. The reload route then answers 404.
With both in place, POST /__fsr/sites/reload re-reads every artifact and swaps the tables. Requests in flight finish on the tables they started with. Sessions survive, so nobody is signed out by a docs deploy.
Pin it for production#
While you're developing, artifact = "sites/docs" pointing at a directory is what you want. For a release, pack the artifact and pin the hash:
Install it into the shell and the mount is pinned to that hash. An artifact whose contents don't match the pin is refused, so a half-copied deploy fails instead of serving a mix of old and new files.
Next up: 320. Port a Next.js route, side by side with the code you already have.