Documentation

Headers, redirects and 404 pages

Vura serves the static files of static sites, Vite apps and the prerendered pages of Vura apps from its edge. You can set response headers, add redirects and use your own 404 page. Headers and redirects go in vura.json, in your project's root directory.

Vura reads headers and redirects after your build command finishes, so your build can write them. One example is a Content-Security-Policy with script hashes. Every other vura.json key is read before the build.

Headers

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": {
        "X-Frame-Options": "DENY",
        "Referrer-Policy": "strict-origin-when-cross-origin",
        "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
        "Strict-Transport-Security": "max-age=63072000; includeSubDomains"
      }
    },
    {
      "source": "/assets/(.*)",
      "headers": { "Cache-Control": "public, max-age=31536000, immutable" }
    }
  ]
}

Headers Vura sets

Header Default Can a rule change it?
X-Content-Type-Options nosniff Only to nosniff
X-Frame-Options SAMEORIGIN Yes, for example to DENY
Strict-Transport-Security Not sent Yes. HSTS stays off until you add it
Cache-Control See "Caching" below Yes
Content-Type From the file extension Yes

A rule can't set these headers, and the build fails if one tries: Cache-Tag, Server, any x-vura-* header, Set-Cookie, Location (use redirects), ETag, Content-Length, Content-Encoding, Content-Range, Transfer-Encoding, Connection, Keep-Alive, TE, Trailer, Upgrade, Proxy-Authenticate and Proxy-Authorization.

Redirects

{
  "redirects": [
    { "source": "/old-pricing", "destination": "/pricing", "permanent": true },
    { "source": "/blog/:slug", "destination": "/posts/:slug" },
    { "source": "/docs/(.*)", "destination": "https://docs.example.com/$1", "permanent": true }
  ]
}

Using parts of the path

In destination Is replaced by
:name The :name, :name* or :name+ part of source
$1 to $9 The first to ninth captured part, in order, counting (.*) too

$0 isn't a token: it stays in destination as ordinary text.

With :name*, an empty match also drops the / in front of it. For example, with /docs/:path* → /guide/:path*, /docs goes to /guide and /docs/a/b goes to /guide/a/b. Parameters work in the path and query of destination, never in the host.

Path patterns

source uses the same patterns in both lists.

Pattern Matches Doesn't match
/about /about /about/, /About, /about/team
/blog/:slug /blog/hello /blog, /blog/a/b
/docs/:path* /docs, /docs/, /docs/a/b /docsx
/docs/:path+ /docs/a, /docs/a/b /docs, /docs/
/assets/(.*) /assets/, /assets/app.js, /assets/a/b /assets
/(.*) Every path
/:path+/ /docs/, /docs/install/ /, /docs

Trailing slashes

/pricing and /pricing/ both serve pricing/index.html. To keep one address per page, like Vercel's "trailingSlash": false, add this redirect:

{ "redirects": [{ "source": "/:path+/", "destination": "/:path+", "permanent": true }] }

It sends /pricing/ to /pricing and leaves / alone.

404 page

If your output has a 404.html at its root, Vura serves it with status 404 whenever a page or file doesn't exist. Without one, Vura shows its own 404 page.

HEAD

A HEAD request to a static file, the SPA shell or the 404 page gets the same status, headers and caching a GET would, with no body.

A server-rendered page, Function or Dedicated, is published for GET only. HEAD on it always returns 404 at the edge, no matter what your own code does: Vura's edge only upgrades HEAD into a GET for static paths. Monitor a server-rendered page with GET, not HEAD.

Caching

File Cache-Control
Name with a content hash, such as main-BNwSecox.js, index.a1b2c3d4.css or chunk-5XKQZ2LM.js public, max-age=31536000, immutable
Everything else, including HTML public, max-age=0, must-revalidate

Vura only treats a name as hashed when the hash looks random, so a file such as settings.js, IMG_1234.jpg or photo-20240101.jpg is never cached for a year. The check is cautious: about one real hash in eight looks too much like a word and keeps revalidating, which costs a quick 304 and nothing else. Rarely, a hand-named file such as hero-Web3Logo.png looks random enough to be cached as immutable. A headers rule overrides the check either way: to cache a whole folder as immutable, add a rule like the /assets/(.*) example above.

Headers written by your build

A strict Content-Security-Policy lists hashes of inline scripts, and those exist only after the build. Write them into vura.json at the end of your build, for example with "build": "vite build && node scripts/write-csp.mjs":

// scripts/write-csp.mjs
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';

const html = readFileSync('dist/index.html', 'utf8');
const hashes = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)]
  .map(([, code]) => `'sha256-${createHash('sha256').update(code).digest('base64')}'`);

const config = JSON.parse(readFileSync('vura.json', 'utf8'));
config.headers = (config.headers ?? []).filter((rule) => !('Content-Security-Policy' in rule.headers));
config.headers.push({
  source: '/(.*)',
  headers: {
    'Content-Security-Policy':
      `default-src 'self'; script-src 'self' ${hashes.join(' ')}; connect-src 'self' ${process.env.VITE_API_URL ?? ''}`.trim(),
  },
});
writeFileSync('vura.json', `${JSON.stringify(config, null, 2)}\n`);

If the file your build leaves behind breaks one of the rules on this page, the build fails with a message naming the rule, and the deploy is refused before it goes live.

Limits

Coming from vercel.json

vercel.json vura.json
"headers": [{ "source": "/(.*)", "headers": [{ "key": "X-Frame-Options", "value": "DENY" }] }] "headers": [{ "source": "/(.*)", "headers": { "X-Frame-Options": "DENY" } }]
"redirects" with permanent The same shape
permanent omitted Vercel sends 308, Vura sends 307. Set permanent explicitly.
"trailingSlash": false The redirect under "Trailing slashes"
"cleanUrls": true Not needed: about.html and about/index.html are both served at /about
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] Not needed: Vite apps get this automatically
Redirects between hosts, such as www to the bare domain Set on the domain in the dashboard
has, missing, statusCode, regex patterns Not supported

Where this applies

Headers and redirects come from builds that Vura runs from Git. vura deploy from the CLI uploads a finished dist/ and doesn't read them. The 404 page and caching work for both. Each deployment keeps the rules it was built with, so a rollback also restores its headers and redirects.