{"id":24786605,"url":"https://github.com/hashbite/strapi-plugin-lexical","last_synced_at":"2025-09-20T23:06:40.719Z","repository":{"id":274657952,"uuid":"923630678","full_name":"hashbite/strapi-plugin-lexical","owner":"hashbite","description":"Integrates the Lexical WYSIWYG editor as a custom field in Strapi.","archived":false,"fork":false,"pushed_at":"2025-05-09T15:23:52.000Z","size":2587,"stargazers_count":5,"open_issues_count":5,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-04T06:25:25.299Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hashbite.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-01-28T15:39:52.000Z","updated_at":"2025-05-02T13:16:23.000Z","dependencies_parsed_at":"2025-03-22T10:21:03.239Z","dependency_job_id":"1a937681-2aab-44b4-8d89-6f5c9a4a4a59","html_url":"https://github.com/hashbite/strapi-plugin-lexical","commit_stats":null,"previous_names":["hashbite/strapi-plugin-lexical"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/hashbite/strapi-plugin-lexical","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashbite%2Fstrapi-plugin-lexical","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashbite%2Fstrapi-plugin-lexical/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashbite%2Fstrapi-plugin-lexical/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashbite%2Fstrapi-plugin-lexical/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hashbite","download_url":"https://codeload.github.com/hashbite/strapi-plugin-lexical/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hashbite%2Fstrapi-plugin-lexical/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276169664,"owners_count":25596956,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-20T02:00:10.207Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-29T15:16:10.414Z","updated_at":"2025-09-20T23:06:40.702Z","avatar_url":"https://github.com/hashbite.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# strapi-plugin-lexical\n\n\u003e Integrates the [Lexical WYSIWYG editor](https://lexical.dev/) as a custom field in Strapi. Basically a port of [Lexical playground](https://playground.lexical.dev/) into strapi environment with some nice extras.\n\n![screenshot-lexical](https://github.com/user-attachments/assets/e861401d-c850-404f-a5c0-9c6bee0a8456)\n\n[![https://nodei.co/npm/strapi-plugin-lexical.png?downloads=true\u0026downloadRank=true\u0026stars=true](https://nodei.co/npm/strapi-plugin-lexical.png?downloads=true\u0026downloadRank=true\u0026stars=true)](https://www.npmjs.com/package/strapi-plugin-lexical)\n\n\u003e **Alpha Software**  \n\u003e This plugin is in active development. Contributions in the form of bug reports, feature suggestions, and pull requests are highly encouraged!\n\u003cdetails\u003e\n\u003csummary\u003eTable of contents\u003c/summary\u003e\n\n\u003c!-- TOC --\u003e\n\n- [strapi-plugin-lexical](#strapi-plugin-lexical)\n  - [Installation](#installation)\n  - [Usage](#usage)\n    - [Handling Media and Internal Links](#handling-media-and-internal-links)\n      - [Opt-in Mechanism](#opt-in-mechanism)\n      - [Integration in Lexical documents](#integration-in-lexical-documents)\n        - [Media References](#media-references)\n        - [Internal Links](#internal-links)\n      - [Rendering Strapi media and links](#rendering-strapi-media-and-links)\n        - [Fetch while rendering](#fetch-while-rendering)\n        - [Prefetch and inject for rendering](#prefetch-and-inject-for-rendering)\n  - [Roadmap](#roadmap)\n    - [v0 - Alpha](#v0---alpha)\n    - [v1 - Stable](#v1---stable)\n  - [Contributing](#contributing)\n  - [Resources](#resources)\n\n\u003c!-- /TOC --\u003e\n\n\u003c/details\u003e\n\n## Installation\n\n1. Install the plugin:\n\n   ```bash\n   npm install strapi-plugin-lexical\n   ```\n\n2. Enable the plugin:\n\n   ```javascript\n   // ./config/plugins.js\n   {\n    lexical: {\n      enabled: true,\n    },\n\n  };\n    ```\n\n3. Include the required CSS and Prism.js in your Strapi admin:\n\n   ```javascript\n   // ./src/admin/app.js\n   import \"strapi-plugin-lexical/dist/style.css\";\n   import \"prismjs\";\n   ```\n\n4. Add Vite support for Prism.js:\n   - Install the plugin:\n\n     ```bash\n     npm install --save-dev vite-plugin-prismjs\n     ```\n\n   - Update your Vite configuration:\n\n     ```javascript\n     // ./src/admin/vite.config.js\n     import { mergeConfig } from \"vite\";\n     import prismjs from \"vite-plugin-prismjs\";\n\n     export default (config) =\u003e\n       mergeConfig(config, {\n         plugins: [\n           prismjs({\n             languages: \"all\", // Load all languages or customize as needed\n           }),\n         ],\n       });\n     ```\n\n    \u003e **Note:** Prism.js is required even if you don't plan to support code blocks. If you find a workaround to avoid this, please share it with us via a pull request or issue. We happily skip this installation step if we can!\n\n## Usage\n\n- A new **Lexical** custom field type will be available in the Strapi content-type builder.\n- Currently, it supports features migrated from the [Lexical playground](https://playground.lexical.dev/).\n- For rendering content on your frontend, consider using libraries like [payload-lexical-react-renderer](https://github.com/atelierdisko/payload-lexical-react-renderer) or similar tools.\n\n### Handling Media and Internal Links\n\nThis plugin ensures reliable rendering of images and internal links by maintaining relationships between rich text content and referenced entities. By using a regular media field and **automatically generated or your own link components** we can ensure that all referenced media and internal links are readily available for your frontend, always reflecting the latest data.\n\n\u003e **Important:** This is readme section is WIP. We will update this soon and give better examples on how to query and render images and links\n\n#### Opt-in Mechanism  \n\nTo enable this feature, you have to create **secondary fields**:\n\n- With the suffix **`Media`** (e.G. `YourFieldNameMedia`): This field must be a multiple media field with editing disabled.\n- With the suffix **`Links`** (e.G. `YourFieldNameLinks`): This must be a component, either use our pregenerated `Links` component or build your own. Important: It should only contain relation fields and the field name must match the linked collection name.\n\n#### Integration in Lexical documents\n\nMedia is stored as a custom Lexical nodes, while store relations to strapi content with a custom URL format for links. These are automatically parsed and extracted into the fields you created above.\n\n##### Media References\n\n- Images are stored as **`strapi-image`** node in Lexical.\n- Other file types are planned but not yet supported.\n- The structure is rather simple, as you can see:\n\n**strapi-image Lexical Node Data Structure:**\n\n```json\n{ \"documentId\": \"id_of_media_asset\" }\n```\n\n##### Internal Links\n\n- Internal links are stored using the regular **`link`** node in Lexical.\n- The URL follows the format:  \n  `strapi://collectionName/documentId`\n  \n  This ensures that even if a page’s slug changes, links remain valid.\n\n#### Rendering Strapi media and links\n\nThere are two options:\n\n##### Fetch while rendering\n\n@todo\n\nBenefit: Less code, multiple API calls while rendering\n\n- adjust the rendering functions of each lexical node\n- actually.. can it even be asnyc? double check... this is the `not recommended way` anyways...\n\n**Example: Fetching the Latest API Data for a link**\n\n```js\nconst [collectionName, documentId] = linkNode.url.replace(\"strapi://\", \"\").split(\"/\");\nconst articles = client.collection(collectionName);\nconst singleArticle = await articles.findOne(documentId);\n// your link generation logic here ...\nreturn `/${singleArticle.locale}/blog/${singleArticle.slug}`\n```\n\n##### Prefetch and inject for rendering\n\n@todo\n\nTo render media and links, we have to query the data from our media field and fields within the link component, then inject the data into our rendering process.\n\nBenefit: only one API call, more control\n\n- fetch fields\n- iterate through the lexical document\n- inject the document from the strapi api response into the lexical node for later rendering\n- the data is now available when rendering the lexical node in your renderer\n\n**Example Renderer with NextJS:**\n\n```tsx\n// LexicalRenderer.tsx\nimport Image from \"next/image\";\nimport Link from \"next/link\";\n\nimport type { Media_Plain } from \"@strapi/common/schemas-to-ts/Media\";\nimport { Links_Plain } from \"@strapi/components/links/interfaces/Links\";\n\nimport React from \"react\";\n\nimport clsx from \"clsx/lite\";\n\nimport { createPath } from \"@/utils/paths\";\n\nimport type {\n  AbstractNode,\n  ElementRenderer,\n  ElementRenderers,\n  Node,\n  PayloadLexicalReactRendererContent,\n} from \"lexical-renderer-atelier-disko\";\nimport {\n  defaultElementRenderers,\n  PayloadLexicalReactRenderer,\n} from \"lexical-renderer-atelier-disko\";\n\ntype StrapiImageNode = {\n  documentId: string;\n  entity: Media_Plain;\n} \u0026 AbstractNode\u003c\"strapi-image\"\u003e;\n\ntype NodeAll = Node | StrapiImageNode;\n\nconst elementRenderers: ElementRenderers \u0026 {\n  \"strapi-image\": ElementRenderer\u003cStrapiImageNode\u003e;\n} = {\n  ...defaultElementRenderers,\n  // Define your custom lexical nodes here\n  link: (element, children, parent, className) =\u003e (\n    \u003cLink\n      href={element.url}\n      className={className}\n      target={element.newTab ? \"_blank\" : \"_self\"}\n    \u003e\n      {children}\n    \u003c/Link\u003e\n  ),\n  \"strapi-image\": (element, children, parent, className) =\u003e (\n    \u003cImage\n      className={clsx(\"mx-auto\", className)}\n      src={`${process.env.NEXT_PUBLIC_IMAGE_BASE_URL}${element.entity.url}`}\n      alt={element.entity.alternativeText}\n      width={Math.floor(element.entity.width / 2)}\n      height={Math.floor(element.entity.height / 2)}\n      sizes={`(max-width: 768px) 100vw, ${Math.floor(\n        (element.entity.width / 2) * 1.25\n      )}px`}\n      loading=\"lazy\"\n    /\u003e\n  ),\n};\n\nexport function LexicalRenderer({\n  children,\n  classNames,\n  media,\n  links,\n}: {\n  children: PayloadLexicalReactRendererContent;\n  classNames?: { [key: string]: string };\n  media?: Media_Plain[];\n  links?: Links_Plain;\n}) {\n  // Inject media and links into our lexical document\n  const injectedDocument = React.useMemo(() =\u003e {\n    if (!children) {\n      return null;\n    }\n\n    if (media || links) {\n      const injectStrapiEntities = (nodes: NodeAll[]) =\u003e {\n        for (const node of nodes) {\n          // Media (Images only for now)\n          if (node.type === \"strapi-image\" \u0026\u0026 media?.length) {\n            const foundMedia = media.find(\n              // @ts-expect-error documentId is there. the ts schema plugin is just outdated :(\n              ({ documentId }) =\u003e documentId === node.documentId\n            );\n            if (foundMedia) {\n              node.entity = foundMedia;\n            }\n          }\n\n          // Links\n          if (\n            node.type === \"link\" \u0026\u0026\n            links \u0026\u0026\n            node.url.indexOf(\"strapi://\") === 0\n          ) {\n            // Extract info from strapi link\n            const [collectionName, linkDocumentId] = (node.url as string)\n              .replace(\"strapi://\", \"\")\n              .split(\"/\") as [keyof Links_Plain, string];\n            if (links[collectionName]) {\n              // Find linked document\n              const foundCollectionDocument = links[collectionName].find(\n                ({ documentId }) =\u003e documentId === linkDocumentId\n              );\n              if (foundCollectionDocument) {\n                // Generate page link with helper function\n                node.url = createPath(\n                  collectionName,\n                  foundCollectionDocument.locale,\n                  foundCollectionDocument.slug\n                );\n              }\n            }\n          }\n          if (node.type !== \"strapi-image\" \u0026\u0026 node.children) {\n            injectStrapiEntities(node.children);\n          }\n        }\n      };\n\n      injectStrapiEntities(children.root.children);\n    }\n\n    return children;\n  }, [children, media, links]);\n\n  if (!children || !injectedDocument) {\n    return null;\n  }\n\n  return (\n    \u003cPayloadLexicalReactRenderer\n      content={injectedDocument}\n      classNames={classNames}\n      elementRenderers={elementRenderers}\n    /\u003e\n  );\n}\n\nexport const lexicalToPlaintext = (json: { root: Node }) =\u003e {\n  const traverse = (node: Node): string =\u003e {\n    if (node.type === \"text\" \u0026\u0026 node.text) return node.text;\n    if (node.children) return node.children.map(traverse).join(\" \");\n    return \"\";\n  };\n  return traverse(json.root);\n};\n```\n\n## Roadmap\n\n### v0 - Alpha\n\n- [x] Implement basic functionality.\n- [x] Port features from the Lexical playground as the initial foundation.\n- [x] Integrate Strapi Media Assets and enable linking to Strapi Collection Entries\n- [ ] Create field presets:\n  - **Simple**, **Complex**, and **Full** (selectable during field setup).\n- [ ] Gather community feedback.\n- [ ] Look for a potential co-maintainer.\n\n### v1 - Stable\n\n- Introduce plugin-based architecture:\n  - Allow users to extend functionality with their own plugins.\n- Enable configuration of presets via plugin settings.\n- Open to community ideas! [Submit an issue](https://github.com/hashbite/strapi-plugin-lexical/issues).\n\n---\n\n## Contributing\n\nWe welcome contributions! Here’s how you can help:\n\n- Report bugs or suggest features via the [issue tracker](https://github.com/hashbite/strapi-plugin-lexical/issues).\n- Submit pull requests to improve functionality or documentation.\n- Share your feedback and ideas to shape the plugin’s future.\n\n---\n\n## Resources\n\n- [Lexical Documentation](https://lexical.dev/docs)\n- [Lexical Playground](https://playground.lexical.dev/)\n- [Payload Lexical React Renderer](https://github.com/atelierdisko/payload-lexical-react-renderer)\n- [Strapi Plugin Development Guide](https://docs.strapi.io/developer-docs/latest/plugin-development/introduction.html)\n\n---\n\n### 🛠️ Sponsored by [hashbite.net](https://hashbite.net) | support \u0026 custom development available\n\nWe welcome everyone to post issues, fork the project, and contribute via pull requests. Together we can make this a better tool for all of us!\n\nIf the contribution process feels too slow or complex for your needs, [hashbite.net](https://hashbite.net) can quickly implement features, fix bugs, or develop custom variations of this plugin on a paid basis. Just reach out through their website for direct support.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhashbite%2Fstrapi-plugin-lexical","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhashbite%2Fstrapi-plugin-lexical","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhashbite%2Fstrapi-plugin-lexical/lists"}