Skip to content

fix(ui): show City Nodes in link previews - #470

Merged
elliotBraem merged 3 commits into
mainfrom
fix/link-preview-metadata
Oct 29, 2026
Merged

elliotBraem merged 3 commits into
mainfrom
fix/link-preview-metadata

Conversation

@adenarc

@adenarc adenarc commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #469

Link previews for citynode.app showed the everything.dev card (upstream description and the "Under construction" image). Everything is set in this repo, so the pinned prod image doesn't depend on upstream.

  • bos.app.ts: add description, so runtime.description (used by the root og:description / twitter:description) no longer falls back to the parent config
  • ui/public/metadata.png: replaced with a 1200x630 City Nodes card
  • site.webmanifest: City Nodes name and description
  • new ui/src/lib/social-meta.ts wraps getSocialImageMeta. /, /explore, /n/$slug and /$accountId now set their own og/twitter title, description and og:url. TanStack dedupes by name/property, so the page's tags win over the root's.

Before: raw HTML from prod (curl -sL https://citynode.app/explore)

<title>Explore · City Nodes</title>
<meta property="og:title" content="City Nodes"
<meta property="og:description" content="Open runtime for apps on NEAR, composed from published config and loaded through a shared host, UI, and API runtime."
<meta property="og:type" content="website"
<meta property="og:image" content="https://citynode.app/metadata.png"
<meta property="og:image:type" content="image/png"
<meta property="og:image:width" content="1200"
<meta property="og:image:height" content="630"
<meta property="og:image:alt" content="Open runtime for apps on NEAR, composed from published config and loaded through a shared host, UI, and API runtime."
<meta property="og:site_name" content="City Nodes"
<meta property="og:url" content="https://citynode.app"
<meta name="twitter:card" content="summary_large_image"
<meta name="twitter:title" content="City Nodes"
<meta name="twitter:description" content="Open runtime for apps on NEAR, composed from published config and loaded through a shared host, UI, and API runtime."
<meta name="twitter:image" content="https://citynode.app/metadata.png"
<meta name="twitter:image:alt" content="Open runtime for apps on NEAR, composed from published config and loaded through a shared host, UI, and API runtime."
<meta name="description" content="Find a CityNode community near you and see what&#x27;s coming up."

After: raw server-rendered HTML (local bos dev --ssr, curl -s)

Titles:

/  City Nodes
/explore  Explore · City Nodes
/n/testville  Testville · City Nodes
== /
<meta name="description" content="CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online."
<meta property="og:title" content="City Nodes"
<meta property="og:description" content="CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online."
<meta property="og:type" content="website"
<meta property="og:image" content="http://localhost:3000/metadata.png"
<meta property="og:image:type" content="image/png"
<meta property="og:image:width" content="1200"
<meta property="og:image:height" content="630"
<meta property="og:image:alt" content="CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online."
<meta property="og:site_name" content="City Nodes"
<meta property="og:url" content="http://localhost:3000"
<meta name="twitter:card" content="summary_large_image"
<meta name="twitter:title" content="City Nodes"
<meta name="twitter:description" content="CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online."
<meta name="twitter:image" content="http://localhost:3000/metadata.png"
<meta name="twitter:image:alt" content="CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online."
<link rel="canonical" href="http://localhost:3000"
== /explore
<meta name="description" content="Find a CityNode community near you and see what&#x27;s coming up."
<meta property="og:title" content="Explore · City Nodes"
<meta property="og:description" content="Find a CityNode community near you and see what&#x27;s coming up."
<meta property="og:type" content="website"
<meta property="og:image" content="http://localhost:3000/metadata.png"
<meta property="og:image:type" content="image/png"
<meta property="og:image:width" content="1200"
<meta property="og:image:height" content="630"
<meta property="og:image:alt" content="Find a CityNode community near you and see what&#x27;s coming up."
<meta property="og:site_name" content="City Nodes"
<meta property="og:url" content="http://localhost:3000/explore"
<meta name="twitter:card" content="summary_large_image"
<meta name="twitter:title" content="Explore · City Nodes"
<meta name="twitter:description" content="Find a CityNode community near you and see what&#x27;s coming up."
<meta name="twitter:image" content="http://localhost:3000/metadata.png"
<meta name="twitter:image:alt" content="Find a CityNode community near you and see what&#x27;s coming up."
<link rel="canonical" href="http://localhost:3000"
== /n/testville
<meta name="description" content="Testville on CityNode — events, local communities and staking pools."
<meta property="og:title" content="Testville · City Nodes"
<meta property="og:description" content="Testville on CityNode — events, local communities and staking pools."
<meta property="og:type" content="website"
<meta property="og:image" content="http://localhost:3000/metadata.png"
<meta property="og:image:type" content="image/png"
<meta property="og:image:width" content="1200"
<meta property="og:image:height" content="630"
<meta property="og:image:alt" content="Testville on CityNode — events, local communities and staking pools."
<meta property="og:site_name" content="City Nodes"
<meta property="og:url" content="http://localhost:3000/n/testville"
<meta name="twitter:card" content="summary_large_image"
<meta name="twitter:title" content="Testville · City Nodes"
<meta name="twitter:description" content="Testville on CityNode — events, local communities and staking pools."
<meta name="twitter:image" content="http://localhost:3000/metadata.png"
<meta name="twitter:image:alt" content="Testville on CityNode — events, local communities and staking pools."
<link rel="canonical" href="http://localhost:3000"

Each og/twitter tag shows up once per page. og:image is absolute and /metadata.png returns 200 image/png (175 KB). Locally the host is localhost:3000; on prod it's hostUrl (https://citynode.app).

Tested

  • pnpm run typecheck, pnpm run lint, pnpm run test: all pass
  • Raw SSR HTML for /, /explore and /n/<test community> (output above). On main, local bos dev --ssr fails because the remote host wants everything-dev rc.27 and main pins rc.25. For this check only, I bumped everything-dev to rc.27 locally without committing it and reverted afterwards. The PR doesn't change versions.
  • Also checked the composed head in the browser in CSR mode, including a profile page
  • Not tested on prod. Telegram caches previews, so a link it already has may keep the old card for a while after deploy.

Image before / after:

Before: everything.dev preview image

After: City Nodes preview image

Set the app description, replace the upstream metadata image with a City Nodes card, update the web manifest, and give the home, Explore, community and profile pages their own og/twitter tags.

Closes #469
@multi-review

multi-review Bot commented Oct 29, 2026

Copy link
Copy Markdown

Multi Review · 2a281cb

The PR gives the app its own description, a new preview image and an updated web manifest. It also adds a small socialMeta helper so the home, Explore, community and profile pages each set their own og/twitter title, description and URL. The changes only affect page metadata. I found no defects; overall risk is low.

No new issues found

What was checked (8 areas)
  • socialMeta helper in social-meta.ts: hostUrl normalization, image/siteUrl fallback when hostUrl is empty, siteName via getAppName/readRuntimeConfig
  • All 4 callers of socialMeta (index, explore, n/$slug, $accountId) and where each gets runtimeConfig (from loaderData or match.context)
  • Root beforeLoad/loader in __root.tsx: confirmed match.context.runtimeConfig and locale are set for child head() calls
  • Root head(): og tags come from runtime.description and title, and use runtimeBasePath (always '/' in this repo)
  • $accountId: hostUrl was removed from loaderData; nothing else reads it (the component only uses accountId)
  • n/$slug loader still returns runtimeConfig, which head() uses
  • The changeset package name 'ui' matches ui/package.json
  • site.webmanifest edits are valid JSON fields

Comment @multi-review to re-review new commits, or @multi-review full for a full review.

@elliotBraem
elliotBraem merged commit 49724dc into main Oct 29, 2026
6 checks passed
@elliotBraem
elliotBraem deleted the fix/link-preview-metadata branch October 29, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Link previews show the everything.dev card instead of CityNode

2 participants