.htaccess Configuration
This guide explains how to configure your Apache web server for both static and dynamic (SEO-enabled) deployments of the eDemand web application using .htaccess rules.
The .htaccess file is automatically generated by scripts/generate-htaccess.mjs, which runs as part of the generate:htaccess npm script — itself run automatically during npm run build (via the prebuild hook).
To generate (or regenerate) it manually, with an optional proxy port argument (defaults to 8001):
npm run generate:htaccess -- 8001
Do not edit .htaccess directly. Any manual changes will be overwritten on the next build. Instead, modify scripts/generate-htaccess.mjs if you need to change rewrite rules or caching logic — it selects which of the two configurations below to write based on NEXT_PUBLIC_SEO.
Apache Configuration (.htaccess)
Option 1: Static File Hosting (NEXT_PUBLIC_SEO unset or false)
Generated for the static export build (npm run build → out/). Dynamic-segment routes (service-details, provider-details, booking, my-service-request-details, blog-details, custom-page, chats) are rewritten to their shared [lang]/… export files, once locale-prefixed and once bare (no locale prefix, since the default locale is served unprefixed):
Options -Indexes -MultiViews
RewriteEngine On
RewriteBase /
# No server exists to detect the visitor's locale under static export —
# serve the default locale's shared build directly at "/", internally, so
# the URL never shows "/en".
RewriteRule ^$ /[lang].html [L]
# Serve a real file exactly as requested (e.g. /_next/static/..., /favicon.ico).
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]
# Prefer a route's own flat "<path>.html" export over the same-named directory.
RewriteCond %{DOCUMENT_ROOT}/$1.html -f
RewriteRule ^(.*[^/])/?$ $1.html [L]
# Dynamic-segment routes, locale-prefixed -> shared build's literal bracket-named file.
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/service-details/.*$ /[lang]/service-details/[...slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/provider-details/.*$ /[lang]/provider-details/[...slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/booking/.*$ /[lang]/booking/[...slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/my-service-request-details/.*$ /[lang]/my-service-request-details/[...slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/blog-details/.*$ /[lang]/blog-details/[slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/custom-page/.*$ /[lang]/custom-page/[slug].html [L]
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/chats(/.*)?$ /[lang]/chats/[[...chatId]].html [L]
# Same dynamic-segment routes again, bare (no locale prefix).
RewriteRule ^service-details/.*$ /[lang]/service-details/[...slug].html [L]
RewriteRule ^provider-details/.*$ /[lang]/provider-details/[...slug].html [L]
RewriteRule ^booking/.*$ /[lang]/booking/[...slug].html [L]
RewriteRule ^my-service-request-details/.*$ /[lang]/my-service-request-details/[...slug].html [L]
RewriteRule ^blog-details/.*$ /[lang]/blog-details/[slug].html [L]
RewriteRule ^custom-page/.*$ /[lang]/custom-page/[slug].html [L]
RewriteRule ^chats(/.*)?$ /[lang]/chats/[[...chatId]].html [L]
# Locale root (e.g. "/en/", "/en") -> the shared build's own root export.
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/?$ /[lang].html [L]
# Everything else under a locale prefix -> the shared "[lang]" build.
RewriteRule ^[a-z]{2}(-[A-Z]{2})?/(.*)$ /[lang]/$2.html [L]
# Everything else, bare -> the shared "[lang]" build, only if it exists
# (prevents an infinite-redirect 500 on a genuinely bad URL).
RewriteCond %{DOCUMENT_ROOT}/[lang]/$1.html -f
RewriteRule ^(.*)$ /[lang]/$1.html [L]
ErrorDocument 404 /404.html
[/] are literal, safe characters in a RewriteRule's target (substitution) — only the pattern treats them as regex. Percent-encoding them (%5B/%5D) is a bug: %5 in a target is mod_rewrite's backreference syntax, not a hex escape, so it silently produces the wrong filename.
Option 2: Reverse Proxy with SEO Optimization (NEXT_PUBLIC_SEO=true)
Generated for the standalone/VPS build. PM2 runs next start on 127.0.0.1:<port> (default 8001, or whatever port you pass to generate:htaccess); Apache reverse-proxies everything to it except static assets served directly off disk:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^favicon\.ico$ - [L]
# Allow SSL certificate verification
RewriteRule ^.well-known/acme-challenge/(.*) /.well-known/acme-challenge/$1 [L]
# Serve Next.js STATIC files directly from filesystem (JS/CSS bundles).
# Only _next/static/ — not all of _next/ (data/image requests still need the proxy).
RewriteRule ^_next/static/(.*) /.next/static/$1 [L]
# Proxy Next.js DATA requests to the Node process (required for client-side navigation).
RewriteRule ^_next/data/(.*) http://127.0.0.1:8001/_next/data/$1 [P,L]
# Serve public/'s own static files directly.
RewriteCond %{REQUEST_URI} \.(js|css|svg|jpg|jpeg|png|gif|ico|woff|woff2|ttf|eot|webp|mp4|webm)$
RewriteRule ^ - [L]
# Forward everything else to the Node process.
RewriteRule ^index.html http://127.0.0.1:8001/$1 [P]
RewriteRule ^index.php http://127.0.0.1:8001/$1 [P]
RewriteRule ^/?(.*)$ http://127.0.0.1:8001/$1 [P]
</IfModule>
Important Notes
-
Port Configuration:
- The port isn't set in
package.json— pass it to the generator instead:npm run generate:htaccess -- <port>or setHTACCESS_PROXY_PORTin your environment. Defaults to8001. - Make sure your PM2 process (
next start) is actually listening on the same port you generated the.htaccessfor.
- The port isn't set in
-
SSL/HTTPS:
- For production, always use HTTPS
- Configure SSL certificates in your Apache virtual host configuration
-
File Permissions:
- Ensure proper file permissions for your web server
- Apache should have read access to all files
- Node.js process should have necessary permissions for dynamic content
-
Performance Tips:
- Enable gzip compression in Apache configuration
- Set appropriate cache headers for static content
- Use CDN for static assets in production
-
Troubleshooting:
- Check Apache error logs if you encounter issues
- Verify that mod_rewrite is enabled for Apache
- Make sure mod_proxy is enabled when using Option 2 (Reverse Proxy)
Remember to reload Apache (sudo systemctl reload apache2) after every regenerated .htaccess — i.e. after every build, or every manual npm run generate:htaccess run.