Static site hosting for webatspeed.de, managed with AWS CDK in infra/.
- AWS S3, CloudFront, Route 53, ACM, SES
- One stack,
WebAtSpeedStack(eu-central-1) - The us-east-1 wildcard certificate is referenced by ARN rather than managed — CloudFormation cannot adopt an ACM certificate, so the stack owns its DNS validation record instead
| Domain | Content | Behaviour |
|---|---|---|
www.webatspeed.de |
Corporate Website (current) | serves the site |
webatspeed.de |
Corporate Website (current) | redirects to www |
micro.webatspeed.de |
Corporate Website (2023) | serves an archived site |
legacy.webatspeed.de |
Corporate Website (2015) | serves another archived site |
easypeasy.webatspeed.de |
Basic Japanese Training | serves the Angular app |
japan.webatspeed.de |
Basic Japanese Training | redirects to easypeasy |
hanashite.webatspeed.de |
Japanese Listening Comprehension | set up separately (see repo) |
Page content is tracked in git under site/. Each directory is named for the bucket it
belongs to:
site/www/ -> www.webatspeed.de the landing page
site/micro/ -> micro.webatspeed.de the previous site, archived
site/legacy/ -> legacy.webatspeed.de
site/easypeasy/ -> easypeasy.webatspeed.de
site/www is the only one that is not a plain S3 mirror — it is a project with a build
step, described under Building site/www.
To re-pull from S3 if the buckets have drifted from the repo:
aws s3 sync s3://www.webatspeed.de site/www
aws s3 sync s3://micro.webatspeed.de site/micro
aws s3 sync s3://legacy.webatspeed.de site/legacy
aws s3 sync s3://easypeasy.webatspeed.de site/easypeasy
There is no CI/CD. Edit the files under site/, then publish by hand. Every command below
needs AWS credentials for the webatspeed account (ambient credentials via a working profile —
same as Infrastructure below).
site/www is the only site with a build step. site/www/deploy.sh (npm run deploy from
site/www, after npm install once) does the whole thing in one command: build, then sync
only the deployable entries — the four HTML files, imprint/, privacy/, favicon.ico,
site.webmanifest and assets/ (including assets/vendor/, assets/theme.js and
assets/contact.js, which are hand-maintained rather than built) — then invalidate. src/,
package*.json, node_modules/ and the READMEs stay out of the bucket, which is why this
site's sync needs --exclude filters and no other site does:
cd site/www
npm install # once
npm run deploy
Equivalent to, run from the repo root:
cd site/www && npm run build && cd ..
aws s3 sync site/www s3://www.webatspeed.de --delete \
--exclude 'src/*' --exclude 'node_modules/*' \
--exclude 'package.json' --exclude 'package-lock.json' --exclude 'README.md' --exclude 'deploy.sh'
aws cloudfront create-invalidation --distribution-id E1I3JETEJR7M6F --paths '/*'
The other sites are plain mirrors — no build, no excludes:
aws s3 sync site/legacy s3://legacy.webatspeed.de --delete
aws cloudfront create-invalidation --distribution-id E3TWTVZQRYIGX0 --paths '/*'
aws s3 sync site/easypeasy s3://easypeasy.webatspeed.de --delete
aws cloudfront create-invalidation --distribution-id E3AG8LU1RIZKGP --paths '/*'
Notes:
--deleteremoves remote files that no longer exist locally. Run without it first if unsure.- Without an invalidation, CloudFront keeps serving the old copy for up to 24 hours (
default_ttl86400). - The
webatspeed.deandjapan.webatspeed.debuckets are intentionally empty — they only hold a redirect configuration, so nothing is synced to them. hanashite.webatspeed.deis a separate stack and repo — see the link in the Domains table — and is not deployed from here.
The landing page is four static pages — index.html, imprint.html, privacy.html and
404.html, plus folder copies of the two legal pages — built from Tailwind 4 + daisyUI 5. Nothing runs at runtime beyond ~50 lines of inline vanilla
JS and assets/theme.js; the build only turns src/app.css into assets/app.css. Fonts and
icon fonts are self-hosted, so the pages contact no third-party host. Verified on node
v26.3.0 / npm 11.16.0.
Daily loop, two terminals (after npm install in site/www, if not done already):
npm run watch # rebuilds assets/app.css on save
python3 -m http.server 8000 # any static server works
Two things to know before editing:
- Never edit
assets/app.css. It is generated and overwritten on every build. All CSS changes belong insrc/app.css. It is committed anyway, because there is no CI and the deploy is a plains3 syncof whatever is in the working tree. - Rebuild after markup changes, not just CSS changes. Tailwind scans the four HTML files
through
@sourceand only emits the classes it actually finds there. Add a class to a div without rebuilding and it silently does nothing.npm run watchcovers this; the risk is editing HTML with no watcher running. A new HTML page needs its own@sourceline insrc/app.cssor every class unique to it is dropped.
The legal pages are German, as required of a German company, and carry noindex. Both reuse
the landing page's footer verbatim, so the Legal column stays identical across the site. Each
page also carries its own copy of the font-boot snippet: app.css sets body{opacity:0} and
only reveals it once JS adds html.boot-done, so a page without that script never appears.
Dark mode follows the operating system until the visitor touches the toggle; from then on their
choice is stored under the localStorage key was-theme and survives navigation and later
visits. assets/theme.js holds all of that logic — the CSS side is still the single
#was-theme checkbox. privacy.html names that key explicitly, so changing it means changing
the privacy page too.
Nothing on these pages is fetched from a third-party host: Font Awesome is vendored under
assets/vendor/ with its fonts in assets/fonts/, Source Sans 3 is declared in src/app.css,
and the brand logos in the tech-stack tiles are inlined <svg> markup (sourced from
Simple Icons, CC0-licensed) rather than an icon font or image.
Adding any external <link>, <script> or webfont invalidates a claim the privacy page makes.
site/www/README.md covers the rest: the two deliberate token systems, the manual-only dark
mode, the stylesheet-relative @font-face URL, adding images, and the screenshot-diff
procedure.
cd infra
npm install
npx cdk diff
npx cdk deploy
Domains and the certificate id live in infra/lib/config.ts. The AWS account is not
committed — it comes from the ambient credentials via CDK_DEFAULT_ACCOUNT, which the
CDK CLI sets for you, so the commands above need no extra setup beyond a working profile.
Running the app outside the CDK CLI needs AWS_ACCOUNT_ID instead; an unresolved account
fails loudly rather than synthesizing an environment-agnostic stack.
The buckets, hosted zone, certificate, distributions and SES identity all carry
DeletionPolicy: Retain. cdk destroy therefore removes the stack but leaves the
infrastructure — deliberate, because the hosted zone is shared with the
hanashite stack and the bucket names are
the domain names.