I’ve got the exact Next helper now. It does append a hash suffix whenever the metadata file lives under a route group like (main), so the route you type is not the unsuffixed one. I’m calculating the actual generated paths for your routes.
The generated paths are hashed. I’m verifying one or two of those exact URLs now so I can give you the concrete answer, not just the theory.
Yes. The other OG images are not broken, but the URLs you’re typing are wrong.
Why:
/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/opengraph-image.tsx/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/[slug]/opengraph-image.tsx/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/s/[id]/opengraph-image.tsxare file-based metadata routes under the (main) route group. Next adds a hash suffix to metadata routes that live under route groups, so the public URL is not the plain /opengraph-image.
I verified the actual generated URLs:
/opengraph-image-12jlf3/mmm/opengraph-image-hegjt8/s/test/opengraph-image-vla3i8Those return 200 OK as image/png. The plain versions like /opengraph-image or /s/test/opengraph-image 404.
Invite is different because we changed it to an explicit route handler at /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image/route.ts, so /invite/yytjv1881/opengraph-image is a stable direct URL.
If you want the others to be directly browsable at clean URLs too, I can convert them from file-based opengraph-image.tsx metadata files into explicit route.ts handlers the same way.
I’m checking the current Next source and a couple of live requests side by side, because this comes down to whether you were hitting a plain metadata route before or an explicit route handler.
Yes, most likely.
The change is caused by the app/(main)/... route group. Once a file-based metadata route like /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/opengraph-image.tsx lives under a group segment, Next adds a hash suffix to the public URL. That behavior is explicit in Next’s metadata router at /Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L40, where it says grouped metadata routes become .../opengraph-image-[hash].
So if these OG files used to be outside (main), then yes: before, /opengraph-image worked directly; after introducing (main), the public route became hashed instead. That is why the explicit invite route handler works at /invite/[code]/opengraph-image, while the file-based ones do not.
If you want the old direct dev behavior back, the fix is to convert those other opengraph-image.tsx files into explicit route.ts handlers too.
Yes. “Live links work” and “/opengraph-image 404s in dev” are consistent.
What’s happening:
og:image URL./Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main), Next rewrites that URL to a hashed path./Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L40. It explicitly says grouped metadata routes become opengraph-image-[hash].So:
og:image tag from the page./mmm/opengraph-image fails because the real route is something like /mmm/opengraph-image-hegjt8.That’s also why the invite route behaves differently now:
/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image/route.ts/invite/[code]/opengraph-image is a normal stable URL.If you want, I can convert the other OG images to explicit route.ts handlers too so direct /opengraph-image URLs work again in dev.
If you mean “would the OG previews work in production after next build?”, yes.
If you mean “would the plain direct URL like /mmm/opengraph-image start working after next build?”, no. The hash suffix is part of Next’s metadata route normalization, not a dev-only quirk. In the Next code at /Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L40, grouped routes like /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/[slug]/opengraph-image.tsx get an opengraph-image-[hash] path.
So:
/opengraph-image URLs under (main): noroute.ts handlers like the invite one at /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image/route.tsNot quite. You can still see them locally, just not at the clean guessed URL.
With file-based metadata routes under /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main), Next gives them hashed public paths. So local viewing options are:
og:image tag.route.ts if you want a clean stable URL.For example, right now these local URLs work:
http://localhost:3000/opengraph-image-12jlf3http://localhost:3000/mmm/opengraph-image-hegjt8http://localhost:3000/s/test/opengraph-image-vla3i8So the answer is:
/opengraph-image URL: yes, you do.If you want, I can add a tiny dev helper script that prints the real local OG URL for any route.
Using the Next.js skill here. I’m checking the frontend scripts and route layout first so I can add a small local helper instead of hardcoding one-off URLs.
I’m adding a small frontend CLI that discovers the app’s OG routes, matches a page path, and prints the exact local image URL. I’ll wire it into package.json so you can call it directly with Bun.
The helper is in place. I’m validating the new script with eslint and typecheck before I hand you the command.
Validation passed. I’m running the helper against a couple of routes so I can give you the exact usage and confirm the output is what you need.
Added a local helper at /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/scripts/print-og-url.ts and wired it into /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/package.json.
Use it like this from /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend:
It prints the real local OG URL, including Next’s hash when the route is file-based metadata. Examples I verified:
/ -> http://localhost:3000/opengraph-image-12jlf3/mmm -> http://localhost:3000/mmm/opengraph-image-hegjt8/invite/yytjv1881 -> http://localhost:3000/invite/yytjv1881/opengraph-imagebunx eslint scripts/print-og-url.ts and bun run typecheck both passed.
Because the OG files are now under the route group /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main).
For file-based metadata routes like:
/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/opengraph-image.tsx/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/[slug]/opengraph-image.tsx/Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/s/[id]/opengraph-image.tsxNext adds a hash suffix to avoid route collisions for grouped segments. That behavior is in /Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L40.
So the route is no longer:
/opengraph-imageIt becomes something like:
/opengraph-image-12jlf3Same for nested routes:
/mmm/opengraph-image-hegjt8/s/test/opengraph-image-vla3i8That’s the whole reason. The clean direct path stopped working because (main) made these metadata routes hashed.
Yes. You don’t need permanent app code for this.
Best options:
og:image from the page source instead of guessing the path.So the real alternative is: no committed helper, just a local one-liner when you need it.
If you want, I can remove the helper script I added and leave you with a short shell snippet in this chat only.
In the HTML metadata for the page itself.
Locally, easiest ways:
View page source in the browser for the page, not the image route:
http://localhost:3000/http://localhost:3000/mmmhttp://localhost:3000/invite/yytjv1881Search the HTML for:
From terminal:
or more specifically:
That content value is the exact URL Next is publishing.
Mostly yes, with one important nuance:
(main).Why: in /Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L54, Next computes the suffix from the parent pathname, not from the image output.
Nuance:
og:image value in the page HTML may include a query string for cache busting./mmm/opengraph-image-hegjt8, should stay the same as long as the route structure stays the same.So for local viewing, the safe rule is:
Yes.
That suffix is a deterministic hash of the metadata route’s parent path, so it will keep producing the same code as long as the route stays in the same place.
For example, Next does this in /Users/maxgeorgopoulos/Documents/traces/node_modules/.bun/next@16.1.6+4c1edb5b51263512/node_modules/next/dist/lib/metadata/get-metadata-route.js#L63:
So:
(main) or otherwise change the parent path -> new suffixThat’s why /opengraph-image-12jlf3 is stable for the current homepage route structure.
I’m switching the invite OG back to the standard file-based metadata route and cleaning up the imports around it. Then I’ll run a targeted lint pass to make sure the route removal didn’t leave anything stale.
I checked the actual linked route at /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/page.tsx#L24 and traced its data source back to the public invite lookup in /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/api/convex/services/invites.ts#L141.
What we can display today from the public invite lookup
displayName, slug, description, type, and avatarUrl come back from GET /v1/invites/:code in invites.ts:153.role, status, expiresAt, maxUses, and useCount also come back there in invites.ts:164."member" and expire after 7 days, from internal/invites.ts:36 and packages/shared/src/invite.ts:42.maxUses = 0), capped, and current usage count. The admin UI already exposes that model in InviteManager.tsx:25 and InviteManager.tsx:162.What the current invite page already shows
@slug, role, expiry countdown, and login/join CTA are rendered in page.tsx:246.page.tsx:164 and page.tsx:190.maxUses, or useCount, even though the API returns them. Its local InviteDetail type omits those fields in page.tsx:12.Important constraint / bug for OG design
accepted, but getInviteStatus() only returns pending, revoked, expired, or fully_used in invite.ts:54.invite.status === "accepted" in page.tsx:215, but public lookup tests expect exhausted invites to come back as fully_used in invites.test.ts:1085. So the page has a stale status branch and no explicit fully_used UI.What we do not have publicly without an API change
invitedBy, email, createdAt, acceptedAt, and acceptedBy exist on the invite model/admin responses in packages/shared/src/invite.ts:11 and api/convex/services/invites.ts:43, but the public lookup does not return them in api/convex/services/invites.ts:153.There’s also no invite-specific generateMetadata or opengraph-image route yet; the app only has that pattern for the homepage and trace pages. If you want, I can make the next pass about what the OG image should include from this data and where to implement it.
I have the current invite metadata and renderer open. The clean change here is to make invite match the other OG routes: no custom route.ts, just a normal opengraph-image.tsx, and stop hardcoding an image URL in the layout metadata.
I’m editing the invite files now. The layout will stop hardcoding an image URL, and the image renderer will become the standard opengraph-image.tsx metadata file so invite behaves like the other routes.
The route swap is done. I’m running a targeted eslint check and a typecheck pass, because metadata file conventions are easy to get subtly wrong if an import or export name is stale.
There’s one small lint warning left: the raw <img> disable comment is no longer needed in the metadata file. I’m removing that and re-running the targeted check.
The invite OG is back on the standard metadata convention now. I removed /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image/route.ts, renamed the renderer to /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image.tsx, and updated /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/layout.tsx so it no longer hardcodes the old direct image URL.
Lint and typecheck passed. One consequence: invite now behaves like the other (main) metadata routes again, so the direct local URL will be hashed rather than plain /invite/<code>/opengraph-image.
Using the Next.js skill here because this is an OG/metadata token change in the frontend. I’m checking the shared OG token file and the current invite image usage before I edit it.
I found the token file. It uses camelCase keys already, so I’m adding faintForeground there and replacing the local hardcoded faint gray in the invite OG with the shared token.
The token addition is straightforward; the invite file patch missed because the title block has shifted a bit since the last edit. I’m reopening the exact section and applying a narrow patch.
I’ve got the exact lines now. I’m applying the shared token change and then I’ll run a quick targeted lint check.
Using the next-best-practices skill here because this is an App Router metadata/image route change. I’m checking the invite route structure and existing OG conventions, then I’ll add invite-specific metadata and an opengraph-image that uses the public invite data without showing expiry.
Added faintForeground to /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/components/og/og-tokens.ts and updated the invite OG title in /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image.tsx to use the shared token instead of a local hardcoded gray.
I used faintForeground rather than faint_foreground to match the existing OG_COLORS camelCase naming. Targeted eslint passed.
I’m renaming the token to match the existing palette style and updating the invite OG usage to point at the new key.
The token rename is in. I’m running a quick targeted lint check to make sure there aren’t any stale references.
Renamed the token to faint and moved it after muted in /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/components/og/og-tokens.ts. I also updated the invite OG title in /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend/app/(main)/invite/[code]/opengraph-image.tsx to use OG_COLORS.faint.
Targeted eslint passed.
I’m checking the worktree status first so I can commit only the invite OG changes and then push the branch with the required co-author trailer.
The worktree still has the temporary local OG helper in frontend/package.json and frontend/scripts/, which conflicts with your earlier note that you don’t want extra permanent code just for local previewing. I’m removing that helper before I commit, then I’ll stage the real invite OG changes and push the branch.
The temporary helper is out. I’m verifying the final diff now, then I’ll stage, commit with the co-author trailer, and push codex/feat-namespace-invite-opengraph.
I’ve got the final file set down to the actual invite OG work. I’m doing one last lint and typecheck pass before I create the commit, so the pushed branch reflects the final state only.
bunx eslint pulled a couple of cached deps in this worktree; I’m waiting for both checks to finish, then I’ll confirm whether that touched any tracked files before I stage the commit.
The typecheck passed. The lint attempt failed for an environment reason, not for these files: bunx pulled ESLint 10 in this worktree and hit a plugin compatibility error. I’m checking for any unintended lockfile churn from that command before I commit.
I’ve confirmed the final diff is just the invite OG work plus the shared OG token. Next step is staging those files, creating the commit with the co-author trailer, and pushing the branch to origin.
The first status check ran in parallel with staging, so it may have raced. I’m re-running staging and status sequentially, then I’ll commit and push.
Staging is correct now. I’m creating the commit with the co-author trailer, then I’ll push codex/feat-namespace-invite-opengraph to origin.
The commit is in. I’m pushing it to origin now and then I’ll give you the branch and commit details.
Committed and pushed to origin/codex/feat-namespace-invite-opengraph.
Commit: 07b47e6f
Message: Add namespace invite Open Graph image
I removed the temporary local OG helper before committing, so the branch only includes the invite OG implementation and the shared OG_COLORS.faint token. bun run typecheck passed in /Users/maxgeorgopoulos/Documents/traces-worktrees/codex-feat-namespace-invite-opengraph/frontend.
PR link:
https://github.com/market-dot-dev/traces/pull/new/codex/feat-namespace-invite-opengraph
I’m checking the live invite route behavior in this worktree, because this sounds like Next is serving an HTML shell instead of the metadata image route you expected.