-output',
};
export default staticConfig;
**Architecture Rationale:**
- `output: 'export'` instructs the compiler to generate a fully static site. The build process resolves all `getStaticProps` and `getStaticPaths` at compile time, outputting pure HTML files.
- `images.unoptimized: true` disables the Next.js image optimization API, which requires a Node.js server. Static exports must serve original or pre-optimized images directly.
- `trailingSlash: true` ensures consistent URL resolution across static file servers, preventing 404 errors on directory-style routes.
- `distDir: 'build-output'` isolates generated files from default `.next` cache directories, simplifying CI artifact targeting and preventing accidental cache pollution during deployment.
### Step 2: Secure Credential Management
Shared hosting providers expose FTP or SFTP endpoints for file management. Hardcoding these credentials in your repository violates security best practices and exposes your infrastructure to credential scraping. GitHub Actions provides encrypted repository secrets that inject values at runtime without persisting them in logs or version control.
Navigate to your repository settings, locate the Secrets and variables section, and register the following encrypted variables:
- `HOSTING_ENDPOINT`: The FTP/SFTP hostname provided by your hosting panel
- `HOSTING_USER`: The account identifier
- `HOSTING_PASS`: The authentication token
These values remain opaque to the workflow definition and are only decrypted during job execution.
### Step 3: Orchestrate the CI/CD Pipeline
The deployment workflow must handle environment provisioning, dependency resolution, static compilation, and artifact transfer. GitHub Actions provides a declarative YAML syntax that executes these steps in an isolated Ubuntu container.
Create `.github/workflows/static-deploy.yml` with the following structure:
```yaml
name: Static Artifact Deployment
on:
push:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: '20'
ARTIFACT_DIR: './build-output'
DEPLOY_TARGET: './public_html'
jobs:
compile-and-sync:
runs-on: ubuntu-latest
steps:
- name: Fetch Source Tree
uses: actions/checkout@v4
- name: Provision Runtime Environment
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Resolve Dependencies
run: npm ci --prefer-offline
- name: Execute Static Compilation
run: npm run build
- name: Synchronize Assets to Hosting
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.HOSTING_ENDPOINT }}
username: ${{ secrets.HOSTING_USER }}
password: ${{ secrets.HOSTING_PASS }}
local-dir: ${{ env.ARTIFACT_DIR }}/
server-dir: ${{ env.DEPLOY_TARGET }}
exclude: |
**/.git*
**/node_modules/**
**/README.md
Architecture Rationale:
workflow_dispatch enables manual triggers for emergency deployments or rollback verification without requiring a code commit.
- Environment variables centralize configuration, making branch-specific deployments or subdomain targeting trivial to adjust.
npm ci --prefer-offline guarantees deterministic builds by strictly following package-lock.json and leveraging cached modules, reducing CI execution time.
- The FTP sync action performs differential uploads by default, comparing file hashes and timestamps to transfer only modified assets. This minimizes bandwidth usage and deployment duration.
- Exclusion patterns prevent unnecessary files from reaching the production server, reducing attack surface and storage consumption.
Pitfall Guide
Static export pipelines introduce specific constraints that differ from traditional server-side deployments. Understanding these failure modes prevents production incidents.
1. Dynamic Route Omission
Explanation: Static compilation cannot resolve paths that depend on runtime data. Routes using [slug] or [id] parameters will fail to generate HTML unless explicitly mapped.
Fix: Implement generateStaticParams to return all possible route combinations at build time. For large datasets, use fallback routing or client-side data fetching with SWR/React Query.
2. Image Optimization Mismatch
Explanation: Disabling image optimization removes automatic WebP conversion, responsive sizing, and lazy loading. Serving raw PNG/JPEG files increases payload size and degrades Core Web Vitals.
Fix: Pre-optimize images using tools like sharp or squash-cli before committing. Alternatively, integrate a third-party image CDN (Cloudinary, Imgix) that handles transformation at the edge.
3. FTP Sync Conflicts
Explanation: Concurrent deployments or interrupted syncs can leave partial files on the server, causing 500 errors or broken UI states.
Fix: Enable atomic deployment strategies by syncing to a temporary directory and using a post-sync script to swap directories. Most FTP actions support --delete flags to clean stale files, but verify provider compatibility first.
4. Environment Variable Leakage
Explanation: Next.js embeds NEXT_PUBLIC_* variables directly into client-side bundles. Accidentally exposing API keys or database credentials in static assets creates irreversible security vulnerabilities.
Fix: Audit all environment variables before build. Use .env.local for secrets and .env.production for public configuration. Implement a pre-build lint step that scans for exposed tokens.
5. Build Cache Pollution
Explanation: The .next directory contains incremental build cache and server middleware. Including this in static exports bloats the artifact and may expose internal routing metadata.
Fix: Explicitly configure distDir to isolate static output. Add .next to .gitignore and verify CI exclusion rules prevent cache directories from reaching the sync step.
6. Subdomain Path Resolution
Explanation: Deploying to a subdomain or subdirectory breaks absolute path references, causing CSS/JS assets to 404.
Fix: Configure basePath in your Next.js config to match the deployment directory. Update all internal links to use relative paths or the next/link component, which automatically respects base path configuration.
7. Ignoring Client-Side Navigation Fallbacks
Explanation: Static exports generate individual HTML files per route. Direct navigation to a deep link (e.g., /about/team) may fail if the web server lacks fallback routing configuration.
Fix: Ensure your hosting environment serves index.html for unmatched routes, or generate explicit HTML files for all client-side routes using exportPathMap. Most shared hosting panels support .htaccess fallback rules for this purpose.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small marketing site (<50 pages) | Static export + FTP sync | Zero runtime overhead, fast builds, minimal bandwidth | $0 additional |
| Blog with frequent updates | Static export + webhook-triggered CI | Automated rebuilds on content changes without manual intervention | $0β$5/mo (GitHub Actions) |
| E-commerce with dynamic pricing | Hybrid: Static shell + client-side API calls | Keeps UI static while fetching real-time data via fetch/SWR | $0 additional |
| Multi-tenant SaaS | Migrate to VPS or serverless | Shared hosting cannot isolate tenant data or run background jobs | $15β$50/mo |
| High-traffic media site | Static export + CDN caching | Offloads bandwidth to edge network, reduces origin server load | $5β$20/mo (CDN) |
Configuration Template
Copy this template into your project root. Adjust environment variables and paths to match your infrastructure.
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
output: 'export',
images: { unoptimized: true },
trailingSlash: true,
distDir: 'static-build',
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://external-api.com/:path*' }
];
},
};
export default config;
# .github/workflows/deploy-static.yml
name: Static Site Deployment
on:
push:
branches: [main]
workflow_dispatch:
env:
RUNTIME_VERSION: '20'
BUILD_DIR: './static-build'
SERVER_PATH: './public_html'
jobs:
deliver:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.RUNTIME_VERSION }}
cache: 'npm'
- run: npm ci --prefer-offline
- run: npm run build
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.HOSTING_ENDPOINT }}
username: ${{ secrets.HOSTING_USER }}
password: ${{ secrets.HOSTING_PASS }}
local-dir: ${{ env.BUILD_DIR }}/
server-dir: ${{ env.SERVER_PATH }}
exclude: |
**/.git*
**/node_modules/**
**/*.map
Quick Start Guide
- Enable static output: Add
output: 'export' and images: { unoptimized: true } to your Next.js configuration file. Commit the change.
- Register hosting credentials: Navigate to GitHub repository Settings > Secrets and variables > Actions. Create
HOSTING_ENDPOINT, HOSTING_USER, and HOSTING_PASS using values from your hosting control panel.
- Initialize the workflow: Create
.github/workflows/deploy-static.yml using the template above. Adjust SERVER_PATH if deploying to a subdirectory.
- Trigger first build: Push to the
main branch or manually dispatch the workflow from the GitHub Actions tab. Monitor the compile and sync steps.
- Verify deployment: Access your domain and confirm asset loading. Check browser network tab for 404s on CSS/JS files. If paths are broken, configure
basePath or server fallback routing.
This pipeline transforms shared hosting from a deployment constraint into a viable production environment. By decoupling build execution from runtime delivery, you gain version-controlled deployments, automated synchronization, and infrastructure cost predictability. The static export model requires upfront architectural adjustments, but the operational stability and financial efficiency justify the investment for the majority of content-driven and client-side interactive applications.