Every developer portfolio has the same problem: you build it once, get excited, and then it slowly goes stale. You ship five new projects over the next year and your portfolio still shows the same three repos from launch day.
So I built one that fixes itself. Push a new repo to GitHub, and it shows up on my portfolio automatically — with an AI-written description, not just the raw GitHub blurb. Connect LinkedIn, and my experience and education populate themselves too.
This is the full breakdown of how it works, why I made the choices I did, and how you can build your own version.
The problem with every portfolio I'd seen
Most portfolio templates fall into one of two categories:
- Static templates — you hardcode your projects in a JSON file or directly in JSX. The moment you ship something new, you're back in your code editor updating an array.
- GitHub-pulling tools that already exist — a few projects auto-list your repos, but they just dump the raw GitHub description with zero polish. No context, no tech stack analysis, nothing that actually sells the project.
I wanted something in between: automatic, but smart enough to write a good description on its own.
The architecture
The core idea is simple:
GitHub API → fetch your repos + READMEs
↓
AI (Groq/Llama) → reads each README → writes a clean 2-sentence summary
↓
Next.js → renders it all as a styled portfolio page
↓
Vercel → redeploys automatically whenever you push
Nothing here needs a database. The portfolio fetches fresh data from GitHub on every page load (cached for an hour via Next.js's revalidate), so there's no sync job, no cron, no webhook server to maintain.
Step 1 — Pulling GitHub data
The GitHub REST API doesn't require OAuth for public repo data — a personal access token with public_repo scope is enough:
const res = await fetch(
`https://api.github.com/users/${process.env.GITHUB_USERNAME}/repos?sort=updated`,
{
headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
next: { revalidate: 3600 }, // refetch every hour
}
);
const repos = await res.json();
That one call gives you repo name, description, language, stars, forks, and the URL. The revalidate option is doing a lot of work here — Next.js's App Router caches this fetch at the edge and automatically refreshes it every hour, so the page stays fast without needing a separate sync process.
Step 2 — Making the AI write better descriptions than GitHub does
GitHub's repo description field is whatever one-liner you typed when you created the repo — often outdated or just "my project lol." I wanted the portfolio to actually explain what each project does.
For every repo, I fetch the raw README and feed it to an LLM:
async function getAIDescription(repo) {
const readmeRes = await fetch(
`https://api.github.com/repos/${username}/${repo.name}/readme`,
{ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github.raw" } }
);
const readme = await readmeRes.text();
const completion = await groq.chat.completions.create({
model: "llama-3.3-70b-versatile",
messages: [{
role: "user",
content: `Write a clear, impressive 2-sentence portfolio description for this GitHub project.
Repo: ${repo.name}
Language: ${repo.language}
README: ${readme.slice(0, 500)}
Only output the description.`,
}],
max_tokens: 120,
});
return completion.choices[0]?.message?.content;
}
Why Groq instead of OpenAI or Claude
I tried a few providers before settling on Groq:
| Provider | Free Tier | Speed | Why I didn't use it |
|---|---|---|---|
| OpenAI | Trial credits only | Fast | Needs a card eventually |
| Anthropic Claude | $5 credit on signup | Fast | Same — eventually needs billing |
| Google Gemini | Free tier | Fast | Model names change constantly; kept hitting 404s |
| Groq (Llama 3.3 70B) | Fully free, no card | Fastest | This is what I shipped with |
Groq's free tier is genuinely free — no credit card required at signup, and the inference speed is the fastest I've used for a hosted LLM. For a task like "summarize this README in two sentences," you don't need a frontier model — Llama 3.3 70B handles it well, and the response comes back in well under a second.
Step 3 — LinkedIn sync without touching LinkedIn's API
This was the trickiest part. LinkedIn's official API is locked down — you need partner access to read profile data programmatically, which isn't realistic for a side project.
The workaround: LinkedIn lets every user export their own data as a ZIP file from their account settings. Inside that ZIP are CSV files — Positions.csv, Education.csv, Skills.csv — containing your full work history.
So instead of an API integration, I built a simple upload flow:
User exports their LinkedIn data (official LinkedIn feature)
↓
Uploads the ZIP to my site
↓
Server unzips it, parses the CSVs
↓
Saves structured JSON
↓
Portfolio reads the JSON and renders it
The parsing itself is straightforward once you have the ZIP open:
import JSZip from "jszip";
const zip = await JSZip.loadAsync(buffer);
const posFile = zip.file("Positions.csv");
const text = await posFile.async("text");
const rows = parseCSV(text); // simple CSV → array of objects
const experience = rows.map(r => ({
role: r["Title"],
company: r["Company Name"],
startDate: r["Started On"],
endDate: r["Finished On"] || "Present",
}));
No OAuth, no API keys, no rate limits — and it's fully compliant since you're only ever handling your own exported data, through LinkedIn's own official export tool.
Step 4 — The design
Functionally complete is one thing; a portfolio also has to look like something people remember. I went with a cyberpunk/glassmorphism aesthetic — dark background, glowing purple-to-cyan gradients, a particle network animation in the background, and an anime-style character illustration in the hero section.
A few details that made it feel less like a template:
- Live clock in the nav bar, just for the "this is a system" feel
-
Cursor glow that follows your mouse using a radial gradient positioned via
mousemove - Canvas-based particle network — 80 small particles connected by faint lines when they're close to each other, redrawn every frame
-
Glassmorphism cards —
backdrop-filter: blur(12px)with low-opacity backgrounds, so content behind them softly bleeds through
None of this is complicated individually, but together it makes the page feel alive instead of static.
What I'd do differently next time
- Cache AI descriptions — right now every page load regenerates descriptions for repos that haven't changed. Storing them keyed by the repo's last-updated timestamp would cut down on unnecessary API calls.
-
Add a webhook instead of polling — using GitHub's webhook system to trigger an instant rebuild on push, rather than relying on the hourly
revalidatewindow. - Support more LinkedIn fields — certifications and recommendations are parsed but not yet displayed.
Try it yourself
The whole project is open source. Clone it, drop in your own GitHub token and Groq API key, and you have a self-updating portfolio in about ten minutes.
git clone https://github.com/Harshi-dhamu/auto-portfolio.git
cd auto-portfolio
npm install
npm run dev
Full setup instructions, environment variables, and the AI provider comparison table are in the README.
If you build on top of it or have ideas for what's missing, I'd genuinely like to hear about it — PRs and issues are open.
Top comments (0)