diff --git a/.astro/data-store.json b/.astro/data-store.json
index edf5924..7f2956d 100644
--- a/.astro/data-store.json
+++ b/.astro/data-store.json
@@ -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}}"]
\ No newline at end of file
+[["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}}"]
\ No newline at end of file
diff --git a/astro.config.mjs b/astro.config.mjs
index 121b974..2bd2f41 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -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'],
+ },
},
});
diff --git a/node_modules/.vite/deps/_metadata.json b/node_modules/.vite/deps/_metadata.json
index ee29d2a..54409c8 100644
--- a/node_modules/.vite/deps/_metadata.json
+++ b/node_modules/.vite/deps/_metadata.json
@@ -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
}
},
diff --git a/public/offline.html b/public/offline.html
new file mode 100644
index 0000000..794cd92
--- /dev/null
+++ b/public/offline.html
@@ -0,0 +1,252 @@
+
+
+
+
+
+ Offline | Justin Deal
+
+
+
+
+ 📶
+ You're Offline
+ It looks like you've lost your internet connection. Some pages may still be available if you've visited them before.
+
+
+
+
Available Pages
+
Loading cached pages...
+
+
+
+
+
+
+
diff --git a/public/service-worker.js b/public/service-worker.js
new file mode 100644
index 0000000..133f351
--- /dev/null
+++ b/public/service-worker.js
@@ -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(
+ 'Offline
You are currently offline.
',
+ {
+ 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();
+ }
+});
diff --git a/src/components/common/OptimizedImage.astro b/src/components/common/OptimizedImage.astro
index 9b9c2be..f08c7a2 100644
--- a/src/components/common/OptimizedImage.astro
+++ b/src/components/common/OptimizedImage.astro
@@ -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}
/>
) : (
-
- {/* Use direct component with conditional attributes to avoid TypeScript errors */}
- {width && height ? (
-
- ) : width ? (
-
- ) : height ? (
-
- ) : (
-
- )}
-
+
)}
+
+
diff --git a/src/components/common/ResponsiveImage.astro b/src/components/common/ResponsiveImage.astro
new file mode 100644
index 0000000..56f7aac
--- /dev/null
+++ b/src/components/common/ResponsiveImage.astro
@@ -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 ? (
+
+ {/* Mobile image */}
+
+
+ {/* Tablet image */}
+
+
+ {/* Desktop image */}
+
+
+ {/* Fallback image */}
+ {isStringSource ? (
+
+ ) : (
+
+ )}
+
+) : (
+ /* Standard responsive image */
+ isStringSource ? (
+
+ ) : (
+
+ )
+)}
+
+{blurUp && (
+
+)}
+
+
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 12255e2..2982f93 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -38,6 +38,23 @@ import "../styles/global.css";
document.documentElement.classList.add('theme-loaded');
})();
+
+
+
+
@@ -52,6 +69,7 @@ import "../styles/global.css";
+
@@ -60,14 +78,14 @@ import "../styles/global.css";
-
+
@@ -79,9 +97,10 @@ import "../styles/global.css";
type="font/woff2"
crossorigin
/>
+
-
-
+
+