Nginx serve static website and kirby panel?

Hello,

Kirby is working nicely (including accessing panel thanks to /edition slug) with this nginx config :

location / {

    # Path to source
    alias /var/www/kirby/www/;


    # Default indexes and catch-all
    index index.php index.html;
    try_files $uri $uri/ /index.php?$args =404;

    # Prevent useless logs
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Deny access to hidden files and directories
    location ~ ^/(.+/|)\.(?!well-known\/) {
        deny all;
    }

# Execute and serve PHP files
location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param REMOTE_USER $remote_user;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param SCRIPT_FILENAME $request_filename;
}

# Remove index.php from URL needed on some cms
if (!-e $request_filename) {
    rewrite ^(.*)$ /index.php?q=$1 last;
}

}

# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Block bad bots
if ($http_user_agent ~* "(BlackWidow|ChinaClaw|larbin|HTTrack|Wget|WebCopier|Go!Zilla|Zeus|EmailSiphon|EmailWolf|Express WebPictures)") {
    return 403;
}

# Disable access to hidden files except .well-known
location ~ /\.(?!well-known) {
    deny all;
}

# Restrict access to system and content files
location ~* ^/(site|kirby)/ {
    deny all;
}

location ~* ^/content/.*\.(txt|md|mdown)$ {
    deny all;
}

# Disable directory listing
autoindex off;

# Disable ETags
etag off;

# MIME and charset
include mime.types;
default_type application/octet-stream;

And that works perfectly. It serves all pages and I can acces Kirby admin panel when I visit kirby.domain.tld/edition (I choose this slug in Kirby config file).

Now, I made this plugin ( GitHub - jonathan-reisdorf/kirby-static-site-generator: Static site generator plugin for Kirby 3/4/5. With this plugin you can create a directory with assets, media and static html files generated from your pages. The result is an even faster site. · GitHub ) build a static version of my website in the /var/www/kirby/static/ directory.

What I would like is for nginx to serve the static website and I want also nginx to server kirby panel when the user visit kirby.domain.tld/edition .

Here is what I wrote (but doesn’t work so far) :

location / {

    # Path to source
    root /var/www/kirby/static/;

    # Default indexes and catch-all
    index index.php index.html;
    try_files $uri $uri/ /index.php?$args =404;

    # Prevent useless logs
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Deny access to hidden files and directories
    location ~ ^/(.+/|)\.(?!well-known\/) {
        deny all;
    }

# redirect /edition (no slash) to /edition/
location = /edition {
    return 301 /edition/;
}

# handle /edition PHP app (top-level location)
location ^~ /edition/ {
    alias /var/www/kirby/www/;
    index index.php index.html;
    try_files $uri $uri/ /edition/index.php?$args =404;

    # Remove index.php from URL needed on some cms
    if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php?q=$1 last;
    }
}

# PHP processing for files under /edition
location ~ [^/edition]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param REMOTE_USER $remote_user;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param SCRIPT_FILENAME $request_filename;
}

# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Block bad bots
if ($http_user_agent ~* "(BlackWidow|ChinaClaw|larbin|HTTrack|Wget|WebCopier|Go!Zilla|Zeus|EmailSiphon|EmailWolf|Express WebPictures)") {
    return 403;
}

# Disable access to hidden files except .well-known
location ~ /\.(?!well-known) {
    deny all;
}

# Restrict access to system and content files
location ~* ^/(site|kirby)/ {
    deny all;
}

location ~* ^/content/.*\.(txt|md|mdown)$ {
    deny all;
}

# Disable directory listing
autoindex off;

# Disable ETags
etag off;

# MIME and charset
include mime.types;
default_type application/octet-stream;

It does serve well the static website but when I go to /edition (to reach kirby panel), my web browser download /var/www/kirby/www/index.php file instead of running it.

Any idea how to make nginx work with static and kirby panel?

The reason your browser is downloading the PHP file instead of executing it is due to Location Block Inheritance.

In Nginx, once a request matches a “prefix” location (like your location ^~ /edition/), it stops looking for other matches unless specifically told otherwise. Since your PHP processing block is a separate top-level block, and you’ve used the ^~ modifier (which prevents regex matching), Nginx finds the file via alias but doesn’t know it should hand it off to the FastCGI socket.

To fix this, you need to nest the PHP handler inside the /edition/ block.

The Corrected Nginx Configuration

Here is the streamlined logic. Replace your location / and /edition/ blocks with this structure:

1. Main Static Site

location / {
root /var/www/kirby/static/;
index index.html;
try_files $uri $uri/ =404;
}

2. Kirby Panel (PHP App)

location ^~ /edition/ {
alias /var/www/kirby/www/;
index index.php;

# Basic Kirby routing within the alias
try_files $uri $uri/ /edition/index.php?$args;

# NESTED PHP processing specifically for the panel
location ~ \.php$ {
    # When using alias, we must manually set the SCRIPT_FILENAME
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    
    # This is the critical line for alias + php
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
    
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param REMOTE_USER $remote_user;
}

}

3. Handle Kirby Assets (Media/Assets for the Panel)

Since your panel is at /edition, Kirby might try to load /media or /assets

if these aren’t in your static folder, you need to route them to the PHP source:

location ~ ^/(media|assets)/ {
root /var/www/kirby/www/;
try_files $uri $uri/ =404;
}

Key Adjustments Explained

  • Nested PHP Block: By putting the location ~ \.php$ block inside the location ^~ /edition/ block, you ensure that any PHP file found within the /edition/ path is actually executed.

  • The alias vs root Trap: Nginx’s $request_filename variable behaves inconsistently with alias. In the nested PHP block, I explicitly set SCRIPT_FILENAME to the absolute path of your Kirby index.php. This is the most reliable way to ensure the FastCGI server knows exactly which file to run.

  • The try_files Logic: In the static block (location /), we removed /index.php?$args because your static site shouldn’t need to fall back to a PHP index unless the static file is missing.

  • Media/Assets: Kirby’s Panel needs access to the media folder (to generate thumbnails) and the assets folder (for the Panel’s own CSS/JS). Since your root / points to the static folder, you must add a specific block (Point #3 above) to tell Nginx to look in the www folder for these specific directories.

A Quick Security Note

Since you are now serving a static site as the “face” of the web, make sure your /var/www/kirby/static/ directory does not contain a .env file or any Kirby system folders that the static generator might have accidentally copied over. Your existing security headers at the bottom of the config will still protect the /edition/ (www) path correctly.

I’m sorry @samtaylor646 for not answering you earlier. I was expecting a notification by email and didn’t come checking in here.

Thank you for your explaination.

I’ll have a look right now at your solution and let you know.

Again, thank you very much for your very clear and precise explanation that allow me to understand better nginx.

The static part include media and assets so I don’t need

location ~ ^/(media|assets)/ {
root /var/www/kirby/www/;
try_files $uri $uri/ =404;
}

You’re right, I should have remove /index.php?$args from the static location bloc.

The static part is working nicely.

However, if I understood your explaination and it made it very clear to me, kind of obvious, unfortunately it’s not exactly working as exepected.

If I go to kirby.domain.tld/editionI get a 404 error and if I go to kirby.domain.tld/edition/ it serves me the home page, not the panel and if I go to kirby.domain.tld/edition/index.php, it serves me a This page cannot be found 😱 website page.

I change that to

location ^~ /edition {
and now, when I go to `kirby.domain.tld/editionurl, I don’t get 404 but it serves me the same thing as going to kirby.domain.tld/edition/which is, as I said, the homepage.

The core issue is that the /edition location block is not properly routing requests to Kirby’s index.php with the correct parameters. When you access /edition/, Nginx is finding files but not executing Kirby’s routing logic correctly.

Corrected Configuration

Replace your /edition location block with this:

Redirect /edition (no trailing slash) to /edition/

location = /edition {
return 301 /edition/;
}

Handle Kirby Panel

location ~ ^/edition {
alias /var/www/kirby/www/;

# Don't use index directive here - let try_files handle it

# First, try to serve the file directly
# If not found, pass everything to index.php with the original URI
try_files $uri @kirby_panel;

# Nested PHP handler for actual PHP files
location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    
    # Critical: construct the proper filesystem path
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
}

}

Named location to handle Kirby routing

location @kirby_panel {
fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
include fastcgi_params;

# Always route to index.php for Kirby routing
fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;

# Pass the original request URI so Kirby can handle routing
fastcgi_param SCRIPT_NAME /index.php;
fastcgi_param REQUEST_URI $request_uri;

}

Alternative Simpler Approach

If the above doesn’t work, try this more straightforward configuration:

location = /edition {
return 301 /edition/;
}

location ^~ /edition/ {
alias /var/www/kirby/www/;

if (!-e $request_filename) {
    rewrite ^/edition/(.*)$ /edition/index.php?$1 last;
}

location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    
    # When the request is /edition/something, we need to strip /edition
    # and pass the rest to Kirby
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
    fastcgi_param SCRIPT_NAME /edition/index.php;
    fastcgi_param REQUEST_URI $request_uri;
}

}

Key Changes:

Proper routing fallback: Using @kirby_panel named location or rewrite to always route missing files to index.php

Correct SCRIPT_FILENAME: Set to the absolute path of Kirby’s index.php

REQUEST_URI preservation: Ensures Kirby receives the full original URI including /edition so it can match against its configured panel slug

Test this configuration and let me know which error messages you receive.

PS - Thank Claude for this extra parts below. lol

The Root Cause (Kirby-Specific)

Kirby’s routing system expects to be served from the document root. When you access /edition, Kirby’s index.php needs to:

  1. Receive the request URI as /edition (matching your configured panel slug)

  2. Process it through its internal routing

  3. Recognize it as the panel route based on your config

The alias directive creates a mismatch because Kirby sees the filesystem path but not the URI path it expects.

Corrected Nginx Configuration for Kirby CMS

Static site

location / {
root /var/www/kirby/static/;
index index.html;
try_files $uri $uri/ =404;
}

Redirect /edition to /edition/

location = /edition {
return 301 /edition/;
}

Kirby Panel at /edition

location /edition/ {
# Use root instead of alias - this is critical for Kirby
root /var/www/kirby/www;

# Rewrite to strip /edition prefix for filesystem lookups
rewrite ^/edition/(.*)$ /$1 break;

# Try to serve static files first, then pass to PHP
try_files $uri $uri/ @kirby;

# Handle PHP files
location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    
    # Critical: Kirby needs to see the /edition prefix in the URI
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
    fastcgi_param SCRIPT_NAME /edition/index.php;
    fastcgi_param REQUEST_URI $request_uri;
    fastcgi_param DOCUMENT_URI $document_uri;
}

}

Fallback for Kirby routing

location @kirby {
fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
include fastcgi_params;

# These params tell Kirby: "you're being accessed at /edition"
fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
fastcgi_param SCRIPT_NAME /edition/index.php;
fastcgi_param REQUEST_URI $request_uri;

}

Kirby assets/media under /edition (if needed)

location ~ ^/edition/(assets|media|api)/ {
root /var/www/kirby/www;
rewrite ^/edition/(.*)$ /$1 break;
try_files $uri @kirby;
}


Alternative: Using Alias Correctly for Kirby

If you must use alias, here’s the proper configuration:

location ^~ /edition/ {
alias /var/www/kirby/www/;

# Critical: inform Kirby of its base path
fastcgi_param KIRBY_SERVER_NAME $host;

try_files $uri $uri/ @edition_kirby;

location ~ \.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    
    if (!-f /var/www/kirby/www/index.php) {
        return 404;
    }
    
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    
    # The magic: tell Kirby it's at /edition
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
    fastcgi_param SCRIPT_NAME /edition/index.php;
    fastcgi_param PHP_VALUE "auto_prepend_file=none";
    fastcgi_param REQUEST_URI $request_uri;
    fastcgi_param DOCUMENT_URI $document_uri;
    fastcgi_param PATH_INFO $fastcgi_path_info;
}

}

location @edition_kirby {
fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
fastcgi_param SCRIPT_NAME /edition/index.php;
fastcgi_param REQUEST_URI $request_uri;
}

Verify Your Kirby Config

Ensure your /var/www/kirby/www/site/config/config.php has:


return [
‘panel’ => [
‘slug’ => ‘edition’
]
];


Test by accessing kirby.domain.tld/edition/ and check your PHP-FPM error log if issues persist.

The first one is kind of working. I reach the login page :fireworks: but without proper css I guess :

but the second one (Alternative Simpler Approach) isn’t working. It gives me a 403 forbidden error.

Same thing for the Corrected Nginx Configuration for Kirby CMS version but I see in there a potentail solution to make the first one working.

and again the same for the Alternative: Using Alias Correctly for Kirby version.

I’ll try the first one adding this to it

Kirby assets/media under /edition (if needed)

location ~ ^/edition/(assets|media|api)/ {
root /var/www/kirby/www;
rewrite ^/edition/(.*)$ /$1 break;
try_files $uri @kirby;
}

and see if it works. I have to go but I’ll try tonight and will let you know.

I confirm that the option which is the “best” working so far is :

location / {
	root /var/www/kirby/static/;
	index index.html;
	try_files $uri $uri/ =404;

	# Prevent useless logs
	location = /favicon.ico {
		log_not_found off;
		access_log off;
	}

	location = /robots.txt {
		allow all;
		log_not_found off;
		access_log off;
	}

	# Deny access to hidden files and directories
	location ~ ^/(.+/|)\.(?!well-known\/) {
		deny all;
	}

}

location = /edition {
return 301 /edition/;
}

#Handle Kirby Panel

location ~ ^/edition {
alias /var/www/kirby/www/;

# Don't use index directive here - let try_files handle it

# First, try to serve the file directly
# If not found, pass everything to index.php with the original URI
try_files $uri @kirby_panel;

# Nested PHP handler for actual PHP files
location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;

    # Critical: construct the proper filesystem path
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
}

}

#Named location to handle Kirby routing
location @kirby_panel {
fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
include fastcgi_params;

# Always route to index.php for Kirby routing
fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;

# Pass the original request URI so Kirby can handle routing
fastcgi_param SCRIPT_NAME /index.php;
fastcgi_param REQUEST_URI $request_uri;

}

because when I go to /edition page, it redirects me automaticaly to /edition/login page which prove that index.php is well interpreted.

However, the page is similar to the screenshot when I still have some cache in my browser but fully blank on a private page.

I tried to add this

location ~ ^/edition/(assets|media|api)/ {
alias /var/www/kirby/www;
rewrite ^/edition/(.*)$ /$1 break;
try_files $uri @kirby_panel;
}

but it didn’t help.

On the page where I had still some cached element, I tried to login.

It failed because it tried to load api/auth/ping page so I guess I need to add a new bloc soimilar to the edition one.

I modified

location ~ ^/edition {

to

location ~ ^/(edition|api) {

and I could login but of course, it’s crumpy

So if we could solve Kirby assets/media under /edition (if needed) with something like
location ~ ^/edition/(assets|media|api)/ {
alias /var/www/my_webapp__4/www;
rewrite ^/edition/(.*)$ /$1 break;
try_files $uri @kirby_panel;
}
it could then be finally working.

I had a similar issues with the handshake and JS and CSS.

Replace your entire Nginx configuration with this:

# Static website (default)
location / {
    root /var/www/kirby/static/;
    index index.html;
    try_files $uri $uri/ =404;
    
    # Prevent useless logs
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }
    
    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }
    
# Deny access to hidden files and directories
    location ~ ^/(.+/|)\.(?!well-known\/) {
        deny all;
    }
}

# Redirect /edition to /edition/
location = /edition {
    return 301 /edition/;
}

# Handle Kirby Panel at /edition
location ~ ^/edition {
    alias /var/www/kirby/www/;
    try_files $uri @kirby_panel;
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
        include fastcgi_params;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_param SCRIPT_FILENAME /var/www/kirby/www$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

# CRITICAL: Handle Panel assets (CSS, JS, fonts)
location ~ ^/panel/ {
    alias /var/www/kirby/www/panel/;
    try_files $uri =404;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# Handle Kirby media files
location ~ ^/media/ {
    alias /var/www/kirby/www/media/;
    try_files $uri =404;
    expires 1y;
    add_header Cache-Control "public";
}

# Handle Kirby API calls
location ~ ^/api/ {
    alias /var/www/kirby/www/;
    try_files $uri @kirby_panel;
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
        fastcgi_param SCRIPT_NAME /index.php;
        fastcgi_param REQUEST_URI $request_uri;
    }
}

# Named location to handle Kirby routing
location @kirby_panel {
    fastcgi_pass unix:/var/run/php/php8.2-fpm-kirby.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /var/www/kirby/www/index.php;
    fastcgi_param SCRIPT_NAME /index.php;
    fastcgi_param REQUEST_URI $request_uri;
}

# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Block bad bots
if ($http_user_agent ~* "(BlackWidow|ChinaClaw|larbin|HTTrack|Wget|WebCopier|Go!Zilla|Zeus|EmailSiphon|EmailWolf|Express WebPictures)") {
    return 403;
}

# Disable access to hidden files except .well-known
location ~ /\.(?!well-known) {
    deny all;
}

# Restrict access to system and content files
location ~* ^/(site|kirby)/ {
    deny all;
}

location ~* ^/content/.*\.(txt|md|mdown)$ {
    deny all;
}

# Disable directory listing
autoindex off;

# Disable ETags
etag off;

# MIME and charset
include mime.types;
default_type application/octet-stream;

Key Additions

The solution adds three critical location blocks:

  1. location ~ ^/panel/ - Serves Panel’s CSS/JS/font assets from /var/www/kirby/www/panel/

  2. location ~ ^/media/ - Serves media files (thumbnails, uploads)

  3. location ~ ^/api/ - Handles Panel API calls for authentication and data

These paths are generated by Kirby as absolute URLs from the root, even when the panel is accessed at /edition.

Summary:

  • Panel accessed at: /edition/

  • Panel assets still live in: /var/www/kirby/www/panel/, /var/www/kirby/www/media/

  • Nginx creates virtual routes (/panel/, /media/, /api/) that map to the physical Kirby directories

  • No files are moved or copied

  • Static site at / remains unchanged

Test the configuration:


sudo nginx -t
sudo systemctl reload nginx

Then access kirby.domain.tld/edition/ and the Panel should load with full CSS/JS functionality.

Hope that finally helps. :grinning_face:

Thank you, it helps me a lot!

I had to change

location ~ ^/panel/ {
    alias /var/www/kirby/www/panel/;

to

location ^~ /assets/ {
    alias /var/www/kirby/www/asstes/;

and

location ~ ^/media/ {

to

location ^~ /media/ {

but now the panel is displaying nicely! Hurray!

However, plugins aren’t working (aren’t displaying) in panel…

it works also if I replace

location ~ ^/edition {
and
location ~ ^/api/ {

by

location ^~ /edition {
and
location ^~ /api/ {

which is better I guess.

Still problem with plugins though.

In browser console I get this error about plugins :

GET
https://kirby.yunohost.accolades.coop/media/plugins/index.css?1776370414
NS_ERROR_CORRUPTED_CONTENT

GET
https://kirby.domain.tld/media/plugins/index.js?1776370414
NS_ERROR_CORRUPTED_CONTENT

The resource from “https://kirby.domain.tld/media/plugins/index.js?1776370414” was blocked due to MIME type (“text/html”) mismatch (X-Content-Type-Options: nosniff). site
The resource from “https://kirby.domain.tld/media/plugins/index.css?1776370414” was blocked due to MIME type (“text/html”) mismatch (X-Content-Type-Options: nosniff). site
GET
https://kirby.domain.tld/media/plugins/index.js?1776370414
NS_ERROR_CORRUPTED_CONTENT

The resource from “https://kirby.domain.tld/media/plugins/index.js?1776370414” was blocked due to MIME type (“text/html”) mismatch (X-Content-Type-Options: nosniff). site
Loading failed for the <script> with source “https://kirby.domain.tld/media/plugins/index.js?1776370414”.

but it’s weird because we already write a /media block in nginx conf.

The issue is that /media/plugins/ isn’t a real directory - it’s a virtual path handled by Kirby’s PHP code to serve plugin assets dynamically.

Your current /media/ block tries to serve static files, can’t find them, falls back to @kirby_panel, which returns HTML instead of CSS/JS, triggering the MIME type error.

Solution (THANKS AGAIN TO CLAUDE CODE)

Add a specific block for /media/plugins/ before your general /media/ block:

Find this section in your Nginx config:

# Handle regular media files (images, thumbnails)

location ^~ /media/ {
alias /var/www/kirby/www/media/;
try_files $uri =404;
expires 1y;
add_header Cache-Control “public”;
}

Replace it with this:

# Handle plugin assets (must come BEFORE general /media/ block)

location ^~ /media/plugins/ {
alias /var/www/kirby/www/;
try_files $uri @kirby_panel;
}

# Handle regular media files (images, thumbnails)

location ^~ /media/ {
alias /var/www/kirby/www/media/;
try_files $uri =404;
expires 1y;
add_header Cache-Control “public”;
}

This routes /media/plugins/ requests to Kirby’s PHP handler (which generates the correct CSS/JS with proper MIME types), while keeping regular media files as static file serving.

Order matters - Nginx uses the first matching location block, so /media/plugins/ must be defined before the broader /media/ pattern.

Test:

sudo nginx -t
sudo systemctl reload nginx

The plugins should now load correctly.