nuxt.jsnuxt3.jsnitro

Is it possible to extend this particular aspect of Nitro's runtime behaviour?


I'm experimenting with Nuxt 3, and was wondering if it was possible to override a particular part of Nitro's runtime behaviour. The Nuxt docs suggest extending Nitro's runtime behaviour is possible through server plugins, it's just a little thin on the details at the moment.

Nuxt will automatically read any files in the ~/server/plugins directory and register them as Nitro plugins. This allows extending Nitro's runtime behavior and hooking into lifecycle events.

Nuxt docs

What I'm trying to achieve:

When a request is made for a static asset, Nitro attempts to serve it from <app root>/public/. If the asset isn't found, Nitro returns a 404. I'd like to have Nitro attempt to serve the same static asset from a different sub directory of <app root>/public/, return it if found there, throw the 404 otherwise.

How this could work:

After running a build doing some digging, I found the code block responsible for serving assets seems to live in .output/server/chunks/node-server.mjs, as follows:

const _f4b49z = eventHandler((event) => {
  if (event.req.method && !METHODS.includes(event.req.method)) {
    return;
  }
  let id = decodeURIComponent(withLeadingSlash(withoutTrailingSlash(parseURL(event.req.url).pathname)));
  let asset;
  const encodingHeader = String(event.req.headers["accept-encoding"] || "");
  const encodings = encodingHeader.split(",").map((e) => EncodingMap[e.trim()]).filter(Boolean).sort().concat([""]);
  if (encodings.length > 1) {
    event.res.setHeader("Vary", "Accept-Encoding");
  }
  for (const encoding of encodings) {
    for (const _id of [id + encoding, joinURL(id, "index.html" + encoding)]) {
      const _asset = getAsset(_id);
      if (_asset) {
        asset = _asset;
        id = _id;
        break;
      }
    }
  }
  if (!asset) {
    if (isPublicAssetURL(id)) {
      throw createError({
        statusMessage: "Cannot find static asset " + id,
        statusCode: 404
      });
    }
    return;
  }
  const ifNotMatch = event.req.headers["if-none-match"] === asset.etag;
  if (ifNotMatch) {
    event.res.statusCode = 304;
    event.res.end();
    return;
  }
  const ifModifiedSinceH = event.req.headers["if-modified-since"];
  if (ifModifiedSinceH && asset.mtime) {
    if (new Date(ifModifiedSinceH) >= new Date(asset.mtime)) {
      event.res.statusCode = 304;
      event.res.end();
      return;
    }
  }
  if (asset.type && !event.res.getHeader("Content-Type")) {
    event.res.setHeader("Content-Type", asset.type);
  }
  if (asset.etag && !event.res.getHeader("ETag")) {
    event.res.setHeader("ETag", asset.etag);
  }
  if (asset.mtime && !event.res.getHeader("Last-Modified")) {
    event.res.setHeader("Last-Modified", asset.mtime);
  }
  if (asset.encoding && !event.res.getHeader("Content-Encoding")) {
    event.res.setHeader("Content-Encoding", asset.encoding);
  }
  if (asset.size && !event.res.getHeader("Content-Length")) {
    event.res.setHeader("Content-Length", asset.size);
  }
  return readAsset(id);
});

Is it possible to override that specific method? I'd basically like to change:

for (const encoding of encodings) {
  for (const _id of [id + encoding, joinURL(id, "index.html" + encoding)]) {
    const _asset = getAsset(_id);
    if (_asset) {
      asset = _asset;
      id = _id;
      break;
    }
  }
}

In to:

for (const encoding of encodings) {
  for (let _id of [id + encoding, joinURL(id, "index.html" + encoding)]) {
    let _asset = getAsset(_id);

    // if the asset wasn't found at 'public/', try 'public/subDir'
    if (!_asset) {
      _asset = assets['/subDir' + _id]
      _id = '/subDir' + _id
    }

    if (_asset) {
      asset = _asset;
      id = _id;
      break;
    }
  }
}

I'm exploring how serving multiple sites from a single Nuxt app might work, without having to change anything outside of the code base. I've managed to get along so far, but I need a solution for static assets (robots.txt, well-known URI, etc).


Solution

  • After investigating, it looks like this isn't possible from within the Nuxt app. While I wanted to achieve this within the app itself, I was able to get this to work using nginx. I'll post my solution here in case it's helpful to anyone else in future.

    Within my nginx config, I mapped the $host variable to a new $static_asset_directory variable. The value of $static_asset_directory then depends on which site the request comes in from, example-1.com or example-2.com

    map $host $static_asset_directory {
        hostnames;
        "example-1.*"         example-1;
        "example-2.*"         example-2;
        default               off;
    }
    

    I can then use the value of $static_asset_directory to define a location block for the static asset I want the Nuxt app to serve.

    location /robots.txt {
        ...
        proxy_pass        http://localhost:3000/$static_asset_directory/robots.txt;
    }
    

    This allows me to structure my Nuxt app as follows, to serve different static assets (with the same names) from the root of the domain. "example-1.com/robots.txt" will serve the file in /public/example-1/robots.txt, while "example-2.com/robots.txt" will serve /public/example-2/robots.txt.

    - public
    |_ example-1
      |_ robots.txt
    |_ example-2
      |_ robots.txt