├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── qa.yml │ ├── sast.yml │ └── sca.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── demo ├── .gitignore ├── astro.config.mjs ├── package.json ├── public │ └── favicon.svg ├── src │ ├── components │ │ ├── Blockquote.astro │ │ ├── BulletList.astro │ │ ├── Button.astro │ │ ├── Heading.astro │ │ ├── InlineCode.astro │ │ ├── Link.astro │ │ ├── Picture.astro │ │ ├── Styled.astro │ │ ├── Table.astro │ │ └── Text.astro │ ├── layouts │ │ └── Layout.astro │ ├── pages │ │ └── index.astro │ └── storyblok │ │ ├── Button.astro │ │ └── RichText.astro └── tsconfig.json ├── lib ├── RichTextRenderer.astro ├── RichTextRenderer.ts ├── package.json ├── src │ ├── index.ts │ ├── types.ts │ └── utils │ │ ├── resolveRichTextToNodes.spec.ts │ │ └── resolveRichTextToNodes.ts ├── tsconfig.json └── vite.config.ts ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # npm files 2 | node_modules 3 | 4 | # project 5 | dist 6 | .yarn 7 | *.cjs 8 | !.prettierrc.js 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | parserOptions: { 6 | ecmaVersion: "latest", 7 | sourceType: "module", 8 | }, 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier", 13 | "plugin:astro/recommended", 14 | ], 15 | globals: { 16 | globalThis: true, 17 | }, 18 | overrides: [ 19 | { 20 | files: ["*.astro"], 21 | plugins: ["astro"], 22 | env: { 23 | node: true, 24 | "astro/astro": true, 25 | es2020: true, 26 | }, 27 | parser: "astro-eslint-parser", 28 | parserOptions: { 29 | parser: "@typescript-eslint/parser", 30 | extraFileExtensions: [".astro"], 31 | sourceType: "module", 32 | }, 33 | }, 34 | ], 35 | ignorePatterns: "dist/", 36 | }; 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | time: "04:00" 8 | commit-message: 9 | prefix: fix 10 | prefix-development: chore 11 | include: scope 12 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read # Apply only required permissions 11 | 12 | jobs: 13 | qa: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 #v3 18 | with: 19 | persist-credentials: 'false' #By default, actions/checkout persists GIT credentials, we do not need this 20 | - name: Setup Node 21 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 #v3 22 | with: 23 | node-version: 22 24 | cache: "npm" 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Build lib 28 | run: npm run build 29 | - name: QA check 30 | run: npm run qa 31 | -------------------------------------------------------------------------------- /.github/workflows/sast.yml: -------------------------------------------------------------------------------- 1 | # By default this workflow will be running for PRs and pushes to other branches except main 2 | on: 3 | pull_request_target: 4 | push: 5 | branches-ignore: 6 | - 'main' 7 | 8 | permissions: {} #Remove permissions 9 | jobs: 10 | sast: 11 | uses: NordSecurity/security-scanner-workflows/.github/workflows/sast.yml@35c715910e21a4b84949be8c8be3432f5c2911ce 12 | secrets: 13 | SAST_TEAM: ${{ secrets.SAST_TEAM }} 14 | SAST_URL: ${{ secrets.SAST_URL }} 15 | SAST_USERNAME: ${{ secrets.SAST_USERNAME }} 16 | SAST_PASSWORD: ${{ secrets.SAST_PASSWORD }} 17 | SAST_CLIENT_SECRET: ${{ secrets.SAST_CLIENT_SECRET }} 18 | SAST_ACTION_KEY: ${{ secrets.SAST_ACTION_KEY }} 19 | UNC_ACTION_KEY: ${{ secrets.UNC_ACTION_KEY }} 20 | with: 21 | project-action-path: storyblok-rich-text-astro-renderer-sast-action 22 | unc-branch-enabled: false 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/sca.yml: -------------------------------------------------------------------------------- 1 | # By default this workflow will be running for PRs (rapid scans) and on push to main branch (full scan) 2 | on: 3 | pull_request_target: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | sca: 14 | uses: NordSecurity/security-scanner-workflows/.github/workflows/sca.yml@35c715910e21a4b84949be8c8be3432f5c2911ce 15 | secrets: 16 | SCA_URL: ${{ secrets.SCA_URL }} 17 | SCA_API_TOKEN: ${{ secrets.SCA_API_TOKEN }} 18 | SCA_ACTION_KEY: ${{ secrets.SCA_ACTION_KEY }} 19 | UNC_ACTION_KEY: ${{ secrets.UNC_ACTION_KEY }} 20 | with: 21 | project-action-path: nordsecurity-storyblok-rich-text-astro-renderer-sca-action 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | # environment variables 14 | .env 15 | .env.production 16 | 17 | # macOS-specific files 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npm test 2 | npx --no -- commitlint --edit "$1" 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | npx lint-staged 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | * 2 | CHANGELOG.md 3 | !*/ 4 | !*.yml 5 | !*.json 6 | !*.js 7 | !*.mjs 8 | !*.cjs 9 | !*.ts 10 | !*.tsx 11 | !*.html 12 | !*.css 13 | !*.astro 14 | 15 | # npm files 16 | node_modules 17 | package-lock.json 18 | 19 | # project 20 | dist 21 | .yarn 22 | .astro 23 | .history 24 | **/coverage/ 25 | .github -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: false, 6 | plugins: [require.resolve("prettier-plugin-astro")], 7 | overrides: [ 8 | { 9 | files: "**/*.astro", 10 | options: { 11 | parser: "astro", 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @NordSecurity/front-web 2 | .github/workflows @edvinasjurele @NordSecurity/devsecops 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We happily accept both issues and pull requests for bug reports, bug fixes, feature requests, feature implementations, and documentation improvements. For new features, we recommend that you create an issue first so the feature can be discussed and to prevent unnecessary work in case it's not a feature we want to support. Although, we realize that sometimes code needs to be in place to allow for a meaningful discussion, creating an issue upfront is not a requirement. 4 | 5 | ## Building and testing 6 | 7 | Inside this project, you'll find 2 npm workspace packages: 8 | 9 | - `lib` - the storyblok-rich-text-astro-renderer package 10 | - `demo` - the Astro project to showcase the usage of the lib package 11 | 12 | To develop either of them you can go to each respective package 13 | ``` 14 | cd lib 15 | npm run dev 16 | ``` 17 | or 18 | ``` 19 | cd demo 20 | npm run dev 21 | ``` 22 | 23 | or run any of the following commands from the root of the project: 24 | 25 | | Command | Action | 26 | | :------------------------ | :---------------------------------------------------- | 27 | | `npm install` | Installs dependencies | 28 | | `npm run dev:lib` | Starts file watcher to rebuild library to `./dist/` | 29 | | `npm run dev:demo` | Starts local dev server at `localhost:3000` | 30 | | `npm run build` | Build both `lib` and `demo` apps | 31 | | `npm run demo` | Build and serve `demo` app | 32 | | `npm run qa` | Run the code health check (test, lint and format) | 33 | 34 | ## Submitting a pull request 35 | 36 | 1. Fork the repository and clone to your development environment 37 | 2. Create a new branch: `git checkout -b my-branch-name` 38 | 3. Implement your changes 39 | 5. Push your fork and submit a pull request 40 | 6. Celebrate your contribution and wait for your pull request to be reviewed and merged. 41 | 42 | ## Licensing 43 | 44 | Storyblok Rich Text Astro Renderer is released under MIT License. For more details please refer to the [LICENSE](./LICENSE) file. 45 | 46 | ## Code of conduct 47 | 48 | Nord Security and all of it's projects adhere to the [Contributor Covenant Code of Conduct](https://github.com/NordSecurity/.github/blob/main/CODE_OF_CONDUCT.md). 49 | When participating, you are expected to honor this code. 50 | 51 | **Thank you!** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2022 Nord Security 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storyblok Rich Text Renderer for Astro 2 | 3 | Renders Storyblok rich text content to Astro elements. 4 | 5 | [![GitHub](https://img.shields.io/github/license/NordSecurity/storyblok-rich-text-astro-renderer?style=flat-square)](https://github.com/NordSecurity/storyblok-rich-text-astro-renderer/blob/main/LICENSE) 6 | [![NPM](https://img.shields.io/npm/v/storyblok-rich-text-astro-renderer/latest.svg?style=flat-square)](https://npmjs.com/package/storyblok-rich-text-astro-renderer) 7 | 8 | ## Demo 9 | 10 | If you are in a hurry, check out live demo: 11 | 12 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/NordSecurity/storyblok-rich-text-astro-renderer/tree/main/demo) 13 | 14 | ## Motivation 15 | 16 | Official Storyblok + Astro integration (`@storyblok/astro`) provides the most basic possibility to render rich-text in Astro. The integration package re-exports the generic rich text utility from `@storyblok/js` package, which is framework-agnostic and universal. 17 | 18 | This renderer utility outputs HTML markup, which can be used in Astro via the [set:html](https://docs.astro.build/en/reference/directives-reference/#sethtml) directive: 19 | 20 | ```js 21 | --- 22 | import { renderRichText } from '@storyblok/astro'; 23 | 24 | const { blok } = Astro.props 25 | 26 | const renderedRichText = renderRichText(blok.text) 27 | --- 28 | 29 |
30 | ``` 31 | 32 | Nevertheless, it is possible to customise `renderRichText` to some extent by passing the options as the second parameter: 33 | 34 | ```js 35 | import { RichTextSchema, renderRichText } from "@storyblok/astro"; 36 | import cloneDeep from "clone-deep"; 37 | 38 | const mySchema = cloneDeep(RichTextSchema); 39 | 40 | const { blok } = Astro.props; 41 | 42 | const renderedRichText = renderRichText(blok.text, { 43 | schema: mySchema, 44 | resolver: (component, blok) => { 45 | switch (component) { 46 | case "my-custom-component": 47 | return `
${blok.text}
`; 48 | break; 49 | default: 50 | return `Component ${component} not found`; 51 | } 52 | }, 53 | }); 54 | ``` 55 | 56 | Although this works fine and may cover the most basic needs, it may quickly turn out to be limiting and problematic because of the following reasons: 57 | 58 | 1. `renderRichText` utility cannot map rich text elements to actual Astro components, to be able to render embedded Storyblok components inside the rich text field in CMS. 59 | 1. Links that you might want to pass through your app's router, are not possible to be reused as they require the actual function to be mapped with data. 60 | 1. It is hard to maintain the string values, especially when complex needs appear, f.e. setting classes and other HTML properties dynamically. It may be possible to minimize the complexity by using some HTML parsers, like [ultrahtml](https://github.com/natemoo-re/ultrahtml), but it does not eliminate the problem entirely. 61 | 62 | Instead of dealing with HTML markup, `storyblok-rich-text-astro-renderer` outputs `RichTextRenderer.astro` helper 63 | component (and `resolveRichTextToNodes` resolver utility for the needy ones), which provides options to map any Storyblok rich text 64 | element to any custom component, f.e. Astro, SolidJS, Svelte, Vue, etc. 65 | 66 | The package converts Storyblok CMS rich text data structure into the nested Astro component nodes structure, with the shape of: 67 | ```ts 68 | export type ComponentNode = { 69 | component?: unknown; // <-- component function - Astro, SolidJS, Svelte, Vue etc 70 | props?: Record; // <-- properties object 71 | content?: string | ComponentNode[]; // <-- content, which can either be string or other component node 72 | }; 73 | ``` 74 | 75 | ## Installation 76 | 77 | ``` 78 | npm install storyblok-rich-text-astro-renderer 79 | ``` 80 | 81 | ## Usage 82 | 83 | To get the most basic functionality, add `RichText.astro` Storyblok component to the project: 84 | 85 | ```js 86 | --- 87 | import RichTextRenderer from "storyblok-rich-text-astro-renderer/RichTextRenderer.astro"; 88 | import type { RichTextType } from "storyblok-rich-text-astro-renderer" 89 | import { storyblokEditable } from "@storyblok/astro"; 90 | 91 | export interface Props { 92 | blok: { 93 | text: RichTextType; 94 | }; 95 | } 96 | 97 | const { blok } = Astro.props; 98 | const { text } = blok; 99 | --- 100 | 101 | 102 | ``` 103 | 104 | ## Advanced usage 105 | 106 | Sensible default resolvers for marks and nodes are provided out-of-the-box. You only have to provide custom ones if you want to 107 | override the default behavior. 108 | 109 | Use `resolver` to enable and control the rendering of embedded components, and `schema` to control how you want the nodes and marks be rendered: 110 | 111 | ```js 112 | ({ 117 | component: Text, 118 | props: { variant: `h${level}` }, 119 | }), 120 | paragraph: () => ({ 121 | component: Text, 122 | props: { 123 | class: "this-is-paragraph", 124 | }, 125 | }), 126 | }, 127 | marks: { 128 | link: ({ attrs }) => { 129 | const { custom, ...restAttrs } = attrs; 130 | 131 | return { 132 | component: Link, 133 | props: { 134 | link: { ...custom, ...restAttrs }, 135 | class: "i-am-link", 136 | }, 137 | }; 138 | }, 139 | } 140 | }} 141 | resolver={(blok) => { 142 | return { 143 | component: StoryblokComponent, 144 | props: { blok }, 145 | }; 146 | }} 147 | {...storyblokEditable(blok)} 148 | /> 149 | ``` 150 | 151 | ### Content via prop 152 | 153 | By default, content in `nodes` is handled automatically and passed via slots keeping configuration as follows: 154 | 155 | ```js 156 | heading: ({ attrs: { level } }) => ({ 157 | component: Text, 158 | props: { variant: `h${level}` }, 159 | }), 160 | ``` 161 | This implies that implementation of `Text` is as simple as: 162 | ```js 163 | --- 164 | const { variant } = Astro.props; 165 | const Component = variant || "p"; 166 | --- 167 | 168 | 169 | 170 | 171 | ``` 172 | However in some cases, the users do implementation via props only, thus without slots: 173 | ```js 174 | --- 175 | const { variant, text } = Astro.props; 176 | const Component = variant || "p"; 177 | --- 178 | 179 | 180 | {text} 181 | 182 | ``` 183 | This way the content must be handled explictly in the resolver function and passed via prop: 184 | ```js 185 | heading: ({ attrs: { level }, content }) => ({ 186 | component: Text, 187 | props: { 188 | variant: `h${level}`, 189 | text: content?.[0].text, 190 | }, 191 | }), 192 | ``` 193 | 194 | ## Schema 195 | 196 | The schema has `nodes` and `marks` to be configurable: 197 | 198 | ```js 199 | schema={{ 200 | nodes: { 201 | heading: (node) => ({ ... }), 202 | paragraph: () => ({ ... }), 203 | text: () => ({ ... }), 204 | hard_break: () => ({ ... }), 205 | bullet_list: () => ({ ... }), 206 | ordered_list: (node) => ({ ... }), 207 | list_item: () => ({ ... }), 208 | horizontal_rule: () => ({ ... }), 209 | blockquote: () => ({ ... }), 210 | image: (node) => ({ ... }), 211 | code_block: (node) => ({ ... }), 212 | emoji: (node) => ({ ... }), 213 | table: (node) => ({ ... }), 214 | }, 215 | marks: { 216 | link: (mark) => { ... }, 217 | bold: () => ({ ... }), 218 | underline: () => ({ ... }), 219 | italic: () => ({ ... }), 220 | styled: (mark) => { ... }, 221 | strike: () => ({ ... }), 222 | superscript: () => ({ ... }), 223 | subscript: () => ({ ... }), 224 | code: () => ({ ... }), 225 | anchor: (mark) => ({ ... }), 226 | textStyle: (mark) => ({ ... }), 227 | highlight: (mark) => ({ ... }), 228 | }; 229 | }} 230 | ``` 231 | 232 | NOTE: if any of the latest Storyblok CMS nodes and marks are not supported, please raise an issue or [contribute](./CONTRIBUTING.md). 233 | 234 | ## Inspiration 235 | 236 | - [storyblok-rich-text-react-renderer](https://github.com/claus/storyblok-rich-text-react-renderer) 237 | - [storyblok/astro](https://github.com/storyblok/storyblok-astro/) 238 | 239 | ## Contributing 240 | 241 | Please see our [contributing guidelines](./CONTRIBUTING.md) and our [code of conduct](https://github.com/NordSecurity/.github/blob/main/CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /demo/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import { storyblok } from "@storyblok/astro"; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | integrations: [ 7 | storyblok({ 8 | accessToken: "", 9 | apiOptions: { 10 | cache: { clear: "auto", type: "memory" }, 11 | }, 12 | components: { 13 | rich_text: "storyblok/RichText", 14 | button: "storyblok/Button", 15 | }, 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "start-stackblitz": "npm install storyblok-rich-text-astro-renderer@latest && npm run dev", 11 | "preview": "astro preview", 12 | "try": "npm run build & npm run preview" 13 | }, 14 | "dependencies": { 15 | "@storyblok/astro": "^6.2.0", 16 | "astro": "^5.7.14" 17 | }, 18 | "stackblitz": { 19 | "startCommand": "npm run start-stackblitz" 20 | }, 21 | "volta": { 22 | "node": "22.13.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /demo/src/components/Blockquote.astro: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 14 | -------------------------------------------------------------------------------- /demo/src/components/BulletList.astro: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
4 | -------------------------------------------------------------------------------- /demo/src/components/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export type Props = { 3 | title: string; 4 | }; 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /demo/src/components/Heading.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 3 | 4 | export type Props = { 5 | as: HeadingTag; 6 | text: string; 7 | }; 8 | 9 | const { as: Element = "h1", text, ...props } = Astro.props; 10 | --- 11 | 12 | 13 | {text} 14 | 15 | -------------------------------------------------------------------------------- /demo/src/components/InlineCode.astro: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/src/components/Link.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from "astro/types"; 3 | import type { Link } from "storyblok-rich-text-astro-renderer"; 4 | 5 | export interface Props extends HTMLAttributes<"a"> { 6 | link: Link["attrs"]; 7 | } 8 | 9 | const { 10 | link: { href, target }, 11 | ...props 12 | } = Astro.props; 13 | --- 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/src/components/Picture.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Image } from "storyblok-rich-text-astro-renderer"; 3 | 4 | export type Props = Image["attrs"]; 5 | 6 | const { src, alt, title } = Astro.props; 7 | 8 | /* 9 | NOTE: src is usually Storyblok native link (https://a-us.storyblok.com/f/.../276x83/.../image.png) and it contains width/height information. You can resolve and use it. 10 | For DEMO purposes I use https://dummyimage.com/300x200/eee/aaa image, thus the resolving is slightly different. 11 | */ 12 | const [width, height] = new URL(src).pathname 13 | .split("/")[1] // in case of Storyblok native link format, this should be [3] 14 | .split("x"); 15 | --- 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/src/components/Styled.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Styled } from "storyblok-rich-text-astro-renderer"; 3 | 4 | export type Props = Styled["attrs"]; 5 | 6 | const { class: className } = Astro.props; 7 | --- 8 | 9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/src/components/Table.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 26 | -------------------------------------------------------------------------------- /demo/src/components/Text.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from "astro/types"; 3 | 4 | type Variants = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 5 | export type Tags = Variants | "span" | "p"; 6 | 7 | export interface Props extends HTMLAttributes<"p"> { 8 | variant: Variants; 9 | tag: Tags; 10 | } 11 | 12 | const { variant, tag } = Astro.props; 13 | 14 | const variantClass = { 15 | h1: "heading-3xl", 16 | h2: "heading-2xl", 17 | h3: "heading-xl", 18 | h4: "heading-lg", 19 | h5: "heading-md", 20 | h6: "heading-sm", 21 | }[variant]; 22 | 23 | const Component = tag || variant || "p"; 24 | --- 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /demo/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { RichTextType } from "storyblok-rich-text-astro-renderer"; 3 | import Layout from "../layouts/Layout.astro"; 4 | import StoryblokComponent from "@storyblok/astro/StoryblokComponent.astro"; 5 | 6 | const richTextFromStoryblok: RichTextType = { 7 | type: "doc", 8 | content: [ 9 | { 10 | type: "heading", 11 | attrs: { 12 | level: 1, 13 | }, 14 | content: [ 15 | { 16 | text: "storyblok-rich-text-astro-renderer", 17 | type: "text", 18 | }, 19 | ], 20 | }, 21 | { type: "horizontal_rule" }, 22 | { 23 | type: "paragraph", 24 | content: [ 25 | { 26 | text: "This is simple paragraph. ", 27 | type: "text", 28 | }, 29 | ], 30 | }, 31 | { 32 | type: "paragraph", 33 | content: [ 34 | { 35 | text: "Marks: ", 36 | type: "text", 37 | }, 38 | { 39 | text: "bold", 40 | type: "text", 41 | marks: [ 42 | { 43 | type: "bold", 44 | }, 45 | ], 46 | }, 47 | { 48 | text: ", ", 49 | type: "text", 50 | }, 51 | { 52 | text: "underline", 53 | type: "text", 54 | marks: [ 55 | { 56 | type: "underline", 57 | }, 58 | ], 59 | }, 60 | { 61 | text: ", ", 62 | type: "text", 63 | }, 64 | { 65 | text: "italic", 66 | type: "text", 67 | marks: [ 68 | { 69 | type: "italic", 70 | }, 71 | ], 72 | }, 73 | { 74 | text: ", ", 75 | type: "text", 76 | }, 77 | { 78 | text: "link", 79 | type: "text", 80 | marks: [ 81 | { 82 | type: "link", 83 | attrs: { 84 | href: "/", 85 | uuid: "6c401799-b2ad-4854-aa3e-f58ac59bf763", 86 | anchor: null, 87 | custom: {}, 88 | target: "_blank", 89 | linktype: "story", 90 | }, 91 | }, 92 | ], 93 | }, 94 | { 95 | text: ", ", 96 | type: "text", 97 | }, 98 | { 99 | text: "styled", 100 | type: "text", 101 | marks: [ 102 | { 103 | type: "styled", 104 | attrs: { 105 | class: "text-green", 106 | }, 107 | }, 108 | ], 109 | }, 110 | { 111 | text: ", ", 112 | type: "text", 113 | }, 114 | { 115 | text: "strike", 116 | type: "text", 117 | marks: [ 118 | { 119 | type: "strike", 120 | }, 121 | ], 122 | }, 123 | { 124 | text: ", ", 125 | type: "text", 126 | }, 127 | { 128 | text: "superscript", 129 | type: "text", 130 | marks: [ 131 | { 132 | type: "superscript", 133 | }, 134 | ], 135 | }, 136 | { 137 | text: ", ", 138 | type: "text", 139 | }, 140 | { 141 | text: "subscript", 142 | type: "text", 143 | marks: [ 144 | { 145 | type: "subscript", 146 | }, 147 | ], 148 | }, 149 | { 150 | text: ", ", 151 | type: "text", 152 | }, 153 | { 154 | text: "code", 155 | type: "text", 156 | marks: [ 157 | { 158 | type: "code", 159 | }, 160 | ], 161 | }, 162 | { 163 | text: ", ", 164 | type: "text", 165 | }, 166 | { 167 | text: "anchor", 168 | type: "text", 169 | marks: [ 170 | { 171 | type: "anchor", 172 | attrs: { 173 | id: "this-is-anchor", 174 | }, 175 | }, 176 | ], 177 | }, 178 | { 179 | text: ", ", 180 | type: "text", 181 | }, 182 | { 183 | text: "textStyle", 184 | type: "text", 185 | marks: [ 186 | { 187 | type: "textStyle", 188 | attrs: { 189 | color: "#ddA03A", 190 | }, 191 | }, 192 | ], 193 | }, 194 | { 195 | text: ", ", 196 | type: "text", 197 | }, 198 | { 199 | text: "highlight", 200 | type: "text", 201 | marks: [ 202 | { 203 | type: "highlight", 204 | attrs: { 205 | color: "#9CFFA4", 206 | }, 207 | }, 208 | ], 209 | }, 210 | { 211 | text: ".", 212 | type: "text", 213 | }, 214 | ], 215 | }, 216 | { 217 | type: "paragraph", 218 | content: [ 219 | { 220 | type: "image", 221 | attrs: { 222 | id: 111111, 223 | alt: "My alt text", 224 | src: "https://dummyimage.com/300x200/eee/aaa", // this will actually be Storyblok native link (https://a-us.storyblok.com/f/.../276x83/.../image.png) and it contains width/height information. 225 | title: "The title", 226 | source: "The source", 227 | copyright: "The copyright text", 228 | meta_data: {}, 229 | }, 230 | }, 231 | ], 232 | }, 233 | { 234 | type: "blockquote", 235 | content: [ 236 | { 237 | type: "paragraph", 238 | content: [ 239 | { 240 | text: "This is a quote", 241 | type: "text", 242 | }, 243 | ], 244 | }, 245 | ], 246 | }, 247 | { 248 | type: "code_block", 249 | attrs: { 250 | class: "language-javascript", 251 | }, 252 | content: [ 253 | { 254 | text: "const IsStoryblokFun = () => {\n return true;\n}", 255 | type: "text", 256 | }, 257 | ], 258 | }, 259 | { 260 | type: "blok", 261 | attrs: { 262 | id: "63f693c0-4a1b-46d7-af9b-b67eadb1cf2b", 263 | body: [ 264 | { 265 | _uid: "i-b29a4416-7e0e-49ed-a9ee-23e2299f8df4", 266 | title: "Button [blok]", 267 | component: "button", 268 | }, 269 | ], 270 | }, 271 | }, 272 | { type: "hard_break" }, 273 | { 274 | type: "heading", 275 | attrs: { 276 | level: 2, 277 | }, 278 | content: [ 279 | { 280 | text: "Heading", 281 | type: "text", 282 | }, 283 | ], 284 | }, 285 | { 286 | type: "paragraph", 287 | content: [ 288 | { 289 | text: "Yet one more paragraph with emoji ", 290 | type: "text", 291 | }, 292 | { 293 | type: "emoji", 294 | attrs: { 295 | name: "rocket", 296 | emoji: "🚀", 297 | fallbackImage: 298 | "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f680.png", 299 | }, 300 | }, 301 | ], 302 | }, 303 | { 304 | type: "bullet_list", 305 | content: [ 306 | { 307 | type: "list_item", 308 | content: [ 309 | { 310 | type: "paragraph", 311 | content: [ 312 | { 313 | text: "one", 314 | type: "text", 315 | }, 316 | ], 317 | }, 318 | ], 319 | }, 320 | { 321 | type: "list_item", 322 | content: [ 323 | { 324 | type: "paragraph", 325 | content: [ 326 | { 327 | text: "two", 328 | type: "text", 329 | }, 330 | ], 331 | }, 332 | ], 333 | }, 334 | { 335 | type: "list_item", 336 | content: [ 337 | { 338 | type: "paragraph", 339 | content: [ 340 | { 341 | text: "three", 342 | type: "text", 343 | }, 344 | ], 345 | }, 346 | ], 347 | }, 348 | ], 349 | }, 350 | { 351 | type: "ordered_list", 352 | attrs: { 353 | order: 1, 354 | }, 355 | content: [ 356 | { 357 | type: "list_item", 358 | content: [ 359 | { 360 | type: "paragraph", 361 | content: [ 362 | { 363 | text: "one", 364 | type: "text", 365 | }, 366 | ], 367 | }, 368 | ], 369 | }, 370 | { 371 | type: "list_item", 372 | content: [ 373 | { 374 | type: "paragraph", 375 | content: [ 376 | { 377 | text: "two", 378 | type: "text", 379 | }, 380 | ], 381 | }, 382 | ], 383 | }, 384 | { 385 | type: "list_item", 386 | content: [ 387 | { 388 | type: "paragraph", 389 | content: [ 390 | { 391 | text: "three", 392 | type: "text", 393 | }, 394 | ], 395 | }, 396 | ], 397 | }, 398 | ], 399 | }, 400 | { 401 | type: "table", 402 | content: [ 403 | { 404 | type: "tableRow", 405 | content: [ 406 | { 407 | type: "tableHeader", 408 | attrs: { 409 | colspan: 1, 410 | rowspan: 1, 411 | colwidth: null, 412 | }, 413 | content: [ 414 | { 415 | type: "paragraph", 416 | content: [ 417 | { 418 | text: "Table Header 1", 419 | type: "text", 420 | }, 421 | ], 422 | }, 423 | ], 424 | }, 425 | { 426 | type: "tableHeader", 427 | attrs: { 428 | colspan: 1, 429 | rowspan: 1, 430 | colwidth: [66], 431 | }, 432 | content: [ 433 | { 434 | type: "paragraph", 435 | content: [ 436 | { 437 | text: "Table Header 2", 438 | type: "text", 439 | }, 440 | ], 441 | }, 442 | ], 443 | }, 444 | { 445 | type: "tableHeader", 446 | attrs: { 447 | colspan: 1, 448 | rowspan: 1, 449 | colwidth: [118], 450 | }, 451 | content: [ 452 | { 453 | type: "paragraph", 454 | content: [ 455 | { 456 | text: "Table Header 3", 457 | type: "text", 458 | }, 459 | ], 460 | }, 461 | ], 462 | }, 463 | { 464 | type: "tableHeader", 465 | attrs: { 466 | colspan: 1, 467 | rowspan: 1, 468 | colwidth: null, 469 | }, 470 | content: [ 471 | { 472 | type: "paragraph", 473 | content: [ 474 | { 475 | text: "Table Header 4", 476 | type: "text", 477 | }, 478 | ], 479 | }, 480 | ], 481 | }, 482 | { 483 | type: "tableHeader", 484 | attrs: { 485 | colspan: 1, 486 | rowspan: 1, 487 | colwidth: null, 488 | }, 489 | content: [ 490 | { 491 | type: "paragraph", 492 | content: [ 493 | { 494 | text: "Table Header 5", 495 | type: "text", 496 | }, 497 | ], 498 | }, 499 | ], 500 | }, 501 | ], 502 | }, 503 | { 504 | type: "tableRow", 505 | content: [ 506 | { 507 | type: "tableCell", 508 | attrs: { 509 | colspan: 1, 510 | rowspan: 1, 511 | colwidth: null, 512 | backgroundColor: null, 513 | }, 514 | content: [ 515 | { 516 | type: "paragraph", 517 | content: [ 518 | { 519 | text: "Table Cell 1", 520 | type: "text", 521 | }, 522 | ], 523 | }, 524 | ], 525 | }, 526 | { 527 | type: "tableCell", 528 | attrs: { 529 | colspan: 1, 530 | rowspan: 1, 531 | colwidth: [66], 532 | backgroundColor: null, 533 | }, 534 | content: [ 535 | { 536 | type: "paragraph", 537 | content: [ 538 | { 539 | text: "Table Cell 2", 540 | type: "text", 541 | }, 542 | ], 543 | }, 544 | ], 545 | }, 546 | { 547 | type: "tableCell", 548 | attrs: { 549 | colspan: 1, 550 | rowspan: 1, 551 | colwidth: [118], 552 | backgroundColor: "#DE2C2C", 553 | }, 554 | content: [ 555 | { 556 | type: "bullet_list", 557 | content: [ 558 | { 559 | type: "list_item", 560 | content: [ 561 | { 562 | type: "paragraph", 563 | content: [ 564 | { 565 | text: "Table Cell 3", 566 | type: "text", 567 | }, 568 | ], 569 | }, 570 | ], 571 | }, 572 | { 573 | type: "list_item", 574 | content: [ 575 | { 576 | type: "paragraph", 577 | content: [ 578 | { 579 | text: "Table Cell 3", 580 | type: "text", 581 | }, 582 | ], 583 | }, 584 | ], 585 | }, 586 | ], 587 | }, 588 | ], 589 | }, 590 | { 591 | type: "tableCell", 592 | attrs: { 593 | colspan: 1, 594 | rowspan: 1, 595 | colwidth: null, 596 | backgroundColor: null, 597 | }, 598 | content: [ 599 | { 600 | type: "paragraph", 601 | content: [ 602 | { 603 | text: "Table Cell 4", 604 | type: "text", 605 | }, 606 | ], 607 | }, 608 | ], 609 | }, 610 | { 611 | type: "tableCell", 612 | attrs: { 613 | colspan: 1, 614 | rowspan: 1, 615 | colwidth: null, 616 | backgroundColor: null, 617 | }, 618 | content: [ 619 | { 620 | type: "paragraph", 621 | content: [ 622 | { 623 | text: "Table Cell 5", 624 | type: "text", 625 | }, 626 | ], 627 | }, 628 | ], 629 | }, 630 | ], 631 | }, 632 | ], 633 | }, 634 | ], 635 | }; 636 | 637 | // this should idealy come from Storyblok CMS 638 | const story = { 639 | content: { component: "rich_text", text: richTextFromStoryblok }, 640 | }; 641 | --- 642 | 643 | 644 |
645 | 646 |
647 |
648 | 649 | 654 | -------------------------------------------------------------------------------- /demo/src/storyblok/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { storyblokEditable, type SbBlokData as Blok } from "@storyblok/astro"; 3 | import Button, { type Props as ButtonProps } from "../components/Button.astro"; 4 | 5 | export type Props = { 6 | blok: Blok & ButtonProps; 7 | }; 8 | 9 | const { blok } = Astro.props; 10 | const { title } = blok; 11 | --- 12 | 13 |