From 2d4e47af331dd076b6fe0d5c8ace739aafb90963 Mon Sep 17 00:00:00 2001 From: corsacca Date: Thu, 23 Apr 2026 11:05:05 +0100 Subject: [PATCH] Central tiptap block definitions --- app/components/admin/RichTextEditor.vue | 28 ++----------------- app/utils/tiptapExtensions.ts | 36 +++++++++++++++++++++++++ scripts/lib/htmlToTiptap.ts | 28 ++----------------- server/utils/renderTiptap.ts | 36 +++++-------------------- 4 files changed, 46 insertions(+), 82 deletions(-) create mode 100644 app/utils/tiptapExtensions.ts diff --git a/app/components/admin/RichTextEditor.vue b/app/components/admin/RichTextEditor.vue index 5be4c118..76b5e07f 100644 --- a/app/components/admin/RichTextEditor.vue +++ b/app/components/admin/RichTextEditor.vue @@ -11,18 +11,7 @@ import { useEditor, EditorContent } from '@tiptap/vue-3' import type { Editor } from '@tiptap/core' -import StarterKit from '@tiptap/starter-kit' -import Image from '@tiptap/extension-image' -import TextAlign from '@tiptap/extension-text-align' -import { TextStyle } from '@tiptap/extension-text-style' -import Color from '@tiptap/extension-color' -import Highlight from '@tiptap/extension-highlight' -import Typography from '@tiptap/extension-typography' -import Subscript from '@tiptap/extension-subscript' -import Superscript from '@tiptap/extension-superscript' -import Youtube from '@tiptap/extension-youtube' -import { Div } from '~/utils/tiptapDiv' -import { UupgsListNode } from '~/utils/tiptapUupgsList' +import { buildTiptapExtensions } from '~/utils/tiptapExtensions' import { uploadImage } from '~/composables/useImageUpload' const props = withDefaults(defineProps<{ @@ -41,20 +30,7 @@ const uploading = ref(false) const editor = useEditor({ content: props.modelValue, - extensions: [ - StarterKit.configure({ link: { openOnClick: false, autolink: true } }), - Image, - TextAlign.configure({ types: ['heading', 'paragraph'] }), - TextStyle, - Color, - Highlight, - Typography, - Subscript, - Superscript, - Youtube, - Div, - UupgsListNode - ], + extensions: buildTiptapExtensions(), onUpdate({ editor: e }) { emit('update:modelValue', e.getJSON()) } diff --git a/app/utils/tiptapExtensions.ts b/app/utils/tiptapExtensions.ts new file mode 100644 index 00000000..a583e2f5 --- /dev/null +++ b/app/utils/tiptapExtensions.ts @@ -0,0 +1,36 @@ +// Shared Tiptap extension set — used by both the editor +// (app/components/admin/RichTextEditor.vue) and the server-side HTML +// renderer (server/utils/renderTiptap.ts) so authored content round-trips +// cleanly to the public page. Keep all extensions framework-agnostic here +// (@tiptap/core + @tiptap/extension-*): no @tiptap/vue-3 imports, or the +// server renderer will drag the Vue runtime into the Nitro bundle. + +import StarterKit from '@tiptap/starter-kit' +import Image from '@tiptap/extension-image' +import TextAlign from '@tiptap/extension-text-align' +import { TextStyle } from '@tiptap/extension-text-style' +import Color from '@tiptap/extension-color' +import Highlight from '@tiptap/extension-highlight' +import Typography from '@tiptap/extension-typography' +import Subscript from '@tiptap/extension-subscript' +import Superscript from '@tiptap/extension-superscript' +import Youtube from '@tiptap/extension-youtube' +import { Div } from './tiptapDiv' +import { UupgsListNode } from './tiptapUupgsList' + +export function buildTiptapExtensions() { + return [ + StarterKit.configure({ link: { openOnClick: false, autolink: true } }), + Image, + TextAlign.configure({ types: ['heading', 'paragraph'] }), + TextStyle, + Color, + Highlight, + Typography, + Subscript, + Superscript, + Youtube, + Div, + UupgsListNode + ] +} diff --git a/scripts/lib/htmlToTiptap.ts b/scripts/lib/htmlToTiptap.ts index bffdf9d3..81f31f59 100644 --- a/scripts/lib/htmlToTiptap.ts +++ b/scripts/lib/htmlToTiptap.ts @@ -11,33 +11,9 @@ import { JSDOM } from 'jsdom' // same logic @tiptap/html runs, just with a DOM we control. import { getSchema } from '@tiptap/core' import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model' -import StarterKit from '@tiptap/starter-kit' -import Image from '@tiptap/extension-image' -import TextAlign from '@tiptap/extension-text-align' -import { TextStyle } from '@tiptap/extension-text-style' -import Color from '@tiptap/extension-color' -import Highlight from '@tiptap/extension-highlight' -import Typography from '@tiptap/extension-typography' -import Subscript from '@tiptap/extension-subscript' -import Superscript from '@tiptap/extension-superscript' -import Youtube from '@tiptap/extension-youtube' -import { Div } from '../../app/utils/tiptapDiv' -import { UupgsListNode } from '../../app/utils/tiptapUupgsList' +import { buildTiptapExtensions } from '../../app/utils/tiptapExtensions' -const EXTENSIONS = [ - StarterKit.configure({ link: { openOnClick: false, autolink: true } }), - Image, - TextAlign.configure({ types: ['heading', 'paragraph'] }), - TextStyle, - Color, - Highlight, - Typography, - Subscript, - Superscript, - Youtube, - Div, - UupgsListNode -] +const EXTENSIONS = buildTiptapExtensions() // @tiptap/html ultimately calls prosemirror-model's DOMParser which // expects a global `window`/`document`. In Bun/Node we provide one via diff --git a/server/utils/renderTiptap.ts b/server/utils/renderTiptap.ts index 0234e8ac..f0abb46e 100644 --- a/server/utils/renderTiptap.ts +++ b/server/utils/renderTiptap.ts @@ -1,39 +1,15 @@ -// Server-side Tiptap JSON → HTML renderer. The extension set here must -// stay aligned with app/components/admin/RichTextEditor.vue (built in -// Phase 6) so content authored in the editor round-trips back out to -// the public page with the same tags/attributes. +// Server-side Tiptap JSON → HTML renderer. The extension set comes from +// app/utils/tiptapExtensions.ts, shared with the editor component so +// authored content round-trips back out to the public page with the +// same tags/attributes. // // Keeping this on the server keeps the public bundle free of Tiptap // runtime code — the renderer serializes once and we send HTML. import { generateHTML } from '@tiptap/html' -import StarterKit from '@tiptap/starter-kit' -import Image from '@tiptap/extension-image' -import TextAlign from '@tiptap/extension-text-align' -import { TextStyle } from '@tiptap/extension-text-style' -import Color from '@tiptap/extension-color' -import Highlight from '@tiptap/extension-highlight' -import Typography from '@tiptap/extension-typography' -import Subscript from '@tiptap/extension-subscript' -import Superscript from '@tiptap/extension-superscript' -import Youtube from '@tiptap/extension-youtube' -import { Div } from '~~/app/utils/tiptapDiv' -import { UupgsListNode } from '~~/app/utils/tiptapUupgsList' +import { buildTiptapExtensions } from '~~/app/utils/tiptapExtensions' -const EXTENSIONS = [ - StarterKit.configure({ link: { openOnClick: false, autolink: true } }), - Image, - TextAlign.configure({ types: ['heading', 'paragraph'] }), - TextStyle, - Color, - Highlight, - Typography, - Subscript, - Superscript, - Youtube, - Div, - UupgsListNode -] +const EXTENSIONS = buildTiptapExtensions() function isEmptyDoc(doc: unknown): boolean { if (!doc || typeof doc !== 'object') return true