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
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://justin.deal',
|
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: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
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",
|
"hash": "22066863",
|
||||||
"configHash": "c5e12679",
|
"configHash": "21423e47",
|
||||||
"lockfileHash": "53cd0e09",
|
"lockfileHash": "53cd0e09",
|
||||||
"browserHash": "c02b8ebb",
|
"browserHash": "ad44e8cf",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"astro > cssesc": {
|
"astro > cssesc": {
|
||||||
"src": "../../.pnpm/cssesc@3.0.0/node_modules/cssesc/cssesc.js",
|
"src": "../../.pnpm/cssesc@3.0.0/node_modules/cssesc/cssesc.js",
|
||||||
"file": "astro___cssesc.js",
|
"file": "astro___cssesc.js",
|
||||||
"fileHash": "22e06d75",
|
"fileHash": "35f908c6",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"astro > aria-query": {
|
"astro > aria-query": {
|
||||||
"src": "../../.pnpm/aria-query@5.3.2/node_modules/aria-query/lib/index.js",
|
"src": "../../.pnpm/aria-query@5.3.2/node_modules/aria-query/lib/index.js",
|
||||||
"file": "astro___aria-query.js",
|
"file": "astro___aria-query.js",
|
||||||
"fileHash": "c870f0ee",
|
"fileHash": "13f7590c",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"astro > axobject-query": {
|
"astro > axobject-query": {
|
||||||
"src": "../../.pnpm/axobject-query@4.1.0/node_modules/axobject-query/lib/index.js",
|
"src": "../../.pnpm/axobject-query@4.1.0/node_modules/axobject-query/lib/index.js",
|
||||||
"file": "astro___axobject-query.js",
|
"file": "astro___axobject-query.js",
|
||||||
"fileHash": "b44b4671",
|
"fileHash": "59c979c2",
|
||||||
"needsInterop": true
|
"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;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
sizes?: string;
|
||||||
|
loading?: 'eager' | 'lazy';
|
||||||
|
decoding?: 'sync' | 'async' | 'auto';
|
||||||
|
format?: 'webp' | 'avif' | 'png' | 'jpeg' | 'jpg';
|
||||||
|
quality?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -15,8 +20,16 @@ const {
|
|||||||
alt,
|
alt,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
class: className = ''
|
class: className = '',
|
||||||
|
sizes = '(min-width: 1024px) 1024px, 100vw',
|
||||||
|
loading = 'lazy',
|
||||||
|
decoding = 'async',
|
||||||
|
format = 'webp',
|
||||||
|
quality = 80
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
|
// Determine if this is likely an above-the-fold image based on props
|
||||||
|
const isAboveFold = loading === 'eager';
|
||||||
---
|
---
|
||||||
|
|
||||||
{typeof src === 'string' ? (
|
{typeof src === 'string' ? (
|
||||||
@ -26,38 +39,39 @@ const {
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
class={className}
|
class={className}
|
||||||
|
loading={loading}
|
||||||
|
decoding={decoding}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Image
|
||||||
{/* Use direct component with conditional attributes to avoid TypeScript errors */}
|
src={src}
|
||||||
{width && height ? (
|
alt={alt}
|
||||||
<Image
|
width={width}
|
||||||
src={src}
|
height={height}
|
||||||
alt={alt}
|
class={className}
|
||||||
width={width}
|
sizes={sizes}
|
||||||
height={height}
|
loading={loading}
|
||||||
class={className}
|
decoding={decoding}
|
||||||
/>
|
format={format}
|
||||||
) : width ? (
|
quality={quality}
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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');
|
document.documentElement.classList.add('theme-loaded');
|
||||||
})();
|
})();
|
||||||
</script>
|
</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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
@ -52,6 +69,7 @@ import "../styles/global.css";
|
|||||||
<!-- Theme colors -->
|
<!-- Theme colors -->
|
||||||
<meta name="theme-color" content="#282828" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#282828" media="(prefers-color-scheme: dark)" />
|
||||||
<meta name="theme-color" content="#ebdbb2" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#ebdbb2" media="(prefers-color-scheme: light)" />
|
||||||
|
|
||||||
<!-- Preconnect to external resources -->
|
<!-- Preconnect to external resources -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
|
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin />
|
||||||
<link rel="dns-prefetch" href="https://fonts.bunny.net" />
|
<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="preconnect" href="https://unpkg.com" crossorigin />
|
||||||
<link rel="dns-prefetch" href="https://unpkg.com" />
|
<link rel="dns-prefetch" href="https://unpkg.com" />
|
||||||
|
|
||||||
<!-- Preload critical fonts -->
|
<!-- Preload critical fonts with font-display: swap -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
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"
|
as="style"
|
||||||
/>
|
/>
|
||||||
<link
|
<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"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -79,9 +97,10 @@ import "../styles/global.css";
|
|||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin
|
crossorigin
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Preload Alpine.js -->
|
<!-- Preload Alpine.js -->
|
||||||
<link rel="preload" href="https://unpkg.com/alpinejs" as="script" />
|
<link rel="preload" href="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" as="script" />
|
||||||
<script src="https://unpkg.com/alpinejs" defer></script>
|
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||||
|
|
||||||
<!-- Font loading observer -->
|
<!-- Font loading observer -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user