Project
alexanderk.dev
This website
★ ---- commits--updated --
This website. Built with Astro into plain HTML, CSS and JavaScript, and hosted on Cloudflare.
- Astro
- HTML
- CSS
- JavaScript
- GitHub API
- Cloudflare
README
Loading README from GitHub…
Walkthrough
How this site works, from the files you see in the repo to the live GitHub numbers. You'll want to know basic HTML, CSS and JavaScript going in.
1. The big picture
What your browser downloads is plain files: HTML pages, one CSS file and a few JavaScript files. There's no framework (like React) running in the browser.
The pages are written with Astro, a static site generator. The header, footer and page shell live in one layout file, and each project and research post is an entry in a small data file. When the site is built, Astro stitches those together into one finished HTML file per page. So a change to the header is one edit, not twenty.
The one exception is a small script that runs on Cloudflare and keeps the GitHub numbers up to date. Here's how a visit works:
your browser ──asks for a page──► Cloudflare ──► the HTML, CSS and JS files
│
└─asks for /data/github.json─► Cloudflare script ──► saved GitHub data
▲
every 5 minutes: asks GitHub for fresh numbers2. The files
| File | What it does |
|---|---|
src/layouts/ | The page shell (head, header, footer) and the project and post layouts |
src/pages/ | One file per page: About, Projects, Research, Stats |
src/data/ | The list of projects and research posts that the pages and cards are built from |
public/css/style.css | All the styling for every page |
public/js/theme.js | Light and dark mode |
public/js/gh.js, github.js, stats.js | Fill in the GitHub numbers, READMEs and the Stats page |
worker/index.js | The Cloudflare script that fetches from GitHub |
wrangler.jsonc, astro.config.mjs | Cloudflare and Astro settings |
3. Styling and dark mode
Every color, space and font size is defined once, as a CSS variable at the top of style.css. The rest of the stylesheet only uses the variables. Dark mode is then just a second set of values for the same variables:
:root { --bg: #eeeeec; --text: #2e3436; } /* light */
:root.dark { --bg: #2e3436; --text: #eeeeec; } /* dark */
body { background: var(--bg); color: var(--text); }Switching themes means adding or removing one class, dark, on the page. theme.js does that. It runs before the page is drawn, so there's no flash of the wrong theme. It uses the theme you picked with the toggle if you've picked one, and otherwise follows your device's light or dark setting:
const saved = localStorage.getItem("theme"); // "dark", "light" or nothing
const deviceIsDark = matchMedia("(prefers-color-scheme: dark)").matches;
const theme = saved || (deviceIsDark ? "dark" : "light");
document.documentElement.classList.toggle("dark", theme === "dark");The colors come from Tango, the palette used by GNOME and most Linux terminals.
4. Live GitHub numbers
The stars, commits and languages on the project cards come from GitHub. The obvious approach is to have each visitor's browser ask GitHub directly. The catch: GitHub only allows 60 requests an hour per visitor without a key, and the Stats page alone needs dozens. Reload a few times and the numbers stop loading.
The fix is to have one program ask GitHub on everyone's behalf and share the answer. That program is a Cloudflare Worker, a small script that runs on Cloudflare's servers. Cloudflare starts it every 5 minutes:
// wrangler.jsonc
"triggers": { "crons": ["*/5 * * * *"] }Each run, the Worker:
- Asks GitHub for every repo's stars, commits, languages and last update, all in one request (a GraphQL query).
- Gets recent activity, plus any README that changed since last time.
- Saves everything as one JSON file in Cloudflare's storage (KV), but only if something changed.
When a browser asks for /data/github.json, the Worker hands back that saved file. Every other address is a normal file:
async fetch(request, env) {
const url = new URL(request.url);
// any other address: a normal file
if (url.pathname !== "/data/github.json") return env.ASSETS.fetch(request);
// the data file: the saved GitHub data
const body = await env.GITHUB_DATA.get("github.json");
return new Response(body, { headers: { "Content-Type": "application/json" } });
}On the pages, each number starts as a placeholder that says which repo and which number it wants:
<b data-gh="amkashyap1220/SwitchToYoutube" data-field="stars">--</b>github.js finds every placeholder, looks the repo up in the data file, and fills in the number:
document.querySelectorAll("[data-gh]").forEach((el) => {
repo(el.dataset.gh).then((data) => {
el.textContent = data[el.dataset.field];
});
});Now visitors' browsers never talk to GitHub at all, and the numbers are never more than about 5 minutes old.
5. The Stats page is a small data pipeline
stats.js is organized like an ETL job, the extract-transform-load pattern data engineers use everywhere:
- Extract: read the repos, languages and activity from the data file.
- Transform: add up totals, like bytes of code per language across every repo, or the busiest weekday.
- Load: draw the results: the number tiles, the language bars and the activity summary.
For example, here's the transform step that totals up languages:
function languageTotals(repos) {
const totals = {};
repos.forEach((r) => {
Object.entries(r.languages).forEach(([lang, bytes]) => {
totals[lang] = totals[lang] || { name: lang, bytes: 0, repos: 0 };
totals[lang].bytes += bytes; // how much code
totals[lang].repos += 1; // how many repos use it
});
});
return Object.values(totals);
}6. Shipping changes
The repo is connected to Cloudflare, so every push to main deploys the site automatically. Cloudflare runs npx wrangler deploy, which builds the pages with Astro and uploads them along with the Worker. It's live in under a minute.
Changes don't go straight to main, though. Each one goes on its own branch with a pull request. Cloudflare builds a preview of the branch, and it gets merged once it looks right:
git checkout -b add-new-project # new branch
# ...make changes...
git push -u origin add-new-project # Cloudflare builds a preview
# open a pull request, check the preview, merge → live