- typed client and useFaq/useFaqApi (SSR, faq:<slug>:<locale> key, server and public bases, x-forwarded-for forwarded also on cached renders) - FaqBlock, FaqList and FaqItem: native details, exclusive and openFirst, server-sanitized description and answers, nothing or #fallback on 404, error or no items - FAQ tokens and base CSS with the .faq-html rules, README - node client tests and a playground SSR smoke against a fixture API
51 lines
1.9 KiB
Vue
51 lines
1.9 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.
|
|
import { computed, useId } from 'vue'
|
|
|
|
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. */
|
|
headingLevel?: 2 | 3 | 4
|
|
showHeading?: boolean
|
|
showDescription?: boolean
|
|
/** Only one item open at a time (native exclusive details). */
|
|
exclusive?: boolean
|
|
}>(),
|
|
{ locale: undefined, headingLevel: 2, showHeading: true, showDescription: true, exclusive: false },
|
|
)
|
|
|
|
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(() => `h${Math.min(4, Math.max(2, Math.trunc(Number(props.headingLevel)) || 2))}`)
|
|
const description = computed(() => (props.showDescription ? (faq.value?.description ?? '').trim() : ''))
|
|
const headingId = `${useId()}-title`
|
|
</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>
|