Files
skill-tree-portfolio/src/lib/components/SeoContent.svelte
T
TehRiehlDealandClaude Opus 5 f83a317520 content: audit phase 4 — lead with the work that differentiates
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>
2026-08-09 00:42:45 -07:00

130 lines
4.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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}