diff --git a/.changeset/link-preview-metadata.md b/.changeset/link-preview-metadata.md new file mode 100644 index 00000000..6c98a051 --- /dev/null +++ b/.changeset/link-preview-metadata.md @@ -0,0 +1,5 @@ +--- +"ui": patch +--- + +Link previews for citynode.app now show City Nodes instead of the everything.dev placeholder. The app sets its own description, the shared preview image is a City Nodes card, and the home page, Explore, community pages and profiles each set their own `og:` and `twitter:` title, description and URL. The web app manifest uses the City Nodes name. diff --git a/bos.app.ts b/bos.app.ts index 30fb877c..c522b80b 100644 --- a/bos.app.ts +++ b/bos.app.ts @@ -6,6 +6,8 @@ export default App({ name: "citynode.app", // Display title for the runtime — shown in the document title, sidebar, and root breadcrumb. title: "City Nodes", + description: + "CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online.", // The base runtime this app extends — inherit the platform, override only what you change. extends: "bos://dev.everything.near/everything.dev", platform: "2.0.0-rc.27", diff --git a/ui/public/metadata.png b/ui/public/metadata.png index 5bd8e158..1bd16826 100644 Binary files a/ui/public/metadata.png and b/ui/public/metadata.png differ diff --git a/ui/public/site.webmanifest b/ui/public/site.webmanifest index ff158ab9..11a67c01 100644 --- a/ui/public/site.webmanifest +++ b/ui/public/site.webmanifest @@ -1,7 +1,7 @@ { - "name": "everything.dev", - "short_name": "everything.dev", - "description": "Open runtime for apps on NEAR, composed from published config and loaded at runtime.", + "name": "City Nodes", + "short_name": "City Nodes", + "description": "CityNodes are local communities that each run a NEAR validator. Find yours, join its events, and stake to keep it online.", "icons": [ { "src": "/web-app-manifest-192x192.png", @@ -43,6 +43,6 @@ "display": "standalone", "theme_color": "#ffffff", "background_color": "#fafafa", - "categories": ["developer tools", "productivity", "web development"], + "categories": ["social", "lifestyle"], "orientation": "portrait-primary" } diff --git a/ui/src/lib/social-meta.ts b/ui/src/lib/social-meta.ts new file mode 100644 index 00000000..32fca6dd --- /dev/null +++ b/ui/src/lib/social-meta.ts @@ -0,0 +1,28 @@ +import { getSocialImageMeta } from "everything-dev/ui/metadata"; +import type { ClientRuntimeConfig } from "@/app"; +import { getAppName } from "@/app"; + +export function socialMeta({ + title, + description, + path, + runtimeConfig, + type, +}: { + title: string; + description: string; + path: string; + runtimeConfig?: Partial | null; + type?: string; +}) { + const hostUrl = (runtimeConfig?.hostUrl ?? "").replace(/\/$/, ""); + return getSocialImageMeta({ + imageUrl: hostUrl ? `${hostUrl}/metadata.png` : "/metadata.png", + title, + description, + siteName: getAppName(runtimeConfig ?? undefined) || "CityNode", + siteUrl: hostUrl ? `${hostUrl}${path}` : "", + type, + alt: description, + }); +} diff --git a/ui/src/routes/_public/$accountId.tsx b/ui/src/routes/_public/$accountId.tsx index a481cab2..4701c27d 100644 --- a/ui/src/routes/_public/$accountId.tsx +++ b/ui/src/routes/_public/$accountId.tsx @@ -1,15 +1,15 @@ import { ArrowUpRightIcon, CompassIcon, UserIcon } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link, Outlet } from "@tanstack/react-router"; -import { getSocialImageMeta } from "everything-dev/ui/metadata"; import { useAuthClient } from "@/app"; import { Avatar, AvatarFallback, AvatarImage, Button, PageContainer } from "@/components"; import { resolveAppLocale, translateAppMessage, useAppTranslation } from "@/i18n/runtime"; import { getNearInitials, resolveNearImageUrl } from "@/lib/near-profile"; +import { socialMeta } from "@/lib/social-meta"; export const Route = createFileRoute("/_public/$accountId")({ loader: async ({ params, context }) => { - const { queryClient, authClient, runtimeConfig } = context; + const { queryClient, authClient } = context; const accountId = params.accountId; await queryClient.prefetchQuery({ @@ -21,12 +21,10 @@ export const Route = createFileRoute("/_public/$accountId")({ staleTime: 5 * 60 * 1000, }); - return { accountId, hostUrl: runtimeConfig?.hostUrl ?? "" }; + return { accountId }; }, - head: ({ loaderData, params, match }) => { + head: ({ params, match }) => { const accountId = params.accountId; - const hostUrl = (loaderData?.hostUrl ?? "").replace(/\/$/, ""); - const siteUrl = hostUrl ? `${hostUrl}/${accountId}` : ""; const title = `${accountId} | CityNode`; const description = translateAppMessage( "meta.profileDescriptionNamed", @@ -38,13 +36,12 @@ export const Route = createFileRoute("/_public/$accountId")({ meta: [ { title }, { name: "description", content: description }, - ...getSocialImageMeta({ - imageUrl: hostUrl ? `${hostUrl}/metadata.png` : "/metadata.png", + ...socialMeta({ title, description, - siteUrl, + path: `/${accountId}`, + runtimeConfig: match.context.runtimeConfig, type: "profile", - alt: description, }), ], }; diff --git a/ui/src/routes/_public/explore.tsx b/ui/src/routes/_public/explore.tsx index b4764edb..0ccd6cd1 100644 --- a/ui/src/routes/_public/explore.tsx +++ b/ui/src/routes/_public/explore.tsx @@ -5,6 +5,7 @@ import { DiscoveryExplorer } from "@/components/discovery/discovery-explorer"; import { PageContainer } from "@/components/layout/page-container"; import { resolveAppLocale, translateAppMessage } from "@/i18n/runtime"; import { pageTitle } from "@/lib/page-title"; +import { socialMeta } from "@/lib/social-meta"; export const Route = createFileRoute("/_public/explore")({ validateSearch: z.object({ campaign: z @@ -20,28 +21,27 @@ export const Route = createFileRoute("/_public/explore")({ region: z.string().max(120).optional().catch(undefined), view: z.enum(["list", "map"]).optional().catch(undefined), }), - head: ({ match }) => ({ - meta: [ - { - title: pageTitle( - translateAppMessage( - "nav.explore", - undefined, - resolveAppLocale(undefined, match.context.locale), - ), - match.context.runtimeConfig, - ), - }, - { - name: "description", - content: translateAppMessage( - "meta.exploreDescription", - undefined, - resolveAppLocale(undefined, match.context.locale), - ), - }, - ], - }), + head: ({ match }) => { + const locale = resolveAppLocale(undefined, match.context.locale); + const title = pageTitle( + translateAppMessage("nav.explore", undefined, locale), + match.context.runtimeConfig, + ); + const description = translateAppMessage("meta.exploreDescription", undefined, locale); + + return { + meta: [ + { title }, + { name: "description", content: description }, + ...socialMeta({ + title, + description, + path: "/explore", + runtimeConfig: match.context.runtimeConfig, + }), + ], + }; + }, component: Explore, }); function Explore() { diff --git a/ui/src/routes/_public/index.tsx b/ui/src/routes/_public/index.tsx index eef3c447..c7b097b6 100644 --- a/ui/src/routes/_public/index.tsx +++ b/ui/src/routes/_public/index.tsx @@ -20,6 +20,7 @@ import { useAppTranslation, } from "@/i18n/runtime"; import { tenantAppsQueryOptions } from "@/lib/queries/tenants"; +import { socialMeta } from "@/lib/social-meta"; const PREVIEW_COUNT = 6; @@ -46,21 +47,27 @@ export const Route = createFileRoute("/_public/")({ await context.queryClient.prefetchQuery(tenantAppsQueryOptions(context.apiClient)); return { runtimeConfig: context.runtimeConfig }; }, - head: ({ loaderData, match }) => ({ - meta: [ - { - title: getActiveRuntime(loaderData?.runtimeConfig)?.title ?? "CityNode", - }, - { - name: "description", - content: translateAppMessage( - "meta.landingDescription", - undefined, - resolveAppLocale(undefined, match.context.locale), - ), - }, - ], - }), + head: ({ loaderData, match }) => { + const title = getActiveRuntime(loaderData?.runtimeConfig)?.title ?? "CityNode"; + const description = translateAppMessage( + "meta.landingDescription", + undefined, + resolveAppLocale(undefined, match.context.locale), + ); + + return { + meta: [ + { title }, + { name: "description", content: description }, + ...socialMeta({ + title, + description, + path: "", + runtimeConfig: loaderData?.runtimeConfig, + }), + ], + }; + }, component: LandingPage, }); diff --git a/ui/src/routes/_public/n/$slug.tsx b/ui/src/routes/_public/n/$slug.tsx index d3c48df6..405b7304 100644 --- a/ui/src/routes/_public/n/$slug.tsx +++ b/ui/src/routes/_public/n/$slug.tsx @@ -29,6 +29,7 @@ import { nodeBySlugQueryOptions, stakingValidatorsQueryOptions, } from "@/lib/queries/nodes"; +import { socialMeta } from "@/lib/social-meta"; import { ProposeHomepageCta } from "./-propose-homepage-cta"; export const Route = createFileRoute("/_public/n/$slug")({ @@ -52,35 +53,29 @@ export const Route = createFileRoute("/_public/n/$slug")({ return { slug, parentId, runtimeConfig, nodeName: node?.name ?? null }; }, - head: ({ loaderData, match }) => ({ - meta: [ - { - title: pageTitle( - loaderData?.nodeName ?? - translateAppMessage( - "nav.community", - undefined, - resolveAppLocale(undefined, match.context.locale), - ), - loaderData?.runtimeConfig, - ), - }, - { - name: "description", - content: loaderData?.nodeName - ? translateAppMessage( - "meta.communityDescriptionNamed", - { name: loaderData.nodeName }, - resolveAppLocale(undefined, match.context.locale), - ) - : translateAppMessage( - "meta.communityDescription", - undefined, - resolveAppLocale(undefined, match.context.locale), - ), - }, - ], - }), + head: ({ loaderData, match, params }) => { + const locale = resolveAppLocale(undefined, match.context.locale); + const title = pageTitle( + loaderData?.nodeName ?? translateAppMessage("nav.community", undefined, locale), + loaderData?.runtimeConfig, + ); + const description = loaderData?.nodeName + ? translateAppMessage("meta.communityDescriptionNamed", { name: loaderData.nodeName }, locale) + : translateAppMessage("meta.communityDescription", undefined, locale); + + return { + meta: [ + { title }, + { name: "description", content: description }, + ...socialMeta({ + title, + description, + path: `/n/${params.slug}`, + runtimeConfig: loaderData?.runtimeConfig, + }), + ], + }; + }, component: NodePage, });