4 min readtroubleshooting

The Runtime Error That Made Me Distrust My Own Zola Build

A troubleshooting note on why Vercel kept crashing my API routes next to a static Zola site, and the three things that were actually wrong.

On this page

I broke my own portfolio deploy trying to add something simple: one API route sitting next to my Zola site, meant to catch a webhook. Nothing fancy. Vercel’s dashboard said the build passed. The route returned FUNCTION_INVOCATION_FAILED every single time. No stack trace worth reading, just a 500 and a request ID.

If you’ve deployed a static site generator alongside serverless functions on Vercel, you already know this genre of error. It doesn’t tell you what’s wrong — it tells you that something upstream of your code never got the chance to run.

Here’s what was actually happening, in the order I found it.

# Zola doesn’t know Vercel exists, and that’s the whole problem

Zola builds a public/ directory and stops there. It has no concept of a functions folder, no build hooks for anything outside its own templating. So the moment you drop an /api directory next to your content/ and templates/ folders, you’ve created two build systems that need to agree on one deploy, and neither of them is aware the other exists.

Vercel’s “Other” framework preset will pick up /api automatically — that part works fine. What it won’t do is run your Zola build unless you tell it to, explicitly, in vercel.json. I’d assumed the build command in the dashboard settings was enough. It wasn’t, because a previous project import had cached an old preset and silently ignored my override. The fix was to stop trusting the dashboard and put everything in the repo:

{
  "buildCommand": "curl -sL https://github.com/getzola/zola/releases/download/v0.22.1/zola-v0.22.1-x86_64-unknown-linux-musl.tar.gz | tar -zx && ./zola build",
  "outputDirectory": "public",
  "functions": {
    "api/**/*.ts": {
      "runtime": "nodejs20.x"
    }
  }
}

That single file removed an entire category of “works on my machine, fails on Vercel” confusion.

# The runtime pin is not optional anymore

This is the one that actually caused the 500s. Vercel deprecated nodejs18.x earlier this year, and if your vercel.json (or worse, a stale project setting from months ago) still references it, the function doesn’t fail loudly at build time — it fails at invocation time. That distinction cost me an hour. The build was green. The function was dead on arrival.

I’ve seen this exact failure mode before in a very different context: a nightly settlement job at a payments integration I worked on, where a runtime dependency quietly aged out of support and nobody noticed until the batch that reconciled the day’s transactions started throwing invocation errors at 2 a.m., with the scheduler reporting “success” because the job had technically started. Green checkmarks lie about the thing you actually care about. A build passing and a function being invokable are two different claims, and Vercel’s UI conflates them just enough to be dangerous.

Pin the runtime explicitly, per function, in vercel.json. Don’t let a dashboard default decide this for you.

# Output directory collisions with clean URLs

The third issue was smaller but worth naming: Zola’s clean-URL output (/about/index.html instead of /about.html) doesn’t automatically resolve against Vercel’s default routing once you’ve added a custom functions block — adding that block opts you out of some of Vercel’s zero-config static handling. I had to add explicit rewrites so /some-page still resolved to /some-page/index.html:

{
  "rewrites": [
    { "source": "/((?!api/).*)", "destination": "/$1/index.html" }
  ]
}

The negative lookahead on api/ is the part that matters. A naive "/:path*" catch-all swallows everything — including your function routes and static assets — so /api/webhook gets rewritten to a /api/webhook/index.html that doesn’t exist, and you’re right back to the 500 you started with. Excluding api/ lets the functions block keep those routes, while every other path still resolves to its clean-URL index.html.

# What I’d check first next time

If you’re in this position — static Zola site, one or two API routes, mysterious runtime failures — check these in order before you start reading logs line by line:

  1. Is the build command for Zola actually declared in vercel.json, not just the dashboard?
  2. Is the function runtime pinned to a currently supported version?
  3. Do your rewrites account for both clean URLs and the /api path, explicitly?

None of this is exotic. It’s the kind of thing that’s obvious in hindsight and invisible while you’re staring at a request ID with no stack trace. Which, if you’ve worked anywhere near a production incident, telecom or banking or otherwise, is a familiar feeling — the outage is rarely the interesting part; the config drift that caused it usually is.

views

Click to show appreciation

Discussion