Add performance optimizations
This commit is contained in:
parent
9179145830
commit
fe566f9e1a
@ -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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
12
node_modules/.vite/deps/_metadata.json
generated
vendored
12
node_modules/.vite/deps/_metadata.json
generated
vendored
@ -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
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
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>
|
||||
|
248
src/components/common/ResponsiveImage.astro
Normal file
248
src/components/common/ResponsiveImage.astro
Normal file
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user