- FaqBlock prop schema (default true) adds a FAQPage node (@id <page URL>#faq, one Question with its acceptedAnswer as plain text per item) to the page's one JSON-LD graph through vue-seo-layer's useSchemaNode (faqSchema, faqAnswerText) - headingLevel 0 renders the title as <p class="faq-block__title"> with the same id, still labelling the section, for sidebar and footer widgets - smoke: count= and repeatable options; expectations for the widget title, one FAQPage node and the answers in the HTML inside details
73 lines
3.0 KiB
Vue
73 lines
3.0 KiB
Vue
<script setup lang="ts">
|
|
// FaqBlock: a FAQ of the FAQ plugin, fetched during SSR (useFaq): the
|
|
// heading (the FAQ name), the server-sanitized description and the items.
|
|
// A missing FAQ (404), a failed request or a FAQ without items renders
|
|
// nothing, or the #fallback slot; never an empty heading.
|
|
//
|
|
// headingLevel 0 renders the title as <p class="faq-block__title"> (same id,
|
|
// still the section's aria-labelledby) for sidebar and footer widgets, which
|
|
// are styled text, not content headings (D-52, UI-SPEC L6). With schema (the
|
|
// default) the block adds a FAQPage node to the page's one JSON-LD graph
|
|
// (vue-seo-layer's useSchemaNode, D-43); set schema false on every block but
|
|
// one when a page shows several FAQs. The answers are in the HTML inside
|
|
// <details>, so crawlers read them without opening an item.
|
|
import { computed, useId } from 'vue'
|
|
import { faqSchema } from '../../utils/faqSchema.ts'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
/** The FAQ slug (GET /faqs/{slug}). */
|
|
slug: string
|
|
/** ?locale= for the API; the API's default locale when unset. */
|
|
locale?: string
|
|
/** Level of the heading element, 2 to 4; 0 renders a styled <p> (widgets). */
|
|
headingLevel?: 0 | 2 | 3 | 4
|
|
showHeading?: boolean
|
|
showDescription?: boolean
|
|
/** Only one item open at a time (native exclusive details). */
|
|
exclusive?: boolean
|
|
/** Add the FAQPage node to the page's JSON-LD graph. */
|
|
schema?: boolean
|
|
}>(),
|
|
{ locale: undefined, headingLevel: 2, showHeading: true, showDescription: true, exclusive: false, schema: true },
|
|
)
|
|
|
|
defineSlots<{ fallback?(): unknown }>()
|
|
|
|
const { data: faq, error } = await useFaq(
|
|
() => props.slug,
|
|
{ locale: () => props.locale },
|
|
)
|
|
|
|
const items = computed(() => (error.value ? [] : (faq.value?.content ?? [])))
|
|
const headingTag = computed(() => {
|
|
const level = Math.trunc(Number(props.headingLevel))
|
|
return level === 0 ? 'p' : `h${Math.min(4, Math.max(2, level || 2))}`
|
|
})
|
|
const description = computed(() => (props.showDescription ? (faq.value?.description ?? '').trim() : ''))
|
|
const headingId = `${useId()}-title`
|
|
|
|
const answer = useSeoAnswer()
|
|
const request = useRequestURL()
|
|
useSchemaNode(() => {
|
|
if (!props.schema || error.value || items.value.length === 0) return null
|
|
const url = answer.value?.action === 'ok' && /^https?:\/\//i.test(answer.value.url) ? answer.value.url : `${request.origin}${request.pathname}`
|
|
return faqSchema(faq.value, url)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
v-if="faq && items.length > 0"
|
|
class="faq-block"
|
|
:aria-labelledby="showHeading ? headingId : undefined"
|
|
:aria-label="showHeading ? undefined : faq.name"
|
|
>
|
|
<component :is="headingTag" v-if="showHeading" :id="headingId" class="faq-block__title">{{ faq.name }}</component>
|
|
<!-- eslint-disable-next-line vue/no-v-html -- sanitized by the FAQ API on output -->
|
|
<div v-if="description" class="faq-block__description faq-html" v-html="description" />
|
|
<FaqList :items="items" :exclusive="exclusive" />
|
|
</section>
|
|
<slot v-else name="fallback" />
|
|
</template>
|