Skip to content

SSG Mini Project — Build a Complete Static Site from Scratch

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SSG Mini Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete SSG-powered blog from scratch combining Next.js, markdown content, image optimization, search, and deployment to Netlify.

What You'll Learn

By the end of this project, you'll build a production-ready static site that demonstrates all the SSG concepts covered in this course: content management, data fetching, image optimization, search, analytics, and deployment.

Why It Matters

Theory without practice doesn't stick. This project brings together every SSG technique you've learned, giving you a real-world portfolio piece that showcases your static site generation skills.

Real-World Use

A developer builds their personal blog as an SSG project. It has markdown-based posts, optimized images, client-side search, privacy-friendly analytics, and auto-deploys on git push. This exact architecture powers thousands of production blogs.

Project Architecture

graph TD
    A[SSG Blog Project] --> B[Next.js SSG]
    B --> C[getStaticProps
+ getStaticPaths] C --> D[Markdown content
in /content] D --> E[Image optimization
Next/Image + Sharp] B --> F[Search
Lunr.js client-side] B --> G[Analytics
Plausible] B --> H[SEO
meta tags + sitemap] B --> I[Deployment
Netlify + CI/CD] E --> J[Build output
static HTML] F --> J G --> J H --> J I --> J J --> K[Global CDN] style A fill:#4a90d9,color:#fff style B fill:#e67e22,color:#fff style J fill:#27ae60,color:#fff

Project Setup

// next.config.js — SSG blog configuration
const withMDX = require('@next/mdx')({
    extension: /\.mdx?$/,
});

module.exports = withMDX({
    output: 'export',
    pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
    images: {
        unoptimized: true,
    },
});
// scripts/generate-content.js — Content generation script
const fs = require('fs');
const path = require('path');

const postsDir = './content/posts';
const publicDir = './public';

// Ensure directories exist
[postsDir, publicDir].forEach(dir => {
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
});

// Generate sample posts
const posts = [
    {
        slug: 'getting-started-with-ssg',
        title: 'Getting Started with Static Site Generation',
        date: '2026-06-28',
        excerpt: 'Learn the fundamentals of SSG and why it matters for modern web development.',
        tags: ['ssg', 'tutorial'],
        content: '## What is SSG?\n\nStatic Site Generation pre-builds HTML pages at compile time...\n\n## Benefits\n- Lightning fast load times\n- No server required\n- Secure by default\n\n## Code Example\n```javascript\nconst site = buildStaticSite();\n```'
    },
    {
        slug: 'image-optimization-guide',
        title: 'Complete Image Optimization Guide for Static Sites',
        date: '2026-06-27',
        excerpt: 'Master image optimization techniques to keep your static site fast and visually appealing.',
        tags: ['images', 'performance'],
        content: '## Why Image Optimization Matters\n\nImages account for 50-70% of page weight...\n\n## Best Practices\n- Use WebP format\n- Generate responsive sizes\n- Lazy load below-fold images'
    },
    {
        slug: 'deploying-static-sites',
        title: 'Deploying Static Sites to Production',
        date: '2026-06-26',
        excerpt: 'A step-by-step guide to deploying SSG sites on Netlify, Vercel, and Cloudflare Pages.',
        tags: ['deployment', 'devops'],
        content: '## Deployment Options\n\n### Netlify\nAutomatic deploys from Git...\n\n### Vercel\nOptimized for Next.js...\n\n### Cloudflare Pages\nGlobal edge network...'
    }
];

Core Implementation

// pages/index.js — Blog homepage
export default function Home({ posts, buildTime }) {
    return (
        <div className="container">
            <header>
                <h1>SSG Blog Project</h1>
                <p>Built with Next.js SSG</p>
                <input
                    type="search"
                    id="search"
                    placeholder="Search posts..."
                    className="search-input"
                />
            </header>

            <div className="posts-grid">
                {posts.map(post => (
                    <article key={post.slug} className="post-card">
                        <h2>
                            <a href={`/posts/${post.slug}`}>{post.title}</a>
                        </h2>
                        <time>{new Date(post.date).toLocaleDateString()}</time>
                        <p>{post.excerpt}</p>
                        <div className="tags">
                            {post.tags.map(tag => (
                                <span key={tag} className="tag">{tag}</span>
                            ))}
                        </div>
                    </article>
                ))}
            </div>

            <footer>
                <p>Built at: {new Date(buildTime).toISOString()}</p>
                <p>{posts.length} posts generated</p>
            </footer>
        </div>
    );
}

export async function getStaticProps() {
    const postsDir = path.join(process.cwd(), 'content', 'posts');
    const filenames = fs.readdirSync(postsDir);

    const posts = filenames
        .filter(f => f.endsWith('.md'))
        .map(f => {
            const filePath = path.join(postsDir, f);
            const content = fs.readFileSync(filePath, 'utf8');
            const { data, content: body } = matter(content);

            return {
                slug: f.replace('.md', ''),
                title: data.title,
                date: data.date.toISOString(),
                excerpt: data.excerpt,
                tags: data.tags || [],
            };
        })
        .sort((a, b) => new Date(b.date) - new Date(a.date));

    return {
        props: {
            posts,
            buildTime: Date.now(),
        },
    };
}
// pages/posts/[slug].js — Individual post page
export default function Post({ post, buildTime }) {
    return (
        <article className="post">
            <header>
                <a href="/" className="back-link"> Back to Home</a>
                <h1>{post.title}</h1>
                <div className="meta">
                    <time>{new Date(post.date).toLocaleDateString()}</time>
                    <div className="tags">
                        {post.tags.map(tag => (
                            <span key={tag} className="tag">{tag}</span>
                        ))}
                    </div>
                </div>
            </header>

            <div className="content">
                {post.content}
            </div>

            <nav className="post-nav">
                {post.prev && (
                    <a href={`/posts/${post.prev.slug}`} className="prev">
                         {post.prev.title}
                    </a>
                )}
                {post.next && (
                    <a href={`/posts/${post.next.slug}`} className="next">
                        {post.next.title} 
                    </a>
                )}
            </nav>
        </article>
    );
}

export async function getStaticPaths() {
    const postsDir = path.join(process.cwd(), 'content', 'posts');
    const filenames = fs.readdirSync(postsDir);

    const paths = filenames
        .filter(f => f.endsWith('.md'))
        .map(f => ({
            params: { slug: f.replace('.md', '') },
        }));

    return { paths, fallback: false };
}

Deployment Script

# .github/workflows/deploy.yml — GitHub Actions deployment
name: Deploy SSG Site

on:
    push:
        branches: [main]
    workflow_dispatch:

jobs:
    deploy:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v4

            - uses: actions/setup-node@v4
              with:
                  node-version: 18
                  cache: 'npm'

            - run: npm ci

            - name: Build static site
              run: npm run build

            - name: Deploy to Netlify
              uses: nwtgck/actions-netlify@v2
              with:
                  publish-dir: './out'
                  production-branch: main
                  github-token: ${{ secrets.GITHUB_TOKEN }}
                  deploy-message: 'Deploy from GitHub Actions'
              env:
                  NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
                  NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Common Mistakes

  1. Skipping error handling in data fetching. Build-time errors crash the entire build. Wrap API calls and file reads in try-catch blocks.
  2. Not optimizing the LCP image. The hero image on each page should load eagerly with priority. Other images should lazy load.
  3. Forgetting to add meta tags for SEO. Each page needs unique title, description, and Open Graph tags. Use Next.js Head component.
  4. Not testing the build before deployment. Always run npm run build locally. Build failures waste CI/CD pipeline time.
  5. Ignoring mobile responsiveness. Test on actual mobile devices, not just browser dev tools. Use CSS media queries for all breakpoints.

Practice Questions

  1. What is the recommended project structure for an SSG blog?
  2. How do you implement post pagination on the homepage?
  3. How do you create prev/next navigation between posts?
  4. What caching Strategy should you use for static assets vs HTML?
  5. How do you set up automatic deployment from a git Repository?

Challenge: Extend the SSG blog project with: category and tag pages, RSS feed generation, sitemap.xml, a custom 404 page, dark mode toggle, and search keyboard shortcut (Ctrl+K).

FAQ

Can this project scale to hundreds of posts?

Yes. The architecture supports thousands of posts. For very large sites, add ISR, pagination, and incremental builds.

Should I use MDX or plain markdown?

Start with plain markdown. Add MDX if you need interactive components in your content (code editors, charts, forms).

How do I add comments to a static blog?

Use Disqus, utterances (GitHub issues), or Webmentions. These are client-side solutions that work with static sites.

Can I use a CMS instead of markdown files?

Yes. Replace the markdown file reading with CMS API calls in getStaticProps. Contentful, Sanity, and Strapi all work well.

How do I handle multiple authors?

Add an author field to frontmatter. Create an author taxonomy page that lists posts by author. Store author bios in a data file.

Mini Project

Build and deploy your own SSG blog: create 5+ posts in markdown, implement homepage listing, individual post pages, tag/category pages, image optimization, client-side search, privacy-friendly analytics, and auto-deploy via GitHub Actions to Netlify.

What's Next

Congratulations on completing the SSG course! You've built a complete static site. Now explore the next frontend topic: Incremental Static Regeneration to add automatic content updates without full rebuilds.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro