home page updated
This commit is contained in:
2
openspec/changes/service-workers/.openspec.yaml
Normal file
2
openspec/changes/service-workers/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-10
|
||||||
52
openspec/changes/service-workers/design.md
Normal file
52
openspec/changes/service-workers/design.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
This change introduces a Service Worker to improve perceived load time and reduce network usage on repeat visits by caching critical assets in the browser.
|
||||||
|
|
||||||
|
The site is a static Astro build. That means:
|
||||||
|
- The Service Worker should live at the site root (`/sw.js`) so it can control all routes.
|
||||||
|
- Navigations (HTML documents) should not be cached in a way that causes indefinite staleness after new deploys.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Improve repeat-visit performance by pre-caching the critical site shell assets.
|
||||||
|
- Add runtime caching for media assets (images) with bounded storage usage.
|
||||||
|
- Ensure safe update behavior: cache versioning and cleanup on activate.
|
||||||
|
- Keep local development predictable by not registering the Service Worker in dev by default.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Full offline-first experience for all routes/content.
|
||||||
|
- Background sync, push notifications, or complex offline fallbacks.
|
||||||
|
- Server-side caching (handled by separate changes, if desired).
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **Implement a lightweight, custom Service Worker (no Workbox)**
|
||||||
|
Rationale: The project already outputs static assets and the needed caching strategies are straightforward. A small custom `/sw.js` avoids adding a build-time dependency and keeps behavior explicit.
|
||||||
|
Alternatives considered:
|
||||||
|
- Workbox: powerful, but adds dependency surface area and build configuration overhead.
|
||||||
|
|
||||||
|
2. **Cache strategy by request type**
|
||||||
|
Rationale: Different resources have different freshness requirements.
|
||||||
|
- Navigations (HTML documents): **Network-first**, fallback to cache on failure. This minimizes stale HTML risks while still helping resiliency.
|
||||||
|
- Static shell assets (CSS/JS/fonts/icons): **Pre-cache** on install and serve from cache for speed.
|
||||||
|
- Images/media: **Cache-first** with a size bound and eviction to avoid unbounded storage.
|
||||||
|
|
||||||
|
3. **Versioned caches + activation cleanup**
|
||||||
|
Rationale: Static sites frequently redeploy; versioning ensures updates can be picked up and old assets are not served after deploy. On activate, the SW deletes prior version caches.
|
||||||
|
Implementation approach:
|
||||||
|
- Use cache names like `shell-v<version>` and `media-v<version>`.
|
||||||
|
- Update the version string on build (initially a constant; later can be automated).
|
||||||
|
|
||||||
|
4. **Disable SW registration in development by default**
|
||||||
|
Rationale: Service worker caching can confuse local iteration and cause stale assets during development.
|
||||||
|
Implementation approach:
|
||||||
|
- Register SW only when `import.meta.env.PROD` is true (Astro build-time flag) or an explicit runtime guard is met.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **[Stale or broken assets after deploy]** → Use versioned caches and delete old caches during activation. Prefer network-first for navigations.
|
||||||
|
- **[Over-caching HTML causes outdated content]** → Do not use cache-first for navigation; do not pre-cache HTML pages.
|
||||||
|
- **[Storage growth due to images]** → Enforce a max-entry limit with eviction for media cache.
|
||||||
|
- **[Browser compatibility gaps]** → Service worker is progressive enhancement; site must still function without it.
|
||||||
|
|
||||||
27
openspec/changes/service-workers/proposal.md
Normal file
27
openspec/changes/service-workers/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Improve page load performance (especially repeat visits) by caching key assets closer to the user and reducing unnecessary network requests.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a Service Worker to the site so the browser can cache and serve core assets efficiently.
|
||||||
|
- Pre-cache the critical shell (CSS, JS, fonts, icons) and use a runtime caching strategy for images and other large assets.
|
||||||
|
- Ensure safe update behavior on deploy (new service worker activates and old caches are cleaned up).
|
||||||
|
- Keep development experience predictable (service worker disabled or bypassed in dev by default).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `service-worker-performance`: Provide a service worker-based caching strategy that improves perceived load time and reduces network usage on repeat visits, while ensuring safe updates on new deploys.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- (none)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Adds new client-side assets for the service worker (e.g., `sw.js`) and registration logic in the site layout.
|
||||||
|
- Changes browser caching behavior; must avoid serving stale HTML indefinitely and ensure caches are versioned/invalidated on deploy.
|
||||||
|
- Service workers require a secure context (HTTPS) in production; local dev behavior should be explicitly controlled to avoid confusing caching during iteration.
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Service Worker registration
|
||||||
|
The site SHALL register a Service Worker on supported browsers when running in production (HTTPS), scoped to the site root so it can control all site pages.
|
||||||
|
|
||||||
|
#### Scenario: Production registration
|
||||||
|
- **WHEN** a user loads any page in a production environment
|
||||||
|
- **THEN** the site registers a service worker at `/sw.js` with scope `/`
|
||||||
|
|
||||||
|
#### Scenario: Development does not register
|
||||||
|
- **WHEN** a user loads any page in a local development environment
|
||||||
|
- **THEN** the site does not register a service worker
|
||||||
|
|
||||||
|
### Requirement: Pre-cache critical site shell assets
|
||||||
|
The Service Worker SHALL pre-cache a set of critical static assets required to render the site shell quickly on repeat visits.
|
||||||
|
|
||||||
|
#### Scenario: Pre-cache on install
|
||||||
|
- **WHEN** the service worker is installed
|
||||||
|
- **THEN** it caches the configured site shell assets in a versioned cache
|
||||||
|
|
||||||
|
### Requirement: Runtime caching for media assets
|
||||||
|
The Service Worker SHALL use runtime caching for media assets (for example images) to reduce repeat network fetches, while ensuring content can refresh.
|
||||||
|
|
||||||
|
#### Scenario: Cache-first for images
|
||||||
|
- **WHEN** a user requests an image resource
|
||||||
|
- **THEN** the service worker serves the cached image when available, otherwise fetches from the network and stores the response in the media cache
|
||||||
|
|
||||||
|
#### Scenario: Enforce cache size bounds
|
||||||
|
- **WHEN** the number of cached media items exceeds the configured maximum
|
||||||
|
- **THEN** the service worker evicts older entries to stay within the bound
|
||||||
|
|
||||||
|
### Requirement: Navigation requests avoid indefinite staleness
|
||||||
|
The Service Worker MUST NOT serve stale HTML indefinitely for navigation requests.
|
||||||
|
|
||||||
|
#### Scenario: Network-first navigation
|
||||||
|
- **WHEN** a user navigates to a page route (a document navigation request)
|
||||||
|
- **THEN** the service worker attempts to fetch from the network first and falls back to a cached response if the network is unavailable
|
||||||
|
|
||||||
|
### Requirement: Safe updates and cache cleanup
|
||||||
|
The Service Worker SHALL use versioned caches and remove old caches during activation to ensure updated assets are used after a new deploy.
|
||||||
|
|
||||||
|
#### Scenario: Activate new version and clean old caches
|
||||||
|
- **WHEN** a new service worker version activates
|
||||||
|
- **THEN** it deletes caches from older versions and begins using the current versioned caches
|
||||||
|
|
||||||
22
openspec/changes/service-workers/tasks.md
Normal file
22
openspec/changes/service-workers/tasks.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## 1. Setup
|
||||||
|
|
||||||
|
- [x] 1.1 Add `sw.js` to site root output (place in `site/public/sw.js`)
|
||||||
|
- [x] 1.2 Add service worker registration to the base layout (register only in production)
|
||||||
|
|
||||||
|
## 2. Pre-cache Site Shell
|
||||||
|
|
||||||
|
- [x] 2.1 Implement versioned cache names and an explicit cache version constant
|
||||||
|
- [x] 2.2 Implement `install` handler to pre-cache critical shell assets
|
||||||
|
- [x] 2.3 Implement `activate` handler to delete old version caches
|
||||||
|
|
||||||
|
## 3. Runtime Caching
|
||||||
|
|
||||||
|
- [x] 3.1 Implement network-first strategy for navigation/document requests with cache fallback
|
||||||
|
- [x] 3.2 Implement cache-first strategy for images/media with network fallback
|
||||||
|
- [x] 3.3 Add a bounded eviction policy for media cache size
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
|
||||||
|
- [ ] 4.1 Verify service worker registers in production build and does not register in dev
|
||||||
|
- [ ] 4.2 Verify repeat navigation and asset loads hit cache (Chrome DevTools Application tab)
|
||||||
|
- [ ] 4.3 Verify a new deploy triggers cache version update and old caches are removed
|
||||||
102
site/content/cache/content.json
vendored
102
site/content/cache/content.json
vendored
File diff suppressed because one or more lines are too long
148
site/public/sw.js
Normal file
148
site/public/sw.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/* Service Worker: lightweight caching for a static Astro site.
|
||||||
|
- Navigations: network-first (avoid stale HTML indefinitely)
|
||||||
|
- Site shell: pre-cache on install
|
||||||
|
- Images: cache-first with bounded eviction
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Bump this value on deploy to invalidate caches.
|
||||||
|
const CACHE_VERSION = "v1";
|
||||||
|
|
||||||
|
const CACHE_SHELL = `shell-${CACHE_VERSION}`;
|
||||||
|
const CACHE_PAGES = `pages-${CACHE_VERSION}`;
|
||||||
|
const CACHE_MEDIA = `media-${CACHE_VERSION}`;
|
||||||
|
|
||||||
|
const SHELL_ASSETS = ["/", "/styles/global.css", "/favicon.svg", "/favicon.ico", "/robots.txt"];
|
||||||
|
|
||||||
|
// Keep media cache bounded so we don't grow indefinitely.
|
||||||
|
const MAX_MEDIA_ENTRIES = 80;
|
||||||
|
|
||||||
|
const isGet = (request) => request && request.method === "GET";
|
||||||
|
|
||||||
|
const isNavigationRequest = (request) =>
|
||||||
|
request.mode === "navigate" || request.destination === "document";
|
||||||
|
|
||||||
|
const isImageRequest = (request, url) => {
|
||||||
|
if (request.destination === "image") return true;
|
||||||
|
const p = url.pathname.toLowerCase();
|
||||||
|
return (
|
||||||
|
p.endsWith(".png") ||
|
||||||
|
p.endsWith(".jpg") ||
|
||||||
|
p.endsWith(".jpeg") ||
|
||||||
|
p.endsWith(".webp") ||
|
||||||
|
p.endsWith(".gif") ||
|
||||||
|
p.endsWith(".avif") ||
|
||||||
|
p.endsWith(".svg")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function trimCache(cacheName, maxEntries) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const keys = await cache.keys();
|
||||||
|
const extra = keys.length - maxEntries;
|
||||||
|
if (extra <= 0) return;
|
||||||
|
// Cache keys are returned in insertion order in practice; delete the oldest.
|
||||||
|
for (let i = 0; i < extra; i += 1) {
|
||||||
|
await cache.delete(keys[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cachePutSafe(cacheName, request, response) {
|
||||||
|
// Only cache successful or opaque responses. Avoid caching 404/500 HTML.
|
||||||
|
if (!response) return;
|
||||||
|
if (response.type !== "opaque" && !response.ok) return;
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
await cache.put(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const cache = await caches.open(CACHE_SHELL);
|
||||||
|
await cache.addAll(SHELL_ASSETS);
|
||||||
|
// Activate new worker ASAP to pick up new caching rules.
|
||||||
|
await self.skipWaiting();
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const keep = new Set([CACHE_SHELL, CACHE_PAGES, CACHE_MEDIA]);
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map((k) => (keep.has(k) ? Promise.resolve() : caches.delete(k))));
|
||||||
|
await self.clients.claim();
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
if (!isGet(request)) return;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Only handle http(s).
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
||||||
|
|
||||||
|
// Network-first for navigations (HTML documents). Cache as fallback only.
|
||||||
|
if (isNavigationRequest(request)) {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fresh = await fetch(request);
|
||||||
|
// Cache a clone so we can serve it when offline.
|
||||||
|
await cachePutSafe(CACHE_PAGES, request, fresh.clone());
|
||||||
|
return fresh;
|
||||||
|
} catch {
|
||||||
|
const cached = await caches.match(request);
|
||||||
|
if (cached) return cached;
|
||||||
|
// Fallback: try cached homepage shell.
|
||||||
|
const home = await caches.match("/");
|
||||||
|
if (home) return home;
|
||||||
|
throw new Error("No cached navigation fallback.");
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-first for images/media with bounded cache size.
|
||||||
|
if (isImageRequest(request, url)) {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const cached = await caches.match(request);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const res = await fetch(request);
|
||||||
|
await cachePutSafe(CACHE_MEDIA, request, res.clone());
|
||||||
|
await trimCache(CACHE_MEDIA, MAX_MEDIA_ENTRIES);
|
||||||
|
return res;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stale-while-revalidate for styles/scripts/fonts from same-origin.
|
||||||
|
if (
|
||||||
|
url.origin === self.location.origin &&
|
||||||
|
(request.destination === "style" ||
|
||||||
|
request.destination === "script" ||
|
||||||
|
request.destination === "font")
|
||||||
|
) {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const cached = await caches.match(request);
|
||||||
|
const networkPromise = fetch(request)
|
||||||
|
.then(async (res) => {
|
||||||
|
await cachePutSafe(CACHE_SHELL, request, res.clone());
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
return cached || (await networkPromise) || fetch(request);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -55,6 +55,24 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
|||||||
<script async defer data-website-id={cfg.umami.websiteId} src={cfg.umami.scriptUrl} />
|
<script async defer data-website-id={cfg.umami.websiteId} src={cfg.umami.scriptUrl} />
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Register SW only in production builds (Astro sets import.meta.env.PROD at build time).
|
||||||
|
import.meta.env.PROD ? (
|
||||||
|
<script is:inline>
|
||||||
|
{`
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
// SW requires HTTPS (or localhost). In prod we expect HTTPS.
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(() => {
|
||||||
|
// noop: SW is progressive enhancement
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</script>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a class="skip-link" href="#main-content">Skip to content</a>
|
<a class="skip-link" href="#main-content">Skip to content</a>
|
||||||
@@ -108,15 +126,7 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
|
|||||||
>
|
>
|
||||||
Blog
|
Blog
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href="/about"
|
|
||||||
data-umami-event="click"
|
|
||||||
data-umami-event-target_id="nav.about"
|
|
||||||
data-umami-event-placement="nav"
|
|
||||||
data-umami-event-target_url="/about"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
|
||||||
import CtaLink from "../components/CtaLink.astro";
|
|
||||||
import { LINKS } from "../lib/links";
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title="About | SanthoshJ"
|
|
||||||
description="About SanthoshJ and where to follow."
|
|
||||||
canonicalPath="/about"
|
|
||||||
>
|
|
||||||
<section class="section">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>About</h2>
|
|
||||||
<span class="muted">Tech, streaming, movies, travel</span>
|
|
||||||
</div>
|
|
||||||
<div class="empty">
|
|
||||||
<p style="margin-top: 0;">
|
|
||||||
This is a lightweight site that aggregates my content so it can be discovered via search and
|
|
||||||
shared cleanly.
|
|
||||||
</p>
|
|
||||||
<div class="cta-row">
|
|
||||||
<CtaLink platform="youtube" placement="about" url={LINKS.youtubeChannel} label="YouTube" />
|
|
||||||
<CtaLink
|
|
||||||
platform="instagram"
|
|
||||||
placement="about"
|
|
||||||
url={LINKS.instagramProfile}
|
|
||||||
label="Instagram"
|
|
||||||
/>
|
|
||||||
<CtaLink platform="podcast" placement="about" url={LINKS.podcast} label="Podcast" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BaseLayout>
|
|
||||||
@@ -26,22 +26,21 @@ const pods = podcastEpisodes(cache)
|
|||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="SanthoshJ | Tech, streaming, movies, travel"
|
title="SanthoshJ | Tech, streaming, movies, travel"
|
||||||
description="A fast, SEO-first home for videos, movie posts, and the Irregular Mind podcast."
|
description="SanthoshJ shares tech, gaming streams, movie recommendations, and travel stories—plus the Irregular Mind podcast. Explore the newest videos and episodes."
|
||||||
canonicalPath="/"
|
canonicalPath="/"
|
||||||
>
|
>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div>
|
<div>
|
||||||
<h1>Fast content. Clear next actions.</h1>
|
<h1>Tech, gaming, movies & travel — videos + podcast by SanthoshJ</h1>
|
||||||
<p>
|
<p>
|
||||||
I post about technology, game streaming, movies, and travel. This site collects the best of
|
Quick takes and long-form stories: tech explainers, game streaming highlights, movie recommendations, and travel notes—curated in one place.
|
||||||
it and points you to the platform you prefer.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="cta-row">
|
<div class="cta-row">
|
||||||
<CtaLink
|
<CtaLink
|
||||||
platform="youtube"
|
platform="youtube"
|
||||||
placement="hero"
|
placement="hero"
|
||||||
url={LINKS.youtubeChannel}
|
url={LINKS.youtubeChannel}
|
||||||
label="Subscribe on YouTube"
|
label="Watch on YouTube"
|
||||||
class="primary"
|
class="primary"
|
||||||
/>
|
/>
|
||||||
<CtaLink
|
<CtaLink
|
||||||
@@ -54,7 +53,7 @@ const pods = podcastEpisodes(cache)
|
|||||||
platform="podcast"
|
platform="podcast"
|
||||||
placement="hero"
|
placement="hero"
|
||||||
url={LINKS.podcast}
|
url={LINKS.podcast}
|
||||||
label="Listen to the podcast"
|
label="Listen to the Irregular Mind on Spotify"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted" style="margin-top: 14px;">
|
<p class="muted" style="margin-top: 14px;">
|
||||||
@@ -62,10 +61,9 @@ const pods = podcastEpisodes(cache)
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<strong>Goal:</strong> 10% month-over-month growth in followers and engagement.
|
|
||||||
<br />
|
|
||||||
<span class="muted"
|
<span class="muted"
|
||||||
>This site is the SEO landing surface that turns search traffic into followers.</span
|
>New videos and episodes every week. Start with the latest drops below.</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user