One dependency-free HTML file that tells visitors what's happening, when you'll be back, and sends them back automatically once you are. Its centerpiece is a clock set to your ETA, like the sign on a shop door. Light and dark, with zero external requests, so nothing on the page can break while your stack is down.
- One file, no dependencies. No fonts, CDNs, or frameworks. Works when everything else is off.
- Configure from the URL. Use the hosted copy with
?site=…&eta=…, no fork needed. - A clock set to your ETA. The wait is shaded on the dial and spelled out below it, in the visitor's own timezone. If you overrun, it turns orange and says so plainly instead of showing a negative timer.
- Auto-return. Checks your site every 30 seconds and sends visitors back to the page they were on the moment it's up. The clock pings on every check, and its hands swing into a check mark when you're back.
- Status page and contact links, shown only when set.
- Light and dark follow the OS, with a toggle that remembers the choice. No flash on load.
- Accessible. Screen-reader friendly status updates, keyboard focus kept, reduced motion respected, readable without JavaScript.
Point visitors at the GitHub Pages copy and pass your details as query parameters:
https://thinkdj.github.io/oopsKit/?site=example.com&eta=2026-10-24T03:00:00Z&status=https://status.example.com
| Parameter | Example | What it does |
|---|---|---|
site |
example.com |
Shown above the headline. Turns on auto-return. |
from |
/pricing?plan=pro |
Path to send the visitor back to. Defaults to /. |
eta |
2026-10-24T03:00:00Z |
When you expect to be back. ISO 8601 with Z or an offset, or Unix seconds. |
status |
https://status.example.com |
Adds a "Status page" link. https only. |
contact |
hello@example.com |
Adds a "Contact us" link. Email or https URL. |
title |
Upgrading our database. |
Replaces the headline. Plain text, 120 characters. |
msg |
Orders placed before 9am are safe. |
Replaces the body text. Plain text, 280 characters. |
probe |
/img/logo.png |
Image used to check if you're back. Defaults to /favicon.ico. |
retry |
0 |
Turns auto-return off. "Try again" then simply opens your site. |
theme |
dark or light |
Forces the initial theme. |
demo |
true |
Sets the ETA to 2.5 hours from page load, overriding eta. For demos. |
Every parameter is optional and validated. Invalid values are ignored with a warning in the browser console, so check there if something doesn't show up.
While your site is up, open https://thinkdj.github.io/oopsKit/?site=yoursite.com. Within a second it should say Back online. That proves the page can see your site. If it keeps saying it's checking, see Troubleshooting.
Redirect your whole site to the maintenance page while you work, with a 302 so search engines don't index the maintenance URL in place of your pages.
-
Cloudflare dashboard, your zone, Rules, Redirect Rules, Create rule.
-
When incoming requests match: All incoming requests. Exclude your own IP here if your team needs to see the real site.
-
URL redirect: Dynamic, with this expression:
concat("https://thinkdj.github.io/oopsKit/?site=", http.host, "&from=", http.request.uri.path, "&eta=2026-10-24T03:00:00Z") -
Status code 302. Save. Disable the rule when you're done, and visitors come back on their own.
-
Rules, Page Rules, Create Page Rule.
-
URL:
*yoursite.com/* -
Setting: Forwarding URL, 302, destination:
https://thinkdj.github.io/oopsKit/?site=$1yoursite.com&from=/$2
Copy index.html to your server and set site in the config block at the top. Setting it there pins it: ?site= in the URL is then ignored, so your copy can never be used to redirect people to someone else's site.
Serve it with HTTP 503 and a Retry-After header, never a 200, so crawlers come back later instead of indexing the maintenance page. When the page is served at your real URLs, visitors return to the exact URL they asked for.
nginx
server {
# ...
root /var/www/oopskit;
error_page 503 /index.html;
add_header Retry-After 3600 always;
location = /index.html { internal; }
location / { return 503; }
}Apache (.htaccess)
ErrorDocument 503 /index.html
Header always set Retry-After "3600"
RewriteEngine On
RewriteCond %{ENV:REDIRECT_STATUS} !=503
RewriteRule ^ - [R=503,L]Both return 503 for everything, including /favicon.ico. That's what lets the page tell you're still down. To let your team through, add an allow for your office IP in nginx or a RewriteCond %{REMOTE_ADDR} exception in Apache.
- The page loads your probe image every 30 seconds. While you're down, the probe hits your 503 or your redirect and fails. Once you're back, it loads.
- Visitors are only redirected after the page has seen your site down at least once. If the probe loads on the very first try, the page shows a "Go to yoursite.com" button instead. This prevents redirect loops when the probe isn't covered by your redirect.
- Hidden tabs don't check. Switching back to the tab, reconnecting to the internet, or the ETA passing all trigger an immediate check.
- If the visitor is offline, the page says so instead of reporting your site as down.
- It never sends visitors back. The probe image can't load from another site. Usual causes are a missing
/favicon.ico, hotlink protection, or aCross-Origin-Resource-Policy: same-originheader. Pointprobeat an image without those. - It says "Back online" while you're still down. Your redirect or 503 doesn't cover the probe path, for example a rule that skips static files. Pick a probe path the rule covers.
- The clock is off by hours. The
etahas no timezone, so each visitor reads it in their own. AddZor an offset like+05:30. - The countdown doesn't show and the hands just turn. The
etais invalid, or more than 24 hours in the past. A stale ETA is hidden rather than shown as days overdue.
Everything lives in index.html.
- Try settings live: open
index.htmllocally (file://orlocalhost) and click the sliders button at the bottom right. Every option is there and applies as you type, with ETA presets and a preview of the back-online state. Copy link gives the hosted-copy URL, Copy config the block for your file. The panel only exists on local copies. Visitors never get it. - Text, ETA, links, probe: edit the
window.OOPS_CONFIGblock at the top of<head>. URL parameters override these, except a pinnedsite. - Look: the design system is a block of tokens in
:root. Each color is defined once withlight-dark(light, dark). See DESIGN.md for the tokens and the reasoning.
node test.mjs
Tests the parsing, validation, countdown, and clock logic in the page's core script. No dependencies. The GitHub Pages deploy runs it first and won't publish if it fails.
├── index.html # The page (styles and script embedded)
├── test.mjs # Tests for the page's core logic
├── DESIGN.md # Design system: principles, tokens, components
├── assets/ # README images
└── .github/workflows/ # Tests, then deploys to GitHub Pages on push
Evergreen browsers. Dark mode needs CSS light-dark(), from 2024 on. Older browsers get the light theme without the toggle. Without JavaScript, visitors still see the full message.
Free to use for your projects. Attribution is appreciated but not required.
🕊️

