Personal portfolio and blog for Mirazul Islam Nahid — a fully static SvelteKit site deployed to Cloudflare Pages.
Every visual element comes from the neo-retro design system (a shadcn-svelte component registry). Nothing on this site uses UI from outside that registry.
- Projects are pulled from the GitHub API at build time — no client-side API calls, no rate limits for visitors.
- Blog posts are markdown files in this repo. Publishing a post is a
git push. - Contact is links only. No forms, no backend, no database.
| Tool | Version | Notes |
|---|---|---|
| Node.js | 22 or newer | pinned in .nvmrc |
| npm | ships with Node | |
| Git | any | |
| Cloudflare account | — | for Pages hosting + the domain |
git clone <your-repo-url> portfolio
cd portfolio
npm install
npm run devOpen http://localhost:5173. The dev server hot-reloads on save.
src/
├── content/blog/ ← your blog posts (.md) — this is the whole CMS
│ └── building-cicd-pipeline-nuxt-aws.md
├── lib/
│ ├── config.ts ← ALL your personal info lives here. Edit this file.
│ ├── blog.ts ← reads the markdown files, parses frontmatter
│ ├── blog.server.ts ← renders markdown → HTML with Shiki (build-time only)
│ ├── projects.server.ts ← the build-time GitHub fetch (shared by home + projects)
│ ├── data/
│ │ └── repos.fallback.json ← offline snapshot of your GitHub repos
│ └── components/
│ ├── site/ ← band / section-title — the page vocabulary
│ └── ui/ ← neo-retro components. Don't hand-edit these.
├── routes/
│ ├── +layout.svelte ← header, nav, footer, fonts, theme wrapper
│ ├── app.css ← fonts + sticker utilities
│ ├── +page.svelte ← home / about
│ ├── projects/
│ │ └── +page.svelte
│ ├── blog/
│ │ ├── +page.svelte ← post list, search + tag filter
│ │ └── [slug]/ ← individual post pages
│ ├── resume/+page.svelte ← experience, education, skills, contact
│ └── +error.svelte ← 404 page
static/
└── Mirazul_Islam_CV.pdf ← the CV served by the resume page
Everything personal lives in src/lib/config.ts. You should never need to edit a component.
| Export | What it controls |
|---|---|
site |
Your name, role, tagline, location, avatar, and the site's canonical URL |
about |
The paragraphs in the About section on the home page |
socials |
The link buttons in the header footer, home page and resume |
skills |
The skill cards (grouped) on the home and resume pages |
experience / education |
The resume timeline cards |
projectRepos |
Which GitHub repos appear on /projects, in order |
projectOverrides |
Custom titles/descriptions/tags for repos |
nav |
The header navigation links |
Everything in here is real content — there are no placeholders left. Edit
config.tsand every page that reads from it updates on the next build.
Any color field must be a neo-retro palette name:
red, green, yellow, blue, magenta, cyan, white, or the bright-* variants.
Don't use hex codes — the palette changes with the active theme, hex codes don't.
The site is light-only. The theme picker lists all 127 light themes and defaults to
gruvbox-light. To change the default, edit the first entry of FavoriteThemes in
src/lib/components/ui/style/favorite-themes.ts.
-
Open
src/lib/config.ts. -
Add the repository name to
projectRepos:export const projectRepos: string[] = [ 'Verso2.0', 'ScrollSense', 'MyNewProject' // ← added ];
-
Optionally give it a nicer title or description in
projectOverrides(useful because several repos have no description set on GitHub):MyNewProject: { title: 'My New Project', blurb: 'What it does, in one sentence.', highlights: ['Rust', 'CLI'], color: 'cyan', // palette name — the card's background featured: true // gives it a full-width card }
-
Commit, then
npm run deploy. The build pulls the fresh star count and language automatically.
If a name in projectRepos doesn't exist on your GitHub, the build logs a warning and skips
it rather than failing.
Create a new file in src/content/blog/. The filename becomes the URL —
my-first-post.md → /blog/my-first-post.
---
title: The title shown on the page
date: 2026-09-20
summary: One sentence used on the blog index and in link previews.
tags: [svelte, machine-learning]
draft: false
---
Your post body, in normal markdown.
## A heading
Regular paragraphs, **bold**, _italic_, [links](https://example.com), lists, tables
and blockquotes all work.
Fenced code blocks are syntax-highlighted with Shiki, in both light and dark themes:
open a fence with three backticks and a language (`python`, `ts`, `bash`, `svelte`, …).| Field | Required | Notes |
|---|---|---|
title |
yes | Falls back to the filename |
date |
yes | YYYY-MM-DD. Posts sort newest-first by this |
summary |
recommended | Shown on the index and in <meta description> |
tags |
no | [a, b] — become clickable filters on the blog index |
draft |
no | true = visible in npm run dev, excluded from production builds |
Reading time is calculated automatically. Then:
git add src/content/blog/my-first-post.md
git commit -m "post: my first post"
git pushThen npm run deploy to push the rebuilt site to Cloudflare.
| Command | What it does |
|---|---|
npm run dev |
Dev server at http://localhost:5173, hot reload, drafts visible |
npm run build |
Prerenders the whole site into build/ |
npm run preview |
Serves the built build/ folder locally, exactly as deployed |
npm run check |
TypeScript + Svelte type checking |
npm run lint |
Prettier check + ESLint |
npm run format |
Auto-format the codebase |
npm run deploy |
Builds, then uploads build/ to Cloudflare Pages via Wrangler |
| Variable | Required | Purpose |
|---|---|---|
GITHUB_TOKEN |
Optional locally, recommended on Cloudflare | Raises the GitHub API rate limit during the build from 60/hr to 5000/hr |
Without it the build still works — it just uses the unauthenticated API, and falls back to the committed snapshot if it gets rate-limited.
To create one:
- GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens.
- Generate new token. Repository access: Public repositories (read-only). No extra permissions are needed — public repo metadata is all this site reads.
- Copy the token.
- In Cloudflare Pages: your project → Settings → Variables and Secrets → add
GITHUB_TOKENas a secret for the Production (and Preview) environment.
Locally, if you want it: GITHUB_TOKEN=ghp_xxx npm run build.
Never commit the token.
The site is a fully prerendered static bundle, deployed by direct upload with Wrangler —
no Git connection on the Cloudflare side. The target project is mirazulislamnahid, declared
in wrangler.jsonc; deploying overwrites what is currently live there.
npx wrangler login # opens a browser, grants Wrangler access to your account
npx wrangler whoami # confirm it picked the right accountIf you would rather not use the browser login (CI, headless box), create an API token with the Cloudflare Pages: Edit permission and export it instead:
export CLOUDFLARE_API_TOKEN=...npm run deployThat runs npm run build and then wrangler pages deploy, uploading build/ to the
mirazulislamnahid project. Only changed files are re-uploaded, so repeat deploys are fast.
Wrangler prints the deployment URL when it finishes.
Custom domains already attached to the project stay attached — direct upload replaces the files, not the project settings.
static/_headerssets caching:/_app/immutable/*is fingerprinted so it is cached for a year; everything else must revalidate, so a redeploy shows up immediately.GITHUB_TOKENis read from your shell duringnpm run build. Without it the build falls back to unauthenticated GitHub API calls (60/hr), which is plenty for six repos, and tosrc/lib/data/repos.fallback.jsonif the API is unreachable.site.urlinsrc/lib/config.tsdrives canonical URLs and social previews — keep it in sync with the domain you actually serve.- To deploy a throwaway build without touching production, use
npx wrangler pages deploy --branch preview, which publishes to a preview URL instead.
Project stars, descriptions and languages are baked in at build time, so they only change when the site rebuilds. Any of these will do it:
-
Push any commit (including a new blog post).
-
Click Retry deployment in the Cloudflare Pages dashboard.
-
Use a Deploy Hook — Pages project → Settings → Builds → Deploy hooks → Add. You get a URL you can call any time:
curl -X POST "https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/<your-hook-id>"To refresh daily, put that curl in a Cloudflare Worker with a Cron Trigger (
0 3 * * *for 03:00 UTC), or in a GitHub Actions scheduled workflow.
src/lib/data/repos.fallback.json is used only when the GitHub API is unreachable during a
build. Regenerate it occasionally so the fallback isn't stale:
curl -s "https://api.github.com/users/TheRealNightmare/repos?per_page=100&sort=pushed" \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const k=['name','description','language','stargazers_count','html_url','homepage','pushed_at'];console.log(JSON.stringify(JSON.parse(s).map(r=>Object.fromEntries(k.map(x=>[x,r[x]]))),null,2))})" \
> src/lib/data/repos.fallback.jsonProjects page shows a yellow "cached snapshot" banner
The GitHub API failed during the build. Usually a rate limit — add GITHUB_TOKEN in
Cloudflare. The page still renders from the committed snapshot, so the site never breaks.
A project is missing from /projects
The repo name in projectRepos must match GitHub exactly, including capitalisation
(Verso2.0, not verso2.0). Check the build log for
"<name>" is in projectRepos but was not found on GitHub.
A blog post doesn't appear
Check draft: false in the frontmatter — drafts only show in npm run dev. Also confirm the
file is directly in src/content/blog/ with a .md extension (subfolders aren't scanned).
A deep link like /blog/some-post 404s after a hard refresh
Every post is prerendered to a real HTML file, so this shouldn't happen. If you added a post
and only see it in dev, you haven't rebuilt. Confirm build/blog/some-post.html exists after
npm run build.
Cloudflare build fails with a Node version error
Pages reads .nvmrc (Node 22). If it's ignoring it, set a NODE_VERSION environment
variable to 22 in the Pages project settings.
npm run build fails on a link 404
The prerenderer is strict: a link to a file that doesn't exist fails the build. Most likely
static/Mirazul_Islam_CV.pdf was deleted or renamed. Put the file back, or update the
href and download on the CV button in src/routes/resume/+page.svelte to match.
The registry has ~58 components; only the ones this site uses are installed. To add another:
npx shadcn-svelte@1.1.1 add --no-depsPick from the list (or pass a name, e.g. add carousel). Files land in
src/lib/components/ui/. If the CLI reports missing peer dependencies, install those with
npm i — the message lists them.
The full component reference lives in .agent/skills/neo-retro-components/SKILL.md
(props for every component) and .agent/skills/neo-retro-common/SKILL.md (colors, types).
Keep everything on-system. Don't introduce another UI library, hand-rolled components that duplicate registry ones, or hard-coded hex colors — the whole point of the registry is that all 40+ themes in the theme picker keep working.
Code: MIT. Blog post content: © Mirazul Islam Nahid.
The look is a deliberate "arcade / zine" take on neo-retro. If you edit pages, keep to these:
- Pages are stacks of full-bleed bands, not one centered column. Use
<Band color="blue">…</Band>fromsrc/lib/components/site/and alternate the colors down the page — that alternation is what stops it looking like a template. - Band grounds are flat colour, no background texture. Keep them that way; patterned grounds were tried and made the pages look busy.
- Never color text with the same palette color as its band. A
text-redheading on acolor="red"band vanishes. Usetext-on-clrand accent with thebright-*ramp. - Card grids:
class="sticker-grid"on the container,class="sticker"on each card, for the alternating tilt and hover pop. Addsticker-flatto any full-width card. - Motion is optional: everything animated is disabled under
prefers-reduced-motion, and the hero intro is applied after mount so the page is complete without JavaScript. - Fonts are self-hosted via fontsource and wired to
--font-display/--font-sans/--font-monoinsrc/routes/app.css. Swap the packages there to change them. - There's a Konami code easter egg in
+layout.svelte(↑↑↓↓←→←→BA) that tilts everything harder. The footer hints at it.