├── .editorconfig ├── .envrc ├── .github ├── public │ └── nuxt-strapi-blocks-renderer.png └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nuxtrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── package-lock.json ├── package.json ├── playground ├── basic │ ├── app.vue │ ├── nuxt.config.ts │ ├── package.json │ ├── pages │ │ └── index.vue │ ├── public │ │ └── example_image_df80dd3023.jpg │ ├── server │ │ └── tsconfig.json │ └── tsconfig.json └── custom │ ├── app.vue │ ├── assets │ ├── css │ │ └── tailwind.css │ └── img │ │ └── nuxt-strapi-blocks-renderer.png │ ├── components │ ├── Header.vue │ └── blocks │ │ ├── CustomCodeNode.vue │ │ ├── CustomImageNode.vue │ │ ├── CustomParagraphNode.vue │ │ ├── CustomQuoteNode.vue │ │ ├── heading │ │ ├── CustomHeading1Node.vue │ │ ├── CustomHeading2Node.vue │ │ ├── CustomHeading3Node.vue │ │ ├── CustomHeading4Node.vue │ │ ├── CustomHeading5Node.vue │ │ └── CustomHeading6Node.vue │ │ ├── inline │ │ ├── CustomBoldInlineNode.vue │ │ ├── CustomCodeInlineNode.vue │ │ ├── CustomItalicInlineNode.vue │ │ ├── CustomLinkInlineNode.vue │ │ ├── CustomListItemInlineNode.vue │ │ ├── CustomStrikethroughInlineNode.vue │ │ └── CustomUnderlineInlineNode.vue │ │ └── list │ │ ├── CustomOrderedListNode.vue │ │ └── CustomUnorderedListNode.vue │ ├── nuxt.config.ts │ ├── package-lock.json │ ├── package.json │ ├── pages │ └── index.vue │ ├── public │ └── example_image_df80dd3023.jpg │ ├── server │ └── tsconfig.json │ ├── tailwind.config.js │ └── tsconfig.json ├── renovate.json ├── shell.nix ├── src ├── .fixtures │ └── sampleBlockNodes.json ├── module.ts └── runtime │ ├── components │ ├── StrapiBlocksText.vue │ └── blocks │ │ ├── CodeNode.vue │ │ ├── ImageNode.vue │ │ ├── ParagraphNode.vue │ │ ├── QuoteNode.vue │ │ ├── heading │ │ ├── Heading1Node.vue │ │ ├── Heading2Node.vue │ │ ├── Heading3Node.vue │ │ ├── Heading4Node.vue │ │ ├── Heading5Node.vue │ │ └── Heading6Node.vue │ │ ├── inline │ │ ├── BoldInlineNode.vue │ │ ├── CodeInlineNode.vue │ │ ├── ItalicInlineNode.vue │ │ ├── LinkInlineNode.vue │ │ ├── ListItemInlineNode.vue │ │ ├── StrikethroughInlineNode.vue │ │ └── UnderlineInlineNode.vue │ │ └── list │ │ ├── OrderedListNode.vue │ │ └── UnorderedListNode.vue │ ├── composables │ └── useBlocksText.ts │ ├── types │ └── index.d.ts │ └── utils │ └── index.ts ├── test ├── basic.test.ts └── custom.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [{*.yml,*.yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix shell.nix -------------------------------------------------------------------------------- /.github/public/nuxt-strapi-blocks-renderer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/.github/public/nuxt-strapi-blocks-renderer.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Test & Release' 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: 'Code Quality & Test' 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '22.16.0' 22 | cache: 'npm' 23 | 24 | - name: Install 25 | run: | 26 | npm install 27 | npm run dev:prepare 28 | 29 | - name: Lint 30 | run: | 31 | npm run lint:es 32 | npm run lint:types 33 | 34 | - name: Test 35 | run: npm run test 36 | 37 | release: 38 | name: 'Release package' 39 | needs: [ test ] 40 | runs-on: ubuntu-latest 41 | if: github.ref == 'refs/heads/main' 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: actions/setup-node@v4 47 | with: 48 | node-version: '22.16.0' 49 | cache: 'npm' 50 | 51 | - name: Install 52 | run: | 53 | npm install 54 | npm run dev:prepare 55 | 56 | - name: Release 57 | run: | 58 | npm set //registry.npmjs.org/:_authToken=${{ secrets.NPM_PUBLISHING_KEY }} 59 | npm run release:ci || true 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .data 23 | .vercel_build_output 24 | .build-* 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | tag-version-prefix="" 4 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | # enable TypeScript bundler module resolution - https://www.typescriptlang.org/docs/handbook/modules/reference.html#bundler 2 | experimental.typescriptBundlerResolution=true 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.18 2 | - Bumped dependencies 3 | 4 | # 1.0.17 5 | - Fixed composable access problems in component prefix 6 | - Updated custom playground 7 | 8 | # 1.0.16 9 | - Bumped dependencies 10 | - Updated package config 11 | 12 | # 1.0.15 13 | - Fixed component prefix handling with nuxt-kit@3.15.4 14 | - Bumped dependencies 15 | 16 | # 1.0.14 17 | - Bumped dependencies 18 | 19 | # 1.0.13 20 | - Switched to nuxt eslint config 21 | - Bumped dependencies 22 | 23 | # 1.0.12 24 | - Bumped dependencies 25 | 26 | # 1.0.11 27 | - Bumped dependencies 28 | 29 | # 1.0.10 30 | - Bumped dependencies 31 | 32 | # 1.0.9 33 | - Updated eslint configuration 34 | - Bumped dependencies 35 | - Configured renovate bot 36 | 37 | # 1.0.8 38 | - Fixed code node handling - Thanks to @TRiBotWillB 39 | - Bumped dependencies 40 | 41 | # 1.0.7 42 | - Added stackblitz configuration 43 | - Bumped dependencies 44 | - Updated documentation 45 | 46 | # 1.0.6 47 | - Fixed an issue with line breaks not rendering 48 | - Bumped dependencies 49 | - Updated documentation 50 | 51 | # 1.0.5 52 | - Bumped dependencies 53 | - Changed type check to vue-tsc 54 | 55 | # 1.0.4 56 | - Bumped dependencies 57 | - Added nix shell for development 58 | - Updated development documentation 59 | 60 | # 1.0.3 61 | - Added type checks in ci 62 | - Updated documentation 63 | 64 | # 1.0.2 65 | - Updated package.json 66 | 67 | # 1.0.1 68 | - Updated README.md 69 | - Added CI 70 | 71 | # 1.0.0 72 | - Updated README.md with proper documentation 73 | 74 | # 0.0.5 75 | - Added vue render function import to utils 76 | - Updated text rendering functions 77 | 78 | # 0.0.4 79 | - Added useRuntimeConfig import to utils 80 | 81 | # 0.0.3 82 | - Added resolveComponent import to utils 83 | 84 | # 0.0.2 85 | - Fixed NPM publishing problems 86 | 87 | # 0.0.1 88 | - Initial module version 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Frederik Bußmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Nuxt Strapi Blocks Renderer 6 | 7 | [![Github Actions][github-actions-src]][github-actions-href] 8 | [![NPM version][npm-version-src]][npm-version-href] 9 | [![NPM downloads][npm-downloads-src]][npm-downloads-href] 10 | [![NPM last update][npm-last-update-src]][npm-last-update-href] 11 | [![License][license-src]][license-href] 12 | 13 | A fully customizable Nuxt 3 module for rendering text with the new Blocks rich text editor element from Strapi CMS. 14 | 15 | The implementation is based on Strapi's [Blocks React Renderer](https://github.com/strapi/blocks-react-renderer/). 16 | 17 | - ✨ [Release notes](/CHANGELOG.md) 18 | - 🏀 [Online stackblitz playground](https://stackblitz.com/github/freb97/nuxt-strapi-blocks-renderer?file=playground%2Fcustom%2Fpages%2Findex.vue) 19 | 20 | ## Installation 21 | 22 | 1. Install the Blocks renderer: 23 | 24 | ```bash 25 | npm install nuxt-strapi-blocks-renderer 26 | ``` 27 | 28 | 2. Add the module to `nuxt.config.{ts|js}`: 29 | 30 | ```typescript 31 | export default defineNuxtConfig({ 32 | modules: [ 'nuxt-strapi-blocks-renderer' ] 33 | }) 34 | ``` 35 | 36 | ## Usage 37 | 38 | To render text, use the `StrapiBlocksText` component: 39 | 40 | ```vue 41 | 42 | ``` 43 | 44 | In this example, the `blockNodes` are taken from the JSON response which Strapi provides when using the Blocks rich 45 | text editor element: 46 | 47 | ```vue 48 | 61 | 62 | 66 | ``` 67 | 68 | To use the `useStrapi` composable, install the [Strapi Nuxt module](https://strapi.nuxtjs.org/). 69 | 70 | ### Advanced Usage 71 | 72 | In situations where your project requires specific styling or behavior for certain HTML tags such as ``, `

`, 73 | and others, you can override the default rendering components used by the Nuxt Strapi Blocks Renderer. 74 | This flexibility allows you to tailor the rendering to align with your project's unique design and functional needs. 75 | 76 | #### Global Component Registration 77 | 78 | First, ensure that your components are globally registered in your Nuxt app. 79 | This step is crucial for your custom components to be recognized and used by the renderer. 80 | 81 | In your Nuxt configuration (`nuxt.config.{js|ts}`), add: 82 | 83 | ```ts 84 | export default defineNuxtConfig({ 85 | components: { 86 | dirs: [ 87 | { 88 | path: '~/components/blocks', 89 | pathPrefix: false, 90 | global: true, 91 | }, 92 | ], 93 | }, 94 | }) 95 | ``` 96 | 97 | #### Customizing the Paragraph Tag 98 | 99 | To customize the rendering of the paragraph (`

`) tag, you need to create a corresponding Vue component. 100 | The name of the component follows a predefined pattern: `'StrapiBlocksText' + [NodeName] + 'Node.vue'`. 101 | To override the default paragraph tag, we create a file called `StrapiBlocksTextParagraphNode.vue`. 102 | 103 | ```html 104 | 105 | 110 | ``` 111 | 112 | This component assigns a custom class `my-custom-class-for-p` to the paragraph tag, which can be styled as needed. 113 | 114 | The prefix for the custom components can be adjusted in your `nuxt.config.{js|ts}`: 115 | 116 | ```ts 117 | export default defineNuxtConfig({ 118 | strapiBlocksRenderer: { 119 | prefix: 'MyCustomPrefix', 120 | blocksPrefix: 'MyCustomBlocksPrefix', 121 | }, 122 | }) 123 | ``` 124 | 125 | With this configuration, the `StrapiBlocksText` component becomes `MyCustomPrefixStrapiBlocksText` and the custom 126 | paragraph node component would be named `MyCustomBlocksPrefixParagraphNode`. 127 | 128 | #### Other Custom Tags 129 | 130 | You can apply similar customizations to all other HTML tags used by the renderer: 131 | 132 |

133 | Headings 134 | 135 | Custom heading tags (`

`, `

`, `

`, etc.): 136 | 137 | ```html 138 | 139 | 144 | 145 | 146 | 151 | ``` 152 | 153 | This pattern also extends to the `h3`, `h4`, `h5` and `h6` tags. 154 |

155 | 156 |
157 | Lists 158 | 159 | Custom list tags (`
    `, `
      ` and `
    • `): 160 | 161 | ```html 162 | 163 | 168 | 169 | 170 | 175 | 176 | 177 | 182 | ``` 183 |
184 | 185 |
186 | Blockquotes and Codes 187 | 188 | Custom blockquote and code tags (`
`, `
`):
189 | 
190 | ```html
191 | 
192 | 
197 | 
198 | 
199 | 
202 | ```
203 | 
204 | 205 |
206 | Inline text nodes 207 | 208 | Custom inline text nodes (``, ``, ``, ``, ``): 209 | 210 | ```html 211 | 212 | 217 | 218 | 219 | 224 | 225 | 226 | 231 | 232 | 233 | 238 | 239 | 240 | 245 | ``` 246 |
247 | 248 |
249 | Links 250 | 251 | Custom link tag (``): 252 | 253 | ```html 254 | 255 | 260 | 261 | 266 | ``` 267 | 268 | When rendering a link tag, the url gets passed as the `url` component property. 269 |
270 | 271 |
272 | Images 273 | 274 | Custom image tag (``): 275 | 276 | ```html 277 | 278 | 283 | 284 | 293 | ``` 294 | 295 | When rendering an image tag, the image object gets passed as the `image` component property. 296 | You can also use different image components here, i.e. `NuxtImg` or others. 297 |
298 | 299 | ## Development 300 | 301 | ### Dependencies 302 | 303 | To install the dependencies, run the `install` command: 304 | 305 | ```bash 306 | npm install 307 | ``` 308 | 309 | The project requires Node.js and NPM to run. 310 | You can either install these manually on your system or if you have the nix package manager installed, use the 311 | provided nix-shell with the following command: 312 | 313 | ```bash 314 | nix-shell 315 | ``` 316 | 317 | This will automatically install the needed software and start up a shell. 318 | 319 | ### Type stubs 320 | 321 | To generate the type stubs for the nuxt module, run the `dev:prepare` command: 322 | 323 | ```bash 324 | npm run dev:prepare 325 | ``` 326 | 327 | ### Development server 328 | 329 | To start the development server with the provided text components, run the `dev` command: 330 | 331 | ```bash 332 | npm run dev 333 | ``` 334 | 335 | This will boot up the playground with the default text components. 336 | To start the development server using custom text components, overriding the provided components, 337 | use the `dev:custom` command: 338 | 339 | ```bash 340 | npm run dev:custom 341 | ``` 342 | 343 | ### Quality 344 | 345 | #### Linter 346 | 347 | To run ESLint, use the following command: 348 | 349 | ```bash 350 | npm run lint:es 351 | ``` 352 | 353 | #### Type checks 354 | 355 | To run the TypeScript type checks, use the following command: 356 | 357 | ```bash 358 | npm run lint:types 359 | ``` 360 | 361 | #### Unit Tests 362 | 363 | To run the Vitest unit tests, run the following command: 364 | 365 | ```bash 366 | npm run test 367 | ```` 368 | 369 | ### Build 370 | 371 | To build the module, first install all dependencies and [generate the type stubs](#type-stubs). 372 | Then run the build script: 373 | 374 | ```bash 375 | npm run build 376 | ``` 377 | 378 | The module files will be output to the `dist` folder. 379 | 380 | ### Release 381 | 382 | To release a new version of the strapi blocks renderer nuxt module, take the following steps: 383 | 384 | 1. Increment version number in the `package.json` file 385 | 2. Add changelog entry for the new version number 386 | 3. Run linters and unit tests 387 | 4. Build the nuxt module 388 | 389 | ```bash 390 | npm run build 391 | ``` 392 | 393 | 5. Log in to NPM using your access token 394 | 6. Run the `release` command 395 | 396 | ```bash 397 | npm run release 398 | ``` 399 | 400 | [github-actions-src]: https://github.com/freb97/nuxt-strapi-blocks-renderer/actions/workflows/ci.yml/badge.svg 401 | [github-actions-href]: https://github.com/freb97/nuxt-strapi-blocks-renderer/actions 402 | 403 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-strapi-blocks-renderer/latest.svg?style=flat&colorA=18181B&colorB=31C553 404 | [npm-version-href]: https://npmjs.com/package/nuxt-strapi-blocks-renderer 405 | 406 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-strapi-blocks-renderer.svg?style=flat&colorA=18181B&colorB=31C553 407 | [npm-downloads-href]: https://npmjs.com/package/nuxt-strapi-blocks-renderer 408 | 409 | [npm-last-update-src]: https://img.shields.io/npm/last-update/nuxt-strapi-blocks-renderer.svg?style=flat&colorA=18181B&colorB=31C553 410 | [npm-last-update-href]: https://npmjs.com/package/nuxt-strapi-blocks-renderer 411 | 412 | [license-src]: https://img.shields.io/github/license/freb97/nuxt-strapi-blocks-renderer.svg?style=flat&colorA=18181B&colorB=31C553 413 | [license-href]: https://github.com/freb97/nuxt-strapi-blocks-renderer/blob/main/LICENSE 414 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat'; 2 | 3 | export default createConfigForNuxt({ 4 | features: { 5 | tooling: true, 6 | typescript: { 7 | strict: true, 8 | }, 9 | stylistic: { 10 | indent: 4, 11 | semi: true, 12 | }, 13 | }, 14 | }).override('nuxt/vue/rules', { 15 | rules: { 16 | 'vue/block-lang': [ 'error', { script: { lang: 'ts' } } ], 17 | 'vue/block-order': [ 'error', { order: [ 'script', 'template', 'style' ] } ], 18 | 'vue/component-name-in-template-casing': [ 'error', 'PascalCase', { registeredComponentsOnly: false } ], 19 | 'vue/multi-word-component-names': 'off', 20 | 'vue/no-ref-object-reactivity-loss': 'error', 21 | }, 22 | }).override('nuxt/stylistic', { 23 | rules: { 24 | '@stylistic/array-bracket-spacing': [ 'error', 'always' ], 25 | '@stylistic/comma-dangle': [ 'error', 'always-multiline' ], 26 | }, 27 | }).override('nuxt/import/rules', { 28 | rules: { 29 | 'import/order': [ 'error', { 30 | 'groups': [ 31 | 'type', 32 | [ 'builtin', 'external' ], 33 | [ 'internal', 'parent', 'sibling', 'index', 'object' ], 34 | ], 35 | 'newlines-between': 'always', 36 | 'alphabetize': { 37 | order: 'asc', 38 | }, 39 | } ], 40 | }, 41 | }).append({ 42 | rules: { 43 | 'prefer-template': [ 'error' ], 44 | 'no-unused-vars': [ 'error', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' } ], 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-strapi-blocks-renderer", 3 | "version": "1.0.18", 4 | "description": "Renderer for the strapi CMS blocks text content element.", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/freb97/nuxt-strapi-blocks-renderer.git" 9 | }, 10 | "author": { 11 | "name": "Frederik Bußmann", 12 | "email": "frederik@bussmann.io" 13 | }, 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/types.d.mts", 18 | "import": "./dist/module.mjs" 19 | } 20 | }, 21 | "main": "./dist/module.mjs", 22 | "typesVersions": { 23 | "*": { 24 | ".": [ 25 | "./dist/types.d.mts" 26 | ] 27 | } 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "dev": "npm run dev:basic", 34 | "dev:prepare": "nuxt-module-build prepare && nuxi prepare playground/basic && nuxi prepare playground/custom", 35 | "dev:basic": "nuxi dev playground/basic", 36 | "dev:custom": "nuxi dev playground/custom", 37 | "build": "nuxt-module-build build", 38 | "release": "npm run lint && npm run test && npm run build && npm publish", 39 | "release:ci": "npm run build && npm publish", 40 | "lint:es": "eslint './**/*.{js,ts,vue}' --max-warnings=0", 41 | "lint:types": "vue-tsc --noEmit", 42 | "test": "vitest run", 43 | "test:watch": "vitest watch", 44 | "renovate:config:validate": "renovate-config-validator" 45 | }, 46 | "dependencies": { 47 | "@nuxt/kit": "^3.17.4", 48 | "defu": "^6.1.4" 49 | }, 50 | "devDependencies": { 51 | "@nuxt/devtools": "^2.4.1", 52 | "@nuxt/eslint-config": "^1.4.1", 53 | "@nuxt/module-builder": "^1.0.1", 54 | "@nuxt/schema": "^3.17.4", 55 | "@nuxt/test-utils": "^3.19.1", 56 | "@nuxtjs/tailwindcss": "^6.14.0", 57 | "eslint": "^9.28.0", 58 | "nuxt": "^3.17.4", 59 | "renovate": "^40.37.1", 60 | "vitest": "^3.1.4", 61 | "vue-tsc": "^2.2.10" 62 | }, 63 | "keywords": [ 64 | "nuxt", 65 | "nuxt3", 66 | "nuxtjs", 67 | "nuxt-module", 68 | "strapi", 69 | "strapi-cms", 70 | "json", 71 | "blocks", 72 | "text", 73 | "renderer" 74 | ], 75 | "engines": { 76 | "node": "^18.x || ^20.x || ^22.x" 77 | }, 78 | "stackblitz": { 79 | "startCommand": "npm run dev:prepare && npm run dev:custom" 80 | } 81 | } -------------------------------------------------------------------------------- /playground/basic/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: [ 3 | '../../src/module', 4 | ], 5 | 6 | compatibilityDate: '2024-11-03', 7 | }); 8 | -------------------------------------------------------------------------------- /playground/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "strapi-blocks-renderer-playground-basic", 4 | "type": "module", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nuxi dev", 8 | "prepare": "nuxi prepare", 9 | "build": "nuxi build", 10 | "generate": "nuxi generate" 11 | }, 12 | "devDependencies": { 13 | "nuxt": "^3.17.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playground/basic/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /playground/basic/public/example_image_df80dd3023.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/playground/basic/public/example_image_df80dd3023.jpg -------------------------------------------------------------------------------- /playground/basic/server/tsconfig.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/playground/basic/server/tsconfig.json -------------------------------------------------------------------------------- /playground/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/custom/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/custom/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html, 7 | body { 8 | @apply font-sans; 9 | } 10 | } -------------------------------------------------------------------------------- /playground/custom/assets/img/nuxt-strapi-blocks-renderer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/playground/custom/assets/img/nuxt-strapi-blocks-renderer.png -------------------------------------------------------------------------------- /playground/custom/components/Header.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/CustomCodeNode.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/CustomImageNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/CustomParagraphNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/CustomQuoteNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading1Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading2Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading3Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading4Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading5Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/heading/CustomHeading6Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomBoldInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomCodeInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomItalicInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomLinkInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomListItemInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomStrikethroughInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/inline/CustomUnderlineInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/list/CustomOrderedListNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/components/blocks/list/CustomUnorderedListNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/custom/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: [ 3 | '../../src/module', 4 | '@nuxtjs/tailwindcss', 5 | ], 6 | 7 | components: { 8 | dirs: [ 9 | { 10 | path: '~/components/blocks', 11 | pathPrefix: false, 12 | global: true, 13 | }, 14 | { 15 | path: '~/components', 16 | pathPrefix: false, 17 | }, 18 | ], 19 | }, 20 | 21 | runtimeConfig: { 22 | public: { 23 | strapiBlocksRenderer: { 24 | prefix: 'Custom', 25 | blocksPrefix: 'Custom', 26 | }, 27 | }, 28 | }, 29 | 30 | compatibilityDate: '2024-08-15', 31 | 32 | typescript: { 33 | tsConfig: { 34 | compilerOptions: { 35 | resolveJsonModule: true, 36 | }, 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /playground/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "strapi-blocks-renderer-playground-custom", 4 | "type": "module", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nuxi dev", 8 | "prepare": "nuxi prepare", 9 | "build": "nuxi build", 10 | "generate": "nuxi generate" 11 | }, 12 | "devDependencies": { 13 | "@nuxtjs/tailwindcss": "^6.14.0", 14 | "nuxt": "^3.17.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/custom/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /playground/custom/public/example_image_df80dd3023.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/playground/custom/public/example_image_df80dd3023.jpg -------------------------------------------------------------------------------- /playground/custom/server/tsconfig.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freb97/nuxt-strapi-blocks-renderer/aa0901614963b8c7e927142ed8531ee539e5e7a4/playground/custom/server/tsconfig.json -------------------------------------------------------------------------------- /playground/custom/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: [ 'Helvetica', 'Arial', 'sans-serif' ], 8 | mono: [ 'Courier New', 'Courier', 'monospace' ], 9 | }, 10 | }, 11 | }, 12 | plugins: [], 13 | }; 14 | -------------------------------------------------------------------------------- /playground/custom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "group:allNonMajor", 5 | "schedule:weekly" 6 | ], 7 | "automerge": true, 8 | "rangeStrategy": "bump", 9 | "commitMessagePrefix": "chore:" 10 | } -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | pkgs.mkShell { 4 | name = "nuxt-strapi-blocks-renderer"; 5 | 6 | buildInputs = [ 7 | pkgs.nodejs_22 8 | pkgs.git 9 | ]; 10 | } 11 | -------------------------------------------------------------------------------- /src/.fixtures/sampleBlockNodes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "heading", 4 | "children": [ 5 | { 6 | "type": "text", 7 | "text": "Heading 1" 8 | } 9 | ], 10 | "level": 1 11 | }, 12 | { 13 | "type": "heading", 14 | "children": [ 15 | { 16 | "type": "text", 17 | "text": "Heading 2" 18 | } 19 | ], 20 | "level": 2 21 | }, 22 | { 23 | "type": "heading", 24 | "children": [ 25 | { 26 | "type": "text", 27 | "text": "Heading 3" 28 | } 29 | ], 30 | "level": 3 31 | }, 32 | { 33 | "type": "heading", 34 | "children": [ 35 | { 36 | "type": "text", 37 | "text": "Heading 4" 38 | } 39 | ], 40 | "level": 4 41 | }, 42 | { 43 | "type": "heading", 44 | "children": [ 45 | { 46 | "type": "text", 47 | "text": "Heading 5" 48 | } 49 | ], 50 | "level": 5 51 | }, 52 | { 53 | "type": "heading", 54 | "children": [ 55 | { 56 | "type": "text", 57 | "text": "Heading 6" 58 | } 59 | ], 60 | "level": 6 61 | }, 62 | { 63 | "type": "paragraph", 64 | "children": [ 65 | { 66 | "type": "text", 67 | "text": "Paragraph" 68 | } 69 | ] 70 | }, 71 | { 72 | "type": "paragraph", 73 | "children": [ 74 | { 75 | "type": "text", 76 | "text": "Paragraph with \nline \nbreaks" 77 | } 78 | ] 79 | }, 80 | { 81 | "type": "paragraph", 82 | "children": [ 83 | { 84 | "type": "text", 85 | "text": "Bold", 86 | "bold": "true" 87 | } 88 | ] 89 | }, 90 | { 91 | "type": "paragraph", 92 | "children": [ 93 | { 94 | "type": "text", 95 | "text": "Italic", 96 | "italic": "true" 97 | } 98 | ] 99 | }, 100 | { 101 | "type": "paragraph", 102 | "children": [ 103 | { 104 | "type": "text", 105 | "text": "Underline", 106 | "underline": "true" 107 | } 108 | ] 109 | }, 110 | { 111 | "type": "paragraph", 112 | "children": [ 113 | { 114 | "type": "text", 115 | "text": "Strikethrough", 116 | "strikethrough": "true" 117 | } 118 | ] 119 | }, 120 | { 121 | "type": "paragraph", 122 | "children": [ 123 | { 124 | "type": "text", 125 | "text": "Code", 126 | "code": "true" 127 | } 128 | ] 129 | }, 130 | { 131 | "type": "paragraph", 132 | "children": [ 133 | { 134 | "type": "link", 135 | "url": "https://www.example.com/", 136 | "children": [ 137 | { 138 | "text": "Link", 139 | "type": "text" 140 | } 141 | ] 142 | } 143 | ] 144 | }, 145 | { 146 | "type": "list", 147 | "format": "unordered", 148 | "children": [ 149 | { 150 | "type": "list-item", 151 | "children": [ 152 | { 153 | "type": "text", 154 | "text": "Unordered list item 1" 155 | } 156 | ] 157 | }, 158 | { 159 | "type": "list-item", 160 | "children": [ 161 | { 162 | "type": "text", 163 | "text": "Unordered list item 2" 164 | } 165 | ] 166 | }, 167 | { 168 | "type": "list-item", 169 | "children": [ 170 | { 171 | "type": "text", 172 | "text": "Unordered list item 3" 173 | } 174 | ] 175 | } 176 | ] 177 | }, 178 | { 179 | "type": "list", 180 | "format": "ordered", 181 | "children": [ 182 | { 183 | "type": "list-item", 184 | "children": [ 185 | { 186 | "type": "text", 187 | "text": "Ordered list item 1" 188 | } 189 | ] 190 | }, 191 | { 192 | "type": "list-item", 193 | "children": [ 194 | { 195 | "type": "text", 196 | "text": "Ordered list item 2" 197 | } 198 | ] 199 | }, 200 | { 201 | "type": "list-item", 202 | "children": [ 203 | { 204 | "type": "text", 205 | "text": "Ordered list item 3" 206 | } 207 | ] 208 | } 209 | ] 210 | }, 211 | { 212 | "type": "quote", 213 | "children": [ 214 | { 215 | "type": "text", 216 | "text": "Quote" 217 | } 218 | ] 219 | }, 220 | { 221 | "type": "code", 222 | "children": [ 223 | { 224 | "type": "text", 225 | "text": "Code" 226 | } 227 | ] 228 | }, 229 | { 230 | "type": "image", 231 | "image": { 232 | "name": "example-image.jpg", 233 | "alternativeText": "Image alternative text", 234 | "caption": null, 235 | "width": 480, 236 | "height": 320, 237 | "hash": "example_image_df80dd3023", 238 | "ext": ".jpg", 239 | "mime": "image/jpeg", 240 | "size": 22.94, 241 | "url": "example_image_df80dd3023.jpg", 242 | "previewUrl": null, 243 | "provider": "local", 244 | "provider_metadata": null, 245 | "createdAt": "2023-12-26T15:47:12.619Z", 246 | "updatedAt": "2023-12-26T15:47:12.619Z" 247 | } 248 | } 249 | ] 250 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentsDir, Nuxt, RuntimeConfig } from '@nuxt/schema'; 2 | 3 | import { addComponent, addComponentsDir, addImports, createResolver, defineNuxtModule } from '@nuxt/kit'; 4 | import { defu } from 'defu'; 5 | 6 | export interface ModuleOptions { 7 | prefix: string; 8 | blocksPrefix: string; 9 | } 10 | 11 | export default defineNuxtModule({ 12 | meta: { 13 | name: 'nuxt-strapi-blocks-renderer', 14 | configKey: 'strapiBlocksRenderer', 15 | }, 16 | 17 | defaults: { 18 | prefix: '', 19 | blocksPrefix: 'StrapiBlocksText', 20 | }, 21 | 22 | setup(options: ModuleOptions, nuxt: Nuxt) { 23 | const { resolve } = createResolver(import.meta.url); 24 | 25 | nuxt.options.alias['#strapi-blocks-renderer'] = resolve('./runtime'); 26 | 27 | const runtimeConfig: RuntimeConfig = nuxt.options.runtimeConfig; 28 | 29 | runtimeConfig.public.strapiBlocksRenderer 30 | = defu(runtimeConfig.public.strapiBlocksRenderer as ModuleOptions, options); 31 | 32 | addImports([ 33 | { 34 | name: 'useBlocksText', 35 | as: 'useBlocksText', 36 | from: resolve('./runtime/composables/useBlocksText'), 37 | }, 38 | ]); 39 | 40 | addComponent({ 41 | name: `${options.prefix}StrapiBlocksText`, 42 | filePath: resolve('./runtime/components/StrapiBlocksText.vue'), 43 | global: true, 44 | }); 45 | 46 | addComponentsDir({ 47 | path: resolve('./runtime/components/blocks'), 48 | pathPrefix: false, 49 | prefix: options.blocksPrefix, 50 | global: true, 51 | priority: 0, 52 | }); 53 | 54 | nuxt.hook('components:dirs', (componentsDir: (string | ComponentsDir)[]) => { 55 | componentsDir.push({ 56 | path: resolve('./runtime/components/blocks'), 57 | pathPrefix: false, 58 | prefix: options.blocksPrefix, 59 | global: true, 60 | priority: -10, 61 | }); 62 | }); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/runtime/components/StrapiBlocksText.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/CodeNode.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/ImageNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/ParagraphNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/QuoteNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading1Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading2Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading3Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading4Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading5Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/heading/Heading6Node.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/BoldInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/CodeInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/ItalicInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/LinkInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/ListItemInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/StrikethroughInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/inline/UnderlineInlineNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/list/OrderedListNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/components/blocks/list/UnorderedListNode.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/composables/useBlocksText.ts: -------------------------------------------------------------------------------- 1 | import type { BlockNode } from '#strapi-blocks-renderer/types'; 2 | import type { VNode } from 'vue'; 3 | 4 | import { useRuntimeConfig } from '#imports'; 5 | import { renderBlocks } from '#strapi-blocks-renderer/utils'; 6 | 7 | export interface UseBlocksTextReturn { 8 | text: VNode[]; 9 | } 10 | 11 | export const useBlocksText = (blockNodes: BlockNode[]): UseBlocksTextReturn => { 12 | const prefix = useRuntimeConfig().public.strapiBlocksRenderer.blocksPrefix as string; 13 | 14 | const textNodes: VNode[] = renderBlocks(blockNodes, prefix); 15 | 16 | return { 17 | text: textNodes, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/runtime/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Inline nodes 2 | 3 | export interface TextInlineNode { 4 | type: 'text'; 5 | text: string; 6 | bold?: boolean; 7 | italic?: boolean; 8 | underline?: boolean; 9 | strikethrough?: boolean; 10 | code?: boolean; 11 | } 12 | 13 | export interface LinkInlineNode { 14 | type: 'link'; 15 | url: string; 16 | children: TextInlineNode[]; 17 | } 18 | 19 | export interface ListItemInlineNode { 20 | type: 'list-item'; 21 | children: DefaultInlineNode[]; 22 | } 23 | 24 | export type DefaultInlineNode = TextInlineNode | LinkInlineNode; 25 | 26 | // Typed nodes 27 | 28 | export interface ParagraphBlockNode { 29 | type: 'paragraph'; 30 | children: DefaultInlineNode[]; 31 | } 32 | 33 | export interface HeadingBlockNode { 34 | type: 'heading'; 35 | level: 1 | 2 | 3 | 4 | 5 | 6; 36 | children: DefaultInlineNode[]; 37 | } 38 | 39 | export interface ListBlockNode { 40 | type: 'list'; 41 | format: 'ordered' | 'unordered'; 42 | children: (ListItemInlineNode | ListBlockNode)[]; 43 | } 44 | 45 | export interface QuoteBlockNode { 46 | type: 'quote'; 47 | children: DefaultInlineNode[]; 48 | } 49 | 50 | export interface CodeBlockNode { 51 | type: 'code'; 52 | children: TextInlineNode[]; 53 | } 54 | 55 | export interface ImageBlockNode { 56 | type: 'image'; 57 | image: Record; 58 | children: [{ type: 'text'; text: '' }]; 59 | } 60 | 61 | export type BlockNode = 62 | ParagraphBlockNode | 63 | QuoteBlockNode | 64 | CodeBlockNode | 65 | HeadingBlockNode | 66 | ListBlockNode | 67 | ImageBlockNode; 68 | -------------------------------------------------------------------------------- /src/runtime/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BlockNode, 3 | CodeBlockNode, 4 | DefaultInlineNode, 5 | HeadingBlockNode, 6 | ImageBlockNode, 7 | LinkInlineNode, 8 | ListBlockNode, 9 | ListItemInlineNode, 10 | ParagraphBlockNode, 11 | QuoteBlockNode, 12 | TextInlineNode, 13 | } from '#strapi-blocks-renderer/types'; 14 | import type { ConcreteComponent, VNode } from 'vue'; 15 | 16 | import { h, resolveComponent } from 'vue'; 17 | 18 | const getNodeText = (node: TextInlineNode): (VNode | string)[] => { 19 | const lines: (VNode | string)[] = []; 20 | 21 | node.text.split('\n').forEach((line: string, index: number, array: string[]): void => { 22 | lines.push(line); 23 | 24 | if (index !== array.length - 1) { 25 | lines.push(h('br')); 26 | } 27 | }); 28 | 29 | return lines; 30 | }; 31 | 32 | export const textInlineNode = (node: TextInlineNode, prefix: string): (VNode | string)[] | VNode => { 33 | const text: (VNode | string)[] = getNodeText(node); 34 | 35 | if (node.bold) return h(resolveComponent(`${prefix}BoldInlineNode`), () => text); 36 | if (node.italic) return h(resolveComponent(`${prefix}ItalicInlineNode`), () => text); 37 | if (node.underline) return h(resolveComponent(`${prefix}UnderlineInlineNode`), () => text); 38 | if (node.strikethrough) return h(resolveComponent(`${prefix}StrikethroughInlineNode`), () => text); 39 | if (node.code) return h(resolveComponent(`${prefix}CodeInlineNode`), () => text); 40 | 41 | return text; 42 | }; 43 | 44 | export const linkInlineNode = (node: LinkInlineNode, prefix: string): VNode => { 45 | const linkComponent: string | ConcreteComponent = resolveComponent(`${prefix}LinkInlineNode`); 46 | 47 | return h(linkComponent, { url: node.url }, () => node.children.map((childNode: TextInlineNode) => { 48 | return textInlineNode(childNode, prefix); 49 | })); 50 | }; 51 | 52 | export const defaultInlineNode = (node: DefaultInlineNode, prefix: string): (VNode | string)[] | VNode | undefined => { 53 | if (node.type === 'link') { 54 | return linkInlineNode(node, prefix); 55 | } 56 | else if (node.type === 'text') { 57 | return textInlineNode(node, prefix); 58 | } 59 | }; 60 | 61 | export const listItemInlineNode = (node: ListItemInlineNode, prefix: string): VNode => { 62 | const listItemComponent: string | ConcreteComponent = resolveComponent(`${prefix}ListItemInlineNode`); 63 | 64 | return h(listItemComponent, () => node.children.map( 65 | (childNode: DefaultInlineNode) => defaultInlineNode(childNode, prefix), 66 | )); 67 | }; 68 | 69 | export const headingBlockNode = (node: HeadingBlockNode, prefix: string): VNode => { 70 | const headingComponent: string | ConcreteComponent = resolveComponent(`${prefix}Heading${node.level}Node`); 71 | 72 | return h(headingComponent, () => node.children.map( 73 | (childNode: DefaultInlineNode) => defaultInlineNode(childNode, prefix), 74 | )); 75 | }; 76 | 77 | export const paragraphBlockNode = (node: ParagraphBlockNode, prefix: string): VNode => { 78 | const paragraphComponent: string | ConcreteComponent = resolveComponent(`${prefix}ParagraphNode`); 79 | 80 | return h(paragraphComponent, () => node.children.map( 81 | (childNode: DefaultInlineNode) => defaultInlineNode(childNode, prefix), 82 | )); 83 | }; 84 | 85 | export const codeBlockNode = (node: CodeBlockNode, prefix: string): VNode => { 86 | const codeComponent: string | ConcreteComponent = resolveComponent(`${prefix}CodeNode`); 87 | 88 | return h(codeComponent, () => node.children.map( 89 | (childNode: TextInlineNode): (VNode | string)[] | VNode => textInlineNode(childNode, prefix), 90 | )); 91 | }; 92 | 93 | export const quoteBlockNode = (node: QuoteBlockNode, prefix: string): VNode => { 94 | const quoteComponent: string | ConcreteComponent = resolveComponent(`${prefix}QuoteNode`); 95 | 96 | return h(quoteComponent, () => node.children.map( 97 | (childNode: DefaultInlineNode) => defaultInlineNode(childNode, prefix), 98 | )); 99 | }; 100 | 101 | export const listBlockNode = (node: ListBlockNode, prefix: string): VNode => { 102 | const listType: string = node.format === 'ordered' ? 'OrderedListNode' : 'UnorderedListNode'; 103 | const listComponent: string | ConcreteComponent = resolveComponent(`${prefix}${listType}`); 104 | 105 | return h(listComponent, () => node.children.map( 106 | (childNode: ListBlockNode | ListItemInlineNode): VNode | undefined => { 107 | if (childNode.type === 'list-item') { 108 | return listItemInlineNode(childNode, prefix); 109 | } 110 | 111 | return listBlockNode(childNode, prefix); 112 | }, 113 | )); 114 | }; 115 | 116 | export const imageBlockNode = (node: ImageBlockNode, prefix: string): VNode => { 117 | const imageComponent: string | ConcreteComponent = resolveComponent(`${prefix}ImageNode`); 118 | 119 | return h(imageComponent, { 120 | image: node.image, 121 | }); 122 | }; 123 | 124 | export const renderBlocks = (blockNodes: BlockNode[], prefix: string): VNode[] => { 125 | return blockNodes.map((blockNode: BlockNode): VNode => { 126 | switch (blockNode.type) { 127 | case 'heading': return headingBlockNode(blockNode, prefix); 128 | case 'code': return codeBlockNode(blockNode, prefix); 129 | case 'list': return listBlockNode(blockNode, prefix); 130 | case 'quote': return quoteBlockNode(blockNode, prefix); 131 | case 'image': return imageBlockNode(blockNode, prefix); 132 | default: return paragraphBlockNode(blockNode, prefix); 133 | } 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { $fetch, setup } from '@nuxt/test-utils'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | const fetchPage = async (): Promise => { 6 | let html: string = await $fetch('/'); 7 | 8 | // Remove html comment nodes 9 | html = html.replace(//g, ''); 10 | html = html.replace(//g, ''); 11 | 12 | return html; 13 | }; 14 | 15 | describe('basic blocks text rendering', async (): Promise => { 16 | await setup({ 17 | rootDir: fileURLToPath(new URL('../playground/basic', import.meta.url)), 18 | }); 19 | 20 | it('renders the heading nodes', async (): Promise => { 21 | const html: string = await fetchPage(); 22 | 23 | expect(html).toContain('

Heading 1

'); 24 | expect(html).toContain('

Heading 2

'); 25 | expect(html).toContain('

Heading 3

'); 26 | expect(html).toContain('

Heading 4

'); 27 | expect(html).toContain('
Heading 5
'); 28 | expect(html).toContain('
Heading 6
'); 29 | }); 30 | 31 | it('renders the text nodes', async (): Promise => { 32 | const html: string = await fetchPage(); 33 | 34 | expect(html).toContain('

Paragraph

'); 35 | expect(html).toContain('

Paragraph with
line
breaks

'); 36 | expect(html).toContain('

Bold

'); 37 | expect(html).toContain('

Italic

'); 38 | expect(html).toContain('

Underline

'); 39 | expect(html).toContain('

Strikethrough

'); 40 | expect(html).toContain('

Code

'); 41 | expect(html).toContain('

Link

'); 42 | }); 43 | 44 | it('renders the list nodes', async (): Promise => { 45 | const html: string = await fetchPage(); 46 | 47 | expect(html).toContain('
    ' 48 | + '
  • Unordered list item 1
  • ' 49 | + '
  • Unordered list item 2
  • ' 50 | + '
  • Unordered list item 3
  • ' 51 | + '
'); 52 | 53 | expect(html).toContain('
    ' 54 | + '
  1. Ordered list item 1
  2. ' 55 | + '
  3. Ordered list item 2
  4. ' 56 | + '
  5. Ordered list item 3
  6. ' 57 | + '
'); 58 | }); 59 | 60 | it('renders the quote node', async (): Promise => { 61 | const html: string = await fetchPage(); 62 | 63 | expect(html).toContain('
Quote
'); 64 | }); 65 | 66 | it('renders the code node', async (): Promise => { 67 | const html: string = await fetchPage(); 68 | 69 | expect(html).toContain('
Code
'); 70 | }); 71 | 72 | it('renders the image node', async (): Promise => { 73 | const html: string = await fetchPage(); 74 | 75 | expect(html).toContain(''); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/custom.test.ts: -------------------------------------------------------------------------------- 1 | import { $fetch, setup } from '@nuxt/test-utils'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | const fetchPage = async (): Promise => { 6 | let html: string = await $fetch('/'); 7 | 8 | // Remove html comment nodes 9 | html = html.replace(//g, ''); 10 | html = html.replace(//g, ''); 11 | 12 | return html; 13 | }; 14 | 15 | describe('custom blocks text rendering', async (): Promise => { 16 | await setup({ 17 | rootDir: fileURLToPath(new URL('../playground/custom', import.meta.url)), 18 | }); 19 | 20 | it('renders the custom heading nodes', async (): Promise => { 21 | const html: string = await fetchPage(); 22 | 23 | expect(html).toContain('

Heading 1

'); 24 | expect(html).toContain('

Heading 2

'); 25 | expect(html).toContain('

Heading 3

'); 26 | expect(html).toContain('

Heading 4

'); 27 | expect(html).toContain('
Heading 5
'); 28 | expect(html).toContain('
Heading 6
'); 29 | }); 30 | 31 | it('renders the custom text nodes', async (): Promise => { 32 | const html: string = await fetchPage(); 33 | 34 | expect(html).toContain('

Paragraph

'); 35 | expect(html).toContain('

Paragraph with
line
breaks

'); 36 | expect(html).toContain('

Bold

'); 37 | expect(html).toContain('

Italic

'); 38 | expect(html).toContain('

Underline

'); 39 | expect(html).toContain('

Strikethrough

'); 40 | expect(html).toContain('

Code

'); 41 | 42 | expect(html).toContain('

' 43 | + 'Link' 44 | + '

'); 45 | }); 46 | 47 | it('renders the custom list nodes', async (): Promise => { 48 | const html: string = await fetchPage(); 49 | 50 | expect(html).toContain('
    ' 51 | + '
  • Unordered list item 1
  • ' 52 | + '
  • Unordered list item 2
  • ' 53 | + '
  • Unordered list item 3
  • ' 54 | + '
'); 55 | 56 | expect(html).toContain('
    ' 57 | + '
  1. Ordered list item 1
  2. ' 58 | + '
  3. Ordered list item 2
  4. ' 59 | + '
  5. Ordered list item 3
  6. ' 60 | + '
'); 61 | }); 62 | 63 | it('renders the custom quote node', async (): Promise => { 64 | const html: string = await fetchPage(); 65 | 66 | expect(html).toContain('
Quote
'); 67 | }); 68 | 69 | it('renders the custom code node', async (): Promise => { 70 | const html: string = await fetchPage(); 71 | 72 | expect(html).toContain('
Code
'); 73 | }); 74 | 75 | it('renders the custom image node', async (): Promise => { 76 | const html: string = await fetchPage(); 77 | 78 | expect(html).toContain(''); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | } 4 | --------------------------------------------------------------------------------