nuxt-modules / sanity

Sanity integration for Nuxt
https://sanity.nuxtjs.org
MIT License
223 stars 33 forks source link

Internal/external link serializer issue #1060

Closed gduteaud closed 1 month ago

gduteaud commented 1 month ago

Hi all,

I am struggling to correctly render portable text internal and external links in Nuxt. I'm not sure which part of my setup is the issue. When I look at the result of my query I can see all the information that should be needed to render internal and external links correctly but I'm unable to get it to actually work.

I'm thinking the way I've defined internal and external links in my schema might be at the root of the problem, but I would really like to find a solution that supports this structure.

Any guidance is appreciated, thank you!

Here is my query:

*[_type == "post" && slug.current == $slug][0]{
  ...,
  body[]{
    ...,
    markDefs[]{
      ...,
      _type == "link" && defined(internal) => {
        "slug": internal->slug.current
      }
    }
  }
}

Serializers:

const serializers = {
  types: {
    image: SanityInlineImage,
  },
  marks: {
    link: SanityLink,
  }
};

Usage:

<SanityContent :blocks="post.body" :serializers="serializers" />

My SanityLink component:

<template>
    <!-- Handle internal link -->
    <NuxtLink v-if="mark.internal && mark.slug" :to="`/blog/${mark.slug}`">
    </NuxtLink>

    <!-- Handle external link -->
    <a v-else :href="mark.external" target="_blank" rel="noopener noreferrer">
    </a>
</template>

<script setup>
  const props = defineProps({
    mark: {
      type: Object
    }
  });
</script>

And finally my blockContent type schema type:

export default defineType({
  title: 'Block Content',
  name: 'blockContent',
  type: 'array',
  components: {
    input: CustomPortableTextEditor,
  },
  of: [
    defineArrayMember({
      title: 'Block',
      type: 'block',
      // Styles let you set what your user can mark up blocks with. These
      // correspond with HTML tags, but you can set any title or value
      // you want and decide how you want to deal with it where you want to
      // use your content.
      // Removed H1 as an option because it's reserved for the page title
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'H2', value: 'h2'},
        {title: 'H3', value: 'h3'},
        {title: 'H4', value: 'h4'},
        {title: 'Quote', value: 'blockquote'},
      ],
      lists: [{title: 'Bullet', value: 'bullet'}],
      // Marks let you mark up inline text in the block editor.
      marks: {
        // Decorators usually describe a single property – e.g. a typographic
        // preference or highlighting by editors.
        decorators: [
          {title: 'Strong', value: 'strong'},
          {title: 'Emphasis', value: 'em'},
        ],
        // Annotations can be any object structure – e.g. a link or a footnote.
        annotations: [
          {
            name: 'link',
            title: 'Link',
            type: 'object',
            fields: [
              {
                name: 'external',
                type: 'url',
                title: 'URL',
                hidden: ({parent, value}) => !value && !!parent?.internal,
              },
              {
                name: 'internal',
                type: 'reference',
                to: [{type: 'post'},],
                hidden: ({parent, value}) => !value && !!parent?.external,
              },
            ],
          },
        ],
      },
    }),
    // You can add additional types here. Note that you can't use
    // primitive types such as 'string' and 'number' in the same array
    // as a block type.
    defineArrayMember({
      type: 'image',
      options: {hotspot: true},
      fields: [
        {
          title: 'Alt text',
          name: 'alt',
          type: 'string',
          description:
            'Important for SEO and accessiblity. Alt text should describe the content of the image.',
          validation: (rule) => rule.required(),
        },
        {
          title: 'Caption',
          name: 'caption',
          type: 'string',
          description: 'Caption for the image.',
        },
      ],
    }),
  ],
})
gduteaud commented 1 month ago

I was able to resolve this as follows.

const serializers = {
  marks: {
    link: (props, children) => {
      return h(SanityLink, {
        internal: props.slug,  // Pass individual props
        external: props.external
      }, children.slots);  // Pass children slots
    },
  },
}

SanityLink.vue:

<template>
    <!-- Use NuxtLink for internal links, 'a' tag for external links -->
    <NuxtLink v-if="internal" :to="`/blog/${internal}`">
      <slot />
    </NuxtLink>
    <a v-else :href="external">
      <slot />
    </a>
  </template>

  <script setup>
  // Props to define whether the link is internal or external
  const { internal, external } = defineProps({
    internal: String,  // For internal links
    external: String,  // For external links
  });
  </script>