A distinctive Iron Man / high-tech HUD developer portfolio and bilingual (EN/KO) MDX blog, built with Next.js 15 (App Router) + TypeScript, Tailwind CSS. Live demo: jooncco.me.
This is a GitHub template repository — all personal content lives in data/*, messages/*, and content/blog/**, so you can rebrand it without editing components. If it helps you, don't forget to drop a ⭐.
- HUD design system — cyan/red/gold palette, glow/scanline effects, light + dark themes with a persisted toggle.
- Home sections — About (jamo-by-jamo typewriter), Projects (expertise-grouped mission log), Skills (reactor clusters), Portfolio (bento grid), Competitive Programming, Experience timeline (infinite scroll), and blog highlights. Plus a
/nowpage and a/contactpage with an animated SVG HUD globe + canvas starfield (dependency-free, no WebGL). - Bilingual blog — MDX posts under
content/blog/**with per-locale files (<slug>.en.mdx/<slug>.ko.mdx), a card (bento) / list view toggle, teaser thumbnails, categories/tags, math (KaTeX), syntax highlighting, and automatic fallback to the other language with a disclaimer banner. - Competitive Programming snapshot — LeetCode + Codeforces stats served from a cached snapshot (with graceful fallbacks) instead of a live fetch on every render.
- SEO-first — per-page metadata, canonical URLs,
hreflangalternates (en/ko/x-default), OpenGraph + Twitter cards, JSON-LD (Person / WebSite / BlogPosting),sitemap.xml, androbots.txt. - Security hardening — CSP + HSTS +
X-Content-Type-Options/X-Frame-Options/Referrer-Policyheaders, env-sourced secrets, validated contact form, fail-safe external calls. - Tooling — strict TypeScript, Vitest + Testing Library, ESLint/Prettier, and a GitHub Actions CI pipeline (lint → typecheck → test → build → audit).
Next.js 15 (App Router, RSC) · React 18 · TypeScript (strict) · Tailwind CSS · next-intl (i18n) · next-mdx-remote + gray-matter (blog) · EmailJS (contact) · Vitest.
git clone https://github.com/jooncco/blog.me.git
cd blog.me
npm install
cp .env.example .env.local # then fill in the values (see below)
npm run dev # http://localhost:3000| Command | Purpose |
|---|---|
npm run dev |
Start the dev server |
npm run build |
Production build (static-optimized) |
npm start |
Serve the production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
npm test |
Run the Vitest suite |
npm run migrate:blog |
Import posts from a Jekyll source into content/blog/** |
All variables are client-exposed (NEXT_PUBLIC_*) by design for this static site, but are sourced from the environment rather than hardcoded (see lib/env.ts). Copy .env.example → .env.local and fill in:
| Variable | Description |
|---|---|
NEXT_PUBLIC_EMAILJS_SERVICE_ID |
EmailJS service id (contact form) |
NEXT_PUBLIC_EMAILJS_TEMPLATE_ID |
EmailJS template id |
NEXT_PUBLIC_EMAILJS_PUBLIC_KEY |
EmailJS public key |
NEXT_PUBLIC_CONTACT_TO_EMAIL |
Where contact messages are delivered |
NEXT_PUBLIC_CONTACT_TO_NAME |
Display name for the recipient |
NEXT_PUBLIC_LEETCODE_USERNAME |
LeetCode handle for the CP snapshot |
NEXT_PUBLIC_CODEFORCES_USERNAME |
Codeforces handle for the CP snapshot |
Missing values degrade gracefully (the contact form disables sending, CP stats fall back to data/cp-fallback.ts) — the build stays green and only logs a warning.
You should never need to touch component code. Edit these instead:
-
Section content →
data/*.tsdata/about.ts— capabilities/order, avatar pathdata/projects.ts— projects grouped by expertise domaindata/skills.ts— skill categories/toolsdata/portfolio.ts— portfolio items + categoriesdata/experience.ts— experience timelinedata/now.ts—/nowpage itemsdata/cp-fallback.ts— fallback CP stats if the snapshot is unavailable
-
All display strings →
messages/*(structure is content, copy is i18n)messages/{en,ko}.json— shell (nav, footer, theme, locale, common)messages/sections.{en,ko}.json— home sections (namespacesections)messages/blog.{en,ko}.json— blog UI (namespaceblog)
-
Blog posts →
content/blog/**- One file per locale:
my-post.en.mdx,my-post.ko.mdx(either may be omitted; the other is served with a fallback banner). - Front-matter:
title,date,lastModified?,category,tags,teaser?,ogImage?. - Images live under
public/blog/images/**and are referenced with absolute paths. - To bulk-import from a Jekyll site, run
npm run migrate:blog(seescripts/migrate-blog.ts).
- One file per locale:
-
Identity & links
- Social profiles:
components/shell/config.ts(SOCIAL_LINKS). - JSON-LD Person /
sameAs:lib/seo/jsonLd.ts(PERSON). - Canonical site URL:
lib/seo/metadata.ts(SITE_URL) andmetadataBaseinapp/[locale]/layout.tsx.
- Social profiles:
-
Static assets
public/assets/images/logo.png,public/assets/images/about/profile.jpgpublic/preview_dark.png/public/preview_light.png(the default social-share image ispreview_dark.png)app/opengraph-image.jpg,app/icon.png,app/apple-icon.png,app/favicon.ico
- SEO is centralized in
lib/seo(buildMetadata, JSON-LD builders) and consumed by every route'sgenerateMetadata;app/sitemap.tsandapp/robots.tsenumerate all locale × page/post URLs. UpdateSITE_URLto your own domain. - Security headers (CSP, HSTS,
X-Frame-Options, etc.) are set inmiddleware.ts. The CSP allowlistsselfplus EmailJS/LeetCode/Codeforces for the features that call them;'unsafe-inline'for styles is required by KaTeX and is documented inline. - Dependency audit:
npm auditruns in CI. A few advisories remain that are only fixable via breaking major upgrades of dev tooling (Vitest/Vite/esbuild) or ofnext/next-intl/next-mdx-remote; they are not exploitable in this static, no-auth, trusted-local-MDX setup and are intentionally not force-upgraded.
Deploy on Vercel with the standard Next.js pipeline. Set the environment variables above in the project settings. The build is fully static/ISR — no server or database required.
MIT License — Copyright (c) 2023 JunHa Jeong. See LICENSE.

