An open source AI-powered personal site builder.
Turn your resume into a live website in seconds.
How It Works • Architecture • Tech Stack • Run Locally
This project transforms a PDF resume into a professional personal website using AI. Here's the complete user journey:
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE USER JOURNEY │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ LANDING │ │ SIGN IN │ │ UPLOAD │
│ PAGE │ ──────► │ (Clerk) │ ──────► │ PDF │
│ / │ │ │ │ /upload │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ YOUR LIVE │ │ PUBLISH │ │ AI PARSES │
│ WEBSITE │ ◄────── │ OR EDIT │ ◄────── │ RESUME │
│ /your-name │ │ /preview │ │ (Gemini) │
└──────────────┘ └──────────────┘ └──────────────┘
| Step | What Happens | Where |
|---|---|---|
| 1. Landing | User visits homepage, sees "Upload Resume" CTA | app/page.tsx |
| 2. Auth | Clerk handles sign-up/sign-in | middleware.ts |
| 3. Upload | PDF dragged/dropped, uploaded to AWS S3 | app/(private)/upload/ |
| 4. Process | Gemini AI extracts resume data into structured JSON | app/(private)/preview/ |
| 5. Preview | User sees parsed resume, can toggle Edit mode | components/resume/ |
| 6. Publish | Toggle "Live" to make site public | hooks/useUserActions.tsx |
| 7. Share | Public profile available at /{username} |
app/[username]/page.tsx |
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
USER UPLOADS PDF
│
▼
┌─────────────────┐
│ AWS S3 │ ◄──── PDF file stored here
│ (Storage) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ pdf-ts │ ◄──── Extract raw text from PDF
│ (Parser) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Gemini 2.5 │ ◄──── AI parses text into structured JSON
│ Flash Lite │ (name, skills, experience, education)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Upstash Redis │ ◄──── Stores: resume data, username mappings
│ (Database) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Public Site │ ◄──── yourname.myself.engineer
│ (Next.js) │
└─────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ REDIS KEYS │
└─────────────────────────────────────────────────────────────────────────────┘
resume:{userId}
└── {
status: 'live' | 'draft',
file: { name, url, size, bucket, key },
fileContent: "extracted PDF text...",
resumeData: {
header: { name, shortAbout, contacts, skills },
summary: "...",
workExperience: [...],
education: [...]
}
}
user:id:{userId} ──► username (get username from user ID)
user:name:{username} ──► userId (get user ID from username)
| Endpoint | Method | Purpose |
|---|---|---|
/api/resume |
GET |
Fetch user's resume data |
/api/resume |
POST |
Store/update resume data |
/api/username |
GET |
Get current user's username |
/api/username |
POST |
Update username |
/api/check-username |
POST |
Check if username is available |
/api/s3-upload |
POST |
Upload PDF to S3 |
myself.engineer/
├── app/
│ ├── page.tsx # Landing page
│ ├── [username]/
│ │ ├── page.tsx # Public profile page
│ │ └── og/route.tsx # OpenGraph image generator
│ ├── (private)/
│ │ ├── upload/ # PDF upload page
│ │ └── preview/ # Resume preview & edit page
│ └── api/
│ ├── resume/route.ts # Resume CRUD
│ ├── username/route.ts # Username management
│ ├── check-username/route.ts # Username availability
│ └── s3-upload/route.ts # File upload
│
├── components/
│ ├── resume/
│ │ ├── FullResume.tsx # Main resume display
│ │ ├── Header.tsx # Name, contacts, skills
│ │ ├── Summary.tsx # Professional summary
│ │ ├── WorkExperience.tsx # Job history
│ │ └── Education.tsx # Education section
│ ├── resume/editing/ # Edit mode components
│ │ ├── EditResume.tsx # Main edit form
│ │ ├── WorkExperienceField.tsx # Job editor
│ │ └── EducationField.tsx # Education editor
│ └── ui/ # shadcn/ui components
│
├── lib/
│ ├── resume.ts # Zod schemas for resume data
│ ├── routes.ts # Protected routes config
│ ├── server/
│ │ ├── redis.ts # Redis client
│ │ ├── redisActions.ts # Database operations
│ │ ├── scrapePdfContent.ts # PDF text extraction
│ │ └── ai/
│ │ ├── generateResumeObject.ts # AI parsing logic
│ │ └── isFileContentBad.ts # Content safety check
│
├── hooks/
│ └── useUserActions.tsx # All React Query mutations/queries
│
└── middleware.ts # Clerk auth middleware
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Frontend | React 19, Tailwind CSS |
| UI Components | shadcn/ui + Radix UI |
| State Management | TanStack Query (React Query) |
| Authentication | Clerk |
| Database | Upstash Redis |
| File Storage | AWS S3 |
| AI/LLM | Google Gemini 2.5 Flash Lite |
| PDF Processing | pdf-ts |
| Animations | Framer Motion |
| Form Validation | React Hook Form + Zod |
The core AI processing happens in lib/server/ai/generateResumeObject.ts:
┌─────────────────────────────────────────────────────────────────────────────┐
│ AI PROCESSING PIPELINE │
└─────────────────────────────────────────────────────────────────────────────┘
INPUT: Raw text extracted from PDF
"John Doe | Software Engineer | john@email.com | 5 years experience..."
│
▼
PROMPT: "Parse this resume into JSON following this schema:
- header (name, contacts, skills - max 10)
- summary
- workExperience (company, title, dates, description)
- education (school, degree, years)"
│
▼
MODEL: Gemini 2.5 Flash Lite
- Fast inference
- Structured JSON output
- Intelligent extraction
│
▼
OUTPUT: {
header: {
name: "John Doe",
shortAbout: "Software Engineer",
contacts: { email: "john@email.com", ... },
skills: ["JavaScript", "React", "Node.js", ...]
},
summary: "Experienced software engineer...",
workExperience: [...],
education: [...]
}
The core data structure (defined in lib/resume.ts):
ResumeData {
header: {
name: string
shortAbout: string
location?: string
contacts: {
website?: string
email?: string
phone?: string
twitter?: string
linkedin?: string
github?: string
}
skills: string[] // max 10
}
summary: string
workExperience: [{
company: string
link: string
location: string
contract: string
title: string
start: "YYYY-MM-DD"
end?: "YYYY-MM-DD" // null if current
description: string
}]
education: [{
school: string
degree: string
start: number // year
end: number // year
}]
}- Node.js 18+
- pnpm (recommended) or npm
- AWS account (for S3)
- Clerk account (for auth)
- Upstash account (for Redis)
- Google AI Studio account (for Gemini API)
git clone https://github.com/paramjeetn/myself.engineer.git
cd myself.engineerpnpm installCreate a .env file in the root directory:
# AWS S3 - For storing uploaded PDFs
S3_UPLOAD_REGION=ap-south-1
S3_UPLOAD_KEY=your-aws-access-key-id
S3_UPLOAD_SECRET=your-aws-secret-access-key
S3_UPLOAD_BUCKET=your-bucket-name
# Upstash Redis - For database
UPSTASH_REDIS_REST_URL=https://your-redis-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-upstash-token
# Google Gemini - For AI processing
GEMINI_API_KEY=your-gemini-api-key
# Clerk - For authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxxAWS S3 Setup
- Go to AWS Console
- Create an S3 bucket with public read access for uploaded files
- Create an IAM user with S3 permissions
- Generate access keys and add to
.env
Bucket Policy (allow public read):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}CORS Configuration:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]Upstash Redis Setup
- Go to Upstash Console
- Create a new Redis database
- Copy the REST URL and REST Token to
.env
Google Gemini Setup
- Go to Google AI Studio
- Click "Get API Key"
- Create a new API key and add to
.env
Clerk Setup
- Go to Clerk Dashboard
- Create a new application
- Copy the Publishable Key and Secret Key to
.env - Configure sign-in options (Email, Google, GitHub, etc.)
pnpm devOpen http://localhost:3000 in your browser.
- Click "Upload Resume"
- Sign in with Clerk
- Upload a PDF resume
- Watch the AI parse your resume
- Edit any sections as needed
- Click "Publish" to make your site live
- Share your URL:
localhost:3000/your-username
# Development
pnpm dev # Start dev server at localhost:3000
# Production
pnpm build # Build for production
pnpm start # Start production server
# Code Quality
pnpm lint # Run ESLint| File | What It Does |
|---|---|
lib/resume.ts |
Defines the Zod schema for all resume data |
lib/server/ai/generateResumeObject.ts |
Core AI logic - sends PDF text to Gemini |
lib/server/redisActions.ts |
All database operations (CRUD) |
hooks/useUserActions.tsx |
React Query hooks for API calls |
app/(private)/preview/client.tsx |
Main preview page with edit functionality |
components/resume/FullResume.tsx |
Renders the complete resume display |
middleware.ts |
Protects routes with Clerk auth |
"Cannot upload PDF"
- Check S3 credentials and bucket permissions
- Ensure CORS is configured on the bucket
"AI parsing failed"
- Verify Gemini API key is valid
- Check if PDF has extractable text (not scanned images)
"Username not available"
- Usernames must be unique
- Max 40 characters, alphanumeric + hyphens
"Page shows 404"
- Resume status must be "live" to be publicly visible
- Check if username mapping exists in Redis
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
MIT License - feel free to use this project for your own personal site!
Built with Next.js, Gemini AI, and lots of coffee.