Beyond Static: Creating a Dynamic Developer Portfolio with Next.js and Modern Web Tech
Introduction
In today’s digital age, personal branding is crucial for developers looking to stand out in a competitive job market. A personal website that showcases your technical skills, personality, and accomplishments can open doors to new opportunities. According to freeCodeCamp’s article, "Personal Branding for Developers – A Step by Step Handbook", having a well-crafted, dynamic online presence allows you to present your capabilities tangibly, making it easier for potential employers or collaborators to see your value. In this article, I’ll break down the key features, technology stack, and technical decisions behind my website—an all-in-one portfolio that does just that.
Quick links:
Portfolio → https://mikeodnis.dev
Repository → https://github.com/WomB0ComB0/portfolio
Key Features
About Page
The About page is essential for personal branding, as it allows visitors to get a detailed overview of who you are and what you bring to the table. A well-crafted about page can improve job prospects. Here, I focus on my skills, background, and career trajectory, making it clear why I’d be a valuable asset to any team.Dashboard
The dashboard demonstrates my ability to work with APIs and manage real-time data visualization. This feature provides visitors with up-to-date stats on my GitHub activity, coding efforts, and live Discord presence, helping to showcase my work dynamically and engagingly. As noted in "What Is Data Visualization? Definition, Examples, And Learning Resources", displaying data visually not only improves comprehension but also demonstrates a mastery of data interaction and presentation.Guestbook
The guestbook is more than just a fun feature—it’s an opportunity to showcase my ability to handle user-generated content and implement CRUD operations. Visitors can leave comments, and then save to Firebase, demonstrating my full-stack skills. You can learn more about building a guestbook using Next.js and Firebase from my GitHub repository.Links Section
This section aggregates my social media and professional links in one convenient location, offering a streamlined way for users to explore my online presence. This feature is crucial in personal branding as it gives visitors easy access to my various platforms, further establishing credibility and professionalism.Places Visited (Map Integration)
With an interactive map showcasing my travels, this feature adds a personal touch to my portfolio while demonstrating my ability to integrate geospatial data and APIs (like Google Maps). It’s a fun way to humanize the site while showcasing my full-stack capabilities.Spotify Integration
Music is a big part of my life, and the Spotify integration lets visitors see what I’m listening to in real time, whether it’s my top tracks or currently playing songs. This feature pulls data from the Spotify API and integrates seamlessly into the site, offering a fun, dynamic experience. You can explore how this works through the Spotify Web API Documentation.Resume Page
A resume page is essential for any developer portfolio. Visitors can view and download my resume, showcasing my technical and professional achievements in a clear, easily accessible format.
Technology Stack
Next.js 14 (App Router): Next.js is a modern React framework that offers built-in features like server-side rendering (SSR) and static site generation (SSG), improving performance and SEO. Its App Router streamlines routing, making it efficient and scalable. Read more about its benefits in "Why Next.js".
React 18: React 18's concurrent rendering and built-in hooks make it an ideal choice for creating fast and interactive user interfaces. It handles dynamic content with ease, ensuring a smooth user experience.
TypeScript: TypeScript improves the maintainability and scalability of the codebase by providing strong type-checking, reducing errors, and making it easier to collaborate. For more on why TypeScript is beneficial, check out "TypeScript for JavaScript Programmers".
Tailwind CSS: Tailwind’s utility-first approach speeds up development by allowing me to quickly build and customize styles without writing new CSS for every element. It also ensures design consistency across the site. Learn more in "Why Tailwind CSS".
Firebase: Firebase simplifies authentication and database management, offering support for Google, GitHub, and anonymous sign-ins. It’s also secure and scalable, making it a great backend option for small-to-medium-sized projects.
API Integrations
Here’s how I integrated several powerful APIs into my project:
Spotify Integration (src/lib/spotify.ts and related API routes)
Explanation:
The Spotify integration uses the OAuth 2.0 flow with refresh tokens.
getAccessToken
function refreshes the access token using the stored refresh token.topTracks
,topArtists
, andcurrentlyPlayingSong
functions use this access token to make requests to the Spotify API.Error handling and type checking are implemented for robustness.
// src/lib/spotify.ts
import axios from 'axios';
const client_id = process.env.SPOTIFY_CLIENT_ID;
const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN;
const basic = Buffer.from(`${client_id}:${client_secret}`).toString('base64');
const getAccessToken = async (): Promise<{ access_token: string }> => {
try {
const response = await axios.post(
'https://accounts.spotify.com/api/token',
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refresh_token!,
}),
{
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
return response.data;
} catch (error) {
console.error('Error getting Spotify access token:', error);
throw error;
}
};
export const topTracks = async (): Promise<any[]> => {
const { access_token } = await getAccessToken();
const response = await axios.get(
'https://api.spotify.com/v1/me/top/tracks?limit=10&time_range=short_term',
{
headers: {
Authorization: `Bearer ${access_token}`,
},
},
);
if (response.status !== 200) {
throw new Error('Failed to fetch top artists.');
}
return response.data.items;
};
// Similar functions for topArtists and currentlyPlayingSong
GitHub Stats Integration (src/app/api/v1/github-stats/route.ts)
Explanation:
This API route fetches GitHub stats using the GitHub API.
It retrieves user information and repository data in parallel for efficiency.
The data is processed to extract relevant information like total stars and top languages.
Zod is used for runtime type checking of the API response.
The processed data is cached to reduce API calls and improve performance.
import axios from 'axios';
import { NextResponse } from 'next/server';
import superjson from 'superjson';
import { z } from 'zod';
// ... (schema definitions)
export async function GET() {
try {
// ... (caching logic)
const [meResponse, reposResponse] = await Promise.all([
axios.get('https://api.github.com/users/WomB0ComB0'),
axios.get('https://api.github.com/users/WomB0ComB0/repos?per_page=100&sort=updated'),
]);
// ... (data processing and validation)
const data = {
user: {
repos: meJson.public_repos,
followers: meJson.followers,
avatar_url: meJson.avatar_url,
},
stats: {
totalStars,
topLanguages: Array.from(languages).slice(0, 5),
},
topRepos: topRepos.map((repo) => ({
name: repo.name,
description: repo.description,
stars: repo.stargazers_count,
language: repo.language,
url: repo.html_url,
})),
};
// ... (caching and response)
} catch (error) {
console.error('Error fetching GitHub stats:', error);
return NextResponse.json(
superjson.stringify({
error: error instanceof Error ? error.message : 'An unknown error occurred',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
);
}
}
Google Analytics Integration (src/app/api/v1/google/route.ts)
Explanation:
This route fetches analytics data using a custom
getAnalytics
function (defined insrc/lib/google.ts
).The response is validated using a Zod schema to ensure data integrity.
Caching is implemented to reduce API calls to Google Analytics.
Error handling provides meaningful error messages for debugging.
import { type GAResponse, getAnalytics } from '@/lib/google';
import { NextResponse } from 'next/server';
import superjson from 'superjson';
import { z } from 'zod';
// ... (schema definition)
export async function GET() {
try {
// ... (caching logic)
const parsedResp = schema.safeParse(await getAnalytics());
if (!parsedResp.success) {
throw new Error(`Umami API responded with status ${parsedResp.error}`);
}
const analytics = parsedResp.data.analytics;
// ... (caching and response)
} catch (error) {
console.error('Error fetching Umami analytics:', error);
return NextResponse.json(
superjson.stringify({
error: `${error instanceof Error ? error.message : ''}`,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
);
}
}
Discord Integration (src/app/api/v1/lanyard/route.ts)
Explanation:
This route uses the Lanyard API to fetch Discord presence information.
The response is validated against a Zod schema to ensure data consistency.
Caching is implemented to reduce API calls and improve performance.
Error handling and logging are in place for easier debugging.
import axios from 'axios';
import { NextResponse } from 'next/server';
import superjson from 'superjson';
import { z } from 'zod';
// ... (schema definition)
export async function GET() {
try {
// ... (caching logic)
const resp = await axios.get(`https://api.lanyard.rest/v1/users/${process.env.NEXT_PUBLIC_DISCORD_ID}`);
const rawData = resp.data;
const parsedResp = schema.safeParse(rawData);
if (!parsedResp.success) {
console.error('Lanyard API schema validation error:', parsedResp.error);
throw new Error(`Lanyard API response validation failed: ${parsedResp.error}`);
}
const lanyard = parsedResp.data.data;
// ... (caching and response)
} catch (error) {
console.error('Error fetching Lanyard data:', error);
return NextResponse.json(superjson.stringify({ error: 'Failed to fetch Lanyard data' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
These integrations demonstrate a consistent pattern of:
Fetching data from external APIs
Validating the response data
Implementing caching to improve performance
Error handling and logging
Using TypeScript and Zod for type safety
Authentication
Security and user experience are paramount in modern web applications. My project implements a robust authentication system using Firebase Authentication, providing a seamless and secure sign-in experience. Here's a breakdown of the authentication architecture:
Firebase Integration (src/core/firebase.ts)
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
// ... other config options
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const firestore = getFirestore(app);
export { auth, app, firestore, serviceAccount };
This setup initializes Firebase with environment-specific configuration, ensuring that sensitive credentials are not exposed in the codebase.
Authentication Logic (src/core/auth.ts)
import { GithubAuthProvider, GoogleAuthProvider, signInAnonymously, signInWithPopup } from 'firebase/auth';
import { atom, useAtomValue } from 'jotai';
import { atomEffect } from 'jotai-effect';
import { loadable } from 'jotai/utils';
export const currentUserValue = atom<User | null | undefined>(undefined);
export const currentUserListener = atomEffect((_get, set) => {
return getAuth(app).onAuthStateChanged((user) => {
set(currentUserValue, user);
});
});
export const currentUserAsync = atom(async (get) => {
get(currentUserListener);
const user = get(currentUserValue);
if (user === undefined) {
const auth = getAuth(app);
await auth.authStateReady();
return auth.currentUser;
}
return user;
});
export function useSignIn(signInMethod: SignInMethod): [signIn: () => void, inFlight: boolean] {
// Implementation for sign-in methods
}
export function useSignOut(): [signOut: () => void, inFlight: boolean] {
// Implementation for sign-out
}
This file implements:
Atom-based state management for the current user using Jotai
Custom hooks for sign-in and sign-out operations
Support for Google, GitHub, and anonymous authentication methods
Authentication UI (src/components/Guestbook.tsx)
import { useCurrentUser } from '@/core/auth';
import { LoginButton } from './custom/login-button';
import { LogoutButton } from './custom/logout-button';
export default function GuestbookComponent() {
const user = useCurrentUser();
return (
<section className="w-full min-h-full">
<article className="space-y-6">
{user ? (
<div className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-white">Welcome, {user.displayName || 'Anonymous'}</p>
<LogoutButton className="text-white hover:text-purple-300" aria-label="Sign out" />
</div>
{/* Guestbook entry form */}
</div>
) : (
<Card className="bg-purple-800 text-white">
<CardContent className="p-4 space-y-4">
<h3 className="text-lg font-semibold text-[#ba9bdd]">Leave a Message 👇</h3>
<p className="text-sm text-purple-200">You need to be signed in to post a message.</p>
<div className="flex gap-2 flex-wrap">
<LoginButton signInMethod="google.com" />
<LoginButton signInMethod="github.com" />
<LoginButton signInMethod="anonymous" />
</div>
</CardContent>
</Card>
)}
{/* Guestbook entries display */}
</article>
</section>
);
}
The Guestbook component demonstrates:
Conditional rendering based on the authentication state
Integration of custom LoginButton and LogoutButton components
User-friendly messaging for authenticated and non-authenticated states
Key Features
Multiple Authentication Methods
- Supports Google, GitHub, and anonymous sign-in, catering to user preferences and privacy concerns.
State Management
- Utilizes Jotai for efficient and reactive user state management across the application.
Secure Token Handling
- Implements Firebase's built-in token management, including automatic refresh of expired tokens.
Persistent Sessions
- Maintains user sessions across page reloads and app restarts without compromising security.
Responsive UI
- Adapt the user interface based on the authentication state, providing a seamless experience.
Security Considerations
Environment Variables
- Sensitive Firebase configuration is stored in environment variables, preventing exposure of credentials in the codebase.
HTTPS
- All authentication operations are performed over HTTPS, ensuring data integrity and confidentiality.
Token-based Authentication
- Uses Firebase's secure token-based authentication system, mitigating risks associated with session hijacking.
Principle of Least Privilege
- The anonymous authentication option allows users to interact with minimal permissions when full authentication isn't necessary.
Performance Optimization
Lazy Loading
- Authentication components are loaded on-demand, reducing the initial bundle size.
Efficient State Updates
- Jotai's atom-based state management ensures minimal re-renders when the authentication state changes.
State Management
In this project, I've implemented a hybrid state management solution that leverages the strengths of different libraries to handle various aspects of the application state efficiently. This approach allows for optimized performance, improved developer experience, and better separation of concerns.
Server-Side State Management with React Query
For managing server-side state, I chose React Query due to its powerful features for data fetching, caching, and synchronization. React Query offers several advantages:
Automatic caching and background updates
Easy pagination and infinite scrolling
Optimistic updates for a smoother user experience
Built-in devtools for debugging
Example usage in API routes:
// src/app/api/v1/top-tracks/route.ts
export async function GET() {
try {
if (cache && Date.now() - cache.timestamp < CACHE_DURATION) {
return NextResponse.json(superjson.stringify(cache.data), {
headers: { 'Content-Type': 'application/json' },
});
}
const resp = await getTopTracks();
const validatedData = schema.parse(resp);
const topTracks = validatedData.map((track) => ({
name: track.name,
artist: track.artists?.[0]?.name,
url: track.external_urls?.spotify,
imageUrl: track.album.images?.[0]?.url,
}));
cache = { data: topTracks, timestamp: Date.now() };
return NextResponse.json(superjson.stringify(topTracks), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=86400, stale-while-revalidate=43200',
},
});
} catch (error) {
console.error('Error fetching top tracks:', error);
return NextResponse.json(superjson.stringify({ error: 'Failed to fetch top tracks data' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
This approach ensures that data is efficiently cached and updated, reducing unnecessary network requests and improving application performance.
Client-Side State Management with Jotai
For managing client-side state, I opted for Jotai due to its simplicity, minimal bundle size, and excellent TypeScript support. Jotai's atom-based approach allows for fine-grained reactivity and easy composition of state.
The auth.ts
file demonstrates how Jotai is used for managing the authentication state:
// src/core/auth.ts
import { atom, useAtomValue } from 'jotai';
import { atomEffect } from 'jotai-effect';
import { loadable } from 'jotai/utils';
export const currentUserValue = atom<User | null | undefined>(undefined);
export const currentUserListener = atomEffect((_get, set) => {
return getAuth(app).onAuthStateChanged((user) => {
set(currentUserValue, user);
});
});
export const currentUserAsync = atom(async (get) => {
get(currentUserListener);
const user = get(currentUserValue);
if (user === undefined) {
const auth = getAuth(app);
await auth.authStateReady();
return auth.currentUser;
}
return user;
});
export const currentUserLoadable = loadable(currentUserAsync);
export function useCurrentUser() {
return useAtomValue(currentUserAsync);
}
export function useCurrentUserLoadable() {
return useAtomValue(currentUserLoadable);
}
Key benefits of this approach:
Atomic updates: Only components that depend on a specific piece of state are re-rendered.
Easy debugging: State changes are traceable and predictable.
Minimal boilerplate: Jotai requires less setup code compared to other state management solutions.
Seamless integration with React's Suspense and concurrent features.
Integration of Server and Client State
The combination of React Query and Jotai allows for seamless integration of server and client state. For example, in the Guestbook component:
// src/components/Guestbook.tsx
export default function GuestbookComponent() {
const user = useCurrentUser();
const { data, error, isLoading } = useQuery<ApiResponse, Error>({
queryKey: ['messages'],
queryFn: () => fetcher<ApiResponse>('/api/v1/messages'),
staleTime: 60000,
refetchInterval: 300000,
});
// ... rest of the component
}
This setup allows the component to reactively update based on both the authentication state (managed by Jotai) and the message data (managed by React Query).
Performance Considerations
Selective re-rendering: Jotai's atomic updates ensure that only components depending on changed state are re-rendered.
Efficient data fetching: React Query's caching and background updates reduce unnecessary network requests.
Code splitting: State management logic is split across relevant files, allowing for better code organization and potential code-splitting optimizations.
By leveraging React Query for server-state management and Jotai for client-state management, this project achieves a balance between simplicity and power. This approach provides efficient data synchronization, optimized rendering, and a developer-friendly state management solution without the complexity of more heavyweight alternatives like Redux.
Testing Setup
In this project, I've implemented a comprehensive testing strategy using Playwright for end-to-end testing and Vitest for unit and integration testing. This dual approach ensures both individual components and the overall user experience are thoroughly tested and reliable.
1. Unit and Integration Testing with Vitest
Vitest is configured in vitest.config.ts
:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
},
});
Key features of the Vitest setup:
React Plugin: Enables testing of React components out of the box.
JSDOM Environment: Simulates a DOM environment, allowing testing of components that interact with the DOM.
This configuration allows for:
Fast, parallel test execution
React component testing with JSX support
Mocking capabilities for isolating units of code
Example unit test:
import { describe, it, expect } from 'vitest';
import { formatDate } from '../utils/dateFormatter';
describe('formatDate utility', () => {
it('formats date correctly', () => {
const date = new Date('2023-01-01');
expect(formatDate(date)).toBe('Jan 1, 2023');
});
});
2. End-to-End Testing with Playwright
Playwright is configured in playwright.config.ts
:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './src/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
{ name: 'Microsoft Edge', use: { ...devices['Desktop Edge'], channel: 'msedge' } },
{ name: 'Google Chrome', use: { ...devices['Desktop Chrome'], channel: 'chrome' } },
],
webServer: {
command: 'bun run dev',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
Key features of the Playwright setup:
Multi-browser Testing: Tests run on Chromium, Firefox, WebKit, and mobile browsers.
CI/CD Integration: Special configurations for CI environments.
Parallel Execution: Tests run in parallel for faster completion.
Trace on Retry: Captures detailed traces on test failures for easier debugging.
Local Dev Server: Automatically starts the development server for testing.
Example E2E test:
import { test, expect } from '@playwright/test';
test('navigation works correctly', async ({ page }) => {
await page.goto('/');
await page.click('text=About');
await expect(page).toHaveURL('/about');
await expect(page.locator('h1')).toContainText('About Me');
});
3. Testing Strategy
Unit Tests: Focus on individual functions and components, ensuring they work correctly in isolation.
Integration Tests: Verify the interaction between different parts of the application, such as API calls and database interactions.
E2E Tests: Cover critical user flows, simulating real user interactions across different browsers and devices.
4. Continuous Integration
The testing setup is designed to integrate seamlessly with CI/CD pipelines:
Playwright tests are configured to run with specific settings in CI environments.
Vitest can be easily integrated into CI workflows for running unit and integration tests.
Best Practices
Test Isolation: Each test is independent and doesn't rely on the state from other tests.
Realistic Data: Use fixtures and factories to generate realistic test data.
Coverage: Aim for high test coverage, especially for critical paths and complex logic.
Regular Updates: Keep tests updated as the application evolves.
By using Vitest for quick and efficient unit and integration testing, along with Playwright for thorough end-to-end testing across various browsers and devices, this testing setup ensures a strong and dependable application. It covers everything from individual function behavior to complete user journeys, giving confidence in the application's functionality and user experience.
Deployment
This project leverages a robust Continuous Integration and Continuous Deployment (CI/CD) pipeline implemented through Vercel, ensuring seamless and reliable deployments with each update to the codebase.
Key aspects of the deployment process include:
Automated Testing
Before each deployment, the CI/CD pipeline automatically runs the comprehensive test suite, including:
Unit tests with Vitest
End-to-end tests with Playwright
This ensures that new changes don't introduce regressions or break existing functionality.
Vercel Integration
Vercel's GitHub integration allows for automatic deployments on push to the main branch. This setup includes:
Preview deployments for pull requests
Automatic rollbacks if a deployment fails
Custom domain configuration and SSL certificate management
Environment Variable Management
Sensitive data such as API keys and tokens are securely managed using Vercel's environment variables feature. This includes:
Spotify API credentials
Firebase configuration
Google Analytics tokens
Lanyard API (used to measure my activity on Discord)
Hadshnode API
Edge Network Deployment
- Vercel's edge network ensures low-latency access to the application globally, enhancing user experience regardless of geographic location.
Serverless Functions
- API routes (like those in
src/app/api/v1/
) are automatically deployed as serverless functions, providing scalable backend functionality without the need for dedicated server management.
- API routes (like those in
Performance Monitoring
Post-deployment, the application's performance is continuously monitored using:
Vercel Analytics for real-time performance insights
Sentry for error tracking and performance monitoring
Checkly for synthetic monitoring and API checks
Content Security Policy
- Implemented strict Content Security Policies to enhance security, preventing XSS attacks and other common web vulnerabilities.
Caching Strategies
- Utilized Vercel's caching capabilities along with custom caching logic in API routes (as seen in
wakatime
,top-tracks
, etc.) to optimize performance and reduce unnecessary API calls.
- Utilized Vercel's caching capabilities along with custom caching logic in API routes (as seen in
This deployment setup ensures that the portfolio remains up-to-date, secure, and performant with minimal manual intervention. It exemplifies a modern, DevOps-oriented approach to web application deployment, showcasing not just development skills but also operational expertise.
SEO & Analytics
I created a blog at blog.mikeodnis.dev using Hashnode, which comes with built-in SEO features. This ensures strong visibility while maintaining brand consistency across platforms. Additionally, the following analytics stack helps monitor site performance and user engagement:
Google Tag Manager (GTM): Centralizes tracking scripts, simplifying updates across analytics platforms without modifying site code directly.
Google Analytics: Tracks user behavior, and traffic sources, and provides insights into content performance. Integrated with GTM for more advanced tracking.
Google Search Console: Monitors search performance and optimizes visibility in Google search results.
Vercel Analytics: Offers real-time performance metrics for Vercel-hosted applications, including deployment success and function performance.
Vercel Speed Insights: Focuses on Core Web Vitals and performance improvements in page load time and overall UX.
Sentry: Provides error tracking and performance monitoring for both frontend and backend, ensuring quick identification of issues.
Checkly: Conducts synthetic monitoring and API checks, ensuring that critical site functions work globally.
This comprehensive stack enables:
Holistic monitoring across user experience, server performance, and API reliability.
Proactive issue resolution.
Data-driven optimizations in content and user interaction.
Continuous SEO tracking for enhanced search visibility.
Scripts Component
import Script from 'next/script';
import React from 'react';
export const Scripts = () => {
return (
<>
<Script
strategy="afterInteractive"
src="https://www.googletagmanager.com/gtag/js?id=G-CS1B01WMJR"
/>
<Script
strategy="afterInteractive"
id="google-analytics"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-CS1B01WMJR');
`,
}}
/>
<Script
async
src={`https://maps.googleapis.com/maps/api/js?key=${
process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
}&callback=console.debug&libraries=maps,marker&v=beta`}
/>
<Script
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-NL4XDQ2B');
`,
}}
/>
</>
);
};
Google Analytics and Tag Manager: The
Script
components load Google Analytics and Google Tag Manager scripts after the page has loaded (strategy="afterInteractive"
). ThedangerouslySetInnerHTML
attribute is used to inject inline scripts for initializing Google Analytics and Google Tag Manager.Google Maps API: Another
Script
component loads the Google Maps API asynchronously, using an API key from environment variables.
RootLayout Component
import '@/styles/globals.css';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/react';
import { Providers } from '@/providers';
import { constructMetadata, constructViewport } from '@/utils';
import type { NextWebVitalsMetric } from 'next/app';
import { Scripts } from '@/scripts/Scripts';
export const metadata = constructMetadata();
export const viewport = constructViewport();
export const reportWebVitals = (metric: NextWebVitalsMetric) => {
if (metric.label === 'web-vital') {
console.log(metric);
}
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
suppressHydrationWarning
data-a11y-animated-images="system"
data-a11y-link-underlines="false"
data-turbo-loaded
>
<head>
<Scripts />
</head>
<body>
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NL4XDQ2B"
height="0" width="0" style={{ display: 'none', visibility: 'hidden' }}></iframe>
</noscript>
<Providers>
{children}
<SpeedInsights />
<Analytics />
</Providers>
</body>
</html>
);
}
Global Styles: Imports global CSS styles.
Performance and Analytics: Integrates Vercel's Speed Insights and Analytics components for performance monitoring and analytics.
Providers: Wraps the application in a
Providers
component, which could be used for context providers.Metadata and Viewport: Constructs metadata and viewport settings using utility functions.
Web Vitals Reporting: Defines a function to log web vitals metrics.
HTML Structure: Sets up the basic HTML structure, including the
Scripts
component in the<head>
and a<noscript>
tag for Google Tag Manager.
Utility Functions
constructMetadata
import { app } from '@/constants';
import type { Metadata, Viewport } from 'next';
export function constructMetadata({
title = `${app.name}`,
description = `${app.description}`,
image = '/opengraph-image.png',
twitter = '/twitter-image.png',
icons = '/assets/svgs/logo.svg',
noIndex = false,
}: {
title?: string;
description?: string;
image?: string;
twitter?: string;
icons?: string;
noIndex?: boolean;
} = {}): Metadata {
return {
title: {
default: title,
template: `${title} - %s`,
},
description: description,
openGraph: {
title,
description,
images: [
{
url: image,
},
],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [twitter],
creator: '@OdnisMike',
},
icons: [
{
url: icons,
href: icons,
},
],
manifest: '/manifest.webmanifest',
metadataBase: new URL(app.url),
other: {
currentYear: new Date().getFullYear(),
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
...(noIndex && {
robots: {
index: false,
follow: false,
},
}),
};
}
Metadata Construction: Constructs metadata for the application, including title, description, Open Graph data, Twitter card data, icons, and other metadata. It also handles the
noIndex
option for SEO purposes.Title
default: Sets the default title for the application, which appears in the browser tab and search engine results. A clear and descriptive title helps improve click-through rates from search engines.
template: Allows for dynamic titles by appending or prepending additional text. This is useful for creating unique titles for different pages while maintaining a consistent brand identity.
Description
description: Provides a brief summary of the page
Open Graph Data
openGraph: Enhances the appearance of shared links on social media platforms like Facebook and LinkedIn. Open Graph properties ensure that shared links include a title, description, and image, making them more engaging and clickable.
title: The title of the content as it should appear when shared.
description: A brief description of the content for social media previews.
images: An array of images to be displayed when the content is shared. Visual content is more likely to attract attention and clicks.
Twitter Card Data
twitter: Similar to Open Graph, but specifically for Twitter. Twitter cards allow you to attach rich media to tweets, which can drive more traffic to your site.
card: Specifies the type of Twitter card to use (e.g., summary, summary_large_image).
title: The title of the content for Twitter.
description: A brief description of the content for Twitter.
images: An array of images for Twitter previews.
creator: The Twitter handle of the content creator, which can help with attribution and increase the reach of the tweet.
Icons
icons: Specifies the icons used for the application, such as the favicon. Icons are important for brand recognition and improving the user experience by making the site easily identifiable in browser tabs and bookmarks.
url: The URL of the icon.
href: The link to the icon file.
Manifest
- manifest: Points to the web app manifest file, which provides metadata about the application (e.g., name, icons, start URL) and is essential for Progressive Web Apps (PWAs). This helps in providing a native app-like experience on mobile devices.
Metadata Base
- metadataBase: Sets the base URL for the metadata, ensuring that relative URLs are correctly resolved. This is important for maintaining consistency and accuracy in metadata across different pages.
Other
currentYear: Dynamically includes the current year, which can be useful for copyright notices and ensuring that the content appears up-to-date.
timeZone: Specifies the time zone, which can be important for displaying time-sensitive information accurately.
NoIndex Option
true
can prevent certain pages (e.g., admin pages, login pages) from appearing in search results, which is important for security and SEO hygiene.
robots: When noIndex
is true, this property ensures that search engines do not index or follow links on the page.
#### constructViewport
typescript
export function constructViewport(): Viewport {
return {
width: 'device-width',
height: 'device-height',
initialScale: 1,
minimumScale: 1,
maximumScale: 5,
userScalable: true,
viewportFit: 'cover',
interactiveWidget: 'resizes-visual',
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#BA9BDD' },
{ media: '(prefers-color-scheme: dark)', color: '#4B0082' },
],
colorScheme: 'dark light',
};
}
Viewport Settings: Constructs viewport settings for responsive design, including initial scale, minimum and maximum scale, user scalability, and theme colors based on the user's color scheme preference.
Width
Height
Initial Scale
- initialScale: Sets the initial zoom level when the page is first loaded. An initial scale of 1 means the page will be displayed at its natural size, ensuring that users see the content as intended without any zoom.
Minimum Scale
- minimumScale: Defines the minimum zoom level that users can scale down to. Setting this to 1 ensures that users cannot zoom out to a level where the content becomes too small to read or interact with.
Maximum Scale
- maximumScale: Defines the maximum zoom level that users can scale up to. Setting this to 5 allows users to zoom in significantly, which can be helpful for accessibility purposes, enabling users with visual impairments to read content more easily.
User Scalable
- userScalable: Determines whether users can manually zoom in and out. Setting this to true allows users to control the zoom level, providing flexibility and enhancing the user experience, especially for those who need to adjust the view for better readability.
Viewport Fit
Interactive Widget
Theme Color
Color Scheme
- colorScheme: Specifies the color schemes that the application supports. Setting this to
Performance Optimization
I have employed Million.js to enhance the performance of React components. Million.js is a lightweight library that boosts React's performance with minimal code refactoring. Key benefits include:
Automatic optimization of React components.
Elimination of the Virtual DOM: Replaces React’s virtual DOM diffing algorithm for faster updates.
Compiler-based optimization at build time.
Seamless integration with React, allowing gradual adoption across projects.
To implement Million.js, I modified the Next.js configuration (next.config.mjs
) by integrating the Million.js plugin, which allows it to optimize components automatically, particularly in complex or frequently updated parts of the application.
The performance benefits from Million.js, combined with other techniques such as Next.js image optimization, API response caching, and client-side data fetching using React Query, contribute to:
Faster initial page load times.
Improved Time-to-Interactive (TTI).
Reduced Cumulative Layout Shift (CLS).
Enhanced performance for users on lower-end devices.
These optimizations, coupled with the analytics setup (Vercel Analytics, Sentry, and Checkly), ensure continuous performance tracking and optimization.
Performance Tools Setup
Here's an overview of my Next.js configuration, incorporating Million.js, Sentry, PWA support, and other performance optimizations:
import pwa from '@ducanh2912/next-pwa';
import MillionLint from '@million/lint';
import withBundleAnalyzer from '@next/bundle-analyzer';
import { withSentryConfig } from '@sentry/nextjs';
const withPwa = pwa({
dest: 'public',
disable: false,
register: true,
sw: '/sw.js',
publicExcludes: ['!noprecache/**/*'],
});
/**
* @type {import('next').NextConfig}
*/
const config = {
reactStrictMode: true,
logging: {
fetches: {
fullUrl: true,
},
},
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'lh3.googleusercontent.com' },
{ protocol: 'http', hostname: 'localhost', port: '3000' },
{ protocol: 'https', hostname: 'mikeodnis.dev' },
{ protocol: 'https', hostname: 'encrypted-tbn0.gstatic.com' },
{ protocol: 'https', hostname: 'avatars.githubusercontent.com' },
{ protocol: 'https', hostname: 'github.com' },
{ protocol: 'https', hostname: 'api.lanyard.rest' },
{ protocol: 'https', hostname: 'i.scdn.co' },
{ protocol: 'https', hostname: 'cdn.discordapp.com' },
],
},
experimental: {
optimizeCss: { preload: true },
swcMinify: true,
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
typescript: {
ignoreBuildErrors: true,
},
async rewrites() {
return [
{ source: '/healthz', destination: '/api/health' },
{ source: '/api/healthz', destination: '/api/health' },
{ source: '/health', destination: '/api/health' },
{ source: '/ping', destination: '/api/health' },
];
},
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: 'https://mikeodnis.dev' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,DELETE,PATCH,POST,PUT' },
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
},
],
},
{
source: '/(.*).png',
headers: [{ key: 'Content-Type', value: 'image/png' }],
},
];
},
webpack: (config) => {
config.module.rules.push({
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
use: [
{
loader: 'file-loader',
options: {
publicPath: '/_next',
name: 'static/media/[name].[hash].[ext]',
},
},
],
});
return config;
},
publicRuntimeConfig: {
basePath: '',
},
};
const withBundleAnalyzerConfig = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
const withMillion = MillionLint.next({
rsc: true,
filter: {
exclude: './src/components/Guestbook.tsx',
include: '**/components/*.{mtsx,mjsx,tsx,jsx}',
},
});
const combinedConfig = withMillion(withBundleAnalyzerConfig(withPwa(config)));
export default withSentryConfig(combinedConfig, {
org: 'womb0comb0',
project: 'portfolio',
sentryUrl: 'https://sentry.io/',
silent: !process.env.CI,
widenClientFileUpload: true,
reactComponentAnnotation: {
enabled: true,
},
tunnelRoute: '/monitoring',
hideSourceMaps: true,
disableLogger: true,
automaticVercelMonitors: true,
});
By leveraging cutting-edge tools like Million.js, Sentry, PWA, and a thorough analytics stack, this project showcases a comprehensive approach to modern web development, focusing on performance, SEO, and user experience.
Conclusion
This portfolio project represents more than just a showcase of skills; it's a testament to the evolving landscape of modern web development and a reflection of my journey as a developer. By leveraging cutting-edge technologies and best practices, I've created a dynamic, performant, and feature-rich application that serves multiple purposes:
Technical Showcase: The project demonstrates proficiency in a wide array of technologies, from Next.js and React to Firebase authentication and various API integrations. It showcases my ability to work with both frontend and backend technologies, as well as my understanding of performance optimization, security, and user experience.
Personal Branding: Through carefully crafted UI/UX design and personalized features like Spotify integration and a guestbook, the portfolio offers visitors a unique glimpse into my personality and interests beyond just technical skills.
Continuous Learning Platform: The modular architecture and use of modern tools like React Query and Jotai provide a flexible foundation for ongoing experimentation and learning. Each new feature or optimization becomes an opportunity to deepen my understanding of web development principles.
Real-world Problem Solving: By integrating various APIs and handling complex state management, the project demonstrates my ability to solve real-world problems and create practical applications.
Performance Benchmark: With its focus on optimization techniques like Million.js integration and efficient API caching, the portfolio serves as a benchmark for high-performance web applications.
Looking ahead, this project will continue to evolve, serving as a sandbox for exploring emerging technologies and methodologies. Potential future enhancements include:
Implementing a light mode for improved accessibility and user preference
Integrating a blog section to share technical insights and contribute to the developer community
Exploring advanced animation techniques for a more engaging user experience
Implementing machine learning features to showcase AI integration capabilities
Expanding API integrations to include platforms like Twitter or Hashnode for a more comprehensive personal dashboard
By maintaining and expanding this portfolio, I aim to not only showcase my current skills but also demonstrate my commitment to continuous improvement and adaptability in the fast-paced world of web development. It stands as a living document of my professional growth and a platform for future innovations.
This project embodies the principle that a developer's portfolio should be more than a static display—it should be a dynamic reflection of one's capabilities, interests, and potential for growth in the ever-evolving field of technology.
Subscribe to my newsletter
Read articles from Mike Odnis directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mike Odnis
Mike Odnis
My name is Mike Odnis, and I am an up an coming full-stack software engineer. Based in Long Island, New York. Mostly self-taught, first-gen. With a wealth of technical experience, and knowledge.