Files
skill-tree-portfolio/static/service-worker.js
T
TehRiehlDealandClaude Opus 4.8 df78dc694f feat: make portfolio content crawlable + add SEO structured data
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>
2026-06-03 11:16:17 -07:00

117 lines
3.0 KiB
JavaScript

// Service Worker for PWA offline support
const CACHE_NAME = 'atlas-of-skills-v2';
const RUNTIME_CACHE = 'atlas-runtime-v2';
// Assets to cache on install
const PRECACHE_ASSETS = [
'/',
'/headshot.jpg',
'/kevin-riehl-resume.pdf',
'/robots.txt',
// Add icon paths when they're created
'/icons/pwa.svg'
];
// Install event - precache critical assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(PRECACHE_ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== RUNTIME_CACHE)
.map((name) => caches.delete(name))
);
})
.then(() => self.clients.claim())
);
});
// Fetch event - network first, fallback to cache
self.addEventListener('fetch', (event) => {
// Skip cross-origin requests
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
// Network first strategy for HTML
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone response to store in cache
const responseClone = response.clone();
caches.open(RUNTIME_CACHE)
.then((cache) => cache.put(event.request, responseClone));
return response;
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(event.request)
.then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page if available
return caches.match('/');
});
})
);
return;
}
// Cache first strategy for assets (CSS, JS, images, icons)
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Not in cache, fetch from network
return fetch(event.request)
.then((response) => {
// Don't cache if not a success response
if (!response || response.status !== 200 || response.type === 'error') {
return response;
}
// Clone response to store in cache
const responseClone = response.clone();
caches.open(RUNTIME_CACHE)
.then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// Return fallback for images if network and cache fail
if (event.request.destination === 'image') {
return new Response(
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect fill="#05050a"/><text x="50%" y="50%" text-anchor="middle" fill="#ffd700">Offline</text></svg>',
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
}
});
})
);
});
// Message event - allow clients to skip waiting
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});