This website¶
https://materialdrain.senko.tools has two parts: the home page at /, with the downloads, and this
documentation at /docs/. Both are made from the website branch, which shares no history with the app's branches.
Where things are¶
Path (on website) |
What it is |
|---|---|
website/ |
The home page: index., style., releases. and 404.. Plain HTML, CSS and JavaScript, no build step. Also robots. and the icons (below) |
docs/ |
These pages, in Markdown, built with MkDocs Material |
mkdocs. |
The documentation's settings: the theme, the Markdown extensions and the navigation |
docs/ |
The exact versions of MkDocs and Material the site is built with |
docs/ |
The documentation's colours, the same palette as website/ |
hooks/ |
Adds to every page as it's built: the line at its end (when it was last changed, and the app's version then), the Nightly labels, and where a name in a table may break over lines on a phone |
overrides/ |
The parts of the theme the site changes: partials/, which shows that line |
docs/ |
How the server serves the site, and the files it needs. Not published |
. |
Builds the site on every push to website, into the site branch |
preview. |
Builds the whole site as the workflow does and serves it locally (see Previewing it) |
Why a branch of its own: the documentation changes far more often than the app, and a change to it should never start
an app build or appear in the app's history. The app's branches (main, dev) hold only the app; see
Development.
How it's published¶
flowchart LR
push[Push to website] --> build[site.yml builds it]
build -->|force-push, one commit| site[site branch]
server[The server, every 5 minutes] -->|git fetch| site
server -->|the releases| releases["/releases.json"]
server --> nginx[nginx serves /var/www/materialdrain]
- A push to
websiteruns the Website workflow (.github/workflows/site.yml). Run workflow on the Actions tab runs it without a change. A newer push cancels a build that's still running. -
The build installs the pinned requirements, then:
mkdocs build --strict --site-dir _site/docs cp -r website/. _site/The documentation goes to
_site/docs, and the home page's files to the root of_site. 3. The result is force-pushed tositeas a single new commit, "Site built from <commit>". The branch is replaced every time, so it never grows. The workflow only needs permission to write to the repository, no secrets. 4. The server pullssite. A systemd timer (materialdrain-site-sync.timer) runs a small script every 5 minutes (and a minute after booting). It fetches the branch; when the commit is a new one, it resets/var/www/materialdrainto it and drops the old builds so the folder doesn't grow. nginx serves that folder. The same script saves the app's releases for the download page (see The downloads).
So a change shows on the site within about 5 minutes of its build finishing.
The server only ever reads from GitHub: nothing on GitHub can reach it, and the sync runs in a systemd sandbox that
can only write the site's folder and the releases' one. Setting up a server is described in docs/server-setup/README.md.
Never edit site by hand
site is output. The next build replaces it entirely, so a change made there is lost within minutes. Change
website instead.
Previewing it¶
You need Python 3 (the workflow uses 3.12). The simplest way, on Windows or anywhere with PowerShell, is the script at the root of the branch:
./preview.ps1
It builds the whole site as the workflow does, into _site, with --strict, and serves it at
http://127.0.0.1:8000: the home page at /, the documentation at /docs/, as on the real site. It also reads the
releases from GitHub into _site/releases.json, as the server's sync does. Run it again after a change. The first time,
it makes a virtual environment in .venv with the versions in docs/requirements.txt (again whenever that file
changes). -Port 8080 serves on another port, -NoServe only builds, and -Offline leaves out the releases, so the
home page asks GitHub itself.
What it can't show is what only nginx does: the 404 pages, and the headers for caching and security.
By hand, from the root of a checkout of website:
python3 -m venv .venv
source .venv/bin/activate
pip install -r docs/requirements.txt
mkdocs serve
mkdocs serve shows the documentation at http://127.0.0.1:8000 and reloads on every change you save. Before pushing,
build it the way the workflow does, so a mistake fails here and not there:
mkdocs build --strict
The home page asks for its files from the root of the site (/style.css, /releases.js), so serve its folder rather
than opening the file:
python3 -m http.server --directory website 8001
Its Documentation link (/docs/) only works on the real site, or if you run the workflow's two build commands and
serve _site instead, which preview.ps1 does. site/, _site/ and .venv/ are in .gitignore.
The versions in docs/requirements.txt are pinned, so a new release of MkDocs or Material can't change or break the
site without anyone noticing. Raise them on purpose, and check the build when you do.
Adding a page¶
- Create the Markdown file under
docs/, in the folder of its section, e.g.docs/development/my-topic.md. Start it with a single#title. -
Add it to
navinmkdocs.yml, where it should appear:nav: - Development: - development/index.md - My topic: development/my-topic.mdA section's
index.mdis listed without a title: it becomes the section's own page (navigation.indexes). 3. Link to it from the pages it belongs with. Links are relative paths to the Markdown files, with an optional anchor:[Conventions](conventions.md#tests),[Hosts](../hosts/index.md).
The build is strict: a link to a page that doesn't exist, or a nav entry for a missing file, fails it, and nothing
is published until it's fixed. A page that's in docs/ but deliberately not in the navigation goes under
not_in_nav in mkdocs.yml (like the old provider-configuration.md); files that aren't pages at all go under
exclude_docs (like server-setup/).
Writing a page¶
Match the pages around it: plain words, short sentences, and the reason behind things, not just the steps. Tables for
reference, a diagram only where it shows something words don't, and code blocks with their language
(```kotlin, ```json, ```bash).
These Markdown extensions are turned on in mkdocs.yml:
| Extension | For | Written as |
|---|---|---|
admonition |
A boxed note, tip or warning | !!! note "Title", then the text indented by four spaces |
pymdownx. |
A box that opens on a click | ??? note "Title" (closed) or ???+ note (open) |
pymdownx. |
Tabs, for alternatives | === "First tab", then the content indented |
pymdownx. |
Code blocks inside lists, tabs and notes, and Mermaid diagrams | ```mermaid |
pymdownx., pymdownx. |
Coloured code, also inline | ```kotlin, `#!kotlin val x = 1` |
pymdownx. |
Keys to press | ++ctrl+c++ |
tables |
Tables | The usual pipes |
def_ |
A term and its meaning | Term on one line, : Meaning on the next |
attr_, md_ |
Classes on Markdown, Markdown inside HTML | [Download](…){ ., <div class="grid cards" markdown> |
toc |
The table of contents on the right, and a link on every heading | Headings (##, ###) |
A few of them, written out:
!!! note "Pixeldrain itself"
The built-in Pixeldrain host isn't a config, so `screens` doesn't apply to it.
Use them sparingly: a page full of boxes has nothing that stands out. note, tip, info and warning are the
usual kinds.
=== "WebDAV"
Text about WebDAV.
=== "S3"
Text about S3.
??? info "Why it works this way"
Hidden until it's clicked.
```mermaid
flowchart LR
app --> api[provider-api]
```
flowchart and sequenceDiagram are the ones used so far.
Press ++ctrl+c++ to copy.
Renders as: Press Ctrl+C to copy.
Stable and Nightly¶
The pages describe the app as it is on dev, the Nightly build. Whatever it has that the newest stable release
doesn't yet is marked with <span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span>, written where the label should be, which hooks/page_info.py turns into a small
yellow Nightly with a tooltip:
| **Compact player** <span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span> | The player opens as a card a third of the screen high |
Mark the feature, not every mention of it: a whole table under a marked heading needs no label on each row. When a
stable release has those changes, take the labels out (grep -rn "<span class="nightly" title="Only in the Nightly build so far, not in a stable release yet">Nightly</span>" docs), and the sentences that say how a
stable release differs ("In a stable release, …").
The line at the end of a page¶
Every page ends with when it was last changed and the newest stable release then: Last updated 23 October 2026 · For
Materialdrain 1.5.20261014.1558, and and the Nightly build where it says so on a page with Nightly labels.
hooks/page_info.py works both out from git, at build time:
- The date is the page's last commit in this branch (
git log -1 --follow), so it changes only when the page does. A page with changes not committed yet says so, with today's date, as in a local preview. - The version is the newest of the app's release tags (
v…) whose commit is no newer than that date. The tags are on the commits the releases were built from, which share this repository, so nothing is asked of GitHub. A page last changed before a release still names the older one, which is the point: it says which version it was written for.
The workflow checks out the whole history and the tags for it (fetch-depth: 0); a shallow checkout would give every
page the date of the last commit and no version. overrides/partials/source-file.html replaces Material's own line
with this one.
The theme's features (mkdocs.yml, theme.features) add instant page loads, tabs for the top-level sections,
"back to top", search suggestions, a copy button on every code block, and tabs that switch together across a page
when they have the same title (content.tabs.link).
The colours¶
The whole site is dark, in one palette: a dark rose. It's defined twice, in the same CSS variables with the same values:
website/style.cssfor the home page.docs/stylesheets/extra.cssfor the documentation, which maps them onto Material's own variables (--md-default-bg-color,--md-typeset-a-color, the code colours and so on).mkdocs.ymlsets the scheme toslate(dark) withcustomprimary and accent colours, so these are the only colours used.
| Variable | Value | Used for |
|---|---|---|
-- |
#1a1617 |
The page |
-- |
#251f21 |
The header, cards, code blocks, notes |
-- |
#d4a5b2 |
Accents, keywords in code |
-- |
#eec1c8 |
The site's name in the header |
-- |
#e2b2be |
Links |
-- |
#f2ecee |
Text |
-- |
#b0a2a6 |
Secondary text |
The rest (borders, the deeper and muted accents, the blue, red, green and yellow) are in both files. To change a colour, change it in both, so the home page and the documentation stay one site.
The downloads¶
The home page's download cards are filled in by website/releases.js, in the visitor's browser, from the repository's
GitHub releases. The app's build workflow publishes those (see Development); the website
only lists them, and every download comes straight from GitHub.
- The server reads the list. Every 5 minutes the sync script asks
https://api.github.com/repos/fidesosu/Materialdrain/releases?per_page=30and saves the answer as/var/www/materialdrain-releases/releases.json, which nginx serves as/releases.json. It's written on every check, so itsLast-Modifiedis when the server last asked GitHub; a failed check keeps the last good list and its time. Visitors never ask GitHub themselves, so its limit of 60 requests an hour for each visitor doesn't matter. The file is apart from the site's checkout, which git resets and cleans on every update. - The page reads
/releases.json, and again every minute while it's open and whenever it's shown again after being in the background. The cards are only drawn again when the server has checked since. Where there's no/releases.json(a local preview, or a server not set up for it yet) the page asks GitHub once, itself. - Drafts and releases without an
.apkare left out. - Stable is the newest release that isn't a pre-release and isn't tagged
dev-build. Nightly is the release taggeddev-build, the development build (it's rebuilt with every push todev, not once a night). Up to 4 older stable releases are listed under Older versions ("None yet." when there are none). - Each card links straight to its APK ("Download APK" at the end of its title's line), and shows the version, the
date and the size, and a line on what the build is, which stays after the date comes in. The version is read from the
APK's name (
Materialdrain-1.4.20261009.1530.apkgives1.4.20261009.1530); releases from before the build workflow named their APKs differently, so for those it's the tag without itsv. The date is when the APK was uploaded, not when the release was published: thedev-buildrelease is published once and then only edited, so its own date stays the first development build's, while each push todevuploads a new APK. - Under each card: What's new (the release's page on GitHub, with its notes) and the APK's SHA-256, shortened,
with a button that copies all of it, to check a download against. GitHub records the checksum (
digest) of uploads since mid-2025; for older ones the line has only the notes. The older versions have What's new beside their version, and their checksum in the version's tooltip. - Next check in … beside the Download heading counts down, by the second, to the server's next read of the list
from GitHub: 5 minutes after its last, which is when
releases.jsonwas written. Its tooltip has the time of the last one. Once that's due it says Checking GitHub…, and the page asks for the list every 10 seconds until the server's is newer (its timer can be up to 30 seconds late). When the page had to ask GitHub itself, there's no next check, and it says so. From 20 minutes without a check the server's checks are failing (journalctl -u materialdrain-site-sync.servicesays why): the line says Couldn't check GitHub lately, and under the cards the page says a newer build may be missing. When the list couldn't be loaded it stays empty.
If neither the server's list nor GitHub can be read, the page says so and the cards keep their plain links to the releases page, which are in the HTML from the start. So the page works, if more plainly, without the script.
A card for a build that isn't published yet is dimmed and has no link, so it can't be clicked or reached with the keyboard.
The icons, link previews and security headers¶
website/icon.svgis the app's launcher icon, drawn from the app's ownpixeldrain_launcher.xmlon its background colour (#97B57C), cut to the part a launcher shows. It's the favicon of both the home page and the documentation (theme.favicon: /icon.svginmkdocs.yml), and the logo before the title.favicon.ico(16, 32 and 48 px),apple-touch-icon.png(180 px) andog-image.png(1200 × 630, the picture a shared link shows in a chat) are made from it, withrsvg-convert. Change the icon, make them again.- The
og:andtwitter:tags inindex.htmlare what Discord, Telegram and the like show for a link to the site. - A Content-Security-Policy, as a
<meta>tag inindex.htmland404.html, lets the pages load only their own files, andreleases.jstalk only to the site andapi.github.com. A new script, style or picture from elsewhere has to be added to it, or the browser blocks it. The documentation has none: Material has inline scripts. robots.txtpoints search engines at the documentation'ssitemap.xml.
The home page's layout¶
On a computer the home page fits one screen, without a sidebar: main is up to 1200 px wide, at least as tall as the
window, and centres its content top to bottom.
- The top: the icon and title, its line, and a few things the app does as one line of plain text (
.features, taken from What it can do), with the Documentation and Source code links level with the title (.top,.top-links). The links go under the title only when there's no room for both. - Download: the Stable and Nightly cards side by side, as wide as the page, each with its notes and checksum under
it (
.build), then a line on installing, with links to how and to What you're installing. The notes-and-checksum line keeps its height while it's empty, so nothing moves when the list comes in. - Older versions: a grid of tiles (
.versions), four to a row from 960 px wide, so the 4 shown make one full row; narrower, as many as fit. "All releases on GitHub" is at the end of the heading's line (.section-head).
All of it fits 1280 × 720, even with the warning about the server's checks. Under 640 px wide (a phone) the page starts at the top, everything stacks, and it scrolls.
Showing more than 4 older versions (OLDER_SHOWN in releases.js) adds rows; 8 still fit 1280 × 720.