harlan-zw / nuxt-schema-org

The quickest and easiest way to build Schema.org graphs for Nuxt.
https://nuxtseo.com/schema-org
140 stars 14 forks source link

Question schema doesn't work with Nuxt Content #2

Closed Barbapapazes closed 9 months ago

Barbapapazes commented 1 year ago

Describe the bug

Expected to have a correct schema when multiple questions are on the same page but with multiples questions, here the results:

mainEntity aren't good. You can find usage in this component: https://github.com/Barbapapazes/le-classement.fr/pull/88/files

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@id": "https://le-classement.fr/#identity",
      "@type": "Organization",
      "name": "Le Classement des Associations",
      "url": "https://le-classement.fr",
      "logo": {
        "@id": "https://le-classement.fr/#logo"
      },
      "sameAs": [
        "https://www.linkedin.com/company/classement-des-associations/",
        "https://www.instagram.com/classementdesassociations/",
        "https://twitter.com/Leclassement"
      ]
    },
    {
      "@id": "https://le-classement.fr/#website",
      "@type": "WebSite",
      "inLanguage": "fr-FR",
      "name": "Le Classement des Associations",
      "url": "https://le-classement.fr",
      "publisher": {
        "@id": "https://le-classement.fr/#identity"
      }
    },
    {
      "@id": "https://le-classement.fr/faq/#webpage",
      "description": "Retrouve toutes les réponses aux questions les plus fréquentes sur le Classement des Associations.",
      "name": "Questions / Réponses",
      "url": "https://le-classement.fr/faq",
      "@type": [
        "WebPage",
        "FAQPage"
      ],
      "about": {
        "@id": "https://le-classement.fr/#identity"
      },
      "isPartOf": {
        "@id": "https://le-classement.fr/#website"
      },
      "mainEntity": [
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca"
        }
      ],
      "primaryImageOfPage": {
        "@id": "https://le-classement.fr/#logo"
      }
    },
    {
      "@id": "https://le-classement.fr/faq/#/schema/question/b9944ca",
      "@type": "Question",
      "inLanguage": "fr-FR"
    },
    {
      "@id": "https://le-classement.fr/#logo",
      "@type": "ImageObject",
      "caption": "Le Classement des Associations",
      "contentUrl": "https://le-classement.fr/logo.png",
      "inLanguage": "fr-FR",
      "url": "https://le-classement.fr/logo.png"
    }
  ]
}

Reproduction

https://github.com/Barbapapazes/le-classement.fr/pull/88

System Info

System:
    OS: Windows 10 10.0.22000
    CPU: (8) x64 Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
    Memory: 2.13 GB / 15.85 GB
  Binaries:
    Node: 16.17.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 3.3.0 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.19.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.22000.120.0), Chromium (107.0.1418.35), ChromiumDev (108.0.1438.1)
    Internet Explorer: 11.0.22000.120

Used Package Manager

yarn

Validations

Barbapapazes commented 1 year ago

If the answer is hard-coded, it's a litle better but it don't support multiple answers:

<template>
  <SchemaOrgQuestion>
    <template #name>
      question 1
    </template>
    <template #acceptedAnswer>
      <div>
        anwser 1
      </div>
    </template>
  </SchemaOrgQuestion>
</template>

will produce

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@id": "https://le-classement.fr/#identity",
      "@type": "Organization",
      "name": "Le Classement des Associations",
      "url": "https://le-classement.fr",
      "logo": {
        "@id": "https://le-classement.fr/#logo"
      },
      "sameAs": [
        "https://www.linkedin.com/company/classement-des-associations/",
        "https://www.instagram.com/classementdesassociations/",
        "https://twitter.com/Leclassement"
      ]
    },
    {
      "@id": "https://le-classement.fr/#website",
      "@type": "WebSite",
      "inLanguage": "fr-FR",
      "name": "Le Classement des Associations",
      "url": "https://le-classement.fr",
      "publisher": {
        "@id": "https://le-classement.fr/#identity"
      }
    },
    {
      "@id": "https://le-classement.fr/faq/#webpage",
      "description": "Retrouve toutes les réponses aux questions les plus fréquentes sur le Classement des Associations.",
      "name": "Questions / Réponses",
      "url": "https://le-classement.fr/faq",
      "@type": [
        "WebPage",
        "FAQPage"
      ],
      "about": {
        "@id": "https://le-classement.fr/#identity"
      },
      "isPartOf": {
        "@id": "https://le-classement.fr/#website"
      },
      "mainEntity": [
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        },
        {
          "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4"
        }
      ],
      "primaryImageOfPage": {
        "@id": "https://le-classement.fr/#logo"
      }
    },
    {
      "@id": "https://le-classement.fr/faq/#/schema/question/759a0d4",
      "@type": "Question",
      "inLanguage": "fr-FR",
      "name": "question 1",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "anwser 1"
      }
    },
    {
      "@id": "https://le-classement.fr/#logo",
      "@type": "ImageObject",
      "caption": "Le Classement des Associations",
      "contentUrl": "https://le-classement.fr/logo.png",
      "inLanguage": "fr-FR",
      "url": "https://le-classement.fr/logo.png"
    }
  ]
}
Barbapapazes commented 1 year ago

It's like the content from MD files wasn't resolved before the schema.

harlan-zw commented 1 year ago

Hey @Barbapapazes

The issue is that you can't render components within the slot of the Schema, this is actually a Vue limitation in how the render tree works. It will only detect shallow string nodes.

Either you can just provide markdown in the schema content, or you can use the useUnwrap composable from nuxt content (essentially what <ContentSlot> does).

HTML is accepted afaik, not sure about markdown but don't think Google would penalize you for it.

<script setup lang="ts">
const { flatUnwrap } = useUnwrap()
</script>
<template>
    <SchemaOrgQuestion>
    <template #name>
      {{ flatUnwrap($slots.question) }}
    </template>
    <template #acceptedAnswer>
        {{ flatUnwrap($slots.answer) }}
    </transition>
</template>

It might be easier to work with using useSchemaOrg with defineQuestion

Barbapapazes commented 1 year ago

Try to use composable:

<script lang="ts" setup>
const { unwrap } = useUnwrap();
const slots = useSlots();

const question = slots.question;
const answer = slots.answer;

useSchemaOrg([
  defineQuestion({
    name: unwrap(question),
    acceptedAnswer: "Le classement des associations est un classement des associations les plus influentes en France. Il est établi par un algorithme qui prend en compte les données publiques disponibles sur les associations. Il est mis à jour chaque année.",
  }),
]);
</script>

But got this error (same with flatUnwrap)

image

harlan-zw commented 1 year ago

You need to call the slot function don't you? Also flat unwrap is better

Barbapapazes commented 1 year ago

First, I've an issue with the type.

image

- MaybeComputedRefOrPromise<string & VNode<RendererNode, RendererElement, { [key: string]: any; }>[]> | undefined
+ MaybeComputedRefOrPromise<string | VNode<RendererNode, RendererElement, { [key: string]: any; }>[]> | undefined

It must be a string or a VNode?

And I've got the same issue when calling slots (I totally forgot to do it)

<script lang="ts" setup>
const { flatUnwrap } = useUnwrap();
const slots = useSlots();

if (!slots.question) {
  throw new Error("FaqQuestion: missing question slot");
}

const question = slots.question();
const flatQuestion = flatUnwrap(question);

useSchemaOrg([
  defineQuestion({
    name: flatQuestion, // question will throw the same issue about reading modules
    acceptedAnswer: "Le classement des associations est un classement des associations les plus influentes en France. Il est établi par un algorithme qui prend en compte les données publiques disponibles sur les associations. Il est mis à jour chaque année.",
  }),
]);
</script>
Barbapapazes commented 1 year ago

Hey @Barbapapazes

The issue is that you can't render components within the slot of the Schema, this is actually a Vue limitation in how the render tree works. It will only detect shallow string nodes.

Either you can just provide markdown in the schema content, or you can use the useUnwrap composable from nuxt content (essentially what <ContentSlot> does).

HTML is accepted afaik, not sure about markdown but don't think Google would penalize you for it.

<script setup lang="ts">
const { flatUnwrap } = useUnwrap()
</script>
<template>
    <SchemaOrgQuestion>
    <template #name>
      {{ flatUnwrap($slots.question) }}
    </template>
    <template #acceptedAnswer>
        {{ flatUnwrap($slots.answer) }}
    </transition>
</template>

It might be easier to work with using useSchemaOrg with defineQuestion

This doesn't work. Vite return an error.

} excerpt=false tag="div" >
[nitro] [dev] [unhandledRejection] [[vite-node] [plugin:vite:import-analysis] [VITE_ERROR] /components/content/molecules/Disclosure.vue 
<br><pre>import { SchemaOrgQuestion as __nuxt_component_0 } from "@unhead/schema-org-vue";
import { default as __nuxt_component_1 } from "C:/Users/esoubiran/projects/le-classement.fr/node_modules/@nuxt/content/dist/runtime/components/ContentSlot.vue";
import { default as __nuxt_component_2 } from "C:/Users/esoubiran/projects/le-classement.fr/components/atoms/icons/ArrowBottom.vue";    
import { ref } from 'vue';
import { defineComponent as _defineComponent } from "vue";
import { flatUnwrap } from "@nuxt/content/dist/runtime/markdown-parser/utils/node";
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "Disclosure",
  setup(__props, { expose }) {
    expose();
    const open = ref(false);
    const __returned__ = { open, get flatUnwrap() {
      return flatUnwrap;
    } };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { resolveComponent as _resolveComponent, withCtx as _withCtx, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, vShow as _vShow, withDirectives as _withDirectives, Transition as _Transition } from "vue";
import { ssrRenderComponent as _ssrRenderComponent, ssrRenderStyle as _ssrRenderStyle, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer";
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _component_SchemaOrgQuestion = __nuxt_component_0;
  const _component_ContentSlot = __nuxt_component_1;
  const _component_AtomsIconsArrowBottom = __nuxt_component_2;
  _push(_ssrRenderComponent(_component_SchemaOrgQuestion, _attrs, {
    name: _withCtx((_, _push2, _parent2, _scopeId) =&gt; {
      if (_push2) {
        _push2(`${_ssrInterpolate($setup.flatUnwrap(_ctx.$slots.question))}`);
      } else {
        return [
          _createTextVNode(_toDisplayString($setup.flatUnwrap(_ctx.$slots.question)), 1)
        ];
      }
    }),
    acceptedAnswer: _withCtx((_, _push2, _parent2, _scopeId) =&gt; {
      if (_push2) {
        _push2(`&lt;div${_scopeId}&gt;`);
        _push2(_ssrRenderComponent(_component_ContentSlot, {
          use: _ctx.$slots.answer
        }, null, _parent2, _scopeId));
        _push2(`&lt;/div&gt;`);
      } else {
        return [
          _createVNode("div", null, [
            _createVNode(_component_ContentSlot, {
              use: _ctx.$slots.answer
            }, null, 8, ["use"])
          ])
        ];
      }
    }),
    default: _withCtx((_, _push2, _parent2, _scopeId) =&gt; {
      if (_push2) {
        _push2(`&lt;article class="pb-4 flex flex-col border-b border-[#808080] border-opacity-50"${_scopeId}&gt;&lt;button class="flex 
flex-row justify-between text-left text-sm md:text-lg leading-4 font-semibold md:font-normal"${_scopeId}&gt;&lt;span${_scopeId}&gt;`);  
        _push2(_ssrRenderComponent(_component_ContentSlot, {
          use: _ctx.$slots.question,
          unwrap: "p"
        }, null, _parent2, _scopeId));
        _push2(`&lt;/span&gt;`);
        _push2(_ssrRenderComponent(_component_AtomsIconsArrowBottom, {
          class: ["transition-transform duration-300", $setup.open ? "transform rotate-180" : ""]
        }, null, _parent2, _scopeId));
        _push2(`&lt;/button&gt;&lt;div class="mt-4 text-sm leading-[1.125rem]" style="${_ssrRenderStyle($setup.open ? null : { display: 
"none" })}"${_scopeId}&gt;`);
        _push2(_ssrRenderComponent(_component_ContentSlot, {
          use: _ctx.$slots.answer
        }, null, _parent2, _scopeId));
        _push2(`&lt;/div&gt;&lt;/article&gt;`);
      } else {
        return [
          _createVNode("article", { class: "pb-4 flex flex-col border-b border-[#808080] border-opacity-50" }, [
            _createVNode("button", {
              onClick: ($event) =&gt; $setup.open = !$setup.open,
              class: "flex flex-row justify-between text-left text-sm md:text-lg leading-4 font-semibold md:font-normal"
            }, [
              _createVNode("span", null, [
                _createVNode(_component_ContentSlot, {
                  use: _ctx.$slots.question,
                  unwrap: "p"
                }, null, 8, ["use"])
              ]),
              _createVNode(_component_AtomsIconsArrowBottom, {
                class: ["transition-transform duration-300", $setup.open ? "transform rotate-180" : ""]
              }, null, 8, ["class"])
            ], 8, ["onClick"]),
            _createVNode(_Transition, {
              "enter-active-class": "transition duration-300 ease-out",
              "enter-from-class": "opacity-0",
              "enter-to-class": "opacity-100",
              "leave-active-class": "transition duration-300 ease-out",
              "leave-from-class": "opacity-100",
              "leave-to-class": "opacity-0",
              persisted: ""
            }, {
              default: _withCtx(() =&gt; [
                _withDirectives(_createVNode("div", { class: "mt-4 text-sm leading-[1.125rem]" }, [
                  _createVNode(_component_ContentSlot, {
                    use: _ctx.$slots.answer
                  }, null, 8, ["use"])
                ], 512), [
                  [_vShow, $setup.open]
                ])
              ]),
              _: 1
            })
          ])
        ];
      }
    }),
    _: 1
  }, _parent));
}
import { useSSRContext as __vite_useSSRContext } from "vue";
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) =&gt; {
  const ssrContext = __vite_useSSRContext();
  (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/content/molecules/Disclosure.vue");
  return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
import _export_sfc from "\0plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender], ["__file", "C:/Users/esoubiran/projects/le-classement.fr/components/content/molecules/Disclosure.vue"]]);
</pre><br>
at /components/content/molecules/Disclosure.vue ] {
  statusCode: 500,
  fatal: false,
  unhandled: false,
  statusMessage: 'Vite Error',
  __nuxt_error: true
}
harlan-zw commented 9 months ago

Going to lose this for a few issues:

Barbapapazes commented 9 months ago

Going to lose this for a few issues:

  • I'm working towards deprecating the components, they are not worth the type issues and the slot unpacking issues.
  • The issue with useUnwrap I'd say isn't related to this module.
  • FAQ's are being removed from Google Rich Results so getting this working is no longer as important

Seems ok for me! Was not aware of your last point.