Next.js Project File Structure (For Frontend): SVG Management Best Practices
Next.js Project File Structure (For Frontend): SVG Management Best Practices
Introduction: Why File Structure Matters in Next.js Projects
The first time I inherited that legacy codebase at StartupX, I spent three days just trying to figure out where the authentication logic lived. Talk about a nightmare!. My coffee consumption tripled that week, and my teammates still joke about how I'd randomly shout 'WHO WROTE THIS?' at my monitor. Look, I'm not just being picky about folder structure because of my mild OCD (though my color-coded sock drawer suggests otherwise). It's a crucial foundation that impacts development speed, team collaboration, and application performance.
With the release of Next.js 13+ and the introduction of the App Router, the recommended project structure has evolved significantly. And when it comes to SVGs—those lovely scalable vector graphics we all love—managing them effectively can be particularly challenging without the right approach.
After three years and five production Next.js apps, I've made every structure mistake possible so you do not have to. Let me show you what actually works. Whether you are starting a new project or looking to refactor an existing one, you will find practical insights to create a more maintainable codebase.
Next.js Project Structure: The Big Picture
When I started my third Next.js project last year, I finally stepped back and sketched our folder structure on a whiteboard before writing a single line of code. That 30-minute planning session saved us weeks of refactoring later—let me show you what worked for us. Here's what an ideal structure looks like:
my-nextjs-project/
├── app/ # App Router (Next.js 13+)
│ ├── (auth)/ # Route group for authentication
│ ├── (dashboard)/ # Route group for dashboard
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/ # Reusable components
├── contexts/ # React context providers
├── hooks/ # Custom React hooks
├── lib/ # Utility functions and libraries
├── public/ # Static assets
│ ├── fonts/ # Font files
│ ├── images/ # Image files
│ └── svg/ # Static SVG files
├── services/ # Frontend service modules for data fetching
├── store/ # Redux toolkit store configuration
│ ├── index.ts # Store setup and configuration
│ └── slices/ # Redux toolkit slices
├── styles/ # Global styles and theme files
├── types/ # TypeScript type definitions
├── next.config.js # Next.js configuration
├── package.json # Project dependencies
└── tsconfig.json # TypeScript configuration
This structure follows a feature-based and domain-driven approach, making it easier to navigate as your application grows. Let's break down each major directory and its purpose.
Understanding the App Directory in Next.js 13+
The app
directory is where the magic happens in Next.js 13+ projects. It uses the new App Router, which offers significant improvements over the older Pages Router.
app/
├── (auth)/ # Route group for authentication
│ ├── login/ # Login route
│ │ └── page.tsx # Login page component
│ ├── register/ # Register route
│ │ └── page.tsx # Register page component
│ └── layout.tsx # Shared layout for auth routes
├── (dashboard)/ # Route group for dashboard
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # Dashboard main page
│ └── [...]/ # Other dashboard routes
├── layout.tsx # Root layout (applied to all pages)
├── page.tsx # Home page
└── globals.css # Global styles
You'll notice the parentheses in (auth)
and (dashboard)
- These are route groups, a powerful Next.js feature that lets you organize routes without affecting the URL structure. They're perfect for grouping related features together while maintaining clean URLs.
The layout.tsx
files define shared UI for multiple pages, enforcing consistent layouts across your application. This is part of what makes Next.js 13's approach to UI composition so elegant.
Components Directory:
A well-organized components directory is crucial for reusability and maintainability. Here's how I structure mine:
components/
├── ui/ # Basic UI components
│ ├── Button/
│ │ ├── Button.tsx # Component code
│ │ ├── Button.test.tsx # Component tests
│ │ └── index.ts # Re-export file
│ ├── Card/
│ ├── Input/
│ └── ...
├── layout/ # Layout components
│ ├── Header/
│ ├── Footer/
│ ├── Sidebar/
│ └── ...
├── features/ # Feature-specific components
│ ├── auth/
│ ├── dashboard/
│ ├── profile/
│ └── ...
└── svgs/ # SVG components
├── icons/
├── illustrations/
└── logo/
This structure divides components into four main categories:
- UI components: Reusable, atomic components that serve as building blocks
- Layout components: Structural components that define the page layout
- Feature components: Components specific to certain features or domains
- SVG components: React components for SVGs (more on this later)
For each component, I follow a consistent structure:
Button/
├── Button.tsx # The main component
├── Button.test.tsx # Tests
└── index.ts # Re-exports the component
The index.ts
file simply re-exports the component, allowing for cleaner imports elsewhere:
// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';
This lets you import components with a cleaner syntax:
// Instead of this:
import { Button } from 'base_folder/components/ui/Button/Button';
// You can do this:
import { Button } from '
base_folder/components/ui/Button';
SVG Management: Best Practices for Next.js
The SVG nightmare hit me when our designer Slack-dropped 140 custom icons with the message 'can we implement these by Friday?' After trying three different approaches and one near-meltdown, I discovered a workflow that actually scales. Here's what saved our launch:
1. Use React Components for Dynamic SVGs
For SVGs that need to be dynamic (changing colors, sizes, or responding to interactions), convert them to React components:
components/svgs/
├── icons/ # Small, UI-related icons
│ ├── ArrowIcon.tsx
│ ├── CloseIcon.tsx
│ └── ...
├── illustrations/ # Larger illustrative graphics
│ ├── EmptyState.tsx
│ ├── Onboarding.tsx
│ └── ...
└── logo/ # Company/product logos
├── Logo.tsx
├── LogoMark.tsx
└── ...
Here's how a typical SVG component might look:
// components/svgs/icons/ArrowIcon.tsx
type ArrowIconProps = { color?: string; size?: number; direction?: 'up' | 'right' | 'down' | 'left'; className?: string; }; export const ArrowIcon = ({ color = 'currentColor', size = 24, direction = 'right', className = '' }: ArrowIconProps) => { // Calculate rotation based on direction const rotation = { up: '270deg', right: '0deg', down: '90deg', left: '180deg' }[direction]; return ( <svg
style={{ transform : `rotate(${rotation})` }}
width={size} height={size} viewBox="0 0 24 24" fill="none" className={className} > <path d="M12 4l-1.41 ..." fill={color} /> </svg> ); };
I switched to this method after a client demanded their brand icons change color on hover but maintain perfect scaling. Nothing else worked, but this saved the project deadline.
2. Use SVGR for Automatic Component Generation
I wasted an entire weekend manually converting 47 icons into React components before discovering SVGR. This little tool saved my sanity by automating the whole process with a single command.
First, install SVGR:
npm install --save-dev @svgr/webpack. Then, add it to your Next.JS configuration:
// next.config.js
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack']
});
return config;
}
};
Now, you can import SVGs directly as React components:
import ArrowIcon from '
base_folder/assets/icons/arrow.svg';
const MyComponent = () => {
return (
<div>
<ArrowIcon width={24} height={24} fill="blue" />
</div>
);
};
3. Optimize SVGs Before Adding Them to Your Project
SVGs often contain unnecessary metadata that bloats their size. Always optimize your SVGs before adding them to your project using tools like SVGO:
npm install -g svgo
svgo path/to/icon.svg -o path/to/optimized-icon.svg
I eventually got tired of manually optimizing SVGs one by one and just added SVGO to our build pipeline. Trust me - it's worth setting up svgo-loader with webpack now rather than kicking yourself later when you're dealing with hundreds of bloated SVG files. Our bundle size dropped by 18% overnight after I finally did this.
4. Create an Icon System for Consistency
Three months into development, our Figma had grown to 76 different icons. The team was copying and pasting SVG code everywhere until I finally snapped during a code review and built a proper system. Creating a consistent icon system will keep things organized:
// components/ui/Icon/Icon.tsx
import * as Icons from '
base_folder/components/svgs/icons';
type IconName = keyof typeof Icons;
type IconProps = {
name: IconName;
color?: string;
size?: number;
className?: string;
};
export const Icon = ({ name, color, size, className }: IconProps) => {
const IconComponent = Icons[name];
if (!IconComponent) {
console.warn(`Icon "${name}" does not exist`);
return null;
}
return (
<IconComponent
color={color}
size={size}
className={className}
/>
);
};
Now you can just drop in icons with clean, simple syntax like
<Icon size={16} name="Arrow" color="blue" />
wherever you need them. I actually printed out this syntax on a sticky note and slapped it on my monitor for the first week after we switched systems. Our junior dev was skeptical until I showed him how we could swap all 23 instances of the "Close" icon from red to our error-red color variable in literally one line of code. The look on his face was priceless - totally worth the 3 hours I spent refactoring the icon system during that rainy Sunday coding session.
5. Static SVGs in the Public Directory
For SVGs that don't need to be dynamic (like background decorations, illustrations, etc.), store them in the public directory:
public/
└── svg/
├── backgrounds/
│ ├── pattern-1.svg
│ └── wave.svg
└── illustrations/
├── empty-state.svg
└── 404.svg
These can then be referenced directly in your HTML or CSS:
const NotFoundPage = () => {
return (
<div>
<img
src="/svg/illustrations/404.svg"
alt="Page not found"
width={500}
height={400}
/>
</div>
);
};
Performance Considerations for SVGs
1. Use SVG Sprites for Multiple Icons
If you're using many icons, consider creating an SVG sprite to reduce HTTP requests:
// public/svg/sprite.svg <svg xmlns="http://xml_dummy_url" style="display: none;"> <symbol id="icon-arrow" viewBox="0 0 24 24"> <path d="M12 4l-1.41..." /> </symbol> <symbol id="icon-close" viewBox="0 0 24 24">
<path d="M19 6.41L17.59..." />
</symbol>
<!-- Add more icons here -->
</svg>
Then, reference them in your components:
// components/ui/IconSprite/IconSprite.tsx
type IconSpriteProps = {
id: string;
color?: string;
size?: number;
className?: string;
};
export const IconSprite = ({
id,
color = 'currentColor',
size = 24,
className = ''
}: IconSpriteProps) => {
return (
<svg
width={size}
height={size}
fill={color}
className={className}
>
<use href={`/svg/sprite.svg#icon-${id}`} />
</svg>
);
};
2. Lazy Load SVGs Below the Fold
For large SVG illustrations that aren't immediately visible, consider lazy loading them:
// components/ui/LazyImage/LazyImage.tsx
import { useState, useEffect } from 'react';
import Image from 'next/image';
type LazyImageProps = {
src: string;
alt: string;
width: number;
height: number;
};
export const LazyImage = ({ src, alt, width, height }: LazyImageProps) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
});
});
observer.observe(document.getElementById('lazy-image-container')!);
return () => {
observer.disconnect();
};
}, []);
return (
<div id="lazy-image-container" style={{ width, height }}>
{isVisible ? (
<Image src={src} alt={alt} width={width} height={height} />
) : null}
</div>
);
};
3. Consider Server Components for Static SVGs
With Next.js 13+, you can use Server Components to include SVGs directly in your server-rendered HTML, reducing client-side JavaScript:
// app/page.tsx (Server Component)
import fs from 'fs';
import path from 'path';
export default function HomePage() {
// Read SVG file directly from the filesystem
const logoSvg = fs.readFileSync(
path.join(process.cwd(), 'public/svg/logo/logo.svg'),
'utf8'
);
return (
<div>
<div dangerouslySetInnerHTML={{ __html: logoSvg }} />
</div>
);
}
Conclusion: Putting It All Together
After blowing up our codebase twice and surviving three painful refactors, I have finally landed on a setup that doesn't make me want to switch careers. Here's a summary of the key recommendations:
- Follow a clear project structure that separates concerns and encourages reusability
- Convert dynamic SVGs to React components for maximum flexibility
- Use SVGR to automate the conversion process
- Optimize your SVGs before adding them to your project
- Create a consistent icon system to maintain design coherence
- Store static SVGs in the public directory for direct referencing
- Consider performance optimizations like SVG sprites and lazy loading
Whether you are a lone wolf developer pulling all-nighters or part of a 20-person engineering team like mine, these patterns have saved us countless hours of 'Where the hell is that component?' frustration.
I learned this lesson the hard way after inheriting three different codebases with zero documentation. Now, I keep a decision log in our README that's saved our new junior dev from repeating my mistakes. Trust me—your sleep-deprived future self will thank you when debugging that weird edge case months from now.
Comments
Post a Comment
Thanks