diff --git a/app/composables/useSiteMeta.js b/app/composables/useSiteMeta.js
new file mode 100644
index 0000000..007a644
--- /dev/null
+++ b/app/composables/useSiteMeta.js
@@ -0,0 +1,58 @@
+/**
+ * useSiteMeta — set page-level SEO + social meta with site defaults baked in.
+ *
+ * Builds absolute URLs from runtimeConfig.public.appUrl so og:image and og:url
+ * resolve for crawlers. Defaults og:type=website, twitter:card=summary_large_image,
+ * og:site_name=Ghost Guild. Set noindex:true to emit robots="noindex, nofollow".
+ *
+ * Pass a function (or refs in fields) to keep tags reactive when content loads
+ * asynchronously via useFetch.
+ */
+export function useSiteMeta(input) {
+ const runtimeConfig = useRuntimeConfig()
+ const route = useRoute()
+ const appUrl = (runtimeConfig.public.appUrl || '').replace(/\/$/, '')
+
+ const resolve = () => (typeof input === 'function' ? input() : input) || {}
+
+ const buildAbsolute = (path) => {
+ if (!path) return undefined
+ if (/^https?:\/\//i.test(path)) return path
+ return `${appUrl}${path.startsWith('/') ? '' : '/'}${path}`
+ }
+
+ const titleGetter = () => resolve().title || 'Ghost Guild'
+ const descGetter = () => resolve().description || undefined
+ const isBareTitle = () => Boolean(resolve().bareTitle)
+ const imageGetter = () => buildAbsolute(resolve().image || '/og/default.png')
+ const typeGetter = () => resolve().type || 'website'
+ const robotsGetter = () =>
+ resolve().noindex ? 'noindex, nofollow' : undefined
+ const canonicalGetter = () => buildAbsolute(route.path)
+
+ useSeoMeta({
+ title: titleGetter,
+ description: descGetter,
+ ogSiteName: 'Ghost Guild',
+ ogTitle: titleGetter,
+ ogDescription: descGetter,
+ ogType: typeGetter,
+ ogUrl: canonicalGetter,
+ ogImage: imageGetter,
+ ogImageWidth: 1200,
+ ogImageHeight: 630,
+ twitterCard: 'summary_large_image',
+ twitterTitle: titleGetter,
+ twitterDescription: descGetter,
+ twitterImage: imageGetter,
+ robots: robotsGetter,
+ })
+
+ useHead({
+ link: [{ rel: 'canonical', href: canonicalGetter }],
+ })
+
+ if (isBareTitle()) {
+ useHead({ titleTemplate: null })
+ }
+}
diff --git a/app/layouts/admin.vue b/app/layouts/admin.vue
index 5e0baad..0c0d201 100644
--- a/app/layouts/admin.vue
+++ b/app/layouts/admin.vue
@@ -217,6 +217,8 @@
+