feat(seo): add meta tags and page titles

Configure default head meta and title template in nuxt.config.ts
Add dynamic SEO meta tags and robots directives to all pages
This commit is contained in:
2026-05-08 17:07:43 +08:00
parent 25720b21e1
commit 4f25f2b2f8
9 changed files with 79 additions and 0 deletions

View File

@@ -606,6 +606,12 @@ definePageMeta({
middleware: 'auth'
})
useSeoMeta({
title: 'Bookings',
description: 'Manage dinner ticket bookings, confirmations, receipts, and seat allocation.',
robots: 'noindex,nofollow'
})
const toast = useToast()
const apiClient = useApiClient()
const auth = useAuth()

View File

@@ -36,6 +36,15 @@ const statusColor = computed(() => booking.value.status === 'confirmed' ? 'succe
const ticketLabel = computed(() => booking.value.ticketLabel || booking.value.ticketType.toUpperCase())
const totalFormatted = computed(() => formatBookingCurrency(booking.value.totalPrice, locale.value))
const receiptPath = computed(() => `/receipt/${booking.value.receiptToken}`)
useSeoMeta({
title: () => `${t('confirm.title')} - ${booking.value.customerName}`,
description: () => t('confirm.description'),
ogTitle: () => `${t('confirm.title')} - ${booking.value.customerName}`,
ogDescription: () => t('confirm.description'),
robots: 'noindex,nofollow'
})
const detailRows = computed(() => {
const rows = [
{

View File

@@ -27,6 +27,20 @@ const [bookingConfig, contactsResponse] = await Promise.all([
apiClient<{ contacts: PublicContact[] }>('/api/public/contacts')
])
const seoDescription = computed(() => {
return `${bookingConfig.event.dateLabel} · ${bookingConfig.event.timeLabel} · ${bookingConfig.event.venue}`
})
useSeoMeta({
title: () => bookingConfig.event.title,
description: () => seoDescription.value,
ogTitle: () => bookingConfig.event.title,
ogDescription: () => seoDescription.value,
twitterTitle: () => bookingConfig.event.title,
twitterDescription: () => seoDescription.value,
robots: 'index,follow'
})
const eventDetails = computed(() => [
{
label: t('common.date'),

View File

@@ -121,6 +121,12 @@ definePageMeta({
middleware: 'guest'
})
useSeoMeta({
title: 'Staff Login',
description: 'Staff login for the dinner ticket management system.',
robots: 'noindex,nofollow'
})
const toast = useToast()
const router = useRouter()
const auth = useAuth()

View File

@@ -255,6 +255,12 @@ definePageMeta({
middleware: 'super-admin'
})
useSeoMeta({
title: 'User Management',
description: 'Manage staff access for the dinner ticket system.',
robots: 'noindex,nofollow'
})
const toast = useToast()
const apiClient = useApiClient()
const auth = useAuth()

View File

@@ -52,6 +52,15 @@ const ticketLabel = computed(() => receipt.value.booking.ticketLabel || receipt.
const statusColor = computed(() => receipt.value.booking.status === 'confirmed' ? 'success' : 'warning')
const sharedSeats = computed(() => receipt.value.seats.filter((seat) => Boolean(seat.sharedAt)))
const availableSeats = computed(() => receipt.value.seats.filter((seat) => !seat.sharedAt))
useSeoMeta({
title: () => `${t('receipt.badge')} - ${eventDetails.value.title}`,
description: () => `${receipt.value.booking.customerName} · ${ticketLabel.value} · ${receipt.value.booking.seatCount} ${t('common.seats')}`,
ogTitle: () => `${t('receipt.badge')} - ${eventDetails.value.title}`,
ogDescription: () => `${receipt.value.booking.customerName} · ${ticketLabel.value}`,
robots: 'noindex,nofollow'
})
const maxShareCount = computed(() => Math.max(availableSeats.value.length, 1))
const normalizedShareCount = computed(() => {
return Math.max(1, Math.min(Math.trunc(Number(shareForm.count) || 1), maxShareCount.value))

View File

@@ -29,6 +29,14 @@ const receipt = ref(initialReceipt)
const eventDetails = computed(() => receipt.value.booking.event)
const ticketLabel = computed(() => receipt.value.booking.ticketLabel || receipt.value.booking.ticketType.toUpperCase())
const totalFormatted = computed(() => formatBookingCurrency(receipt.value.booking.totalPrice))
useSeoMeta({
title: () => `${getSeatLabel(receipt.value.seat.seatNumber)} - ${eventDetails.value.title}`,
description: () => `${receipt.value.seat.recipientName || receipt.value.booking.customerName} · ${ticketLabel.value}`,
ogTitle: () => `${getSeatLabel(receipt.value.seat.seatNumber)} - ${eventDetails.value.title}`,
ogDescription: () => `${receipt.value.seat.recipientName || receipt.value.booking.customerName} · ${ticketLabel.value}`,
robots: 'noindex,nofollow'
})
</script>
<template>

View File

@@ -114,6 +114,12 @@ definePageMeta({
middleware: 'auth'
})
useSeoMeta({
title: 'Security Settings',
description: 'Manage password and passkey settings for the dinner ticket system.',
robots: 'noindex,nofollow'
})
const toast = useToast()
const router = useRouter()
const auth = useAuth()

View File

@@ -6,6 +6,21 @@ export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
app: {
head: {
title: 'Dinner Ticket System',
titleTemplate: (titleChunk) => titleChunk && titleChunk !== 'Dinner Ticket System'
? `${titleChunk} · Dinner Ticket System`
: 'Dinner Ticket System',
meta: [
{ name: 'description', content: 'Dinner ticket booking, confirmation, receipt, and seat sharing system.' },
{ property: 'og:site_name', content: 'Dinner Ticket System' },
{ property: 'og:type', content: 'website' },
{ name: 'twitter:card', content: 'summary' },
{ name: 'theme-color', content: '#dc2626' }
]
}
},
runtimeConfig: {
databaseUrl: '',
redisUrl: '',