From 3d3e243a302e4b005ecf86c59ab3399e5e43e470 Mon Sep 17 00:00:00 2001 From: Vula Builder Date: Tue, 15 Sep 2026 18:02:55 +0000 Subject: [PATCH] Deploy --- src/lib/siteMeta.ts | 81 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/lib/siteMeta.ts diff --git a/src/lib/siteMeta.ts b/src/lib/siteMeta.ts new file mode 100644 index 0000000..94f5de9 --- /dev/null +++ b/src/lib/siteMeta.ts @@ -0,0 +1,81 @@ +import type { Metadata } from 'next' + +interface Profile { + businessName: string | null + locality: string | null + logoUrl: string | null + siteUrl: string | null +} + +async function profile(): Promise { + const base = process.env.NEXT_PUBLIC_VULA_COMMERCE_URL + const id = process.env.NEXT_PUBLIC_VULA_PROJECT_ID + if (!base || !id) return null + try { + const res = await fetch(base + '/api/public/profile/' + id, { next: { revalidate: 3600 } }) + return res.ok ? ((await res.json())?.profile ?? null) : null + } catch { + return null + } +} + +/** + * A title for one section of the site. + * + * "Services | Riverside Dental, Melville" — the section first, because + * Google truncates the end and the section is what distinguishes this page + * from every other page on the site. The town is included when we know it: + * someone searching "chiropractor Richards Bay" is searching for the town as + * much as the trade. + * + * Falls back to the bare section name. A title that says "Services" is worse + * than one that says "Services | Riverside Dental" and far better than a + * template that reads "Services | undefined". + */ +export async function sectionMetadata(section: string, description?: string): Promise { + const p = await profile() + const where = p?.businessName + ? p.locality + ? p.businessName + ', ' + p.locality + : p.businessName + : null + + const title = where ? section + ' | ' + where : section + + return { + title, + ...(description ? { description } : {}), + // The card WhatsApp, Facebook and Slack show when someone pastes the link. + // Without og: tags they fall back to the title and description, which is why + // a share already reads correctly — the picture is the only thing missing. + // + // Her own logo, or nothing. Never a stock photograph: the same one can end + // up on several sites, and a gleaming clinic on the card with a modest room + // at the other end is the kind of overselling this whole pass removed. + openGraph: { + title, + ...(description ? { description } : {}), + ...(p?.businessName ? { siteName: p.businessName } : {}), + ...(p?.siteUrl ? { url: p.siteUrl } : {}), + type: 'website', + locale: 'en_ZA', + ...(p?.logoUrl ? { images: [{ url: p.logoUrl }] } : {}), + }, + } +} + +/** + * A page that must never appear in search results. + * + * A customer's own booking page is reached from a WhatsApp message with a + * code in the link. Those links get forwarded, pasted into groups and picked + * up, and a booking of hers turning up in Google is a breach she would hear + * about from the patient, not from us. Checkout and account pages are the + * same: private by purpose, not by obscurity. + */ +export function privatePage(section: string): Metadata { + return { + title: section, + robots: { index: false, follow: false }, + } +}