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

Rich-Text Editor

The template's TipTap editor for HTML fields — load it only when an admin edits, keep it out of visitors' prefetch hints, and choose its buttons.

The module doesn't ship a rich-text editor. The template has one, app/app/components/TipTapHtmlEditor.vue, built on TipTap. Its HtmlContent component shows the editor to admins in edit mode, and the HTML through v-cwa-html to everyone else. This page covers the editor; HTML Content covers rendering the HTML.

Loading an Editor Lazily

A rich-text component often shows an editor to admins, such as TipTap. Import the editor with defineAsyncComponent, not statically. A static import puts the editor into the preloads of every page with body text, for anonymous visitors too. In the template, that was about 424 KB of TipTap and ProseMirror (135 KB gzipped).

<template>
  <article>
    <TipTapHtmlEditor v-if="$cwa.admin.isEditing" v-model="model" />
    <div v-else ref="container" v-cwa-html="content" class="prose" />
  </article>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// Fetched only when an admin starts editing.
const TipTapHtmlEditor = defineAsyncComponent(() => import('~/components/TipTapHtmlEditor.vue')) // ...
</script>

Anywhere else that only needs the editor's type, use import type. A value import keeps the editor in the dev bundle.

import type TipTapHtmlEditor from '~/components/TipTapHtmlEditor.vue'

const editorComponent = ref<typeof TipTapHtmlEditor | undefined>()

Nuxt still adds a prefetch hint for the async chunk, so browsers may download it in the background. The template removes that hint with a build:manifest hook in nuxt.config.ts. It filters the editor out of each chunk's dynamicImports. The editor still loads on demand when it is first shown.

// nuxt.config.ts
const isEditorSource = (id: string) => id.endsWith('components/TipTapHtmlEditor.vue')

export default defineNuxtConfig({
  modules: [
    (_options, nuxt) => {
      nuxt.hook('build:manifest', (manifest) => {
        for (const chunk of Object.values(manifest)) {
          chunk.dynamicImports = chunk.dynamicImports?.filter(id => !isEditorSource(id))
        }
      })
    },
  ],
})

The module already keeps its own admin UI out of visitors' prefetch hints. It can't see your editor, because the editor is app code, so keep this hook.

Choosing the Editor's Buttons

The template's app/app/components/TipTapHtmlEditor.vue shows a menu of formatting buttons: H1, H2, Bold, Italic, Underline, Link and Bullet List. Every button shows by default. Pass the config prop to hide some of them for one field.

For example, a short description that allows only bold, italic and links:

<TipTapHtmlEditor
  v-if="$cwa.admin.isEditing"
  v-model="model"
  :config="{ h1: false, h2: false, underline: false, bulletList: false }"
/>

The keys are h1, h2, bold, italic, underline, link and bulletList. Set a key to false to hide that button. The prop is typed, so a misspelt key fails the type check. A project whose copy of the file has no config prop can copy the current file from the template.

H1, H2 and Bullet List also appear in the menu shown on an empty line. That menu is hidden when all three are off.

Hiding a button doesn't stop the style. The editor still has every extension, so pasted content and keyboard shortcuts (such as Ctrl+U, or Cmd+U on a Mac, for underline) still apply it. config only changes the menu. To forbid a style, remove its extension from the editor, or clean the HTML on the API.
Image placeholder: the editor's selection menu for a field with config set to bold, italic and link only, next to the full menu with all seven buttons.