Landing page for Morphic Blocks — the marketing/entry site at
https://morphicblocks.com. Built with Astro.
Docs (docs.morphicblocks.com) and the interactive playground
(playground.morphicblocks.com) live in separate repos.
bun install
bun run dev # local dev server
bun run build # static build to ./dist
bun run preview # preview the built siteEvery switchable value (site name, tagline, university, and all links) is an environment variable — nothing is hardcoded in components. Change them in one place and the whole site updates.
cp .env.example .env # then edit .env| Variable | Purpose |
|---|---|
PUBLIC_SITE_NAME |
Brand name in header/footer/title |
PUBLIC_SITE_URL |
Canonical site URL (canonical tags, sitemap, link previews) |
PUBLIC_SITE_TAGLINE |
Footer tagline |
PUBLIC_SITE_DESCRIPTION |
<meta description> |
PUBLIC_UNIVERSITY |
Footer attribution (hidden if empty) |
PUBLIC_UNIVERSITY_DE |
German name of the organization, used on /de/ |
PUBLIC_UNIVERSITY_URL |
Link target of the footer attribution |
PUBLIC_NPM_PACKAGE |
Package name in the install command |
PUBLIC_NPM_URL |
"View on npm" link |
PUBLIC_DOCS_URL |
Docs link |
PUBLIC_PLAYGROUND_URL |
Playground link |
PUBLIC_GITHUB_URL |
Repository link |
PUBLIC_IMPRINT_URL |
Imprint link |
PUBLIC_PRIVACY_URL |
Privacy link |
PUBLIC_DISCLAIMER_URL |
Disclaimer (Haftungsausschluss) link |
PUBLIC_IMPRINT_URL_EN |
English imprint, if a translated page exists |
PUBLIC_PRIVACY_URL_EN |
English privacy page, if one exists |
PUBLIC_DISCLAIMER_URL_EN |
English disclaimer, if one exists |
These are build-time PUBLIC_* vars baked into the static output. On
Cloudflare Pages, set them in the project's environment variables. Reading
happens in one place: src/config.ts.
public/ # static assets served as-is
logo.svg # PLACEHOLDER logo, replace with the real mark
favicon.svg
modes/ # one capture per mode, paired by order with i18n/ui.ts
screenshots/ # every image here appears in the slider, see below
src/
config.ts # single source for all env-driven values
env.d.ts # typed env vars
i18n/ui.ts # display strings, per language
i18n/utils.ts # language detection and locale-aware paths
data/screenshots.json # optional slider captions, keyed by filename
layouts/Base.astro
components/Header.astro, Footer.astro, Landing.astro
pages/index.astro # English (default, unprefixed)
pages/de/index.astro # German
styles/global.css # design tokens — restyle the site from here
English is the default and lives at /; German lives at /de/. The header has
a language toggle styled like the theme toggle: it shows the language it
switches to (DE on the English page) and links to the current page in that
language. With only two languages it reads as a toggle; if more are added it
cycles through them, and a list or dropdown would suit better.
All display copy sits in src/i18n/ui.ts, one entry per language. English
takes its tagline and description from the PUBLIC_SITE_TAGLINE /
PUBLIC_SITE_DESCRIPTION variables so .env stays the source for those;
everything else is literal text. src/i18n/ui.ts types the German entry
against the English one, so a missing key is a type error rather than a blank
spot on the page.
Both locales render the same components/Landing.astro, so markup and styles
are written once. To add a language: add an entry to ui.ts, list its code in
languages, add it to i18n.locales in astro.config.mjs, and create
src/pages/<code>/index.astro mirroring the German page. Code samples stay in
English.
The slider shows every image in public/screenshots/, so adding a capture
is a matter of dropping the file in. Files are ordered by name, so prefix them
(01-, 02-) when the order matters.
Captions are optional. src/data/screenshots.json maps a filename to alt text
and a caption per language; a file with no entry still appears, with a generic
alt and no caption. The slider is CSS scroll-snap with no JavaScript, so it
works with any number of images, one included: with a single image the
"scroll or swipe" hint is not rendered, and with none the section is dropped
entirely.
- The logo (
public/logo.svg) is a placeholder — replace the file to swap it everywhere. Imprint/Privacy/Disclaimerpoint whereverPUBLIC_IMPRINT_URL,PUBLIC_PRIVACY_URLandPUBLIC_DISCLAIMER_URLsay. Each has an optional_ENcounterpart used on the English pages; leave it empty when no translated page exists and both languages fall back to the base URL.- A link whose
PUBLIC_*variable is unset or empty is not rendered at all (no dead#links). A footer column with no remaining links is dropped too. - Links to other sites open in a new tab; in-site paths do not.
- The output is a plain static site, so any static host works. See Deploy for the Docker route.
The site ships as a Docker image: a bun stage builds it, an nginx stage
serves the result. Two compose files, so the same image can be run with or
without a reverse proxy in front.
deploy_docker.sh picks the compose files for you, so the only thing you
choose is which machine you are on:
cp .env.example .env # once, then edit DEPLOY_DOMAIN
./deploy_docker.sh local # build and start here, on :9350
./deploy_docker.sh prod # build and start behind Traefik
./deploy_docker.sh prod down # stop and remove
./deploy_docker.sh prod logs -f # follow the logsGiven no action it runs up -d --build, which is what you want almost every
time. Anything after the mode is handed straight to docker compose, so
ps, build --no-cache and the rest work as well. Before running it checks
that .env exists, and in prod mode that the traefik network is there,
since both failures are otherwise obscure.
The same thing without the script:
docker compose up -d --build # local
docker compose -f docker-compose.yaml \
-f docker-compose.prod.yaml up -d --build # prodThe second file adds only the Traefik router labels and the external traefik
network. It expects Traefik to be running already and attached to that
network. Traefik terminates TLS and forwards plain HTTP to the container, so
nginx listens on port 80 only and holds no certificate.
DEPLOY_DOMAIN is the one value that differs per deployment, along with
HTTP_PROXY and friends if the build host needs a proxy. Everything else
(image and container names, the loopback port, the entrypoint and network
names) is the same for every clone and is written directly in the compose
files.
Because the PUBLIC_* values are baked in at build time, changing any of them
means rebuilding: docker compose … up -d --build again.