Applying for jobs is a numbers game, but sending a generic resume to 100 listings rarely works. Recruiter screens and applicant tracking systems look for specific alignments with the Job Description (JD).
Manually tailoring your resume for every single job can easily eat up hours of your day.
To solve this, I built a local web application that automates this workflow:
- Scrapes the Job Description directly from a URL.
- Tailors your resume using a state-of-the-art LLM (with support for Google Gemini or NVIDIA APIs).
- Provides a side-by-side live editor to tweak the resume before saving it to your workspace.
Here's a link to the GitHub repo: resumeTailor
In this blog post, I'll walk you through how I built it using Python (FastAPI) and Vanilla Web Technologies (HTML, CSS, JS).
The Tech Stack
I kept the app lightweight, fast, and easy to run locally:
- Backend: FastAPI (Python) for API endpoints, static file mounting, and interacting with Gemini/NVIDIA.
- Frontend: HTML5, Vanilla CSS (SaaS Slate & Indigo Dark/Light theme), and vanilla JavaScript.
-
Markdown Parser:
marked.js(loaded via CDN) to render live side-by-side previews. -
LLM Integrations:
-
google-generativeai(Gemini API) - Direct
requestsconnections to NVIDIA's OpenAI-compatible API.
-
Core Architecture & Code Walkthrough
Here’s a look at how the data flows from job posting to finished resume:
[Job URL] ➔ FastAPI (/api/extract-jd) ➔ Text Job Description
│
[Base Resume] ➔ FastAPI (/api/tailor-resume) ◄──┘
│
[Gemini or NVIDIA API]
│
▼
[Interactive Editor & Preview] ➔ Saved Markdown File
1. Scraping the Job Description Text
Writing parser rules for every job board (LinkedIn, Indeed, greenhouse.io) is a nightmare. Instead, we use BeautifulSoup to strip script tags and grab cleaned textual content:
# app/main.py (excerpt)
@app.post("/api/extract-jd")
def extract_jd(req: ExtractRequest):
try:
headers = {"User-Agent": "Mozilla/5.0 ..."}
res = requests.get(req.url, headers=headers, timeout=15)
res.raise_for_status()
soup = BeautifulSoup(res.text, "html.parser")
# Strip script, style, nav, and header/footer elements
for element in soup(["script", "style", "nav", "footer", "header"]):
element.decompose()
# Get cleaned plain text
text = soup.get_text(separator="\n")
cleaned_text = "\n".join(
chunk.strip() for chunk in text.splitlines() if chunk.strip()
)
return {"text": cleaned_text}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
2. The AI Tailoring Prompt
The prompt instructs the LLM to align skills and frame experiences honestly without inventing credentials:
prompt = f"""
You are an expert technical recruiter and resume writer. Your job is to tailor the candidate's resume to the provided Job Description (JD).
Ensure that you:
1. Align the resume keywords, skills, and experience with the requirements highlighted in the JD.
2. Maintain honesty: Do not invent credentials, work experience, or certifications that are not present in the original resume.
3. Keep the output in standard Markdown format.
Job Description:
{req.job_description}
Original Resume:
{req.base_resume}
"""
3. Supporting Gemini & NVIDIA APIs
To give users options, the backend handles both Google Gemini and NVIDIA API keys. Since NVIDIA uses an OpenAI-compatible spec, we connect to it using a standard POST request:
if req.provider == "nvidia":
url = "https://integrate.api.nvidia.com/v1/chat/completions"
headers = {
"Authorization": f"Bearer {nvidia_key}",
"Content-Type": "application/json"
}
body = {
"model": req.nvidia_model or "meta/llama-3.1-70b-instruct",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2,
"max_tokens": 4096
}
res = requests.post(url, json=body, headers=headers)
text = res.json()["choices"][0]["message"]["content"]
Building the Interactive UI Workspace
A backend API is only as good as the workspace that exposes it. We built a beautiful, side-by-side dashboard using CSS Grid and structured it into two panels:
- Left Panel (Inputs): Textareas for your base resume, job description extraction URL, and optional tailoring constraints (e.g. \"Focus on my cloud DevOps skills\").
- Right Panel (Interactive Workspace): A live editor and renderer area to inspect, iterate, and save your tailored resume.
1. Side-by-Side Dual Pane Layout
To make comparison natural, the application splits the viewport into a split-screen container:
.app-container {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.25rem;
padding: 1.25rem;
}
2. Live Markdown Parser & Edit Modes
Candidates want to see a formatted preview of their resume, but they also need the ability to make manual adjustments. We built a toggleable workspace using marked.js (a fast client-side markdown compiler):
// Switching between live markdown preview and raw text editing
btnToggleView.addEventListener('click', () => {
if (currentMode === 'preview') {
currentMode = 'edit';
btnToggleView.textContent = 'Preview Mode';
resumePreview.classList.add('hidden');
resumeEditContainer.classList.remove('hidden');
} else {
currentMode = 'preview';
btnToggleView.textContent = 'Edit Mode';
tailoredContent = resumeEditor.value;
renderPreview(tailoredContent);
resumeEditContainer.classList.add('hidden');
resumePreview.classList.remove('hidden');
}
});
function renderPreview(markdownText) {
resumePreview.innerHTML = marked.parse(markdownText);
}
3. Local Storage Persistence
We store the base resume, API key preferences, and theme selections in localStorage. You can refresh the app or close your tab, and your environment remains exactly as you left it.
🚀 How to Run It Yourself
You can clone the code and start the project in under 2 minutes:
# Clone the repository
git clone https://github.com/ShriramShanbhag/resumeTailor
cd resumeTailor
# Setup virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Start local server
python -m uvicorn app.main:app --reload --port 8000
Open http://localhost:8000, enter your API key in settings, and start tailoring!
💡 Key Takeaways
- Scraping is simplified: Stripping script tags inside BeautifulSoup is a robust fallback for raw text extraction.
-
Client-side markdown rendering: Incorporating
marked.jslets you write clean markdown directly into text fields and render it in real-time. -
Root-level themes: Toggling dark/light modes on the
<html>element instead of the<body>element prevents jarring layout flashes on page reload.
What integrations would you add next? Let me know in the comments below!

Top comments (0)