CWA 2.0 is in alpha: try it, build with it, give feedback. APIs can still change between releases; see Releases.
Component Helpers

HTML Content

Render rich-text HTML from a CWA resource with CwaLink replacing anchor tags so internal links stay client-side.

Rich-text components (e.g. a WYSIWYG or markdown-rendered field) output raw HTML containing <a> tags. Those tags cause full-page reloads for internal links. useHtmlContent solves this by replacing every <a> tag in the container with a <CwaLink> Vue component after mount.

It also returns vCwaHtml, a directive you use in place of v-html. It renders the same HTML without redrawing it during hydration.

Use useHtmlContent for any component that renders HTML strings from the API into the page — rich-text editors, markdown output, or any field where the content author controls anchor tags. Without it, every internal link causes a hard navigation.

useHtmlContent is the reference for its parameters and how each link is converted.

Usage

<!-- app/app/cwa/components/RichText/RichText.vue -->
<template>
    <div ref="container" v-cwa-html="content" class="prose" /></template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent, useHtmlContent } from '#imports'

const props = defineProps<IriProp>()

const { resource, exposeMeta } = useCwaComponent(props)

const content = computed(() => resource.value?.data?.content ?? '')

const container = ref<HTMLElement | null>(null)
const { vCwaHtml } = useHtmlContent(container, content) 
defineExpose(exposeMeta)
</script>

Pass container, a template ref to the element that holds the HTML, and content, the HTML source itself. useHtmlContent walks the container's DOM, finds <a> elements, and mounts <CwaLink> components in their place. It runs again whenever the element or the HTML changes.

The directive is not registered globally. Destructure vCwaHtml in <script setup>, and Vue makes it available as v-cwa-html.

Pass the second argument. It is optional for backwards compatibility, but with only the container ref the conversion tracks the element rather than the HTML — so content that changes while the same element stays mounted keeps its raw <a> tags, and those internal links then cause full page loads. This bites whenever a field can change in place: an inline editor, or a resource updated over Mercure.

What It Does

  • Finds every <a href="..."> inside the container
  • Creates a <CwaLink> Vue component instance for each
  • Mounts it as a replacement — internal paths get <NuxtLink> behaviour, external URLs open in a new tab
  • Unmounts the replacement link apps before re-converting, and again when the parent component unmounts

Why Not v-html

Since Vue 3.5.39, hydration sets innerHTML again for every v-html, even when the server HTML is identical. The paragraph the browser has already painted is thrown away and parsed again.

That matters when body text is the page's largest element. Its Largest Contentful Paint (LCP) moves from first paint to after the scripts have run. In the template's production Lighthouse runs, this added 0.45–0.68s to LCP in 7 of 8 loads.

v-cwa-html avoids the redraw:

  • On the server, it renders the HTML as innerHTML, like v-html.
  • On mount, it sets innerHTML only if the DOM differs from the value.
  • On update, it replaces the HTML only when the value has changed.

It patches in beforeUpdate, not updated. So it never replaces the HTML after useHtmlContent has converted the links.

Rich-Text Editors

A rich-text component usually shows admins an editor. Rich-Text Editor covers the template's TipTap editor: loading it only for admins, and choosing its buttons.