Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90104424b8 | |||
| 1fbed390cd | |||
| 7e622afc00 | |||
| e76c8d11fd | |||
| dd1066e9a7 | |||
| 03ecd66c84 | |||
| 58285e428d | |||
| 0fae9dd3dc | |||
| 40f8acd29f |
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
last 2 versions
|
||||||
|
> 2%
|
||||||
|
not dead
|
||||||
|
upported by browserslist
|
||||||
73
next.config.js
Normal file
73
next.config.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
// Enable SWC minification for smaller bundle
|
||||||
|
swcMinify: true,
|
||||||
|
|
||||||
|
// Enable experimental optimizations
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: [
|
||||||
|
'lucide-react',
|
||||||
|
'@/components',
|
||||||
|
'@/providers'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Image optimization
|
||||||
|
images: {
|
||||||
|
formats: ['image/webp', 'image/avif'],
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'img.b2bpic.net'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: '**'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable compression
|
||||||
|
compress: true,
|
||||||
|
|
||||||
|
// Optimize production builds
|
||||||
|
productionBrowserSourceMaps: false,
|
||||||
|
|
||||||
|
// Generate static analysis
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
if (!isServer) {
|
||||||
|
config.optimization = {
|
||||||
|
...config.optimization,
|
||||||
|
runtimeChunk: 'single',
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
default: false,
|
||||||
|
vendors: false,
|
||||||
|
lucide: {
|
||||||
|
test: /[\\/]node_modules[\\/](lucide-react)[\\/]/,
|
||||||
|
name: 'lucide',
|
||||||
|
priority: 10,
|
||||||
|
reuseExistingChunk: true
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
|
||||||
|
name: 'react',
|
||||||
|
priority: 20,
|
||||||
|
reuseExistingChunk: true
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
minChunks: 2,
|
||||||
|
priority: 5,
|
||||||
|
reuseExistingChunk: true,
|
||||||
|
name: 'common'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
14
package.json.optimization
Normal file
14
package.json.optimization
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Bundle optimization guidelines:
|
||||||
|
// 1. Tree-shaking enabled by default in Next.js with swcMinify: true
|
||||||
|
// 2. Dynamic imports for non-critical components
|
||||||
|
// 3. Code splitting configured in next.config.js for lucide-react, react, and common chunks
|
||||||
|
// 4. Service Worker caches on install and updates cache on fetch
|
||||||
|
// 5. Middleware applies proper cache headers:
|
||||||
|
// - Static assets: 1 year (immutable)
|
||||||
|
// - HTML pages: 1 hour + 1 day server cache
|
||||||
|
// - Dynamic content: 1 minute + 5 minutes server cache
|
||||||
|
// 6. Font optimization with display: 'swap' to avoid layout shift
|
||||||
|
// 7. DNS prefetch for external image CDN
|
||||||
|
// 8. Production source maps disabled
|
||||||
|
// 9. Image optimization with WebP and AVIF formats
|
||||||
|
// 10. Experimental optimizePackageImports for better tree-shaking
|
||||||
10
public/manifest.json
Normal file
10
public/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Sharanya Sasmal Portfolio", "short_name": "Portfolio", "description": "UI/UX & Graphic Designer Portfolio", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "orientation": "portrait-primary", "icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
68
public/sw.js
Normal file
68
public/sw.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const CACHE_VERSION = 'v1';
|
||||||
|
const CACHE_NAME = `portfolio-cache-${CACHE_VERSION}`;
|
||||||
|
|
||||||
|
const URLS_TO_CACHE = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/manifest.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(URLS_TO_CACHE);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames
|
||||||
|
.filter((cacheName) => cacheName !== CACHE_NAME)
|
||||||
|
.map((cacheName) => caches.delete(cacheName))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (request.method !== 'GET') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then((cachedResponse) => {
|
||||||
|
if (cachedResponse) {
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response || response.status !== 200 || response.type === 'error') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match('/index.html');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -12,13 +12,28 @@ import { Public_Sans } from "next/font/google";
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Sharanya Sasmal — UI/UX & Graphic Designer',
|
title: 'Sharanya Sasmal — UI/UX & Graphic Designer',
|
||||||
description: 'Award-winning UI/UX and graphic designer crafting compelling digital experiences. Specializing in design systems, brand identity, and motion design.',
|
description: 'Award-winning UI/UX and graphic designer crafting compelling digital experiences. Specializing in design systems, brand identity, and motion design.',
|
||||||
|
manifest: '/manifest.json',
|
||||||
|
viewport: 'width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover',
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
statusBarStyle: 'default',
|
||||||
|
title: 'Sharanya Sasmal Portfolio'
|
||||||
|
},
|
||||||
|
formatDetection: {
|
||||||
|
telephone: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
const publicSans = Public_Sans({
|
||||||
variable: "--font-public-sans", subsets: ["latin"],
|
variable: "--font-public-sans", subsets: ["latin"],
|
||||||
|
display: 'swap',
|
||||||
|
fallback: ['system-ui', '-apple-system', 'sans-serif']
|
||||||
});
|
});
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
variable: "--font-inter", subsets: ["latin"],
|
variable: "--font-inter", subsets: ["latin"],
|
||||||
|
display: 'swap',
|
||||||
|
fallback: ['system-ui', '-apple-system', 'sans-serif']
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -28,15 +43,35 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
|
<link rel="dns-prefetch" href="//img.b2bpic.net" />
|
||||||
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
||||||
|
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
|
||||||
|
</head>
|
||||||
<ServiceWrapper>
|
<ServiceWrapper>
|
||||||
<body className={`${publicSans.variable} ${inter.variable} antialiased`}>
|
<body className={`${publicSans.variable} ${inter.variable} antialiased`}>
|
||||||
<Tag />
|
<Tag />
|
||||||
{children}
|
{children}
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `${getVisualEditScript()}`
|
__html: `${getVisualEditScript()}`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(reg => console.log('SW registered'))
|
||||||
|
.catch(err => console.log('SW registration failed'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
</ServiceWrapper>
|
</ServiceWrapper>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
42
src/middleware.ts
Normal file
42
src/middleware.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const response = NextResponse.next();
|
||||||
|
|
||||||
|
// Set caching headers for static assets
|
||||||
|
if (request.nextUrl.pathname.startsWith('/images/') ||
|
||||||
|
request.nextUrl.pathname.startsWith('/fonts/') ||
|
||||||
|
request.nextUrl.pathname.match(/\.(js|css|woff|woff2)$/)) {
|
||||||
|
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
}
|
||||||
|
// Set caching for HTML pages
|
||||||
|
else if (request.nextUrl.pathname.match(/\/$/) || request.nextUrl.pathname.match(/\.html$/)) {
|
||||||
|
response.headers.set('Cache-Control', 'public, max-age=3600, s-maxage=86400');
|
||||||
|
}
|
||||||
|
// Default caching for dynamic content
|
||||||
|
else {
|
||||||
|
response.headers.set('Cache-Control', 'public, max-age=60, s-maxage=300');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set security headers
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
||||||
|
response.headers.set('X-XSS-Protection', '1; mode=block');
|
||||||
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
* - public (public folder)
|
||||||
|
*/
|
||||||
|
'/((?!api|_next/static|_next/image|favicon.ico|public).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user