Brand/content phase. The engineering was never the weak part of this site;
the copy was. Decisions on title, bio angle, download figures and node copy
were the owner's — this implements them.
Identity:
- jobTitle was 'Software Development - Lead Associate', an internal HR grade
that means nothing to a recruiter or a search engine. Now 'Lead Software
Engineer' — a faithful translation, not a promotion.
- aboutMe opened with "Intermediate Software Engineer", grading himself down
in his own headline, and named the most generic possible stack
(React/Node/SQL/ShadCN/Docker). It now leads with the work almost nobody
else has done: WebRTC telehealth, real-time gesture recognition that made
chat usable for speech-impaired patients, and air-gapped deployment
tooling. Those were previously buried 3-4 clicks deep in leaf nodes and
appeared nowhere on the surface.
- Title/description/OG/Twitter copy realigned to match. JSON-LD needed no
change — it already derives from the origin node.
Node copy:
- The five job nodes had placeholder descriptions ('Junior developer at the
start of my career.', 'Where I am now.') that are the first thing a tooltip
shows. Each is now a one-line hook naming the most substantial thing from
that role.
- Normalised highlight punctuation: only exp-job5's bullets ended in periods.
Proof of work:
- `downloads` existed on ProjectNode and was used by zero nodes despite two
published PyPI packages. Populated with verified lifetime figures from
pypistats.org (tvdbAPI 2,277, tmdbAPI 784, checked 2026-08-09) and dated in
a comment. Lifetime, deliberately, not the recent rate — 27 and 12 in the
last month is not a number worth advertising.
- Nothing rendered the field. NodeModal now shows it beside the status badge,
and SeoContent renders it too so it's crawlable.
Data integrity:
- edu-degree's description said "Computer Science" while its own structured
fields said Applied Science / IT-Software Development. Its label was the
generic "University"; now Green River College.
- calculatePlayerLevel() hardcoded '2020-10-01', duplicating
exp-job1.startDate — two sources for one fact, free to drift. It now
derives the earliest start from the experience nodes, via an explicit
"Month YYYY" parser rather than handing a bare "October 2020" to
`new Date` (not a format engines are required to accept).
- Added the first tests for calculatePlayerLevel, previously the only
untested export in stats.ts. 14 -> 16 tests.
- Fixed the 'tool-vscod' id typo.
Corrections to the audit this phase acted on: the self-hosted repos at
git.tehriehldeal.com ARE publicly reachable (all three return 200
anonymously) — the earlier claim that recruiters couldn't reach them was
asserted without checking. All three live project sites are up too.
Verified in the production build: modal shows the new title and bio, the
downloads badge renders as "↓ 2,277 downloads", figures are crawlable in the
prerendered HTML, and no filler copy survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
4.2 KiB
Svelte
130 lines
4.2 KiB
Svelte
<script lang="ts">
|
||
// Screen-reader / crawler-only rendition of the portfolio.
|
||
//
|
||
// The interactive tree lives entirely on a PixiJS canvas, which search
|
||
// engines and assistive tech can't read. This component renders the same
|
||
// content (sourced from data.ts — the single source of truth) as real,
|
||
// prerendered semantic HTML, visually hidden via the global `.sr-only` class.
|
||
import {
|
||
treeNodes,
|
||
type ExperienceNode,
|
||
type ProjectNode,
|
||
type EducationNode,
|
||
type OriginNode
|
||
} from '$lib/data';
|
||
import { aggregateSkills, BRANCH_IDS } from '$lib/utils/stats';
|
||
|
||
// Set when the canvas can't render, so this becomes the visible page rather
|
||
// than a screen-reader-only mirror of it.
|
||
const { revealed = false }: { revealed?: boolean } = $props();
|
||
|
||
const origin = treeNodes.find((n): n is OriginNode => n.category === 'origin');
|
||
|
||
// Leaf nodes only — container/branch nodes lack these category fields.
|
||
const jobs = treeNodes.filter(
|
||
(n): n is ExperienceNode => n.category === 'experience' && !!n.company
|
||
);
|
||
const projects = treeNodes.filter(
|
||
(n): n is ProjectNode => n.category === 'projects' && !!n.techStack?.length
|
||
);
|
||
const education = treeNodes.filter(
|
||
(n): n is EducationNode => n.category === 'education' && !!n.institution
|
||
);
|
||
|
||
// Newest experience first (reverse-chronological, résumé convention).
|
||
const jobsNewestFirst = [...jobs].reverse();
|
||
|
||
const skillsByBranch = aggregateSkills();
|
||
</script>
|
||
|
||
{#if origin}
|
||
<section class="sr-only" class:revealed aria-label="Portfolio (text version)">
|
||
<h1>{origin.fullName} — {origin.jobTitle}</h1>
|
||
{#if origin.aboutMe}<p>{origin.aboutMe}</p>{/if}
|
||
{#if origin.location}<p>Location: {origin.location}</p>{/if}
|
||
|
||
<nav aria-label="Contact and profiles">
|
||
<ul>
|
||
{#if origin.email}<li><a href="mailto:{origin.email}">Email: {origin.email}</a></li>{/if}
|
||
{#if origin.linkedIn}<li><a href={origin.linkedIn}>LinkedIn</a></li>{/if}
|
||
{#if origin.github}<li><a href={origin.github}>GitHub</a></li>{/if}
|
||
{#if origin.website}<li><a href={origin.website}>Website</a></li>{/if}
|
||
{#if origin.resumeUrl}<li><a href={origin.resumeUrl}>Résumé (PDF)</a></li>{/if}
|
||
</ul>
|
||
</nav>
|
||
|
||
<section aria-label="Work experience">
|
||
<h2>Work Experience</h2>
|
||
{#each jobsNewestFirst as job (job.id)}
|
||
<article>
|
||
<h3>{job.jobTitle} — {job.company}</h3>
|
||
{#if job.startDate}<p>{job.startDate} – {job.endDate || 'Present'}</p>{/if}
|
||
{#if job.highlights?.length}
|
||
<ul>
|
||
{#each job.highlights as highlight (highlight)}
|
||
<li>{highlight}</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
</article>
|
||
{/each}
|
||
</section>
|
||
|
||
<section aria-label="Projects">
|
||
<h2>Projects</h2>
|
||
{#each projects as project (project.id)}
|
||
<article>
|
||
<h3>{project.label}</h3>
|
||
<p>{project.description}</p>
|
||
{#if project.techStack?.length}
|
||
<p>Tech: {project.techStack.join(', ')}</p>
|
||
{/if}
|
||
{#if project.downloads}
|
||
<p>{project.downloads}</p>
|
||
{/if}
|
||
<ul>
|
||
{#if project.projectUrl}<li><a href={project.projectUrl}>Live site</a></li>{/if}
|
||
{#if project.repoUrl}<li><a href={project.repoUrl}>Source code</a></li>{/if}
|
||
{#if project.pypiUrl}<li><a href={project.pypiUrl}>PyPI package</a></li>{/if}
|
||
{#if project.npmUrl}<li><a href={project.npmUrl}>npm package</a></li>{/if}
|
||
</ul>
|
||
</article>
|
||
{/each}
|
||
</section>
|
||
|
||
<section aria-label="Technical skills">
|
||
<h2>Technical Skills</h2>
|
||
{#each BRANCH_IDS as branchId (branchId)}
|
||
{@const branch = skillsByBranch[branchId]}
|
||
{#if branch && branch.skills.length}
|
||
<h3>{branch.label}</h3>
|
||
<ul>
|
||
{#each branch.skills as skill (skill.id)}
|
||
<li>
|
||
{skill.label}{#if skill.proficiency}
|
||
({skill.proficiency}{#if skill.yearsOfExperience}, {skill.yearsOfExperience} years{/if})
|
||
{/if}
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
{/if}
|
||
{/each}
|
||
</section>
|
||
|
||
<section aria-label="Education">
|
||
<h2>Education</h2>
|
||
{#each education as edu (edu.id)}
|
||
<article>
|
||
<h3>
|
||
{edu.degree}{#if edu.field}
|
||
in {edu.field}{/if}
|
||
</h3>
|
||
<p>
|
||
{edu.institution}{#if edu.graduationYear}, {edu.graduationYear}{/if}
|
||
</p>
|
||
</article>
|
||
{/each}
|
||
</section>
|
||
</section>
|
||
{/if}
|