Add performance optimizations

This commit is contained in:
Justin Deal 2025-05-03 14:20:57 -07:00
parent 9179145830
commit fe566f9e1a
8 changed files with 856 additions and 45 deletions

@ -1 +1 @@
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.7.5","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://justin.deal\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[]},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false},\"legacy\":{\"collections\":false}}"]
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.7.5","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://justin.deal\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{\"quality\":80,\"formats\":[\"webp\",\"avif\",\"png\",\"jpg\"]}},\"domains\":[],\"remotePatterns\":[]},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false},\"legacy\":{\"collections\":false}}"]

@ -5,7 +5,60 @@ import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({
site: 'https://justin.deal',
// Performance optimizations
compressHTML: true,
// Build optimizations
build: {
inlineStylesheets: 'auto', // Inline small stylesheets for better performance
},
// Image optimizations
image: {
service: {
entrypoint: 'astro/assets/services/sharp',
config: {
quality: 80, // Default image quality
formats: ['webp', 'avif', 'png', 'jpg'], // Supported formats in order of preference
},
},
},
// Vite configuration
vite: {
plugins: [tailwindcss()],
// Build optimizations
build: {
// Enable chunk splitting
cssCodeSplit: true,
// Optimize chunks
rollupOptions: {
output: {
// Customize chunk naming
manualChunks: {
// Group Alpine.js and related code
alpine: ['alpinejs'],
},
},
},
},
// Optimize dependencies
optimizeDeps: {
include: ['alpinejs'],
},
// CSS optimization
css: {
devSourcemap: true,
},
// Enable server-side rendering optimizations
ssr: {
noExternal: ['@astrojs/tailwind'],
},
},
});

@ -1,25 +1,25 @@
{
"hash": "492ba4b0",
"configHash": "c5e12679",
"hash": "22066863",
"configHash": "21423e47",
"lockfileHash": "53cd0e09",
"browserHash": "c02b8ebb",
"browserHash": "ad44e8cf",
"optimized": {
"astro > cssesc": {
"src": "../../.pnpm/cssesc@3.0.0/node_modules/cssesc/cssesc.js",
"file": "astro___cssesc.js",
"fileHash": "22e06d75",
"fileHash": "35f908c6",
"needsInterop": true
},
"astro > aria-query": {
"src": "../../.pnpm/aria-query@5.3.2/node_modules/aria-query/lib/index.js",
"file": "astro___aria-query.js",
"fileHash": "c870f0ee",
"fileHash": "13f7590c",
"needsInterop": true
},
"astro > axobject-query": {
"src": "../../.pnpm/axobject-query@4.1.0/node_modules/axobject-query/lib/index.js",
"file": "astro___axobject-query.js",
"fileHash": "b44b4671",
"fileHash": "59c979c2",
"needsInterop": true
}
},

252
public/offline.html Normal file

@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline | Justin Deal</title>
<style>
:root {
--color-bg: #fbf1c7;
--color-text: #3c3836;
--color-accent: #d65d0e;
--color-muted: #7c6f64;
--font-mono: 'IBM Plex Mono', monospace;
--font-display: 'Press Start 2P', monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #282828;
--color-text: #ebdbb2;
--color-accent: #fe8019;
--color-muted: #a89984;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-mono);
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 2rem;
}
main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-family: var(--font-display);
font-size: 2rem;
margin-bottom: 1rem;
color: var(--color-accent);
}
p {
margin-bottom: 1.5rem;
font-size: 1.1rem;
}
.offline-icon {
font-size: 4rem;
margin-bottom: 2rem;
animation: pulse 2s infinite;
}
.button {
display: inline-block;
background-color: var(--color-accent);
color: var(--color-bg);
padding: 0.75rem 1.5rem;
border-radius: 0.25rem;
text-decoration: none;
font-weight: bold;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
margin-top: 1rem;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.cached-pages {
margin-top: 2rem;
text-align: left;
width: 100%;
max-width: 400px;
}
.cached-pages h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
color: var(--color-accent);
}
.cached-pages ul {
list-style: none;
}
.cached-pages li {
margin-bottom: 0.5rem;
}
.cached-pages a {
color: var(--color-text);
text-decoration: none;
border-bottom: 1px solid var(--color-accent);
padding-bottom: 2px;
}
.cached-pages a:hover {
color: var(--color-accent);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
</style>
</head>
<body>
<main>
<div class="offline-icon">📶</div>
<h1>You're Offline</h1>
<p>It looks like you've lost your internet connection. Some pages may still be available if you've visited them before.</p>
<button class="button" id="retry-button">Retry Connection</button>
<div class="cached-pages" id="cached-pages">
<h2>Available Pages</h2>
<p>Loading cached pages...</p>
<ul id="cached-pages-list"></ul>
</div>
</main>
<script>
// Check if we're actually offline
function checkConnection() {
return navigator.onLine;
}
// Update UI based on connection status
function updateConnectionStatus() {
if (checkConnection()) {
// We're back online, reload the page
window.location.reload();
} else {
// Still offline
document.querySelector('.offline-icon').textContent = '📶';
document.querySelector('h1').textContent = "You're Offline";
}
}
// Listen for online/offline events
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
// Retry button
document.getElementById('retry-button').addEventListener('click', () => {
document.querySelector('.offline-icon').textContent = '🔄';
document.querySelector('h1').textContent = "Checking Connection...";
// Try to fetch the homepage
fetch('/')
.then(() => {
// If successful, we're online
window.location.reload();
})
.catch(() => {
// Still offline
updateConnectionStatus();
});
});
// List cached pages if service worker and caches are available
if ('caches' in window && 'serviceWorker' in navigator) {
caches.open('justin-deal-v1')
.then(cache => {
return cache.keys()
.then(requests => {
const cachedPagesList = document.getElementById('cached-pages-list');
if (requests.length === 0) {
document.getElementById('cached-pages').innerHTML = '<h2>No Cached Pages Available</h2><p>Try visiting some pages when you\'re back online.</p>';
return;
}
// Filter for HTML pages
const htmlRequests = requests.filter(request => {
const url = new URL(request.url);
return url.pathname === '/' ||
url.pathname.endsWith('.html') ||
!url.pathname.includes('.');
});
// Sort by URL
htmlRequests.sort((a, b) => {
return new URL(a.url).pathname.localeCompare(new URL(b.url).pathname);
});
// Create list items
const listItems = htmlRequests.map(request => {
const url = new URL(request.url);
let pageName = url.pathname === '/' ? 'Home' : url.pathname
.replace(/\/$/, '')
.replace(/^\//, '')
.replace(/\.html$/, '')
.split('/')
.pop()
.replace(/-/g, ' ');
// Capitalize first letter of each word
pageName = pageName
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return `<li><a href="${url.pathname}">${pageName}</a></li>`;
});
if (listItems.length === 0) {
document.getElementById('cached-pages').innerHTML = '<h2>No Cached Pages Available</h2><p>Try visiting some pages when you\'re back online.</p>';
return;
}
cachedPagesList.innerHTML = listItems.join('');
document.querySelector('#cached-pages p').style.display = 'none';
});
})
.catch(error => {
console.error('Error accessing cache:', error);
document.getElementById('cached-pages').innerHTML = '<h2>Could Not Access Cache</h2><p>There was an error accessing the cached pages.</p>';
});
} else {
document.getElementById('cached-pages').innerHTML = '<h2>Cache Not Available</h2><p>Your browser does not support caching or service workers.</p>';
}
</script>
</body>
</html>

225
public/service-worker.js Normal file

@ -0,0 +1,225 @@
/**
* Service Worker for justin.deal
* Provides caching and offline support
*/
const CACHE_NAME = 'justin-deal-v1';
// Assets to cache immediately on service worker install
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/favicon.svg',
'/site.webmanifest',
'/favicons/favicon.png',
'/favicons/apple-touch-icon.png',
'/favicons/favicon-16x16.png',
'/favicons/favicon-32x32.png'
];
// Cache strategies
const CACHE_STRATEGIES = {
// Cache first, falling back to network
CACHE_FIRST: 'cache-first',
// Network first, falling back to cache
NETWORK_FIRST: 'network-first',
// Cache only (no network request)
CACHE_ONLY: 'cache-only',
// Network only (no cache lookup)
NETWORK_ONLY: 'network-only',
// Stale while revalidate (serve from cache, update cache in background)
STALE_WHILE_REVALIDATE: 'stale-while-revalidate'
};
// Route patterns and their corresponding cache strategies
const ROUTE_STRATEGIES = [
// HTML pages - network first
{
pattern: /\.html$|\/$/,
strategy: CACHE_STRATEGIES.NETWORK_FIRST
},
// CSS and JS - stale while revalidate
{
pattern: /\.(css|js)$/,
strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE
},
// Images - cache first
{
pattern: /\.(jpe?g|png|gif|svg|webp|avif)$/,
strategy: CACHE_STRATEGIES.CACHE_FIRST
},
// Fonts - cache first
{
pattern: /\.(woff2?|ttf|otf|eot)$/,
strategy: CACHE_STRATEGIES.CACHE_FIRST
},
// API requests - network first
{
pattern: /\/api\//,
strategy: CACHE_STRATEGIES.NETWORK_FIRST
}
];
// Determine cache strategy for a given URL
function getStrategyForUrl(url) {
const matchedRoute = ROUTE_STRATEGIES.find(route => route.pattern.test(url));
return matchedRoute ? matchedRoute.strategy : CACHE_STRATEGIES.NETWORK_FIRST;
}
// 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(cacheName => cacheName !== CACHE_NAME)
.map(cacheName => caches.delete(cacheName))
);
}).then(() => self.clients.claim())
);
});
// Helper function to handle network-first strategy
async function networkFirstStrategy(request) {
try {
// Try network first
const networkResponse = await fetch(request);
// If successful, clone and cache the response
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// If network fails, try cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If no cache, throw error
throw error;
}
}
// Helper function to handle cache-first strategy
async function cacheFirstStrategy(request) {
// Try cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If not in cache, get from network
const networkResponse = await fetch(request);
// Cache the response for future
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// Helper function to handle stale-while-revalidate strategy
async function staleWhileRevalidateStrategy(request) {
// Try to get from cache
const cachedResponse = await caches.match(request);
// Fetch from network to update cache (don't await)
const fetchPromise = fetch(request)
.then(networkResponse => {
if (networkResponse.ok) {
const cache = caches.open(CACHE_NAME);
cache.then(cache => cache.put(request, networkResponse.clone()));
}
return networkResponse;
})
.catch(error => {
console.error('Failed to fetch and update cache:', error);
});
// Return cached response immediately if available
return cachedResponse || fetchPromise;
}
// Fetch event - handle all fetch requests
self.addEventListener('fetch', event => {
// Skip non-GET requests and browser extensions
if (event.request.method !== 'GET' ||
event.request.url.startsWith('chrome-extension://') ||
event.request.url.includes('extension://')) {
return;
}
// Get the appropriate strategy for this URL
const strategy = getStrategyForUrl(event.request.url);
// Apply the selected strategy
switch (strategy) {
case CACHE_STRATEGIES.NETWORK_FIRST:
event.respondWith(networkFirstStrategy(event.request));
break;
case CACHE_STRATEGIES.CACHE_FIRST:
event.respondWith(cacheFirstStrategy(event.request));
break;
case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE:
event.respondWith(staleWhileRevalidateStrategy(event.request));
break;
case CACHE_STRATEGIES.CACHE_ONLY:
event.respondWith(caches.match(event.request));
break;
case CACHE_STRATEGIES.NETWORK_ONLY:
event.respondWith(fetch(event.request));
break;
default:
// Default to network first
event.respondWith(networkFirstStrategy(event.request));
}
});
// Handle offline fallback
self.addEventListener('fetch', event => {
// Only handle HTML navigation requests that fail
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
// If fetch fails, return the offline page
return caches.match('/offline.html')
.then(response => {
return response || new Response(
'<html><body><h1>Offline</h1><p>You are currently offline.</p></body></html>',
{
headers: { 'Content-Type': 'text/html' }
}
);
});
})
);
}
});
// Listen for messages from the client
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

@ -8,6 +8,11 @@ interface Props {
width?: number;
height?: number;
class?: string;
sizes?: string;
loading?: 'eager' | 'lazy';
decoding?: 'sync' | 'async' | 'auto';
format?: 'webp' | 'avif' | 'png' | 'jpeg' | 'jpg';
quality?: number;
}
const {
@ -15,8 +20,16 @@ const {
alt,
width,
height,
class: className = ''
class: className = '',
sizes = '(min-width: 1024px) 1024px, 100vw',
loading = 'lazy',
decoding = 'async',
format = 'webp',
quality = 80
} = Astro.props;
// Determine if this is likely an above-the-fold image based on props
const isAboveFold = loading === 'eager';
---
{typeof src === 'string' ? (
@ -26,38 +39,39 @@ const {
width={width}
height={height}
class={className}
loading={loading}
decoding={decoding}
/>
) : (
<Fragment>
{/* Use direct component with conditional attributes to avoid TypeScript errors */}
{width && height ? (
<Image
src={src}
alt={alt}
width={width}
height={height}
class={className}
/>
) : width ? (
<Image
src={src}
alt={alt}
width={width}
class={className}
/>
) : height ? (
<Image
src={src}
alt={alt}
height={height}
class={className}
/>
) : (
<Image
src={src}
alt={alt}
class={className}
/>
)}
</Fragment>
<Image
src={src}
alt={alt}
width={width}
height={height}
class={className}
sizes={sizes}
loading={loading}
decoding={decoding}
format={format}
quality={quality}
/>
)}
<style>
/* Prevent layout shifts by maintaining aspect ratio */
img {
display: block;
max-width: 100%;
height: auto;
}
/* Add subtle loading animation for lazy-loaded images */
img:not([loading="eager"]) {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>

@ -0,0 +1,248 @@
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
/**
* ResponsiveImage component with advanced features for optimal performance
* @component
*/
interface Props {
/**
* The image source (either an imported image or a URL)
*/
src: ImageMetadata | string;
/**
* Alternative text for the image
*/
alt: string;
/**
* Base width of the image
*/
width?: number;
/**
* Base height of the image
*/
height?: number;
/**
* CSS class to apply to the image
*/
class?: string;
/**
* Sizes attribute for responsive images
* @default "(min-width: 1280px) 1280px, (min-width: 1024px) 1024px, (min-width: 768px) 768px, 100vw"
*/
sizes?: string;
/**
* Loading strategy
* @default "lazy"
*/
loading?: 'eager' | 'lazy';
/**
* Decoding strategy
* @default "async"
*/
decoding?: 'sync' | 'async' | 'auto';
/**
* Image format
* @default "auto"
*/
format?: 'webp' | 'avif' | 'png' | 'jpeg' | 'jpg' | 'auto';
/**
* Image quality (1-100)
* @default 80
*/
quality?: number;
/**
* Whether to add a blur-up effect
* @default false
*/
blurUp?: boolean;
/**
* Whether this is a priority image (above the fold)
* @default false
*/
priority?: boolean;
/**
* Breakpoints for responsive images
* @default [640, 768, 1024, 1280]
*/
breakpoints?: number[];
/**
* Whether to use art direction (different images for different breakpoints)
* @default false
*/
artDirected?: boolean;
/**
* Mobile image source (for art direction)
*/
mobileSrc?: ImageMetadata | string;
/**
* Tablet image source (for art direction)
*/
tabletSrc?: ImageMetadata | string;
/**
* Desktop image source (for art direction)
*/
desktopSrc?: ImageMetadata | string;
}
const {
src,
alt,
width,
height,
class: className = '',
sizes = '(min-width: 1280px) 1280px, (min-width: 1024px) 1024px, (min-width: 768px) 768px, 100vw',
loading: propLoading,
decoding = 'async',
format = 'auto',
quality = 80,
blurUp = false,
priority = false,
breakpoints = [640, 768, 1024, 1280],
artDirected = false,
mobileSrc,
tabletSrc,
desktopSrc
} = Astro.props;
// Determine loading strategy based on priority
const loading = priority ? 'eager' : propLoading || 'lazy';
// Generate a unique ID for this image instance
const imageId = `img-${Math.random().toString(36).substring(2, 11)}`;
// Determine if we're using a string URL or an imported image
const isStringSource = typeof src === 'string';
// Placeholder for blur-up effect (simplified version)
const placeholderStyle = blurUp ? 'filter: blur(20px); transition: filter 0.5s ease-out;' : '';
---
{artDirected ? (
<picture>
{/* Mobile image */}
<source
media="(max-width: 640px)"
srcset={typeof mobileSrc === 'string' ? mobileSrc : typeof src === 'string' ? src : ''}
/>
{/* Tablet image */}
<source
media="(min-width: 641px) and (max-width: 1023px)"
srcset={typeof tabletSrc === 'string' ? tabletSrc : typeof src === 'string' ? src : ''}
/>
{/* Desktop image */}
<source
media="(min-width: 1024px)"
srcset={typeof desktopSrc === 'string' ? desktopSrc : typeof src === 'string' ? src : ''}
/>
{/* Fallback image */}
{isStringSource ? (
<img
src={src}
alt={alt}
width={width}
height={height}
class={className}
loading={loading}
decoding={decoding}
id={imageId}
style={placeholderStyle}
/>
) : (
<Image
src={src}
alt={alt}
width={width}
height={height}
class={className}
sizes={sizes}
loading={loading}
decoding={decoding}
quality={quality}
id={imageId}
/>
)}
</picture>
) : (
/* Standard responsive image */
isStringSource ? (
<img
src={src}
alt={alt}
width={width}
height={height}
class={className}
loading={loading}
decoding={decoding}
id={imageId}
style={placeholderStyle}
/>
) : (
<Image
src={src}
alt={alt}
width={width}
height={height}
class={className}
sizes={sizes}
loading={loading}
decoding={decoding}
format={format === 'auto' ? undefined : format}
quality={quality}
id={imageId}
/>
)
)}
{blurUp && (
<script define:vars={{ imageId }}>
// Simple blur-up effect
document.addEventListener('DOMContentLoaded', () => {
const img = document.getElementById(imageId);
if (img) {
img.onload = () => {
img.style.filter = 'blur(0)';
};
}
});
</script>
)}
<style>
/* Prevent layout shifts by maintaining aspect ratio */
img {
display: block;
max-width: 100%;
height: auto;
}
/* Add subtle loading animation for lazy-loaded images */
img:not([loading="eager"]) {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>

@ -38,6 +38,23 @@ import "../styles/global.css";
document.documentElement.classList.add('theme-loaded');
})();
</script>
<!-- Service worker registration -->
<script is:inline>
// Register service worker for offline support and caching
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('SW registered: ', registration.scope);
})
.catch(error => {
console.log('SW registration failed: ', error);
});
});
}
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
@ -52,6 +69,7 @@ import "../styles/global.css";
<!-- Theme colors -->
<meta name="theme-color" content="#282828" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ebdbb2" media="(prefers-color-scheme: light)" />
<!-- Preconnect to external resources -->
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
<link rel="dns-prefetch" href="https://fonts.bunny.net" />
@ -60,14 +78,14 @@ import "../styles/global.css";
<link rel="preconnect" href="https://unpkg.com" crossorigin />
<link rel="dns-prefetch" href="https://unpkg.com" />
<!-- Preload critical fonts -->
<!-- Preload critical fonts with font-display: swap -->
<link
rel="preload"
href="https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i"
href="https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i&display=swap"
as="style"
/>
<link
href="https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i"
href="https://fonts.bunny.net/css?family=ibm-plex-mono:400,400i,500,500i,600,600i,700,700i&display=swap"
rel="stylesheet"
/>
@ -79,9 +97,10 @@ import "../styles/global.css";
type="font/woff2"
crossorigin
/>
<!-- Preload Alpine.js -->
<link rel="preload" href="https://unpkg.com/alpinejs" as="script" />
<script src="https://unpkg.com/alpinejs" defer></script>
<link rel="preload" href="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" as="script" />
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<!-- Font loading observer -->
<script is:inline>