The silent page awaits the waking dawn.
outputFileTracingExcludes not working for sharp, and old .edgeone artifacts being packaged again. By cleaning up after the build and removing unnecessary dependencies and leftover files, the function bundle was reduced from 141 MiB to about 112 MiB, and deployment succeeded. The author then fixed platform differences between EdgeOne and Vercel, including moving rewrites to edgeone.json, changing the edge runtime to nodejs, and reading geolocation and IP from request headers. Finally, GitHub Actions was used to automate releases by syncing from main to the deployment branch.
Introduction
I’d been self-hosting Yohaku — mx-space’s Next.js frontend — with Docker for a while. This time I wanted to see whether Tencent Cloud EdgeOne Makers (formerly EdgeOne Pages) could run it. The answer is yes: yohaku-eo.zwh.moe is now live in EdgeOne’s overseas region. But along the way, AI helped me step on quite a few landmines: function bundle size limits, differences between the platform and Vercel, and backend CORS. In this post, I’ll let AI record, in chronological order, which pitfalls it ran into and what changes it made.yohaku-eo.zwh.moe now runs in EdgeOne’s overseas region. But along the way, AI helped me step on quite a few landmines: function bundle size limits, differences between the platform and Vercel, and backend CORS. In this post, I’ll let AI record, in chronological order, which pitfalls it ran into and what changes it made.
Let’s start with the limits
Yohaku itself: Next.js 16.3.7 (built with Turbopack), a pnpm 12 monorepo, plus a dependency on the yohaku-oss submodule.
Here are the EdgeOne Makers constraints that mattered for this deployment:
Item
Description
Next.js support
13.5 through 16; App Router, SSR, ISR, RSC, and `proxy.ts` middleware are all supported
Cloud Functions
Free tier code bundle limit: 128 MiB; runtime: Node 20
next.config
The docs say `rewrites` / `redirects` aren’t supported; use `edgeone.json` instead
I deployed via the CLI: edgeone makers deploy installs dependencies locally and builds, then the @edgeone/opennextjs-pages adapter packages the output into the .edgeone/ directory for upload. That directory has three parts: assets/ is static assets, cloud-functions/ssr-node/ is the SSR function, and edge-functions/ is the compiled proxy.ts.
First small snag: if a brand-new project is deployed with -e preview preview directly, it fails with Please create a production deployment first; you need one production deployment first.
141 MiB: function bundle too large
The parts I was originally most worried about — the monorepo, pnpm 12, the submodule, Next 16 — weren’t the problem. The build and upload both went through on the first try. What got stuck was the platform validation step:
If you unpack ssr-node , the biggest chunk is .next/server (about 72 MB, and not something that’s easy to trim). The parts you can actually cut are these:
Content
Size
Source
sharp + libvips
about 19 MiB
Next’s image optimization dependency, but EdgeOne has its own image processing
`src/`, `public/`
about 12.6 MB
These shouldn’t have been bundled into the server package in the first place
sourcemap
about 3 MB
Not needed at runtime
Pitfall 1: dynamic file reads traced the whole project into the bundle
The build log actually dropped a hint:
The problem was in og-image.server.ts for the OG image generator. It used dynamic paths to read the resvg wasm and fallback fonts, and Turbopack couldn’t tell which files might be read, so it traced the entire project. Those resources were already explicitly included via outputFileTracingIncludes, so all I had to do was add one ignore comment:
That change also helps for Docker deployments.
Pitfall 2: outputFileTracingExcludes wouldn’t exclude sharp
My first instinct was to use next.config.mjs in `next.config.mjs` to exclude sharp, but neither of the two forms I tried worked. I had to dig into the Next source to find out why: outputFileTracingExcludes `outputFileTracingExcludes`collectBuildTraces only runs for non-Turbopack builds, and under Turbopack the dependency tracing for `next-server` is generated by native code, so it doesn’t read this config at all.next-serverIn the end I switched to deleting it after the build.
runs a script that only takes effect for EdgeOne builds, removing sharp, postbuild from .next/standalone, and @img/sharp-*. The adapter copies files from the standalone directory only after *.map, so anything deleted there won’t make it into the function bundle.postbuild build
Pitfall 3: 158 MiB — leftovers from the previous build got bundled too
After the fix, the next deployment was even larger: 158 MiB. It turned out the extra 26 MiB came from the `.edgeone/` directory left behind by the previous local build. pnpm has a workspace link, .edgeone/ -> `apps/web`, and Next followed that link and traced the old output as a dependency, effectively nesting one old bundle inside the new one.@yohaku/web -> apps/webapps/web ->
The fix was to clear the old prebuild directories in .edgeone/cloud-functions first, and the cleanup script also removes any lingering .edgeone inside standalone. .edgeone/project.json.`project.json` records the project binding, so that one has to stay.
Pitfall 4: environment variables weren’t reaching the build
To avoid affecting Docker builds, the cleanup script initially used the environment variable EDGEONE_BUILD to decide whether to run. Later I found that when edgeone makers deploy runs the build, it doesn’t pass through environment variables set in the terminal, so the script never deleted anything.
I საბოლოized this by checking for the marker file written by the adapter during the build, .edgeone/.image-loader-injected. That needs no config, and Docker builds aren’t affected.
After those four steps, the function bundle dropped from 141 MiB to about 112 MiB, and the deployment finally passed.
404: "The site does not exist"
For a while, every path on the site returned EdgeOne’s gateway 404, and even static files like favicon.ico did the same. I first suspected DNS or sync delays, but eventually confirmed the reason was simple: the project still had no successful production deployment yet because of the earlier size issue, so there was nothing behind the domain. So if you see a gateway 404, check the deployment status first — don’t rush to blame DNS.
Also, the preview URL from the CLI includes an eo_token parameter and expires after about an hour, so it’s not great for repeated testing. Binding a custom domain is much more convenient, and overseas regions don’t require ICP filing.
Adapting to platform differences
Once the size issue was fixed, the site was basically usable. I tested the routes one by one and changed three things:
Automated deployment with GitHub Actions
I wanted the EdgeOne-specific changes to live on a separate feat/edgeone-makers branch without polluting main, while still keeping up with main updates. So I set up the workflow like this:main On every push to main, merge into the deployment branch, push it back to the remote, then build and deploy from the merged result.
A few things to note:
are stored in the EdgeOne project config; the CLI pulls them automatically during deployment, so CI only needs an API token.
The first run succeeded: dependency installation took 32 seconds, the whole process took about 6 minutes, and the SSR bundle was 101.4 MiB.