Skip to main content

Serving a Sub-App at a Clean URL Path Alongside Drupal in NGINX

I needed to serve a separate frontend app, a Vite build, at /editor on the same domain as a Drupal site. No subdomain, no port, just domain.com/editor pointing cleanly to the app's index.html while everything else continued hitting Drupal normally.

Sounded simple at first. I took a couple of wrong turns to get it right. Here's the working setup.

The Goal

domain.com/         → Drupal (existing)
domain.com/editor   → custom-app/dist/index.html (separate Vite app)

The underlying path I wanted to hide: /var/www/custom-app/dist/index.html. The URL I wanted to expose: /editor.

Wrong Turn 1: The Second Server Block

My first attempt was adding a second server {} block in NGINX for the editor, containing just the location /editor block. That immediately broke the Drupal site with a 404.

The problem: a bare location block without any listen or server_name directives isn't valid on its own. NGINX either ignored the block or used it as a default, routing requests somewhere unexpected. The server blocks were stepping on each other.

The fix: Don't create a second server block. Add the location block inside the existing server block for the domain.

Wrong Turn 2: root vs. alias

When you have a location block that maps to a path that doesn't match the URL structure, you need alias, not root.

root appends the request URI to the path:

# Requesting /editor → looks for /var/www/custom-app/dist/editor
location /editor {
    root /var/www/custom-app/dist;
}

alias replaces the matched part of the URI with the specified path:

# Requesting /editor → looks for /var/www/custom-app/dist/index.html
location /editor {
    alias /var/www/custom-app/dist;
    try_files $uri $uri/ /editor/index.html;
}

The alias directive is what you want here.

The Working Configuration

Here's what I added inside the existing domain.com server block (in my case, managed through Laravel Forge at /etc/nginx/sites-available/domain.com):

# Add this inside your existing server {} block
location /editor {
    alias /var/www/custom-app/dist;
    index index.html;
    try_files $uri $uri/ /editor/index.html;
}

The try_files line ensures that any path under /editor/* falls back to index.html — essential for single-page apps that use client-side routing. Without it, refreshing a deep route like /editor/canvas/123 returns a 404.

After adding this, test your config before reloading:

sudo nginx -t
sudo systemctl reload nginx

Coexisting with Drupal

The key thing that makes this work alongside Drupal is placement. The Drupal configuration likely has a catch-all location / block that passes requests to PHP-FPM:

location / {
    try_files $uri /index.php?$query_string;
}

The /editor location block needs to appear before this catch-all, or be more specific, so NGINX matches it first. NGINX uses the most specific matching location block, so a literal /editor prefix match will take precedence over the / catch-all regardless of order — but being explicit about placement avoids surprises.

End State

GET /              → Drupal via PHP-FPM
GET /editor        → custom-app/dist/index.html
GET /editor/       → custom-app/dist/index.html
GET /editor/foo    → custom-app/dist/index.html (SPA fallback)
GET /editor/app.js → custom-app/dist/app.js (static asset)

Clean URLs, one domain, two completely separate apps. The alias + try_files combo is the right tool for this — once you know to reach for alias instead of root, it's a pretty painless setup.