Next.js Project File Structure (For Frontend): SVG Management Best Practices

Next.js Project File Structure (For Frontend): SVG Management Best Practices

Next.js project file structure hierarchy with highlighted SVG management components.

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:

  1. UI components: Reusable, atomic components that serve as building blocks
  2. Layout components: Structural components that define the page layout
  3. Feature components: Components specific to certain features or domains
  4. 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

I learned about SVG performance the hard way when our dashboard page jumped to a 3.8s load time. Turns out we were loading the entire icon library on every single component. Rookie mistake.

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:

  1. Follow a clear project structure that separates concerns and encourages reusability
  2. Convert dynamic SVGs to React components for maximum flexibility
  3. Use SVGR to automate the conversion process
  4. Optimize your SVGs before adding them to your project
  5. Create a consistent icon system to maintain design coherence
  6. Store static SVGs in the public directory for direct referencing
  7. 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.

Sources:

I pieced together this approach after diving into Next.js docs, spending too many late nights on the React SVG documentation, and bookmarking every SVG article on Web.dev and Smashing Magazine. The CSS-Tricks guide on SVGs was my constant companion during this journey (I still have it open in my browser tab after 3 months). And yes, I shamelessly borrowed ideas from several example projects in the Next.js GitHub repo - no need to reinvent the wheel when you're on a deadline!

Comments

Popular posts from this blog

Climate Crisis and Innovation: Navigating Earth's Future

AI and Employment: Navigating the Changing Landscape of Work in 2024

Bing vs. Google: Has Bing Surpassed Google in 2024?