Nginx configuration
Configure clean URLs, private drafts, protected directories and API headers for VoxelSite on Nginx. Includes a Forge configuration example.
If you run VoxelSite on Nginx, add URL rewrites and protected-directory rules to the site configuration. Nginx ignores the supplied Apache .htaccess files.
Without this configuration, visiting /about will show your homepage instead of about.php, because Nginx doesn't know to try the .php extension.
Configure clean URLs
VoxelSite uses PHP files such as about.php with clean URLs such as /about. Apache uses the supplied rewrite rule. On Nginx, add a named location to rewrite 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;
}
Do not add
$uri.phpas a middle argument intry_files. It can serve PHP source as text instead of passing it to PHP-FPM. Use@cleanurlswithrewrite ... lastso Nginx rechecks the PHP location.
Private Studio and API routing
The authenticated Studio MCP endpoint is /_studio/mcp.php. It must reach PHP-FPM with its Authorization header. The public visitor endpoint /mcp.php is separate. Do not expose _studio/preview/ to make preview work: Studio serves authorized previews through its API.
The examples below assume installation at the domain root. For a subdirectory installation, prefix each installation-specific location and rewrite with that directory. Keep the configured site URL consistent. Run workspace activation after configuring the server; it checks public reachability and private-path denial.
Agent API routing
Current Studio builds forward
/agent/v1/requests to the Agent API router. The optional rule below routes those requests to it without the Studio fallback.
For direct routing, add this before the location / block:
# Agent API: route all /_studio/api/agent/v1/* to the router
# The negative lookahead (?!.*\.php) prevents a rewrite loop -
# without it, the rewritten router.php URI matches this block again.
location ~ ^/_studio/api/agent/v1/(?!.*\.php)(.*)$ {
rewrite ^/_studio/api/agent/v1/(.*)$ /_studio/api/agent/v1/router.php?_path=$1&$args last;
}
This mirrors the Apache .htaccess rule, which passes the path as ?_path=$1. For example, a request to /_studio/api/agent/v1/pages will be rewritten to /_studio/api/agent/v1/router.php?_path=pages.
Recommended security blocks
Nginx ignores .htaccess. Add these rules before the location / block and any PHP handler.
Keep these denials before the generic PHP handler. Do not use a broad allow rule for the whole _studio/ directory.
PHP ZIP exports store form submissions in _submissions/. Block web access to that directory before accepting submissions, or visitors who know a file URL could read the submitted data.
# Block sensitive directories
location ~ ^/(\.ai|\.git|_data|_submissions|scripts|vendor|node_modules|docs)/ {
deny all;
return 403;
}
# Deny private Studio storage and development handlers before the PHP location
location ~ ^/_studio/(preview|data|engine|revisions|snapshots|backups|logs|templates|prompts|custom_prompts|tests|worker)(/|$) {
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)
Example for Laravel Forge: replace YOUR_SITE_ID and the PHP-FPM socket path with the values for your server.
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|_data|_submissions|scripts|vendor|node_modules|docs)/ {
deny all;
return 403;
}
# Deny private Studio storage and development handlers before the PHP location
location ~ ^/_studio/(preview|data|engine|revisions|snapshots|backups|logs|templates|prompts|custom_prompts|tests|worker)(/|$) {
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;
}
# ── Agent API routing ──
location ~ ^/_studio/api/agent/v1/(?!.*\.php)(.*)$ {
rewrite ^/_studio/api/agent/v1/(.*)$ /_studio/api/agent/v1/router.php?_path=$1&$args last;
}
# Native Studio API, including authorized preview and workspace checks
location ~ ^/_studio/api/(?!.*\.php)(.*)$ {
rewrite ^/_studio/api/(.*)$ /_studio/api/router.php?_path=/$1&$args last;
}
# ── 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;
# Forward the Authorization header for Agent REST and Studio MCP credentials
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
location ~ /\.(?!well-known).* {
deny all;
}
Keep
fastcgi_param HTTP_AUTHORIZATION $http_authorization;in the PHP handler. If PHP receives no authorization header, authenticated Agent REST and Studio MCP requests fail with HTTP 401.
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. Check that Forge reloads Nginx without a configuration error.
Found a mistake in these instructions? Open an issue.
Download VoxelSite