Table of Contents
- Introduction
- Prerequisites
- What is Static Export?
- Step-by-Step Setup
- Building Your Project
- Uploading to cPanel
- Post-Deployment Verification
- Troubleshooting
- Advanced Configuration
- 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)
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)
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
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;
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
}
β
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" }
});
}
β 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>;
}
β
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>;
}
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"
};
}
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() }
];
}
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>
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!'"
}
}
Building Your Project
Step 1: Install Dependencies
npm install
# or
yarn install
Step 2: Test Locally (Optional)
Build and test before uploading:
npm run build
npx http-server out -p 3000
Then visit http://localhost:3000 in your browser to test.
Step 3: Build for Production
npm run build
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
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)
Uploading to cPanel
Method 1: Using cPanel File Manager (Easiest)
-
Login to cPanel
- Visit
yourdomain.com:2083 - Enter your cPanel username and password
- Visit
-
Open File Manager
- Click "File Manager" in cPanel home
- Navigate to
public_htmlfolder
-
Enable Hidden Files
- In File Manager, click "Settings" (top right)
- Check "Show Hidden Files"
- Click "Save"
-
Upload Files
- Click "Upload" button
- Select all contents from your local
out/folder - Upload all files and folders
-
Verify Upload
- Ensure
.htaccessis inpublic_html/ - Ensure
_next/folder is completely uploaded - Check file count matches your local
out/folder
- Ensure
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
Steps:
- Open FTP client
- Host: Your domain or FTP host (from cPanel)
- Username: Your cPanel username
- Password: Your cPanel password
- Port: 21 (or check with your host)
- Navigate to
public_html - Upload all files from
out/folder - Ensure
.htaccessis included
Method 3: Using SFTP (Most Secure)
sftp username@yourdomain.com
cd public_html
put -r out/*
quit
Post-Deployment Verification
Checklist After Upload
- [ ] All files uploaded successfully
- [ ]
.htaccessis inpublic_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:
- Select all files in
public_html/ - Right-click β "Change Permissions"
- Set to:
-
Folders:
755 -
Files:
644
-
Folders:
Or via command line:
chmod -R 755 public_html
find public_html -type f -exec chmod 644 {} \;
Test Your Website
-
Visit your domain:
https://yourdomain.com/- Should load homepage
-
Check styling:
- CSS should be applied
- Fonts should load
- Colors should display correctly
-
Test navigation:
- Click links between pages
- Test different routes
- Check blog posts load (if applicable)
-
Verify SEO files:
- Visit
https://yourdomain.com/robots.txt - Visit
https://yourdomain.com/sitemap.xml - Both should be accessible
- Visit
-
Test error handling:
- Visit
https://yourdomain.com/nonexistent-page/ - Should show your 404 page (not generic server error)
- Visit
Troubleshooting
Problem 1: CSS and JavaScript Not Loading
Symptoms: Site loads but looks broken, no styling
Solutions:
-
Check if
.htaccessis uploaded:
# Via terminal/SSH
ls -la public_html/.htaccess
-
Verify
.htaccessis readable:- Permissions should be
644 - Not in a subfolder, directly in
public_html/
- Permissions should be
-
Check
_next/folder:- Should be in
public_html/_next/ - Should contain
static/subfolder - Should have CSS and JS files
- Should be in
-
Verify basePath configuration:
- If deploying to subfolder, update
next.config.ts:
- If deploying to subfolder, update
basePath: "/subfoldername"
Problem 2: 404 Errors or Blank Pages
Symptoms: Some pages show 404, or pages are blank
Solutions:
-
Check
.htaccesssyntax:- Paste content into Apache validator
- Ensure no syntax errors
-
Verify trailing slashes:
- Try with and without trailing slash
-
/about/vs/about
-
Check index.html files:
- Every folder should have
index.html - Example:
public_html/blog/index.html
- Every folder should have
Rebuild with trailing slashes:
// In next.config.ts
trailingSlash: true
Problem 3: ".htaccess not allowed" Error
Symptoms: Upload works but get "htaccess not allowed" error
Solutions:
-
Contact your hosting provider:
- Ask to enable
AllowOverride Allfor your domain
- Ask to enable
-
Temporary workaround:
- Rename
.htaccesstohtaccess.txt - Ask host to enable it manually
- Rename
-
Alternative:
- Some hosts don't support
.htaccess - Ask about alternative (nginx, etc.)
- Some hosts don't support
Problem 4: Images Not Showing
Symptoms: Broken image icons or missing images
Solutions:
-
Verify
unoptimized: truein config:
images: {
unoptimized: true // Critical for static export
}
-
Check image paths:
- Images in
public/should work - Use relative paths:
/images/my-image.png
- Images in
Rebuild and upload:
npm run build
# Re-upload out/ folder
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" }
});
}
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);
}
Option 3: EmailJS (Direct Email)
import emailjs from '@emailjs/browser';
emailjs.init("YOUR_PUBLIC_KEY");
await emailjs.send("service_id", "template_id", templateParams);
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 }
};
Then upload contents of out/ to public_html/myapp/
Custom Domain Configuration
For multi-domain setup in cPanel:
- Add parked/addon domain in cPanel
- Create folder for domain (e.g.,
public_html/mydomain) - Upload
out/contents to that folder - Create
.htaccessin 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>
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>
3. Set Cache-Control Headers:
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=86400"
</IfModule>
SSL/HTTPS Configuration
Most cPanel hosts offer free SSL via AutoSSL:
- Go to cPanel β "AutoSSL"
- Click "Check for certificates" or "Install"
- Update in code:
// next.config.ts
const nextConfig = {
// ... your config
// Next.js automatically uses https if available
};
SEO Optimization
1. robots.txt:
User-agent: *
Allow: /
Disallow: /_next/
Disallow: /.htaccess
Sitemap: https://yourdomain.com/sitemap.xml
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>
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/(updatebasePath)
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 }));
}
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:
- Move to a Node.js hosting (Heroku, Railway)
- Keep static parts, add serverless functions
- Use hybrid approach (static + external APIs)
Q: How do I update my site?
A:
- Make changes locally
- Run
npm run build - Upload new
out/folder contents - 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:
- Query database at build time
- Generate static pages with data
- Update when new data is added
- Use serverless functions for real-time queries
Example:
// During build
async function generateStaticParams() {
const posts = await fetchPostsFromDatabase();
return posts.map(post => ({ slug: post.slug }));
}
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/
Only commit source code, not build output.
Summary Checklist
Before deploying to cPanel:
- [ ] Updated
next.config.tswithoutput: "export" - [ ] Created
.htaccessinpublic/folder - [ ] Removed or replaced API routes
- [ ] Added
generateStaticParamsto dynamic pages - [ ] Fixed special routes (robots.ts, sitemap.ts)
- [ ] Ran
npm run buildsuccessfully - [ ] 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
.htaccessis 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
- Next.js Static Export Documentation
- Apache .htaccess Guide
- cPanel File Manager Tutorial
- FTP Upload Guide
Support & Troubleshooting
If you encounter issues:
- Check our Troubleshooting section (above)
-
Review logs:
- cPanel β Error Log
- Check for
.htaccesserrors
-
Contact your hosting provider:
- Ask if mod_rewrite is enabled
- Ask if AllowOverride is set to All
-
Verify file structure:
- Ensure
_next/folder is complete - Ensure
.htaccessis in correct location
- Ensure
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)