Personal site for Jonas Chukwuemeka, founder of Weblanda, full-stack developer and biologist growing into data engineering. It's written for three audiences: clients, recruiters and academic admissions.
Live at https://jonas-chukwuemeka.vercel.app (deployed on Vercel). The planned domain is jonaschukwuemeka.com.
- Homepage: hero, tech strip, work (Weblanda, its stores, Formtified), data lab, experience timeline, about, latest blog posts, newsletter and contact.
- Case studies at
/work/[slug], one per project, with a screenshot gallery and lightbox. - Contact: a form that saves enquiries and emails them, a Cal.com booking pop-up, WhatsApp, and an "Ask Jay" FAQ chat (pre-written answers, no AI).
- Blog at
/blog: posts written in the dashboard with a rich text editor (headings, links, images, code with syntax highlighting, YouTube embeds), tag filters, table of contents, share links, related posts, an RSS feed at/blog/rss.xml, and drafts, scheduling and previews. - Newsletter with double opt-in and one-click unsubscribe.
- Dashboard at
/admin: first-party analytics, enquiries, blog posts and media library, subscribers, newsletter sending, chat logs, bookings, CV upload and settings. - SEO: metadata, generated share images,
sitemap.xml,robots.txt, JSON-LD (Person,WebSite, oneCreativeWorkper case study,Blog, andBlogPostingwith breadcrumbs per post) and a web app manifest.
| Area | Tools |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack, Cache Components), React 19, TypeScript (strict) |
| Styling and motion | Tailwind CSS v4, Motion, CSS scroll reveals and view transitions |
| Content | Typed data files in src/content (no CMS) |
| Data and email | Neon Postgres with Drizzle ORM, Resend |
| Booking | Cal.com embed and webhook |
| Hosting | Vercel, with Vercel Analytics and Speed Insights alongside the site's own first-party stats |
You need Node.js 20.9 or newer (22 recommended), npm and Git.
git clone https://github.com/jonascodes15/jonas_chukwuemeka_portfolio.git
cd jonas_chukwuemeka_portfolio
npm install
cp .env.example .env.local # on Windows PowerShell: Copy-Item .env.example .env.local
npm run devOpen http://localhost:3000. Pages render with no variables set; features that need a service (forms, dashboard, emails) explain what's missing until you add it.
| Command | What it does |
|---|---|
npm run dev |
Dev server with hot reload |
npm run build |
Production build (what Vercel runs) |
npm start |
Serve the production build locally (run build first) |
npm run lint / npm run typecheck |
ESLint / route types plus TypeScript |
npm run format |
Format every file with Prettier |
npm run db:generate |
Write a migration after changing src/server/db/schema.ts |
npm run db:migrate |
Apply migrations to the database in DATABASE_URL |
npm run db:studio |
Browse the database in Drizzle Studio |
npm run hash-password |
Make the bcrypt hash for the dashboard password |
npm run image -- <source> <folder> <name> |
Make AVIF and WebP versions of a screenshot (see below) |
npm run icons |
Rebuild the favicon files from the JC mark |
Before pushing, run npm run typecheck, npm run lint and npm run build. If all three pass locally, the Vercel build should too.
Every variable is documented in .env.example. Locally they go in .env.local, which git never commits. On Vercel, add them under Project > Settings > Environment Variables, then redeploy so the build picks them up. Variables starting with NEXT_PUBLIC_ are visible to visitors, so never put a secret in one.
To generate a random secret (for SESSION_SECRET, IP_HASH_SALT and CAL_WEBHOOK_SECRET, each different):
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"NEXT_PUBLIC_SITE_URL is the address used in canonical links, the sitemap and share images, with no trailing slash. Set it to the live address (today https://jonas-chukwuemeka.vercel.app) and change it when the domain moves.
- Create a project at https://neon.tech. Under Connect, turn on connection pooling and copy the connection string into
DATABASE_URL. - Create the tables once:
npm run db:migrate. Run it again whenever a new migration is added todrizzle/.
- Create an API key at https://resend.com and put it in
RESEND_API_KEY. - Verify a domain you own under Domains (a subdomain such as
mail.weblanda.comis fine) by adding the DNS records Resend shows. Avercel.appaddress can't be verified. - Set
RESEND_FROM_EMAIL, e.g.Jonas Chukwuemeka <hello@mail.weblanda.com>, andCONTACT_TO_EMAIL(where enquiries go).
- Set
ADMIN_EMAIL. - Run
npm run hash-passwordand follow the prompt. Put the hash inADMIN_PASSWORD_HASH. In.env.localevery$must be written as\$(the script prints that version); on Vercel paste the plain hash. - Set
SESSION_SECRETandIP_HASH_SALTto two different random strings. - Sign in at
/admin/login. Settings shows which services are connected.
- Set
NEXT_PUBLIC_CAL_LINKto the booking page, e.g.https://cal.com/jonaschukwuemeka/introcall. Every "Book a call" button then opens it in a pop-up. - To list bookings in the dashboard: in Cal.com go to Settings > Developer > Webhooks, add
https://<your-site>/api/cal/webhook, choose Booking created, rescheduled and cancelled, and set a secret. Put the same secret inCAL_WEBHOOK_SECRET.
The site is verified with the file public/google4b64cbd418b46fe1.html; keep it. Submit sitemap.xml in Search Console after each new page type ships.
- In Vercel, choose Add New > Project and import this repository. Keep the defaults: Vercel detects Next.js and runs
npm run build. - Add the environment variables from the section above.
- Under the project's Analytics and Speed Insights tabs, click Enable. Until you do, their scripts return 404 (harmless, but noisy in the browser console).
- Deploy. Each push to
maindeploys to production; other branches get preview URLs. - To use a custom domain, add it under Settings > Domains, add the DNS records Vercel shows at your registrar, then update
NEXT_PUBLIC_SITE_URLand redeploy.
- Open the failed deployment and read the Build Logs. The first red line is usually the cause.
- Run
npm run buildlocally on the same commit. If it fails locally too, fix that first. - If it builds locally but not on Vercel, the usual causes are file name case (Vercel runs on Linux, where
Hero.tsxandhero.tsxdiffer), a file you didn't commit (git status), or a missing environment variable. - Use Redeploy from the deployment's menu to retry a build that failed for a temporary reason.
All copy lives in src/content, so most edits never touch a component.
| File | Controls |
|---|---|
site.ts |
Name, hero text, About paragraphs, contact details, CTA wording, budget ranges |
socials.ts |
LinkedIn, GitHub, X and Facebook links (also used in the JSON-LD) |
projects.ts |
Weblanda and Formtified, their screenshots, and the stores built on Weblanda |
dataProjects.ts |
Data projects, metrics and architecture diagrams |
caseStudies.ts |
The copy on each /work/[slug] page |
experience.ts |
Work history, newest first |
education.ts |
Degree, training and the publication |
stack.ts |
Tools in the scrolling strip |
faq.ts |
Chat answers and the keywords that match typed questions |
Rules for site copy:
- Only real, checkable facts and numbers. If something isn't confirmed, leave a
// TODO:comment instead of guessing. - No em dashes, and no "Hire me" or "Book a session" wording.
The availability label in the hero and chat, the downloadable CV and blog posts are changed from the dashboard rather than in code. The copy around the blog (headings, empty state, author line) is under blog in site.ts.
Go to /admin/blog and choose New post. Drafts save themselves after a pause in typing; Ctrl+S saves too. Paste or drop images straight into the text, or use the image button for the media library or a link. Select text for quick formatting, or press Ctrl+K for a link (the dialog suggests pages on this site). Set a future publish date to schedule a post: it goes live within the hour after that time. Preview shows a draft exactly as readers will see it. Uploaded images are resized, stripped of location data and stored as WebP in the database, so no storage service is needed.
- Data project: append an object to
dataProjectsindataProjects.ts. It needs a GitHub link and either screenshots or anarchitecturediagram (nodeson a grid withcol,rowand optionalcolSpan;edgesbetween node ids,control: truefor a dashed orchestration line). - Case study: add an entry keyed by the project's slug in
caseStudies.ts, then setcaseStudy: caseStudies["<slug>"]on the project. The page, its share image, the homepage button and the sitemap entry appear automatically.
Images are compressed once, ahead of time, and served as AVIF with a WebP fallback at several widths.
- Save the full-size screenshot outside
public/. - Run
npm run image -- "C:\path\to\screenshot.png" public/projects/<project-slug> <short-name>. For phone screenshots add a maximum width:... <short-name> 800. - Paste the printed object into the project's
screenshotsarray, then fill inalt,captionanddevice("desktop"or"phone").
The first screenshot is the cover image. Don't commit the full-size sources.
src/
app/
(site)/ Public pages: homepage, /work/[slug] case studies, /blog, newsletter pages
admin/ Dashboard (protected by src/proxy.ts)
api/ Analytics beacon, newsletter unsubscribe, Cal.com webhook
cv/ Serves the uploaded CV, or the bundled one
media/ Serves blog images uploaded in the dashboard
opengraph-image.tsx, sitemap.ts, robots.ts, manifest.ts
components/
layout/ Nav, footer, logo, in-page section links
sections/ Homepage sections
case-study/ Case study hero, sections, gallery and lightbox
chat/ "Ask Jay" launcher and chat panel
booking/ Opens Cal.com links in the pop-up
motion/ Reveal, Enter, counters, page transitions
seo/ JSON-LD
admin/, ui/, icons/
content/ All site copy and project data
lib/ FAQ matching, Cal.com loader, share image helpers
server/ Database, email, auth, rate limiting, server actions
assets/fonts/ Fonts for the generated share images (SIL Open Font License)
drizzle/ Database migrations
public/ Photos, logos, screenshots, bundled CV, Search Console file
scripts/ Image optimisation, password hashing, icon building
- Security headers in
next.config.ts, including a Content-Security-Policy that only allows this site, Cal.com and Vercel's analytics script host. Adding another embed or script means adding its origin there. - Forms are validated with Zod on the server, rate limited and protected by a honeypot field.
- The dashboard uses a bcrypt-checked password and a signed, httpOnly session cookie;
src/proxy.tsguards every/adminroute. - The site's own analytics never store raw IP addresses, only salted hashes that rotate daily. Bots and the owner's own visits are filtered out.