A full-stack, ultra-modern, multilingual portfolio website featuring a built-in headless CMS and robust security architecture - powered by Supabase and deployed on Vercel.
Every piece of content is admin-editable. No code changes needed to update your portfolio.
This is not a static portfolio template. It's a production-grade Content Management System (CMS) disguised as a premium developer portfolio. Everything you see on the public-facing website is dynamically fetched from a Supabase PostgreSQL database and fully manageable through a deeply secured admin dashboard at /admin.
Key idea: Clone it, connect your Supabase, and you have a fully functional portfolio site with an admin panel - no backend code to write.
A complete CMS dashboard with categorized sidebar navigation for managing every piece of your portfolio:
| Category | What You Can Manage |
|---|---|
| Profile & Identity | Name, role, tagline, bio, profile photo, favorite quote, custom stats |
| Portfolio Content | Projects/works (with live links, GitHub, tags, multi-image galleries) and blog posts |
| Resume Data | Experience, education, skills, languages, certifications, activities |
| Content Linking | Relationally link any work or blog to skills, experiences, education, certs, and more |
| Configuration | Social links, section reordering, visibility toggles, maintenance mode |
Every field supports 4 languages (EN, TR, DE, ES) with an intuitive language tab switcher. Sections can be individually hidden/shown and reordered with drag-style up/down controls.
The CMS features a powerful relational linking engine that lets you connect content across all sections:
- Works & Blogs can be linked to Experiences, Education, Skills, Languages, Activities, and Certifications
- Multi-select skill binding - assign multiple skill categories to a single work or blog via interactive pill-tag checkboxes
- Bidirectional display - linked content appears as interactive badges on both the source item and the target section's homepage card
- All links are managed through a clean "Link Related Items" accordion in the admin forms
Certifications on the homepage are fully interactive:
- Click any certification card to open a spring-animated detail modal
- The modal displays the certification name, issuer, date, credential link, and icon
- Related skills (via junction table) are rendered as tags
- Linked projects and blog posts appear as clickable navigation cards inside the modal
Blog posts support optional featured images (cover photos):
- Add image URLs via the admin panel using the smart "Recent Images" selector that remembers previously used URLs
- Images render as aspect-ratio cover photos on both the blog card grid and the expanded blog modal
- Fully responsive with smooth hover-scale animations
- Real-time language switching without page reloads
- 4 languages supported out of the box: English, Turkish, German, Spanish
- Translations are managed per-field in the admin panel - not in JSON files
- Static UI strings use a typed
translations.tsdictionary
- Staggered fade-in animations on every section via a reusable
<FadeIn />component - Apple-style Dock navigation with magnetic hover magnification effect
- Spring-animated modals for blog posts, project details, and certifications
- Maintenance & Error Screens - Enhanced maintenance mode with randomized dynamic media (cat macros!) and local 401 Unauthorized fallbacks
- Smooth page transitions powered by Framer Motion
Seamless theme switching via next-themes with system preference detection. All components are designed for both modes.
A custom-built contribution heatmap that fetches your real GitHub activity through a serverless API route (/api/github), using the GitHub GraphQL API. Includes interactive tooltips with contribution counts per day.
Auto-generated RSS feed at /feed.xml for blog syndication, built as a Next.js Route Handler.
Unlike typical starter templates, this project implements a rigorous, multi-layered security model preventing unauthorized access, bot attacks, and database abuse:
- Cloudflare Turnstile CAPTCHA: Invisible algorithm-based bot protection on the login page.
- Server-Side IP Rate Limiting: Intelligent brute-force protection (max 5 attempts per 15 mins).
- Next.js Middleware Protection: HTTP-Only, Secure cookies enforce strict access control to all
/adminroutes. - Strict Content-Security-Policy (CSP): Robust headers mitigating XSS, Clickjacking, and framing attacks.
- Row Level Security (RLS) on every table - write access is locked to your specific user UUID.
- Sign-up disabled - no one can create accounts on your Supabase instance.
- PostgreSQL Triggers & Limits: Database resource quotas prevent spam creation and URL validation constraints block cross-site exploits.
Real-time visual feedback for all admin operations with a polished, non-intrusive notification system:
- Success/Error Feedback: Instant confirmation for saves, updates, and deletions
- Fixed Positioning: Bottom-right corner with 3-second auto-dismiss
- Solid Design: Non-transparent backgrounds matching your theme
- Multilingual: Toast messages in your selected language
A full-featured WYSIWYG markdown editor for blog posts with live preview and comprehensive help:
- Rich Toolbar: Bold, italic, headings, lists, links, inline code, code blocks, tables, horizontal rules
- Live Preview: Side-by-side editing mode to see rendered output instantly
- Built-in Help Guide: Comprehensive syntax reference with examples for all supported formats
- XSS Protection: Automatic sanitization via
rehype-sanitize - Full Multilingual Support: Available in EN/TR/DE/ES tabs for both create and edit modes
Manage multiple contact emails directly from the admin panel with smart organization:
- Multiple Email Types: Personal, School, Work, Club, and custom labels
- Multilingual Labels: Each email label can be translated (label_tr, label_de, label_es)
- Integrated Contact Modal: Reorganized dock navigation with Contact button opening a popup
- Smart Display: Shows both social links and contact emails in the modal
- One-Click Copy: Copy email addresses with visual feedback
Additional security measures beyond the enterprise-grade foundation:
- Input Validation: Zod schemas validate all API inputs and form submissions
- URL Sanitization:
sanitizeUrlhelper prevents XSS via malicious URLs - Safe Image Loading: All image URLs validated before rendering (info.tsx, blog, etc.)
- Production Logging: Console logs hidden in production, visible only in development
- Type Safety: Centralized TypeScript interfaces prevent runtime errors
- Eliminated IIFEs: Replaced immediately-invoked functions with
useMemofor better performance - Pre-calculated Maps: Related items cached to avoid repeated filtering on every render
- Type Safety: Specific types (Project[], Blog[]) instead of any[] for faster operations
- Memory Management: Proper cleanup in admin-error-context prevents memory leaks
- Server/Client component splitting with Next.js App Router
- Vercel Speed Insights & Analytics integration
- Optimized image loading with
next/image - Edge-deployed on Vercel's global CDN
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16 |
| UI Library | React | 19 |
| Language | TypeScript | 5 |
| Styling | Tailwind CSS + tailwind-merge + clsx |
4 |
| Animations | Framer Motion | 12 |
| Database & Auth | Supabase (PostgreSQL + Auth + RLS) | Latest |
| Security | Cloudflare Turnstile, Next.js Middleware, CSP | Latest |
| Validation | Zod | 3.23+ |
| Notifications | Sonner | Latest |
| Markdown | react-markdown + rehype-sanitize | Latest |
| Hosting | Vercel | - |
.
βββ supabase_schema.sql # Full database schema with RLS policies
βββ .env.example # Environment variable template
β
βββ src/
βββ app/ # Next.js App Router
β βββ page.tsx # π Homepage - assembles all sections dynamically
β βββ admin/
β β βββ login/ # π Auth gate (email/password + Turnstile)
β β βββ page.tsx # π Admin dashboard (full CMS)
β βββ blog/ # π Blog feed with animated modals + featured images
β βββ works/ # πΌ Portfolio feed with animated modals + gallery
β βββ credits/ # π Tech credits & security details
β βββ api/github/ # π GitHub GraphQL API route handler
β βββ feed.xml/ # π‘ RSS feed generator
β
βββ components/
β βββ admin/
β β βββ admin-tabs.tsx # CMS forms: About, Skills, CRUD, Social, Layout, Contact Emails
β β βββ markdown-editor.tsx # π Rich markdown editor with toolbar & preview
β βββ home/
β β βββ info.tsx # Hero section (name, photo, tagline)
β β βββ about.tsx # Bio + custom stats
β β βββ skills.tsx # Skill categories grid + linked works/blogs
β β βββ profile-sections.tsx # Experience, Education, Activities, Certs (modals)
β β βββ github-contribution.tsx # Live GitHub heatmap
β β βββ contact-form.tsx # Contact section
β βββ motion/
β β βββ fade-in.tsx # Reusable staggered animation wrapper
β βββ navigation/
β β βββ dock.tsx # Apple-style magnetic dock navigation
β βββ theme-provider.tsx # Dark/light mode provider
β
βββ config/
β βββ locales/ # Static UI translations (EN, TR, DE, ES)
β βββ translations.ts # Typed i18n dictionary
β
βββ context/
β βββ language-context.tsx # Global language provider with getLocalized()
β βββ site-data-context.tsx # Supabase data cache (fetches all tables once)
β
βββ types/
β βββ index.ts # π Centralized TypeScript interfaces (Project, Blog, etc.)
β
βββ lib/
βββ supabase.ts # Supabase client singleton
βββ utils.ts # cn() + π sanitizeUrl(), isValidEmail(), stripHtml()
The Supabase database consists of 14 tables, all with Row Level Security enabled:
| Table | Purpose | Key Fields |
|---|---|---|
about_me |
Profile information | name, role, bio, photo, stats, quote + translations |
skill_categories |
Grouped skills | title, skills (JSON array) + translations |
experiences |
Work history | title, company, dates, description + translations |
educations |
Academic history | university, degree, major, dates |
languages |
Language proficiencies | name, level (dropdown) |
activities |
Leadership & extracurriculars | organization, role, description + translations |
certifications |
Professional certifications | name, issuer, date, link, icon + translations |
certification_skills |
Junction: certs β skills | certification_id, skill_category_id |
projects |
Portfolio works | title, description, links, tags, image, linked_* IDs + translations |
project_images |
Multi-image gallery per project | project_id, image_url, order_index |
blogs |
Blog posts (Markdown) | title, excerpt, content, content_tr/de/es, date, image_url, linked_* IDs |
social_links |
Dock navigation links | platform, URL |
contact_emails |
Contact email addresses | label, email, label_tr/de/es, order_index |
section_order |
Homepage section ordering | section_id, order_index |
erDiagram
projects ||--o| experiences : "linked_experience_id"
projects ||--o| educations : "linked_education_id"
projects ||--o| languages : "linked_language_id"
projects ||--o| activities : "linked_activity_id"
projects ||--o| certifications : "linked_certification_id"
projects }o--o{ skill_categories : "linked_skill_category_ids"
projects ||--|{ project_images : "has"
blogs ||--o| projects : "linked_project_id"
blogs ||--o| experiences : "linked_experience_id"
blogs ||--o| educations : "linked_education_id"
blogs ||--o| languages : "linked_language_id"
blogs ||--o| activities : "linked_activity_id"
blogs ||--o| certifications : "linked_certification_id"
blogs }o--o{ skill_categories : "linked_skill_category_ids"
certifications ||--|{ certification_skills : "has"
skill_categories ||--|{ certification_skills : "has"
projects { uuid id PK }
blogs { uuid id PK }
experiences { uuid id PK }
educations { uuid id PK }
skill_categories { uuid id PK }
languages { uuid id PK }
activities { uuid id PK }
certifications { uuid id PK }
project_images { uuid id PK }
certification_skills { uuid certification_id FK }
about_me { uuid id PK }
social_links { uuid id PK }
section_order { text section_id PK }
Both projects and blogs tables support relational linking:
| Column | Type | Links To |
|---|---|---|
linked_experience_id |
uuid |
experiences |
linked_education_id |
uuid |
educations |
linked_skill_category_ids |
uuid[] |
skill_categories (multiple) |
linked_language_id |
uuid |
languages |
linked_activity_id |
uuid |
activities |
linked_certification_id |
uuid |
certifications |
linked_project_id |
uuid |
projects (blogs only) |
Every content table supports 4-language translations (EN, TR, DE, ES) with dedicated columns per language.
flowchart LR
subgraph Browser["π₯οΈ Browser"]
A["Public Visitor"]
C["Admin User"]
end
subgraph Edge["β‘ Vercel Edge"]
B["Next.js App Router"]
D["Admin CMS Dashboard"]
G["SiteDataContext Cache"]
end
subgraph Supa["ποΈ Supabase"]
I["PostgreSQL + RLS"]
N["GitHub GraphQL API"]
end
A --> B --> G -->|"SELECT"| I
C --> D -->|"INSERT / UPDATE / DELETE"| I
B -->|"/api/github"| N
flowchart TD
A["π Public Visitor"] -->|"SELECT only"| B["Supabase RLS"]
C["π Admin"] -->|"Cloudflare Turnstile + Password"| D["Supabase Auth"]
D -->|"Authenticated Session"| B
B --> E{"Operation Type"}
E -->|"SELECT"| F["β
Allow - Public read"]
E -->|"INSERT / UPDATE / DELETE"| G{"auth.uid matches admin?"}
G -->|"Yes β
"| H["Allow Write"]
G -->|"No β"| I["Block - 403"]
H --> J["CHECK Constraints"]
J --> K["β
Data Saved"]
| Layer | Protection |
|---|---|
| RLS Policies | All tables have RLS enabled. Write operations (INSERT, UPDATE, DELETE) are locked to your specific user UUID. |
| Input Validation | Zod schemas validate all API inputs and form data, preventing malformed requests. |
| Turnstile Protection | Invisible captchas prevent automated script brute-forcing against the Next.js login API logic. |
| Rate Limiting | Track failed authentication requests by IP. Lock out abusive attackers instantly. |
| Authentication | Supabase Email Auth. Sign-ups are disabled so no one else can create an account. Admin API routes are protected by Next.js middleware using secure HttpOnly cookies. |
| SQL Injection | Impossible. Supabase uses PostgREST which parameterizes all queries automatically. |
| XSS Protection | rehype-sanitize sanitizes all markdown content. sanitizeUrl() validates all user-generated URLs before rendering. |
| Data Validation | CHECK constraints enforce URL format validation and content length limits to prevent cross-site payload execution and DoS. |
| CSP Headers | Mitigate XSS, script-injections, and unapproved external embeds centrally in next.config.ts. |
| Safe Logging | Console error logs hidden in production (NODE_ENV check), visible only in development. |
- Node.js v18 or higher
- A free Supabase account
- A free Cloudflare account (for Turnstile)
- A GitHub Personal Access Token (classic, with
read:userscope) for the contribution graph - A Vercel account (for deployment, optional for local dev)
git clone https://github.com/batuhd/batuhd.github.io.git
cd batuhd.github.io
npm installcp .env.example .env.localFill in your .env.local:
# Supabase - get these from your Supabase project dashboard β Settings β API
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6...
# Cloudflare Turnstile - create a site key via Cloudflare dashboard
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x0000000000000000000000
# GitHub - create at https://github.com/settings/tokens (classic token, read:user scope)
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxThis is the most important step. The database uses Row Level Security (RLS) to ensure only you can modify data.
Go to Supabase Dashboard β Authentication β Users β Add user and create your account with email and password. This is the account you'll use to log into the /admin dashboard.
Open SQL Editor in Supabase and run:
SELECT id, email FROM auth.users;You'll see a result like this:
| id | |
|---|---|
a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
[email protected] |
Copy the id value. This is your User UUID - it uniquely identifies your admin account.
Open supabase_schema.sql in your editor and find & replace all occurrences of:
YOUR-USER-UUID-HERE
with the UUID you copied. For example:
- auth.uid() = 'YOUR-USER-UUID-HERE'::uuid
+ auth.uid() = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'::uuidπ‘ Tip: Use
Ctrl+H(Windows) orCmd+H(Mac) to replace all occurrences at once.
Copy the entire contents of your modified supabase_schema.sql and paste it into Supabase SQL Editor β New Query. Click Run. This creates all tables, enables RLS, and sets up your security policies.
Go to Authentication β Settings β Auth Providers β Email and toggle off "Allow new users to sign up".
Go to Authentication β Settings β Auth Providers β Email and enable "Cloudflare Turnstile". Paste your Secret Key there.
β οΈ Critical: Do NOT skip steps 3.3 and 3.5. Without them, anyone who discovers your Supabase URL could potentially create an account and modify your portfolio data.
npm run dev| URL | Description |
|---|---|
http://localhost:3000 |
Your portfolio website |
http://localhost:3000/admin |
CMS admin dashboard |
http://localhost:3000/blog |
Blog feed |
http://localhost:3000/works |
Portfolio works feed |
http://localhost:3000/credits |
Tech credits page |
http://localhost:3000/feed.xml |
RSS feed |
- Push your code to GitHub
- Import the repository in Vercel
- Add the same environment variables from
.env.localto your Vercel project settings - Deploy - Vercel will automatically build and serve your site
| What | Where | How |
|---|---|---|
| All content | /admin dashboard |
Log in and edit everything from the UI |
| Colors & theme | src/app/globals.css |
Modify CSS custom properties |
| Static text | src/config/translations.ts |
Edit/add translation keys |
| Navigation links | Admin β Social Links | Add/remove/reorder from the dashboard |
| Section order | Admin β Page Layout | Drag sections up/down or hide them |
| Profile photo | Admin β About Me | Toggle visibility on/off with checkbox |
| Favorite quote | Admin β About Me | Toggle visibility on/off with checkbox |
| Maintenance mode | Admin β Page Layout | Toggle to temporarily block public access (displays dynamic random images) |
| Link content | Admin β Works/Blogs edit | Use "Link Related Items" accordion |
This project is licensed under the CC BY-NC 4.0 (Creative Commons Attribution-NonCommercial 4.0).
You can freely use, modify, share, and deploy this project for personal or educational purposes.
You cannot sell it, monetize it, or use it for any commercial purpose.
See the LICENSE file for details.
Crafted with passion by Batuhan
If you found this useful, consider giving it a β


