The entire portfolio rendered only on the PixiJS canvas, so crawlers saw a near-empty body and the site ranked poorly for non-personalized searches. Fixes: - NEW SeoContent.svelte: screen-reader/crawler-only (.sr-only) semantic HTML (h1 + bio + contact + Experience/Projects/Skills/Education), generated from data.ts (single source of truth, reuses aggregateSkills). Prerenders into build/index.html — verified it now contains the job/project/education text that was previously canvas-only. Also a big screen-reader accessibility win. - JSON-LD schema.org Person in +layout (name, jobTitle, alumniOf, knowsAbout, sameAs LinkedIn/GitHub) — strong entity signal for name searches. - Canonical URL; static/sitemap.xml + robots.txt Sitemap reference. - Keyword-tuned <title>, description, og/twitter titles (name + role + stack). - Demoted the transient welcome <h1> to a <div> so SeoContent's <h1> is the single canonical heading. Bumped SW cache to v2. .sr-only utility added to app.css. check/lint/test/build clean; verified no visual regression and no runtime exceptions (background + canvas unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
5.8 KiB
Svelte
153 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import favicon from '$lib/assets/favicon.ico';
|
|
import '../app.css';
|
|
import { treeNodes, type OriginNode, type SkillNode, type EducationNode } from '$lib/data';
|
|
|
|
const { children } = $props();
|
|
|
|
// schema.org Person structured data, generated from data.ts (single source of
|
|
// truth) so it stays in sync. Ties this domain to the LinkedIn/GitHub identity.
|
|
const origin = treeNodes.find((n): n is OriginNode => n.category === 'origin');
|
|
const knowsAbout = treeNodes
|
|
.filter((n): n is SkillNode => n.category === 'skills' && !!n.proficiency)
|
|
.map((n) => n.label);
|
|
const edu = treeNodes.find(
|
|
(n): n is EducationNode => n.category === 'education' && !!n.institution
|
|
);
|
|
|
|
const personJsonLd = JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Person',
|
|
name: origin?.fullName,
|
|
jobTitle: origin?.jobTitle,
|
|
description: origin?.aboutMe,
|
|
url: 'https://tehriehldeal.com',
|
|
image: 'https://tehriehldeal.com/headshot.jpg',
|
|
email: origin?.email,
|
|
address: {
|
|
'@type': 'PostalAddress',
|
|
addressLocality: 'Kent',
|
|
addressRegion: 'WA',
|
|
addressCountry: 'US'
|
|
},
|
|
alumniOf: edu ? { '@type': 'CollegeOrUniversity', name: edu.institution } : undefined,
|
|
knowsAbout,
|
|
sameAs: [origin?.linkedIn, origin?.github].filter(Boolean)
|
|
// Escape '<' so the JSON can't break out of the <script> element.
|
|
}).replace(/</g, '\\u003c');
|
|
|
|
// Wrap in the <script> tag here (close tag split across a concat) so the
|
|
// literal closing tag never appears verbatim in source — that would either
|
|
// terminate this block early or trip the Svelte ESLint parser.
|
|
const personLdScript = '<script type="application/ld+json">' + personJsonLd + '</' + 'script>';
|
|
|
|
onMount(() => {
|
|
// Register service worker for PWA support
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker
|
|
.register('/service-worker.js')
|
|
.then((registration) => {
|
|
if (import.meta.env.DEV) {
|
|
console.log('✨ Service Worker registered successfully:', registration.scope);
|
|
}
|
|
|
|
// Check for updates periodically
|
|
registration.addEventListener('updatefound', () => {
|
|
const newWorker = registration.installing;
|
|
if (newWorker) {
|
|
newWorker.addEventListener('statechange', () => {
|
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
if (import.meta.env.DEV) {
|
|
console.log('🔄 New service worker available. Refresh to update.');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.warn('Service Worker registration failed:', error);
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<!-- Favicon -->
|
|
<link rel="icon" href={favicon} />
|
|
|
|
<!-- PWA Meta Tags -->
|
|
<meta name="application-name" content="Atlas of Skills" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<meta name="apple-mobile-web-app-title" content="Atlas of Skills" />
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
<meta name="theme-color" content="#ffd700" />
|
|
|
|
<!-- Web App Manifest -->
|
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
|
|
<!-- Apple Touch Icons -->
|
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
|
|
|
|
<!-- PWA Icons -->
|
|
<link rel="icon" type="image/png" sizes="192x192" href="/icons/pwa-192x192.png" />
|
|
<link rel="icon" type="image/png" sizes="512x512" href="/icons/pwa-512x512.png" />
|
|
|
|
<!-- SEO Meta Tags -->
|
|
<link rel="canonical" href="https://tehriehldeal.com/" />
|
|
<meta
|
|
name="description"
|
|
content="Kevin Riehl — Software Engineer with 5+ years building full-stack apps in React, Node.js, and TypeScript. Explore his experience, projects, and skills in an interactive skill-tree portfolio."
|
|
/>
|
|
<meta
|
|
name="keywords"
|
|
content="Kevin Riehl, software engineer, full-stack developer, React, Node.js, TypeScript, portfolio, interactive resume, skill tree"
|
|
/>
|
|
<meta name="author" content="Kevin Riehl" />
|
|
|
|
<!-- Open Graph Meta Tags for social sharing -->
|
|
<meta property="og:type" content="website" />
|
|
<meta
|
|
property="og:title"
|
|
content="Kevin Riehl — Software Engineer (React, Node.js, TypeScript)"
|
|
/>
|
|
<meta
|
|
property="og:description"
|
|
content="Software Engineer with 5+ years building full-stack apps in React, Node.js, and TypeScript — shown as an interactive Path of Exile-inspired skill tree."
|
|
/>
|
|
<meta property="og:url" content="https://tehriehldeal.com" />
|
|
<meta property="og:site_name" content="Atlas of Skills" />
|
|
<meta property="og:image" content="https://tehriehldeal.com/og-preview.jpg" />
|
|
<meta property="og:image:secure_url" content="https://tehriehldeal.com/og-preview.jpg" />
|
|
<meta property="og:image:type" content="image/jpeg" />
|
|
<meta property="og:image:width" content="1200" />
|
|
<meta property="og:image:height" content="630" />
|
|
<meta property="og:image:alt" content="Kevin Riehl - The Atlas of Skills interactive portfolio" />
|
|
|
|
<!-- Twitter Card Meta Tags -->
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta
|
|
name="twitter:title"
|
|
content="Kevin Riehl — Software Engineer (React, Node.js, TypeScript)"
|
|
/>
|
|
<meta
|
|
name="twitter:description"
|
|
content="Software Engineer with 5+ years building full-stack apps in React, Node.js, and TypeScript — shown as an interactive Path of Exile-inspired skill tree."
|
|
/>
|
|
<meta name="twitter:image" content="https://tehriehldeal.com/og-preview.jpg" />
|
|
<meta
|
|
name="twitter:image:alt"
|
|
content="Kevin Riehl - The Atlas of Skills interactive portfolio"
|
|
/>
|
|
|
|
<!-- Structured data (schema.org Person). Safe: built from our own data.ts
|
|
with '<' escaped; not user input. -->
|
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
{@html personLdScript}
|
|
</svelte:head>
|
|
|
|
{@render children()}
|