📰 IT NachrichtenDie Kult-Kamera der 2000er jetzt refurbished bei Back Market(17.09.2026 um 07:30 Uhr)
📰 IT NachrichtenGute Nachrichten für Android-Nutzer: Google liefert mehr Freiheit(17.09.2026 um 07:42 Uhr)
📰 IT NachrichtenDie Kult-Kamera der 2000er jetzt refurbished bei Back Market(17.09.2026 um 07:30 Uhr)
📰 IT NachrichtenGute Nachrichten für Android-Nutzer: Google liefert mehr Freiheit(17.09.2026 um 07:42 Uhr)
🔧 Programmierung 🕛 vor 6 Monaten 5 Min Lesezeit
0

How I Made My Portfolio Infinitely Extendable with 4 Lines of JSON

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Adding a new project to my portfolio takes 30 seconds. No component changes. No new routes. No layout adjustments. Just 4 lines of JSON and an image dropped into a folder.



I got tired of the typical portfolio maintenance cycle: build something cool, then spend an hour wiring it into your portfolio site, adjusting layouts, making sure the new card doesn't break the grid. So I designed mine to be completely data-driven from the start.



Here's how it works.






The Single Source of Truth



Every project on my portfolio lives in one file: projects.js. Each project is an object with exactly 4 fields:




CODE
{
id: 'crystal-index',
title: 'Crystal Index',
techStack: 'TypeScript, Next.js, Prisma, SQL, GPT4, React 3 Fiber',
info: 'Custom CMS for cataloguing crystals with structured filters for colour, chakra, and properties, and GPT-4-generated descriptions.',
}






That's it. Four lines. The entire portfolio renders from an array of these objects.






The ID Does Triple Duty



The id field is where the design gets interesting. It's not just an identifier. It serves three purposes simultaneously:



1. GitHub link path. The portfolio constructs repository URLs by appending the ID to a base GitHub URL. crystal-index becomes https://github.com/sammii-hk/crystal-index. No separate URL field needed.



2. Image filename lookup. An auto-generated image map resolves the ID to the correct image file and extension. crystal-index maps to /assets/images/crystal-index.jpg. No manual image path configuration.



3. React key. The ID serves as the unique key when mapping over the array. Standard React pattern, but it comes free because the ID already exists for the other two purposes.



One field, three jobs. This means there's no redundant data, no fields that can fall out of sync, and no opportunity for a GitHub link to point to the wrong repo because the image and the link derive from the same source.



For projects under a GitHub organisation, the ID includes the org path: unicorn-poo/succulent. The image utility handles this by splitting on / and taking the last segment for the filename lookup, while the full path constructs the correct GitHub URL.






Auto-Generated Image Map



I didn't want to manually track whether each project screenshot is a .jpg or .png. So I wrote a build script that scans the images directory and generates a JSON map:




CODE
import { readdir, writeFile } from 'fs/promises';
import { join } from 'path';

const imagesDir = join(process.cwd(), 'public', 'assets', 'images');

async function generateImageMap() {
const files = await readdir(imagesDir);
const imageMap = {};

files.forEach(file => {
if (file === 'sammii.png') return;
const name = file.replace(/\.(jpg|png)$/, '');
const ext = file.endsWith('.png') ? 'png' : 'jpg';
if (!imageMap[name]) imageMap[name] = ext;
});

const outputPath = join(process.cwd(), 'app', 'common', 'utils', 'image-map.json');
await writeFile(outputPath, JSON.stringify(imageMap, null, 2));
}

generateImageMap();






The output is a simple lookup:




CODE
{
"crystal-index": "jpg",
"lunary": "png",
"succulent": "png",
"day-lite": "jpg"
}






Then a utility function resolves any project ID to its full image path:




CODE
import imageMapData from './image-map.json';
const imageMap = imageMapData;

export const getImagePath = (projectId) => {
const projectBaseId = projectId.split('/').pop() || projectId;
const extension = imageMap[projectBaseId] || 'jpg';
return `/assets/images/${projectBaseId}.${extension}`;
};






Drop an image in the folder, run the script, done. The portfolio picks it up automatically.






Zero Component Changes



The portfolio has two completely different view modes: a responsive grid with click-to-expand modals, and a full-screen vertical carousel. Both consume the exact same projects array.



The grid view maps over the array and renders cards:




CODE
{projects.map((project) => (
<div key={project.id} onClick={() => setSelectedProject(project)}>
<ProjectItem project={project} isGrid={true} />
</div>
))}






The carousel view maps over the same array and renders full-width slides:




CODE
<Carousel
items={projects}
renderItem={({ item, isSnapPoint }) => (
<CarouselItem key={item.id} isSnapPoint={isSnapPoint}>
<ProjectItem project={item} />
</CarouselItem>
)}
/>






ProjectItem accepts an isGrid prop that switches between compact card layout and expanded layout. Same component, same data, two presentations. Adding a project to the array means it automatically appears in both views with no additional work.






The 30-Second Workflow



When I finish a new project, here's what I actually do:




  1. Take a screenshot

  2. Drop it in /public/assets/images/ as project-name.png

  3. Run node scripts/generate-image-map.mjs

  4. Add 4 lines to projects.js

  5. Push to GitHub



The portfolio rebuilds and the new project appears in both views, with the correct image, the correct GitHub link, and the correct layout. Five steps, thirty seconds, zero component files touched.






The Philosophy Behind It



This isn't just about saving time on a portfolio. It's a pattern I use everywhere.



My astrology app Lunary has a grimoire with over 2,000 articles. They're stored as structured data and rendered through shared components. Adding a new article about a crystal or a tarot card doesn't require touching any UI code.



My publishing tool Spellcast manages multiple social media accounts and platforms. Account configurations are data objects. Adding a new platform means adding to the config, not rebuilding the interface.



The principle is always the same: separate data from presentation. Make the data structure do the heavy lifting. Keep the components generic enough that they never need to know about specific content.



If you're spending more time wiring new content into your portfolio than building the projects themselves, you've got the architecture backwards. Make your portfolio work for you, not the other way around.






I'm Sammii, founder of or check out the code on GitHub.

Vollständiger Original-Artikel
Den kompletten Beitrag mit allen Details direkt auf dev.to lesen.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Built a PPL-aware ALPC enumerator because standard handle duplication was leaving blind spots in the attack surface
1 Quelle
SindriKit V2.0.0 (C framework to decouple technique logic from execution mechanics)
1 Quelle
Heap-Buffer-Überlauf im Discord-Backend
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How I Made My Portfolio Infinitely Extendable with 4 Lines of JSON

Thematisch verwandte Begriffe: Made, Portfolio, Infinitely, Extendable · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...