Files
Jakub Zych 2d62924b78 feat: FaqBlock layer for the SummerCMS FAQ API
- 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
2026-10-12 18:22:03 +02:00

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>