Whambulance
Whambulance

Reputation: 123

Nuxt 3 - How to add Meta tags on a Dynamic route at Build

The issue I've encounted originates from attempting to apply dynamic OpenGraph meta tags to a dynamiclly generated route in Nuxt 3 (and by extension, Vue 3).

I've tried to set the meta tags dynamically through Javascript - which appears to be the only dynamic option which Nuxt 3 currently supports, to no avail. Obviously when the Open Graph scraper requests the page, it doesn't run any Javascript, meaning my meta tags do not get applied.

I do not want to server-side render these pages, keeping them dynamically generated is an important part of this problem.

So far I have attempted using the <Head> tag, with the content property generate dynamically:

<Head>
  <Meta hid="og:url" property="og:url" :content="`https://my-site.com/{$route.path}`" />
</Head>

This causes the meta tags to be applied properly, but only after the Javascript has been executed. So as I mentioned before, the Open Graph web scrapers do not correctly apply it.

The solution I was hoping to find was a method that could add the meta tags at build time - is this possible? Or is there a better solution I'm not considering?

Upvotes: 0

Views: 11843

Answers (5)

Georgi Nonchev
Georgi Nonchev

Reputation: 1

I use this approach:

<script setup>
import { useRoute } from 'vue-router';

const route = useRoute();
// Get the course ID from the route
const { id } = route.params;

const title = ref('test222');
const ogImage = ref('default-image-url.jpg');

useSeoMeta({
  title: () => title.value,
  ogTitle: () => title.value,
  description: 'This is my amazing site, let me tell you all about it.',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: () => ogImage.value,
  twitterCard: 'summary_large_image',
})

// Fetch the event data
const { data: event, status } = await useFetch(() => `/api/courses/${id}`);

// Watch for changes in event data and update meta and Open Graph tags
watch(
  event,
  (newEvent) => {
    if (newEvent?.name) {
      title.value = newEvent.name;
      ogImage.value = newEvent.cover?.source || 'default-image-url.jpg'; // Ensure default image if undefined
    }
  },
  { immediate: true } // This will trigger the watch immediately after the component mounts
);

</script>

Upvotes: 0

giver0
giver0

Reputation: 1

I take useSeoMeta and it work for me

<script setup lang="ts">
useSeoMeta({
  title: () => 'My Amazing Site',
  ogTitle: () => 'My Amazing Site',
  description: () => 'This is my amazing site, let me tell you all about it.',
  ogDescription: () => 'This is my amazing site, let me tell you all about it.',
  ogImage: () => 'https://example.com/image.png',
  twitterCard: () => 'summary_large_image',
})
</script>

Upvotes: 0

Ivan Lim
Ivan Lim

Reputation: 229

useSeoMeta works for me while useHead does not. I was trying to change the description meta using the following code.

useSeoMeta({
  description: () => 'New meta description',
});

Upvotes: 0

dxxta
dxxta

Reputation: 129

Have you tried define-nuxt-route-middleware? it allows to run your Composables Function at the build time. So your meta should've applied properly for SEO. Instead, i use definePageMeta rather than useHead in every page like so :

[some page].vue

<script setup>
definePageMeta({
  order: 1,
  label: "Perusahaan",
  title: "Perusahaan/Klien",
  description:
    "Kami memudahkan administrasi, semua absensi pekerja dapat dengan mudah dilacak riwayatnya serta memantau serta mengatur kehadiran pekerja dengan Geotagging dan pengelompokan area kerja untuk perusahaan atau klien",
  icon: "domain",
  transparent: true,
  image: "/perusahaan/invoice.png",
});
<script/>

And use useHead once,

middleware/meta.js

export default defineNuxtRouteMiddleware(async (to, from) => {
  let data = null,
    url = null,
    params = null;

  if (to.fullPath?.includes("berita") && to?.params?.slug) {
    url = new URL(`${useRuntimeConfig().public?.database}/Articles/get`);
    params = {
      jsonQuery: JSON.stringify({
        slug: to?.params?.slug,
      }),
      limit: 1,
    };
  } else if (to.fullPath?.includes("faq") && to?.params?.id) {
    url = new URL(`${useRuntimeConfig().public?.database}/FAQ/get`);
    params = {
      jsonQuery: JSON.stringify({
        _id: to?.params?.id,
      }),
      limit: 1,
    };
    // console.log(data);
  }

  if (url && params) {
    Object.keys(params).forEach((key) =>
      url.searchParams.append(key, params[key])
    );

    data = await fetch(url, {
      method: "GET",
    });

    data = await data?.json();
  }

  if (data?.success)
    data = data?.result?.[0];

  if (data || (to?.meta?.title && to?.meta?.description)) {
    useHead(
      useNuxtApp().$metaGenerator(
        data?.title || to?.meta?.title,
        data?.description || data?.excerpt || to?.meta?.description,
        to?.fullPath,
        data?.picture || to?.meta?.image,
        to?.meta?.keywords
      )
    );
  }
});

$metaGenerator (plugins/index.js) :

export default defineNuxtPlugin((nuxtApp) => {
  return {
    metaGenerator: (
        title,
        description,
        path,
        image,
        keywords,
        site = "@website"
      ) => {
        const defaultKeywords = [
          "lowongan kerja",
        ];
        if (Array.isArray(keywords)) keywords.concat(defaultKeywords);
        else keywords = defaultKeywords.concat(keywords || "");

        if (!image) {
          image = "/favicon.ico";
        }

        const url =
          `${useRuntimeConfig().hostname}${path}` ||
          useRuntimeConfig().hostname;

        return {
          title,
          meta: [
            {
              name: "description",
              content: description,
            },
            {
              rel: "canonical",
              href: url,
            },
            {
              rel: "amphtml",
              href: url,
            },
            {
              name: "keywords",
              content: keywords,
            },
            // google
            {
              itemprop: "name",
              content: title,
            },
            {
              itemprop: "description",
              content: description,
            },
            {
              itemprop: "image",
              content: image,
            },
            // twitter card
            {
              name: "twitter:card",
              content: "summary_large_image",
            },
            { name: "twitter:site", content: site },
            {
              name: "twitter:title",
              content: title,
            },
            {
              name: "twitter:description",
              content: description,
            },
            {
              name: "twitter:image",
              content: image,
            },
            {
              name: "twitter:image:alt",
              content: title,
            },
            {
              name: "twitter:url",
              content: url,
            },
            // Open Graph
            { property: "og:site_name", content: site },
            { property: "og:type", content: "website" },
            {
              property: "og:title",
              content: title,
            },
            {
              property: "og:description",
              content: description,
            },
            {
              property: "og:image",
              content: image,
            },
            {
              property: "og:url",
              content: url,
            },
            {
              property: "og:image:secure_url",
              content: image,
            },
            {
              property: "og:image:alt",
              content: title,
            },
          ],
          link: [
            {
              rel: "canonical",
              href: url,
            },
            {
              rel: "amphtml",
              href: url,
            },
          ],
        };
      },
  }
})

Upvotes: 0

sadeq shahmoradi
sadeq shahmoradi

Reputation: 2365

I believe you need a server to fix this problem for you so pages should be SSR or you need to generate these pages in build time (SSG).

you can use useHead composable too, but I think you should use SSR or SSG at least for these pages:

The properties of useHead can be dynamic, accepting ref, computed, and reactive properties. meta parameter can also accept a function returning an object to make the entire object reactive.

learn more here: https://nuxt.com/docs/api/composables/use-head

pls check this:https://stackblitz.com/edit/nuxt-starter-51pnfj

and there is a mistake in your code. your code should be this if you using composition API :

<Head>
  <Meta hid="og:url" property="og:url" :content="`https://my-site.com${route.path}`" />
</Head>

<script setup>
const route = useRoute();
</script>

if you using option API your code should be this:

<Head>
  <Meta hid="og:url" property="og:url" :content="`https://my-site.com${$route.path}`" />
</Head>

if you didn't set ssr to false, by default it will be true or if you don't want SSR for all pages I think you can use Hybrid rendering. so the result will be this :

enter image description here

Upvotes: 0

Related Questions