Nginx Configuration.
Clean URL routing and security rules for VoxelSite on Nginx servers (Forge, RunCloud, Ploi, custom VPS).
Nginx Configuration
VoxelSite ships with an .htaccess file that handles clean URL routing and security rules on Apache. Nginx ignores .htaccess entirely — you need to add equivalent rules to your Nginx site configuration.
Without this configuration, visiting /about will show your homepage instead of about.php, because Nginx doesn't know to try the .php extension.
The essential fix: clean URLs
VoxelSite generates pages as PHP files (about.php, contact.php, etc.) and links to them with clean URLs (/about, /contact). On Apache, the .htaccess rewrite rule handles this automatically. On Nginx, you need to add a named location that rewrites extensionless URLs to .php:
Find the location / block in your Nginx config and replace it with:
# Clean URLs: /about → /about.php (processed by PHP)
location / {
try_files $uri $uri/ @cleanurls;
}
location @cleanurls {
rewrite ^/(.+)$ /$1.php last;
}
Why not
$uri.phpintry_files? Adding$uri.phpas a middle argument intry_filesfinds the file but serves it as raw text — it bypasses the PHP handler. The named@cleanurlslocation withrewrite ... lastdoes a proper internal redirect that re-evaluates againstlocation ~ \.php$, ensuring PHP-FPM processes the file.
Recommended security blocks
The .htaccess also blocks access to sensitive directories and files. Add these before the location / block:
# Block sensitive directories
location ~ ^/(\.ai|\.git|scripts|vendor|node_modules|docs)/ {
deny all;
return 403;
}
# Block sensitive root files
location ~ ^/(composer\.(json|lock)|package(-lock)?\.json)$ {
deny all;
return 403;
}
# Block form definition files (contain spam protection config)
location ~ ^/assets/forms/.*\.json$ {
deny all;
return 403;
}
# Block sensitive file types
location ~ \.(db|sqlite|sqlite3|sql|sh|env|bak|log)$ {
deny all;
return 403;
}
Complete Nginx config example (Forge)
Here's a complete site configuration block for Laravel Forge. Replace the site ID (3050636) with your own:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparams.pem;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.html index.htm index.php;
charset utf-8;
# FORGE CONFIG (DO NOT REMOVE!)
include forge-conf/YOUR_SITE_ID/server/*;
# ── Security blocks ──
location ~ ^/(\.ai|\.git|scripts|vendor|node_modules|docs)/ {
deny all;
return 403;
}
location ~ ^/(composer\.(json|lock)|package(-lock)?\.json)$ {
deny all;
return 403;
}
location ~ ^/assets/forms/.*\.json$ {
deny all;
return 403;
}
location ~ \.(db|sqlite|sqlite3|sql|sh|env|bak|log)$ {
deny all;
return 403;
}
# ── Clean URL routing ──
location / {
try_files $uri $uri/ @cleanurls;
}
location @cleanurls {
rewrite ^/(.+)$ /$1.php last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /favicon.svg { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/YOUR_SITE_ID-error.log error;
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
include forge_fastcgi_defaults;
}
location ~ /\.(?!well-known).* {
deny all;
}
Where to edit this on Forge
- Go to Sites → your site → Nginx Configuration (the "Edit" button on the files panel)
- Find the
server { ... }block - Replace the
location /block and add the security rules - Click Save — Forge automatically reloads Nginx
Ready to build?
One-time purchase. Self-hosted. Own every file forever.