ehsan.blog
~/blog/how-to-keep-all-site-content-in-one-typed-data-file — zsh
cat how-to-keep-all-site-content-in-one-typed-data-file.md

How to keep all your site content in one typed data file

·4 min read

Early on, my portfolio had my job history hardcoded inside the experience component, my skills inside the skills component, my email in three different places. Then I changed jobs and had to hunt through JSX to update it — and I missed a spot. The fix was to pull all the content out of the components and into one file. Now every component renders from that file, and updating the site means editing data, not markup.

One file, plain exported objects

Everything lives in src/data/portfolio.ts as plain exported constants. No classes, no framework — just typed data:

ts
// src/data/portfolio.ts
export const personal = {
  name: "EHSAN SHAHID",
  title: "Full Stack Developer",
  location: "Lahore, Punjab, Pakistan",
  email: "ehsanshahid787@gmail.com",
  website: "developerehsan.com",
  githubUrl: "https://github.com/developerehsan",
  summary:
    "Full Stack Developer skilled in frontend (React, Next.js, Expo, Astro) and backend (Node.js, Electron) development...",
}

personal is a single object. My email, name, and links each exist in exactly one place. Change the email here and every mailto: link, footer, and contact button across the site updates at once.

Structured, repeating content is an array of objects. Experience, for example:

ts
// src/data/portfolio.ts
export const experience = [
  {
    company: "Platas",
    role: "Frontend Lead Engineer",
    period: "July 2025 — Present",
    status: "active",
    stack: ["Bash", "React", "TypeScript", "Next.js", "Firebase"],
    highlights: [
      "Spearheaded end-to-end delivery of a staging platform in 3 weeks",
      "Architected a highly scalable Next.js application",
      // ...
    ],
  },
  // ...more jobs, same shape
]

Every entry has the identical shape — company, role, period, status, stack, highlights. That consistency is what lets a component .map() over the whole array and render each job the same way.

Skills are split two ways, because the UI shows them two ways:

ts
// src/data/portfolio.ts
export const skills = {
  languages: ["JavaScript (ES6+)", "TypeScript", "HTML5", "Bash" /* ... */],
  frontend: ["React.js", "Next.js", "Redux", "Tailwind CSS" /* ... */],
  backend: ["Node.js", "Express.js", "Fastify", "GraphQL" /* ... */],
  // mobile, databases, services, devops, testing...
}

export const skillLevels = [
  { name: "React / Next.js", level: 95 },
  { name: "TypeScript", level: 92 },
  // ...
]

skills is grouped tags by category; skillLevels is the numbers for the animated proficiency bars. Same subject, two shapes, both data.

How components consume it

A component imports what it needs and maps over it. Nothing about my career lives in the JSX anymore:

tsx
// src/components/portfolio/SkillsSection.tsx
import { skills, skillLevels } from "@/data/portfolio"

{skillLevels.map(({ name, level }) => (
  <div key={name}>
    <span>{name}</span>
    <Bar value={level} />
  </div>
))}
tsx
// src/components/portfolio/HeroSection.tsx
import { personal } from "@/data/portfolio"

<h1 data-text={personal.name}>{personal.name}</h1>
<a href={`mailto:${personal.email}`}>Get in touch</a>

The component’s job is now purely presentation — how the data looks. The data itself is somewhere else entirely.

flowchart LR
  D["portfolio.ts (data)"] --> H[HeroSection]
  D --> S[SkillsSection]
  D --> E[ExperienceSection]
  D --> P[ProjectsSection]

Why this beats hardcoding

One source of truth. Update a job title once and it’s correct everywhere it appears. No stale copies hiding in components.

Free type safety. Because these are TypeScript exports, the object shapes become types automatically. If a component does job.tittle (typo) or expects a field that isn’t there, tsc fails the build. The data file’s structure literally enforces correct usage in the components — I’ve caught real mistakes this way before they shipped.

Editing content is low-risk. Changing a bullet point or adding a job is editing a plain array, not touching JSX with its animations and event handlers. Much less chance of breaking the layout.

It scales cleanly. Adding a new job or project is appending one object of the known shape. The component already maps over the array, so it renders with zero component changes.

Keeping content in one place also pays off for SEO: because every section renders from this file, it’s straightforward to mirror the data into crawlable sr-only markup for one-at-a-time UI. The whole approach is part of building a CRT-terminal portfolio.

One small thing to keep consistent: because content is now decoupled from markup, any place that shows a subset — say the hero showing only the top four skill levels with skillLevels.slice(0, 4) — still reads from the same array, so the numbers can never disagree between sections.

What to remember

  • Put all copy — personal info, experience, skills, projects — in one file like src/data/portfolio.ts.
  • Export plain typed objects and arrays; give repeating items a consistent shape so components can .map() them.
  • Components import and render the data — they hold layout, never content.
  • TypeScript turns the data shapes into types, so typos and missing fields fail the build.
  • Updating the site becomes editing data, not hunting through JSX.
ls ./related
cat ./comments

Comments are not configured yet. Enable GitHub Discussions and paste the giscus repo-id / category-id into src/consts.ts.