The project site for Silo — a self-hosted media server. Built with Astro, deployed to GitHub Pages, AGPL-3.0-or-later.
bun install
bun run dev # → http://localhost:4321
bun run build # → dist/
bun run preview # serves the built dist/Node 20+ also works; Bun is preferred for parity with the site's build tooling.
src/
├── assets/home/ artwork for the homepage mockups and the rooms around them
├── components/
│ ├── home/ homepage sections, site header, and footer
│ ├── mock/ Silo app screens (tvOS, iPhone) rebuilt for the homepage
│ └── PolicyPage.astro shell for /privacy and /account-deletion
├── content/docs/ Starlight docs content — Markdown under docs/
├── data/ content + config — edit copy here, not in templates
│ ├── siteConfig.ts site-wide constants (name, license, repo names)
│ ├── home.ts homepage copy and mockup content
│ └── download.ts apps and server install on /download
├── layouts/ Marketing.astro (head, fonts, shell) and MarketingPage.astro
├── pages/ file-based routing — index.astro is the homepage
├── scripts/home.ts homepage motion: hero focus, library, pinned scroll scene
└── styles/ marketing.css (site design), mock.css (app mockups),
docs.css (Starlight theme)
| To change | Edit |
|---|---|
| Site name, license, repo URLs | src/data/siteConfig.ts |
| Homepage copy, setup steps, FAQ | src/data/home.ts |
| Mockup titles and metadata | src/data/home.ts (see its header note) |
| Download page apps | src/data/download.ts |
| Header and footer links | src/components/home/SiteHeader.astro, SiteFooter.astro |
| Documentation pages | src/content/docs/docs/**/*.md |
| Documentation sidebar | src/data/sidebar.mjs |
| Old documentation URLs | src/data/docs-redirects.mjs |
| Colors, spacing, typography | src/styles/marketing.css |
Almost every copy change is a data-file edit, not a markup edit. That's deliberate — the components don't need to be touched unless the layout itself changes.
Docs are built with Astro Starlight and
served under /docs. Add or edit Markdown files in src/content/docs/docs/.
New pages are listed in src/data/sidebar.mjs; astro.config.mjs does not
need to change.
Organize pages by the reader's task and audience:
get-started/: app choice, prerequisites, and the default installation walkthrough.using-silo/: personal settings, client use, and connecting other apps.running-a-server/: administration, integrations, deployment, and operator reference.beta/: features outside the supported 1.0 scope, including audiobooks, ebooks, and Audiobookshelf compatibility. Keep availability tables and feature instructions here, with explicit Beta titles and notices.developers/: API, plugin, and code-contribution entry points.help/: troubleshooting entry points, reports, and documentation contributions.
Each article declares an explicit slug: docs/article-name in its frontmatter.
The homepage uses slug: docs. Public URLs are flat and do not include audience,
Beta, or release-version directories. Source folders organize contributions;
src/data/sidebar.mjs independently organizes navigation by those stable slugs.
Changing a title or moving a source file must not silently change its slug.
Use the existing sidebar data file to curate the reading order. Do not add
empty pages for planned features. A guide can link to another audience's
guide instead of repeating its setup steps. Old published paths are retained
in src/data/docs-redirects.mjs; update internal links to canonical paths.
Do not add redirects for unpublished pre-1.0 reorganizations. Once 1.0 is
published, record URL changes there and preserve useful section anchors.
Run bun run test:docs and bun run build when changing this structure.
Each guide's footer shows Last updated from the latest Git commit that
changed its source file. Production and preview builds fetch full history so
unrelated commits do not change that date. Local uncommitted edits retain the
previous commit's date; new uncommitted pages have no date. This is an edit date,
not a source-review or feature-acceptance date. Leave lastUpdated out of page
frontmatter so Starlight uses Git history.
src/data/docs-release.mjs records the shared release channel and internal
source-review baseline. Guides show a short prerelease notice without revision
lists or review-method disclaimers. Source revisions and the review date remain
in the data file and internal review notes; they do not certify device acceptance.
At 1.0, set the channel to stable, set release to the actual server release,
and update the source revisions and review date after reviewing the docs against
the release. Record the website commit alongside the server and client versions
in the release record. Keep unreleased behavior in PR previews until it ships.
There is no /docs/1.0/ namespace or automatic release-publishing workflow.
An article can declare optional requires fields for server, apple, and
android. Each value is a quoted release version such as "1.2.0", meaning
that version or later. Use these only when a minimum version is established;
do not fill them with the milestone target or infer them from an audit date.
The site renders the requirements above the guide. These are minimum versions,
not introduced-in or last-tested claims. Record platform exceptions in prose.
Beta articles declare beta: true, retain their visible Beta notice, and appear
only in the Beta sidebar group. Their flat URLs do not change when they graduate.
The build validates slugs, navigation coverage, Beta placement, version fields,
the shared baseline, links, and the retained published redirects.
See the manual review for source revisions, executed checks, and procedures still awaiting hands-on validation. The feature map links the 35 milestone features to their guide sources. A mapped page is not a completed acceptance check.
The shared client instructions review records the follow-up source check, combined Apple/Android steps, and server prerequisites for AI features.
The beta audit records the server, Apple, and Android source revisions, coverage limits, and checks for the Beta section. Keep an ordinary guide's reference to a beta feature short and link to that section instead of duplicating instructions. After the 1.0 publication, preserve published URLs with direct redirects when changing slugs.
The extra nested docs/ directory is intentional: Starlight routes pages
from src/content/docs/, so nesting the public docs there gives the site
the /docs URL prefix while keeping everything in the same Astro project.
When a release is published in silo-server, silo-apple, or
silo-android, the site rebuilds automatically. Wire it up by adding
this workflow to each app repo:
# .github/workflows/notify-website.yml
name: Notify website
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Trigger website rebuild
env:
GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }}
run: |
gh api repos/Silo-Server/siloserver.org/dispatches \
--method POST \
-f event_type=release-published \
-f client_payload[repo]=${{ github.repository }} \
-f client_payload[tag]=${{ github.event.release.tag_name }}This needs a WEBSITE_DISPATCH_TOKEN secret on each app repo — a fine-
grained GitHub PAT scoped to siloserver.org with Contents: Read
and Actions: Read & Write permissions. Standard pattern, set once per
repo.
The 6-hour cron is a fallback for missed dispatches and edits that happen outside a release (changed README, added a new app, etc).
Every pull request runs .github/workflows/pr-build.yml: a full bun run build
with internal-link validation (starlight-links-validator). The build fails on
a broken /docs link or anchor, so fix those before asking for review.
The same workflow builds the site as a preview and hands the output to
preview-deploy.yml, which uploads it to Cloudflare Pages and posts one sticky
comment on the pull request with the URL:
https://pr-<number>.siloserver-org.pages.dev
The alias is updated after a successful build and deployment. Previews show an orange banner
linking back to the pull request, carry noindex, and are deleted by
preview-teardown.yml when the pull request is merged or closed (plus a weekly
sweep of anything older than 30 days).
Cloudflare can keep serving a deleted deployment on its alias for a while, so teardown first publishes a one-rule redirect to the alias and keeps it. Each page redirects to the same path on siloserver.org, so a page that never shipped gets the site's 404 page. Teardown waits until the alias returns the redirect, then deletes every other deployment for that pull request. If the redirect fails, teardown still deletes the preview. Pull requests that never published a preview get no redirect. To repeat this for an already closed pull request, run Preview teardown manually with its number; without a number the manual run does the weekly sweep. The weekly sweep removes the redirect once it is 30 days old.
The optional preview commit status appears while the build is queued, then
reports building, waiting for deployment, deploying, and the final result.
Pending or failed statuses link to the workflow; successful ones link to the
preview. Build or deployment failures and cancellations report an unavailable
preview instead of leaving the status pending. This does not make preview a
required merge check; keep only the intended build checks required in rulesets.
preview-progress.yml receives trusted build lifecycle events, including reruns.
It and the deployment workflow call preview-report.yml, which serializes status
writes per commit using trusted tooling. The reporter checks the current PR head,
build run, and attempt, and rejects stale or regressive updates. It requires no
Cloudflare credentials and never checks out PR code. These workflow_run changes
take effect after they reach the default branch; a PR preview cannot test them live.
scripts/preview-banner.mjs adds the banner to every built HTML file, including
standalone pages copied from public/, documentation, and redirects. It runs
only when valid preview metadata is supplied. The banner reserves space above
navigation and adjusts when its text wraps; production HTML is unchanged.
A preview marks what the pull request changed in the docs. A box in the
bottom-right corner of every docs page has a Show changes switch, buttons
that step through each change across all changed pages, and a list of those
pages. Added blocks are outlined in green, changed blocks in amber with the
inserted and removed words marked, and removed blocks become a red note that
expands to show the old text. Side by side shows the whole list, table, or
paragraph from before and after the change, which is easier to read when a
section was rewritten. Press ] and [ to step through changes and c to
switch the highlights on or off.
When you open a preview again after a new push, Only new since your last visit limits the list to changes you haven't seen. The browser remembers the changes on each preview it has shown, so this works per browser. The preview comment on the pull request lists the changed pages, each linked to its first change.
scripts/preview-changes.mjs does this after a preview build. It builds the
commit the pull request is compared with in a temporary worktree: on GitHub,
the target branch in the pull request's merge commit; locally, where the branch
left origin/main. It then compares the rendered page title and Markdown body
of both builds, block by block, and adds the differences and
scripts/preview-changes-overlay.js to each docs page. This adds about 15
seconds to a preview build. A failed comparison shows a notice in the box and
does not fail the build. The pull request comment comes from
.github/preview-tools/docs-changes-summary.cjs, which runs in the trusted
deploy workflow and accepts only validated paths, counts, and plain-text
titles from the build artifact.
To try it locally, build with preview metadata and serve the result:
PUBLIC_PREVIEW_PR_NUMBER=1 \
PUBLIC_PREVIEW_PR_URL=https://github.com/Silo-Server/siloserver.org/pull/1 \
bun run build && bun run previewSet PREVIEW_CHANGES_BASE to a commit or branch to compare with something
else, or PREVIEW_CHANGES=0 to skip the comparison.
The split into two workflows is deliberate: pr-build.yml runs contributor
code, including from forks, with no secrets and no write permissions.
preview-deploy.yml holds the Cloudflare token but never checks out or
executes pull request code. It checks out deployment tooling from the trusted
workflow commit and installs Wrangler with its committed npm lockfile before
uploading the built artifact. It verifies that Cloudflare reports terminal
deployment success before publishing the preview link. Deploy and teardown
share a concurrency queue, so cleanup cannot be overtaken by publication.
This serializes preview operations across the project, including weekly sweeps. Keep it that
way, and do not add a token to the build job.
For the same reason, the deploy workflow derives the pull request number and
commit from the trusted workflow_run event and the GitHub API, never from
the artifact. A fork can edit the build workflow and write anything into an
artifact, so artifact contents must not decide where a deployment lands or
which comment and commit status are written.
Pull requests from forks get previews too. GitHub doesn't link a commit that
exists only in a fork to this repository's pull requests, so
.github/preview-tools/find-pull-request.cjs also looks the pull request up
by the fork's owner and branch from the build run. Either way, the pull request
must be open, with the built commit as its head, from the same repository and
branch. A first-time contributor's build waits until a maintainer selects
Approve and run on the pull request. To stop someone's pull requests from
building at all, block them in the organization settings, which stops them
from opening pull requests.
One visible consequence: the build job has no GITHUB_TOKEN, so the
build-time release lookup in src/data/releases.ts may be rate-limited on
shared runners. Client cards then fall back to plain repository links instead
of showing a version. That is expected in a preview and never fails the build.
- Create a Cloudflare Pages project (direct upload, no Git integration);
production stays on GitHub Pages. The project name is set once per workflow
as
PREVIEW_PROJECT, currentlysiloserver-org. Keep this value consistent across the three preview workflows. - Create a GitHub environment named
Preview, restrict its deployment branches to Selected branches and tags → branchmain, and addCLOUDFLARE_API_TOKEN(Account · Cloudflare Pages · Edit, scoped to that one account) andCLOUDFLARE_ACCOUNT_IDas environment secrets. Keeping them out of repository secrets and restricting the environment tomainprevents PR workflows from reading them, including workflows edited on same-repository branches. Do not allowrefs/pull/*/mergeor arbitrary tags. - Require the GitHub Actions
buildcheck onmainusing a branch ruleset or branch protection.
When editing preview workflows, run python3 scripts/test-preview-workflows.py
(requires Python 3, Node.js, Bun, and jq). These checks mock the provider APIs to cover
cleanup failures, pagination, timestamp formats, deployment status, and PR
closure or reopening during queued operations.
The PR build runs them before building the site.
GitHub Pages, configured by .github/workflows/deploy.yml. The repo
is named siloserver.org and the canonical domain is
https://siloserver.org,
configured via public/CNAME.
If you ever change hosting or domain, override the build with repo
variables (vars.SITE, vars.BASE_PATH) — the workflow honors both,
so the same code deploys to a different URL shape without code changes.
GitHub Pages remains the origin. Cloudflare caches the public website at the
edge, while browsers revalidate pages and files whose URLs can change contents.
Files with hashes in their names under /_astro/ can be cached for a year.
Every successful production deployment writes /_build.json with the commit,
workflow run, and attempt. The deploy job waits until that marker matches the
new artifact, purges the siloserver.org hostname, and checks the homepage. This
covers pushes, scheduled builds, release dispatches, and manual deployments.
The marker bypasses Cloudflare caching; a failed purge fails the deployment job.
The repository needs CLOUDFLARE_ZONE_ID as an Actions variable and
CLOUDFLARE_CACHE_PURGE_TOKEN as an Actions secret. The token needs only Cache
Purge permission for this zone. It is sent only to Cloudflare's API.
The site is dark only, set in Plus Jakarta Sans (loaded from Google Fonts at
runtime), with Silo's blue, red, and orange as accent light. The homepage
shows the product itself: its mockups rebuild real Silo screens from the
tvOS and iOS app source at real device ratios, so check a label or layout
against the files listed at the top of src/data/home.ts before changing it.
The hero and the pinned TV-and-phone scene sit in painted rooms; every other
page uses the docs' dark theme.
Colors live as CSS custom properties at the top of src/styles/marketing.css.
The downloadable logo originals live under public/brand/. The Starlight docs
are themed via src/styles/docs.css.
The site's source is licensed AGPL-3.0-or-later, matching the rest of the Silo
project. See LICENSE.
The Silo name, logo, and wordmark are trademarks of Silo Media L.L.C. and are not covered by the AGPL. You're free to fork and redistribute the code, but forks and redistributions must not use the Silo brand as their identity and must remove or replace the brand assets. See TRADEMARK.md for what's permitted — including referential use like "compatible with Silo."
Read CONTRIBUTING.md before opening a pull request. Site-wide design, navigation, deployment, and product-claim changes should start as an issue.