DEV Community

Waseem Akram
Waseem Akram

Posted on

πŸš€ Complete Guide: Deploy Next.js Apps as Static Sites on cPanel

Table of Contents

  1. Introduction
  2. Prerequisites
  3. What is Static Export?
  4. Step-by-Step Setup
  5. Building Your Project
  6. Uploading to cPanel
  7. Post-Deployment Verification
  8. Troubleshooting
  9. Advanced Configuration
  10. FAQ

Introduction

This guide explains how to deploy a Next.js application to cPanel as a static website. This approach is perfect for:

  • βœ… Blogs and portfolio sites
  • βœ… Marketing websites
  • βœ… Landing pages
  • βœ… Documentation sites
  • βœ… Static content with minimal dynamic features

Benefits of Static Deployment:

  • πŸš€ Lightning-fast loading times
  • πŸ’° Extremely cheap hosting (shared hosting)
  • πŸ”’ Secure (no backend vulnerabilities)
  • πŸ“ˆ SEO-friendly (pre-rendered HTML)
  • ♾️ Unlimited scalability (CDN compatible)
  • 🎯 Works on any cheap hosting (no special Node.js requirements)

Prerequisites

Before you begin, ensure you have:

Required Software

  • Node.js (v16 or higher) - Download
  • npm or yarn (comes with Node.js)
  • Text editor - VS Code, Sublime Text, or any code editor
  • Git (optional, for version control)

Required Access

  • Access to your cPanel (username and password)
  • FTP credentials or cPanel File Manager access
  • Ability to upload files to your hosting

Knowledge

  • Basic understanding of command line/terminal
  • Familiarity with Next.js (or willingness to learn)

What is Static Export?

Dynamic vs. Static Sites

Traditional Next.js (Dynamic):

User Request β†’ Next.js Server β†’ Generate HTML β†’ Send to Browser
(Requires Node.js server running 24/7)
Enter fullscreen mode Exit fullscreen mode

Static Export:

Build Time: Next.js Pre-generates ALL HTML files
Deploy Time: Upload static HTML files to hosting
User Request β†’ Web Server β†’ Serve pre-made HTML β†’ Browser
(No Node.js needed, just a web server)
Enter fullscreen mode Exit fullscreen mode

When to Use Static Export

βœ… Use Static Export if your site has:

  • Static content (blogs, portfolios, landing pages)
  • Pre-defined pages
  • Content that doesn't change per user request
  • Product/service information pages

❌ Don't Use Static Export if you need:

  • Real-time data updates
  • User-specific content
  • Server-side authentication
  • Form submissions to backend database
  • Dynamic API endpoints

Hybrid Approach

You can still have some interactivity with:

  • Client-side form handling (local state)
  • Third-party services (Formspree, EmailJS, Firebase)
  • JAMstack approach (Netlify Functions, Vercel Edge Functions)

Step-by-Step Setup

Step 1: Prepare Your Next.js Project

Ensure your project structure looks like this:

your-nextjs-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── (other pages)
β”œβ”€β”€ components/
β”œβ”€β”€ lib/
β”œβ”€β”€ public/
β”œβ”€β”€ package.json
β”œβ”€β”€ next.config.ts
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure next.config.ts for Static Export

Open your next.config.ts file and update it:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
  poweredByHeader: false,
  output: "export",           // ← Enable static export
  basePath: "",               // Change if deploying to subdirectory
  trailingSlash: true,        // Add trailing slashes for SEO
  images: {
    unoptimized: true,        // Required for static export
    formats: ["image/avif", "image/webp"]
  }
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Configuration Breakdown

Setting Purpose
output: "export" CRITICAL - Enables static export mode
basePath: "" Set to "/subdirectory" if deploying to subfolder
trailingSlash: true Adds / to all URLs (SEO best practice)
unoptimized: true Disables Next.js image optimization (incompatible with static export)

Step 3: Remove Dynamic Features

If you have these features, you need to modify or remove them:

A. Remove or Replace API Routes

❌ These won't work in static export:

// app/api/contact/route.ts
export async function POST(request: Request) {
  // This will NOT work
}
Enter fullscreen mode Exit fullscreen mode

βœ… Solution 1: Use Third-Party Service

// Form component
async function handleSubmit(formData) {
  await fetch("https://formspree.io/f/YOUR_ID", {
    method: "POST",
    body: JSON.stringify(formData),
    headers: { "Content-Type": "application/json" }
  });
}
Enter fullscreen mode Exit fullscreen mode

βœ… Solution 2: Use Third-Party Services

B. Update Dynamic Pages

If you have dynamic routes like /blog/[slug]/page.tsx:

❌ Without Static Generation:

// Won't pre-render during build
export default function BlogPost({ params }) {
  return <div>{params.slug}</div>;
}
Enter fullscreen mode Exit fullscreen mode

βœ… With Static Generation:

export async function generateStaticParams() {
  // Tell Next.js which pages to pre-render
  return [
    { slug: "first-post" },
    { slug: "second-post" },
    { slug: "third-post" }
  ];
}

export default function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  return <div>Post: {params.slug}</div>;
}
Enter fullscreen mode Exit fullscreen mode

C. Fix Special Routes

For robots.ts:

export const dynamic = "force-static";  // ← Add this line

export default function robots() {
  return {
    rules: { userAgent: "*", allow: "/" },
    sitemap: "https://yourdomain.com/sitemap.xml"
  };
}
Enter fullscreen mode Exit fullscreen mode

For sitemap.ts:

export const dynamic = "force-static";  // ← Add this line

export default function sitemap() {
  return [
    { url: "https://yourdomain.com", lastModified: new Date() },
    { url: "https://yourdomain.com/blog", lastModified: new Date() }
  ];
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create .htaccess for Routing

Create a file named .htaccess in your public/ folder:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /

  # Prevent direct access to certain folders
  RewriteRule ^\.git - [F]
  RewriteRule ^node_modules - [F]

  # Don't rewrite if the file exists
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d

  # Add trailing slash
  RewriteCond %{REQUEST_URI} !/$
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ $1/ [L,R=301]

  # Rewrite all requests to index.html for SPA routing
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^(.*)$ index.html [L]
</IfModule>

# Cache static assets
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html "access plus 1 day"
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType text/css "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
Enter fullscreen mode Exit fullscreen mode

Step 5: Update package.json Scripts

Ensure your package.json has these scripts:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "export": "next build && echo 'Export complete!'"
  }
}
Enter fullscreen mode Exit fullscreen mode

Building Your Project

Step 1: Install Dependencies

npm install
# or
yarn install
Enter fullscreen mode Exit fullscreen mode

Step 2: Test Locally (Optional)

Build and test before uploading:

npm run build
npx http-server out -p 3000
Enter fullscreen mode Exit fullscreen mode

Then visit http://localhost:3000 in your browser to test.

Step 3: Build for Production

npm run build
Enter fullscreen mode Exit fullscreen mode

Expected Output:

βœ“ Compiled successfully
βœ“ Linting and checking validity of types
βœ“ Collecting page data
βœ“ Generating static pages (XX/XX)
βœ“ Collecting build traces
βœ“ Exporting (X/X)
βœ“ Finalizing page optimization

Route (app)                    Size
β—‹ /                           X kB
β—‹ /about                       X kB
● /blog/[slug]                 X kB
...

β—‹ (Static) - prerendered as static
● (SSG)    - prerendered with generateStaticParams
Enter fullscreen mode Exit fullscreen mode

What Gets Generated

After successful build, you'll have an out/ folder containing:

out/
β”œβ”€β”€ index.html                 ← Homepage
β”œβ”€β”€ 404.html                   ← Error page
β”œβ”€β”€ robots.txt                 ← SEO
β”œβ”€β”€ sitemap.xml                ← Sitemap
β”œβ”€β”€ .htaccess                  ← Apache routing (critical!)
β”œβ”€β”€ _next/                     ← CSS, JS, assets
β”‚   β”œβ”€β”€ static/
β”‚   β”œβ”€β”€ app-build-manifest.json
β”‚   └── build-manifest.json
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ index.html
β”‚   └── [slug]/
β”‚       β”œβ”€β”€ post-1/index.html
β”‚       └── post-2/index.html
└── (other folders and assets)
Enter fullscreen mode Exit fullscreen mode

Uploading to cPanel

Method 1: Using cPanel File Manager (Easiest)

  1. Login to cPanel

    • Visit yourdomain.com:2083
    • Enter your cPanel username and password
  2. Open File Manager

    • Click "File Manager" in cPanel home
    • Navigate to public_html folder
  3. Enable Hidden Files

    • In File Manager, click "Settings" (top right)
    • Check "Show Hidden Files"
    • Click "Save"
  4. Upload Files

    • Click "Upload" button
    • Select all contents from your local out/ folder
    • Upload all files and folders
  5. Verify Upload

    • Ensure .htaccess is in public_html/
    • Ensure _next/ folder is completely uploaded
    • Check file count matches your local out/ folder

Method 2: Using FTP

For Windows:

  • Download FileZilla
  • Or use Windows built-in FTP

For Mac/Linux:

ftp yourdomain.com
# Enter username when prompted
# Enter password when prompted
cd public_html
put -r out/*
quit
Enter fullscreen mode Exit fullscreen mode

Steps:

  1. Open FTP client
  2. Host: Your domain or FTP host (from cPanel)
  3. Username: Your cPanel username
  4. Password: Your cPanel password
  5. Port: 21 (or check with your host)
  6. Navigate to public_html
  7. Upload all files from out/ folder
  8. Ensure .htaccess is included

Method 3: Using SFTP (Most Secure)

sftp username@yourdomain.com
cd public_html
put -r out/*
quit
Enter fullscreen mode Exit fullscreen mode

Post-Deployment Verification

Checklist After Upload

  • [ ] All files uploaded successfully
  • [ ] .htaccess is in public_html/ (not in a subfolder)
  • [ ] _next/ folder is complete
  • [ ] File permissions are set correctly

Set Correct File Permissions

After uploading, set permissions in File Manager:

  1. Select all files in public_html/
  2. Right-click β†’ "Change Permissions"
  3. Set to:
    • Folders: 755
    • Files: 644

Or via command line:

chmod -R 755 public_html
find public_html -type f -exec chmod 644 {} \;
Enter fullscreen mode Exit fullscreen mode

Test Your Website

  1. Visit your domain:

    • https://yourdomain.com/
    • Should load homepage
  2. Check styling:

    • CSS should be applied
    • Fonts should load
    • Colors should display correctly
  3. Test navigation:

    • Click links between pages
    • Test different routes
    • Check blog posts load (if applicable)
  4. Verify SEO files:

    • Visit https://yourdomain.com/robots.txt
    • Visit https://yourdomain.com/sitemap.xml
    • Both should be accessible
  5. Test error handling:

    • Visit https://yourdomain.com/nonexistent-page/
    • Should show your 404 page (not generic server error)

Troubleshooting

Problem 1: CSS and JavaScript Not Loading

Symptoms: Site loads but looks broken, no styling

Solutions:

  1. Check if .htaccess is uploaded:
   # Via terminal/SSH
   ls -la public_html/.htaccess
Enter fullscreen mode Exit fullscreen mode
  1. Verify .htaccess is readable:

    • Permissions should be 644
    • Not in a subfolder, directly in public_html/
  2. Check _next/ folder:

    • Should be in public_html/_next/
    • Should contain static/ subfolder
    • Should have CSS and JS files
  3. Verify basePath configuration:

    • If deploying to subfolder, update next.config.ts:
   basePath: "/subfoldername"
Enter fullscreen mode Exit fullscreen mode

Problem 2: 404 Errors or Blank Pages

Symptoms: Some pages show 404, or pages are blank

Solutions:

  1. Check .htaccess syntax:

    • Paste content into Apache validator
    • Ensure no syntax errors
  2. Verify trailing slashes:

    • Try with and without trailing slash
    • /about/ vs /about
  3. Check index.html files:

    • Every folder should have index.html
    • Example: public_html/blog/index.html
  4. Rebuild with trailing slashes:

   // In next.config.ts
   trailingSlash: true
Enter fullscreen mode Exit fullscreen mode

Problem 3: ".htaccess not allowed" Error

Symptoms: Upload works but get "htaccess not allowed" error

Solutions:

  1. Contact your hosting provider:

    • Ask to enable AllowOverride All for your domain
  2. Temporary workaround:

    • Rename .htaccess to htaccess.txt
    • Ask host to enable it manually
  3. Alternative:

    • Some hosts don't support .htaccess
    • Ask about alternative (nginx, etc.)

Problem 4: Images Not Showing

Symptoms: Broken image icons or missing images

Solutions:

  1. Verify unoptimized: true in config:
   images: {
     unoptimized: true  // Critical for static export
   }
Enter fullscreen mode Exit fullscreen mode
  1. Check image paths:

    • Images in public/ should work
    • Use relative paths: /images/my-image.png
  2. Rebuild and upload:

   npm run build
   # Re-upload out/ folder
Enter fullscreen mode Exit fullscreen mode

Problem 5: Form Submissions Not Working

Symptoms: Contact form submits but no response

Solutions:

Since you can't use backend API routes, use:

Option 1: Formspree (Email)

async function handleSubmit(e) {
  e.preventDefault();
  await fetch("https://formspree.io/f/YOUR_FORM_ID", {
    method: "POST",
    body: new FormData(e.target),
    headers: { "Accept": "application/json" }
  });
}
Enter fullscreen mode Exit fullscreen mode

Option 2: Firebase (NoSQL Database)

import { initializeApp } from "firebase/app";
import { getFirestore, collection, addDoc } from "firebase/firestore";

const firebaseConfig = { /* your config */ };
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

async function submitForm(data) {
  await addDoc(collection(db, "submissions"), data);
}
Enter fullscreen mode Exit fullscreen mode

Option 3: EmailJS (Direct Email)

import emailjs from '@emailjs/browser';

emailjs.init("YOUR_PUBLIC_KEY");
await emailjs.send("service_id", "template_id", templateParams);
Enter fullscreen mode Exit fullscreen mode

Advanced Configuration

Deploying to Subdirectory

If deploying to yourdomain.com/myapp/ instead of root:

// next.config.ts
const nextConfig: NextConfig = {
  basePath: "/myapp",  // Add this line
  output: "export",
  trailingSlash: true,
  images: { unoptimized: true }
};
Enter fullscreen mode Exit fullscreen mode

Then upload contents of out/ to public_html/myapp/

Custom Domain Configuration

For multi-domain setup in cPanel:

  1. Add parked/addon domain in cPanel
  2. Create folder for domain (e.g., public_html/mydomain)
  3. Upload out/ contents to that folder
  4. Create .htaccess in that folder with same rules

Performance Optimization

1. Enable Gzip Compression:

<IfModule mod_gzip.c>
  mod_gzip_on Yes
  mod_gzip_level 6
  mod_gzip_type text/plain text/html text/xml text/css
  mod_gzip_type application/json application/javascript
</IfModule>
Enter fullscreen mode Exit fullscreen mode

2. Add to .htaccess for faster caching:

<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType text/html "access plus 1 day"
  ExpiresByType application/javascript "access plus 30 days"
  ExpiresByType text/css "access plus 30 days"
  ExpiresByType image/* "access plus 1 year"
</IfModule>
Enter fullscreen mode Exit fullscreen mode

3. Set Cache-Control Headers:

<IfModule mod_headers.c>
  Header set Cache-Control "public, max-age=86400"
</IfModule>
Enter fullscreen mode Exit fullscreen mode

SSL/HTTPS Configuration

Most cPanel hosts offer free SSL via AutoSSL:

  1. Go to cPanel β†’ "AutoSSL"
  2. Click "Check for certificates" or "Install"
  3. Update in code:
// next.config.ts
const nextConfig = {
  // ... your config
  // Next.js automatically uses https if available
};
Enter fullscreen mode Exit fullscreen mode

SEO Optimization

1. robots.txt:

User-agent: *
Allow: /
Disallow: /_next/
Disallow: /.htaccess
Sitemap: https://yourdomain.com/sitemap.xml
Enter fullscreen mode Exit fullscreen mode

2. sitemap.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://yourdomain.com/</loc>
    <lastmod>2024-01-15</lastmod>
  </url>
  <url>
    <loc>https://yourdomain.com/blog/</loc>
    <lastmod>2024-01-14</lastmod>
  </url>
</urlset>
Enter fullscreen mode Exit fullscreen mode

FAQ

Q: Can I still use Next.js features with static export?

A: Yes, most features work:

  • βœ… React components
  • βœ… CSS/Tailwind
  • βœ… Image optimization (set unoptimized: true)
  • βœ… Font optimization
  • βœ… Dynamic routes with generateStaticParams
  • ❌ API routes (use external service)
  • ❌ Server-side rendering
  • ❌ Real-time database queries

Q: How often should I rebuild and redeploy?

A:

  • Rebuild every time you change code
  • Rebuild when adding new blog posts
  • Redeploy by uploading the new out/ folder
  • Can automate with GitHub Actions/Webhooks

Q: Can I deploy multiple Next.js apps on one cPanel account?

A: Yes! Use subdomains or subdirectories:

  • Subdomain: app1.yourdomain.com (separate addon domain)
  • Subdirectory: yourdomain.com/app1/ (update basePath)

Q: Is static export secure?

A: Yes, it's very secure:

  • No server vulnerabilities (no server code)
  • No database exposure
  • No API keys leaked
  • HTML is read-only
  • Safer than traditional CMS

Q: Can I have a blog with static export?

A: Yes! Two approaches:

1. Static Blog Posts:

export function generateStaticParams() {
  return posts.map(post => ({ slug: post.slug }));
}
Enter fullscreen mode Exit fullscreen mode

2. Headless CMS:

  • Use Strapi, Contentful, or Sanity
  • Pre-fetch content during build
  • Pre-render all pages with content

Q: What if I need dynamic features later?

A: You can:

  1. Move to a Node.js hosting (Heroku, Railway)
  2. Keep static parts, add serverless functions
  3. Use hybrid approach (static + external APIs)

Q: How do I update my site?

A:

  1. Make changes locally
  2. Run npm run build
  3. Upload new out/ folder contents
  4. Delete old files if needed

Or automate with GitHub Actions.

Q: Will SEO suffer with static export?

A: No! Actually benefits SEO:

  • All HTML pre-rendered (better crawling)
  • Instant page loads (better metrics)
  • Proper meta tags included
  • Robots.txt and sitemap.xml included

Q: Can I use a database with static export?

A: Not directly, but you can:

  1. Query database at build time
  2. Generate static pages with data
  3. Update when new data is added
  4. Use serverless functions for real-time queries

Example:

// During build
async function generateStaticParams() {
  const posts = await fetchPostsFromDatabase();
  return posts.map(post => ({ slug: post.slug }));
}
Enter fullscreen mode Exit fullscreen mode

Q: What's the maximum site size?

A: Depends on your hosting:

  • Typical cPanel: 50GB-100GB+
  • Most sites: 10MB-100MB
  • Can use CDN for images

Q: Should I version control the out/ folder?

A: No! Add to .gitignore:

# .gitignore
out/
.next/
node_modules/
Enter fullscreen mode Exit fullscreen mode

Only commit source code, not build output.


Summary Checklist

Before deploying to cPanel:

  • [ ] Updated next.config.ts with output: "export"
  • [ ] Created .htaccess in public/ folder
  • [ ] Removed or replaced API routes
  • [ ] Added generateStaticParams to dynamic pages
  • [ ] Fixed special routes (robots.ts, sitemap.ts)
  • [ ] Ran npm run build successfully
  • [ ] Tested locally (npm run dev)
  • [ ] Have cPanel login credentials
  • [ ] Know your FTP/hosting details
  • [ ] Backed up existing content (if any)

Deployment Checklist:

  • [ ] Connected to cPanel
  • [ ] Navigated to public_html
  • [ ] Uploaded all contents from out/
  • [ ] Verified .htaccess is uploaded
  • [ ] Set file permissions (755/644)
  • [ ] Enabled hidden files in File Manager
  • [ ] Tested homepage loads
  • [ ] Verified CSS/styling works
  • [ ] Tested multiple pages
  • [ ] Checked SEO files (robots.txt, sitemap.xml)
  • [ ] Tested 404 page

Additional Resources


Support & Troubleshooting

If you encounter issues:

  1. Check our Troubleshooting section (above)
  2. Review logs:
    • cPanel β†’ Error Log
    • Check for .htaccess errors
  3. Contact your hosting provider:
    • Ask if mod_rewrite is enabled
    • Ask if AllowOverride is set to All
  4. Verify file structure:
    • Ensure _next/ folder is complete
    • Ensure .htaccess is in correct location

Last Updated: January 2026

Author: Waseem Akram

License: Free to use and share


Questions or Feedback?

This knowledge base was created to help Next.js developers deploy to cPanel easily.

If you have suggestions, found errors, or need clarification, please reach out to our team.

Happy Deploying! πŸš€

Top comments (0)