How to serve content based on client's request header's Accept

Hi, after days of trying about 5 different approaches to solving this I come here in defeat hoping for some help from you fine people. I can think of several possible solutions, and im not sure it can be solved purely in Hugo, most of my solutions involve huge and my server (Vercel) working together, but even then i keep reaching deadends. Let me start by explaining the problem im trying to solve, then I will explain the few solutions I tried.

If it helps the full code for this project as I describe is here, its open source. I will try to leave the relevant parts here though: Fedipage / Fedipage · GitLab

The problem

The problem is rather simple… I am using default permalinks and my page posts have the default format of /:section/:slug. For example one such page is

My problem is I need every page, using the same url, to be able to serve up either an html version, or a json version, depending on what the client sends in the request header.

For example the following should return the html (and does):

curl -X GET -H "Accept: text/html"

And the following should return json (it doesnt):

curl -X GET -H "Accept: application/activity+json"

I should point out the json version has its own template and obviously looks very different from the html version… all that is working fine when their entirely separate urls… i just cant get them both to work with the same url depending on the accept parameter.

The attempts

Now I should point out that I dont expect to fully get to a solution using pure Hugo, if I am wrong about this that is great and I welcome any such solutions,

Instead I am trying to solve this by a combination of using hugo and a feature from vercel called rewrites which let you direct a url that goes nowhere to one that goes somewhere, transparently. The priomary challenge with using rewrites has to do with the structure, ill explain. Right now when I compile the page i am able to successfully produce something like this:


as is this means when i curl, regardless of what I set in the accept parameter, it will always just return the html version. If I want the json version I can get it by curling instead.

Coming back to the vercel rewrite unfortunately it doesnt work when there is an index.html in the directory. So while it does have the support to rewrite conditionally and only rewrite when the accept parameter is json, it wont work because the index.html file is there.

Another thing I tried was to change the baseName produced to something other than “index” so now the directory structure looks like this:


When i do this the rewrite through vercel works great and i can make it so one of those two files is served up based on what I put in the accept header. However this gives rise to a new problem… all the permalinks now point to instead of the actual url I want them to point to, which is So while this does sort of give me what I am asking for it literally breaks everything else.

I tried several ways to approach this, all similar ideas… i tried finding ways to explicitly configure permalinks, and all sorts of weird hacks… nothing

I am willing to consider any reasonable solution that gets me to my goal, any ideas?

I do not understand why the rewrite cannot specifically request index.json. Is this a Vercel limitation?

1 Like

Its a limitation of vercel… you cant redirect away from a page that provides a file. When Hugo provides the index.html for that path, the vercel rewrite is ignored… vercel can only rewrite paths that would otherwise result in a 404.

Yuck. You might explore hosting alternatives.

1 Like

yea, thankfully i think im pretty close to a solution, its hacky though.

So I solved the problem with a hack. I will leave the solution here in case anyone else comes here with this problem. I still welcome a more elegant solution if anyone has one though.

Ok so the solution works by letting Hugo create the index.html variant the usual way and then writing a script that rewrites it.

The relevant part of my hugo.toml looks like this

suffixes = ["ajson"]
suffixes = ["json"]

mediaType = "application/activity+json"
notAlternative = false
baseName = "status"


This in turn will generate a structure like the following


I then write a simple node script that will rewrite the index.html to status.html as follows

'use strict';

const path = require('path');
const fs = require('fs');

const listDir = (dir, fileList = [], active = false ) => {

    let files = fs.readdirSync(dir);

    files.forEach(file => {
        if (fs.statSync(path.join(dir, file)).isDirectory()) {
            if(! ("page" === file) ) {
              fileList = listDir(path.join(dir, file), fileList, true);
        } else {
            if( (/^index\.html$/.test(file)) && (active) ) {
                const name = 'status.html';
                let src = path.join(dir, file);
                let newSrc = path.join(dir, name);
                    oldSrc: src,
                    newSrc: newSrc

    return fileList;

let foundFiles = listDir( './public/news');
listDir('./public/resource', foundFiles);
listDir('./public/projects', foundFiles);

foundFiles.forEach(f => {
   fs.renameSync(f.oldSrc, f.newSrc);

Finally I add the following rewrites to my vercel.json file:

  "rewrites": [
      "source": "/([^/]*)/(.*)/",
      "has": [
          "type": "header",
          "key": "Accept",
          "value": "application/activity\\+json"
      "destination": "/$1/$2/status.ajson"
      "source": "/([^/]*)/(.*)/",
      "missing": [
          "type": "header",
          "key": "Accept",
          "value": "application/activity\\+json"
      "destination": "/$1/$2/status.html"

Note above I needed the double backslash due to the +.

Now finally in itself in the settings of the project, general tab, you want to override the build step and use the following instead:

hugo --gc && node clean-build.cjs

That seems to do the trick, very hacky I know. Thanks everyone who viewed this and provided some consideration.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.