A fast, minimal, and fully themeable developer portfolio — configured from a single file.
📖 Complete configuration reference → CONFIGURATION.md
Warning
The built-in light theme is currently broken and produces incorrect colours across several components. Use dark or catppuccin. If you need a light theme, defining a custom one takes about 30 seconds — see Adding a New Theme.
- ✨ Features
- 🛠️ Tech Stack
- 📁 Project Structure
- 🚀 Getting Started
- ⚙️ Configuration
- 📝 Adding Content
- 🗂️ Pages Reference
- 🏗️ Architecture
- 🚢 Build & Deployment
- 🎨 Customisation Tips
- ❓ FAQ
|
🎨 Theming
📝 Blog & Projects
|
🔍 SEO
⚡ Performance & Accessibility
|
.
├── public/
│ ├── blogs/ ← Markdown files for blog posts
│ ├── projects/ ← Markdown files for projects
│ ├── img/ ← Card images (WebP recommended)
│ ├── fonts/ ← Self-hosted woff2 font files
│ ├── pfp.webp ← Profile picture (LCP element — keep < 200 KB)
│ └── favicon.*
│
├── src/
│ ├── components/
│ │ ├── ArticleLayout.tsx ← Shared blog/project layout + syntax highlighting
│ │ ├── BasePopup.tsx ← Accessible modal (focus trap, Escape, focus restore)
│ │ ├── BlogCard.tsx ← Blog list card with hover-prefetch
│ │ ├── ContactPopup.tsx ← Contact details modal
│ │ ├── ErrorBoundary.tsx ← Top-level error boundary
│ │ ├── Loading.tsx ← Route-transition loading dots
│ │ ├── ProjectCard.tsx ← Project grid / text-list card
│ │ ├── ProjectPopup.tsx ← Project preview modal
│ │ ├── SearchPage.tsx ← Generic searchable + filterable list page
│ │ ├── SEO.tsx ← Per-page Helmet SEO + JSON-LD structured data
│ │ ├── TableOfContents.tsx ← Scroll-aware collapsible TOC sidebar
│ │ └── ThemeSwitcher.tsx ← Theme picker popup
│ │
│ ├── context/
│ │ └── ThemeContext.tsx ← Theme state, CSS var injection, localStorage
│ │
│ ├── data/
│ │ ├── config.ts ← ★ PRIMARY CONFIG — start here
│ │ ├── blogs.ts ← Blog post metadata array
│ │ └── projects.ts ← Project metadata array
│ │
│ ├── pages/
│ │ ├── Home.tsx · AboutMe.tsx · BlogList.tsx · BlogPost.tsx
│ │ └── ProjectCatalogue.tsx · Project.tsx · Links.tsx · NotFound.tsx
│ │
│ ├── App.tsx ← Router setup, lazy loading, navigation loader
│ ├── main.tsx ← React root, providers, font-loader signal
│ ├── index.css ← Tailwind @theme tokens + global markdown styles
│ └── vite-env.d.ts
│
├── scripts/
│ └── generate-sitemap.ts ← Prebuild script → writes public/sitemap.xml
│
├── index.html ← Font-loader splash + zero-flash theme prevention script
├── vite.config.ts ← Custom Vite plugins: HTML transform, robots, webmanifest, theme
└── package.json
Prerequisites: Node.js 18+.
# 1. Clone
git clone https://github.com/ikrdikhit/ikrdikhit.github.io.git your_preferred_name
cd your_preferred_name
# 2. Install
npm install
# 3. Run — opens at http://localhost:3000
npm run dev| Command | What it does |
|---|---|
npm run dev |
Start dev server with HMR on port 3000 |
npm run build |
Generate sitemap → typecheck → Vite production build → dist/ |
npm run preview |
Serve the production build locally |
npm run typecheck |
tsc --noEmit without building |
npm run lint |
ESLint over all .ts / .tsx files |
npm run format |
Prettier over the whole project |
npm run clean |
rm -rf dist/ |
npm run buildautomatically runsscripts/generate-sitemap.tsvia theprebuildnpm hook, sopublic/sitemap.xmlis always up to date before every deployment.
All personalisation lives in src/data/config.ts — name, bio, themes, social links, feature flags, skills, experience. You should rarely need to touch a component for routine changes.
Here is a quick map of what lives where:
| File | Controls |
|---|---|
src/data/config.ts |
Name, bio, hero, links, themes, flags, skills, experience, stats |
src/data/blogs.ts |
Blog post metadata: slug, title, date, tags, featured |
src/data/projects.ts |
Project metadata: slug, title, URLs, type, display style |
src/components/ArticleLayout.tsx |
Syntax highlighting languages + code block colour theme |
src/index.css |
Markdown visual styles, @theme CSS variable tokens, font faces |
vite.config.ts |
Bundle splitting, Vite plugins (robots, webmanifest, theme script) |
1. Write your post at public/blogs/your-post-slug.md.
2. Register it in src/data/blogs.ts:
{
slug: 'your-post-slug', // → URL: /blogs/your-post-slug
title: 'Your Post Title',
description: 'One-sentence summary for cards and meta tags.',
image: 'card-image.webp', // filename only — lives in public/img/
markdownFile: 'your-post-slug.md',
date: '2024-06-15', // YYYY-MM-DD — used for sorting and SEO dates
tags: ['tech', 'guide'], // first 2 shown on the card; all usable as filters
featured: true, // true → also shown on the homepage
}3. Drop card-image.webp into public/img/ — 16:9 at ~800×450 px WebP is ideal.
The post will appear in the blog list, on the homepage (if featured: true), and will receive a sitemap entry on the next build automatically.
1. Write public/projects/your-project-slug.md.
2. Register it in src/data/projects.ts:
{
slug: 'your-project-slug', // → URL: /projects/your-project-slug
title: 'My Project',
description: 'What it does and why it matters.',
image: 'my-project.webp', // filename only — lives in public/img/
markdownFile: 'your-project-slug.md',
previewUrl: 'https://live-demo.com',
sourceUrl: 'https://github.com/you/repo',
type: 'project', // freeform label: 'tool', 'showcase', 'experiment', …
tags: ['react', 'typescript'],
featured: true,
displayStyle: 'image', // 'image' = thumbnail card · 'text' = minimal row
}The renderer supports full GitHub Flavored Markdown — tables, strikethrough, task lists — plus two custom behaviours:
Syntax-highlighted code fences use the language name as the fence identifier (e.g. ```typescript). If a block renders without colour, the language isn't registered yet — see CONFIGURATION.md → Syntax Highlighting for the full language list and how to add more.
Poem blocks use the special identifier poem. The content renders as a styled italic serif blockquote with a decorative pen icon instead of a code block:
```poem
Two roads diverged in a yellow wood,
And sorry I could not travel both.
```Heading levels are remapped automatically: markdown # and ## both produce <h2> in the DOM, since the article title already occupies the page's one <h1>. This keeps accessibility and SEO correct without any extra effort on your part.
| Route | Component | Description |
|---|---|---|
/ |
Home.tsx |
Hero, info strip, featured projects, featured writings |
/about |
AboutMe.tsx |
Story, skills grid, experience timeline, know-more section |
/projects |
ProjectCatalogue.tsx |
Searchable + filterable project grid with popup preview |
/projects/:id |
Project.tsx |
Full project article with live preview + source buttons |
/blogs |
BlogList.tsx |
Searchable + filterable blog list, sorted newest-first |
/blogs/:id |
BlogPost.tsx |
Full blog post with scroll progress bar + floating TOC |
/links |
Links.tsx |
Linktree-style social links page |
* |
NotFound.tsx |
404 — noindex |
Pages are added and removed based on showBlog, showProjects, and showLinks in PERSONAL_INFO. Setting showBlog: false removes both /blogs and /blogs/:id from the router entirely — they don't just hide, they cease to exist as routes.
The system is built around one hard constraint: no colour flash, ever, even on hard refresh.
1 — Build time: vite.config.ts's themeScriptPlugin serialises the entire THEMES map into a minimal inline <script> and injects it at <head-prepend>, before any stylesheet or React bundle.
2 — Before first paint: that script reads localStorage.getItem('theme'), finds the matching CSS variable map, and calls document.documentElement.style.setProperty() for all tokens synchronously. The browser applies these before drawing a single pixel.
3 — After React mounts: ThemeProvider reads the same key and calls applyTheme() on change. Since the DOM already has the correct values, this is a no-op on first load and a seamless transition on user action.
4 — CSS consumption: src/index.css declares an @theme block mapping Tailwind utilities (bg-base, text-t-primary, border-border, …) to CSS variables. No component ever hardcodes a colour.
SEO.tsx wraps react-helmet-async and injects a complete set of meta tags per page in a single call: <title>, description, keywords, canonical link, full Open Graph tags, Twitter Card tags, article publication dates, and three JSON-LD blocks (Person, WebSite, and BlogPosting for articles). vite.config.ts emits robots.txt and sitemap.xml automatically on every production build.
DM Sans and Playfair Display are self-hosted as .woff2 files with <link rel="preload"> hints in index.html, so they download at maximum network priority before the CSS is parsed. The splash screen (#font-loader) uses a two-gate system:
Gate 1 → document.fonts.ready (fonts downloaded and parsed)
Gate 2 → window.__onAppMounted() (React has fully rendered)
Both must open before the overlay fades. A 4-second safety timeout ensures it always clears regardless of network conditions.
ArticleLayout.tsx is the shared engine for both BlogPost and Project pages. It fetches the markdown file at navigation time (keeping it out of the JS bundle), computes a reading-time estimate at 200 wpm, extracts headings outside fenced blocks to build the TOC, drives the scroll-progress bar via useScroll + useSpring, and fires history.replaceState after content loads so Firefox Reader Mode correctly detects the article body.
TableOfContents.tsx tracks the active heading via getBoundingClientRect().top on scroll, renders a collapsible heading hierarchy with animated carets, moves a floating accent-coloured dot to the active item with smooth CSS transitions, auto-scrolls the TOC panel to keep the active entry in view, and persists sidebar open/closed state to localStorage. Desktop gets a fixed left sidebar; mobile gets a floating button that opens a modal overlay.
npm run build
# prebuild → scripts/generate-sitemap.ts runs first
# then → tsc + vite build → dist/The only deployment requirement is a SPA fallback — the host must serve index.html for all paths, otherwise direct navigation to /blogs/my-post will return a server-level 404.
Changing fonts — Replace the .woff2 files in public/fonts/, update the @font-face rules in src/index.css, and update the <link rel="preload"> hints in index.html. Full procedure in CONFIGURATION.md → Fonts.
Changing the profile picture — Replace public/pfp.webp, or update profilePicture in PERSONAL_INFO and the preload hint in index.html. Size and format guidance in CONFIGURATION.md → Profile Picture.
Adding or changing a theme — Add a new object to THEMES in config.ts and set ACTIVE_THEME to its key. What each of the 11 tokens controls is explained token-by-token in CONFIGURATION.md → Themes In Depth. The built-in light theme is currently broken — define a new one instead.
Adding syntax highlighting for a new language — If a code fence renders without colour, the language isn't registered. Import it and call SyntaxHighlighter.registerLanguage() in ArticleLayout.tsx. Full step-by-step guide, complete list of registered languages, and instructions for swapping the colour theme at CONFIGURATION.md → Syntax Highlighting.
Scroll progress bar colour — Currently reads from THEME.accent, which is a static snapshot of the default theme. To make it react to the user's active theme, replace THEME.accent with useTheme().theme.accent in ArticleLayout.tsx.
Why are markdown files fetched at runtime instead of bundled?
Bundling markdown into JS would bloat the initial chunk with content the user may never read. Fetching at navigation time means each article only loads when visited, and the browser HTTP cache handles everything after the first load. Cards also pre-fetch their markdown on hover, so in practice the file is already cached by the time the user clicks.
Why does the sitemap script use tsx instead of compiling first?
tsx executes TypeScript directly, which lets the script import blogs.ts and projects.ts with full type safety using the exact same data the app uses. This rules out a whole class of bugs (a regex failing silently on a multi-line object, for instance) and means the sitemap is always a perfect, current reflection of the app's content.
How do I add a completely custom page?
Create src/pages/Gallery.tsx, add a lazy import in App.tsx (const Gallery = lazy(() => import('./pages/Gallery'))), add a <Route path="/gallery" element={<Gallery />} /> inside the <Routes> block, and optionally add the path to STATIC_ROUTES in scripts/generate-sitemap.ts so it gets a sitemap entry.
The TOC isn't showing on my blog post. Why?
The TOC only renders when the markdown contains at least one h1, h2, or h3. Also check that your headings aren't inside a fenced code block — the heading extractor in ArticleLayout.tsx explicitly strips content inside fences before scanning.
A code block is rendering without syntax highlighting. Why?
The language identifier in your fence hasn't been registered with SyntaxHighlighter. Only languages that are explicitly imported and registered in ArticleLayout.tsx will highlight — all others render as plain text silently. See CONFIGURATION.md → Syntax Highlighting for the complete list of registered languages and how to add more.
How do I disable the loading splash screen?
Remove the #font-loader <div> and its accompanying inline <script> from index.html. The app continues to work — you just lose the guarantee of correct fonts on the very first paint.
Made with ♥ by Dikhit Das
If this saved you time, a ⭐ goes a long way — it genuinely helps others find it.