A personal portfolio with an editorial, archival aesthetic — "Specimen / Manifest." Warm paper-and-ink, a characterful serif (Fraunces) for warmth, tracked monospace (JetBrains Mono) for the infrastructure texture, and one restrained ember accent.
Built with vanilla HTML, CSS, and JavaScript — no framework, no build step.
Open index.html and it runs. The whole thing is a handful of static files, which
is the right tool for a portfolio: sub-second LCP, nothing to hydrate, full control
over every pixel of craft.
portfolio/
├── index.html # semantic structure (shells that JS fills from data.js)
├── css/styles.css # the entire design system, one organised file
├── js/
│ ├── data.js # ← ALL your content lives here. Edit this, nothing else.
│ └── app.js # rendering + interactions (no need to touch this)
└── README.md
To change any content, edit js/data.js only. Never touch the
HTML, CSS, or app.js. The layout reads from the DATA object and renders itself.
Every section is commented in that file.
No build, no dependencies. Either:
-
Just open it — double-click
index.html. (Photos load from Cloudinary over the network, so they need an internet connection.) -
Or serve it (nicer for development):
python3 -m http.server 8000 # then visit http://localhost:8000
The gallery is driven by Cloudinary's free tier. You never
write an image transform by hand — the site builds optimised, responsive URLs
(f_auto, q_auto, a blurred low-quality placeholder, and srcset) for you.
One-time setup
-
Create a free Cloudinary account.
-
On the Dashboard (top-left) copy your Cloud name.
-
In
js/data.js, set it:photography: { cloudName: "your-cloud-name", // ← paste it here ... }
Adding a photo
-
Upload the image in Cloudinary (Media Library). Put your photos in a folder, e.g.
portfolio/. -
Copy the photo's Public ID (it includes the folder, e.g.
portfolio/derwent-water). -
Add an entry to the
photosarray:{ publicId: "portfolio/derwent-water", ratio: 1.5, caption: "Derwent Water, dawn" },
ratioiswidth ÷ height. It reserves the right space so the page never jumps while images load. Common values:- Landscape 3:2 →
1.5 - Portrait 2:3 →
0.667 - Square →
1
- Landscape 3:2 →
captionis optional; it shows on hover and in the lightbox.
Removing a photo — delete its line from the photos array. Show up to ~10 for
the best composition.
No Cloudinary yet? You can drop a full image URL straight into
publicId(anything starting withhttp) and it's used as-is — so you can wire up the content first and switch to Cloudinary later.
Also set your Instagram link in the same photography block.
All in js/data.js:
| What | Where | Notes |
|---|---|---|
| Hero headline & deck | hero |
Wrap a phrase in {{ }} to colour it with the accent. |
| Projects | work[] |
The entry with featured: true is rendered large as the centrepiece. manifest is the small spec-sheet; stack are the tech tags; links are the buttons. |
| About copy | about.paragraphs[] |
Each string is one paragraph. |
| "Field log" margin notes | about.notes[] |
{ k: "label", v: "value" }. |
| Reading shelves | reading.current[], reading.finished[] |
note is optional. |
| Name / role / socials / email | site |
Used across the masthead, connect, and footer. |
| Live status location & timezone | site.timezone, site.locationLabel |
Any IANA timezone string. |
It's static, so anywhere that serves files works.
git add .
git commit -m "Portfolio"
git push origin mainThen in the repo: Settings → Pages → Build and deployment → Source: Deploy from a
branch → Branch: main / root → Save. Your site appears at
https://<username>.github.io/<repo>/ within a minute or two.
Go to app.netlify.com/drop and drag the project folder in. Done. (No build command, no publish directory needed — it's all static.)
Connect the repo. Build command: none. Publish/Output directory: the project
root (.).
- Type — Fraunces (display serif: warmth, the reader-of-books feel),
Inter (UI), JetBrains Mono (metadata, the infrastructure texture). Loaded with
display=swapso text paints instantly on a serif fallback while the webfont arrives — that keeps LCP low. - Colour — a warm "paper" light theme by default (most dev portfolios are dark;
this one isn't) with a considered warm-dark counterpart. One ember accent, used
sparingly. Toggle in the masthead; choice is remembered, and it honours your
system
prefers-color-schemeon first visit. - Motion — restrained and purposeful: clip-reveal headlines, lift-and-fade on
scroll, a viewfinder cursor over photographs, an ambient "control plane" status
strip (a quiet nod to the day job). All of it is removed automatically under
prefers-reduced-motion. - Performance — no framework or build; lazy-loaded images with Cloudinary
blur-up placeholders and
srcset; aspect ratios reserved up front so there's no layout shift. - Accessibility — semantic landmarks, a skip link, keyboard-operable menu and lightbox (arrows / Esc), visible focus styles, and high contrast on both themes.
Built with vanilla HTML · CSS · JS.