{"id":500883,"date":"2026-10-29T14:56:36","date_gmt":"2026-10-29T11:56:36","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=500883"},"modified":"2026-10-29T14:56:36","modified_gmt":"2026-10-29T11:56:36","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=500883","title":{"rendered":"Quill \u0432 Next.js App Router, editor, autosave \u0438 HTML viewer \u0441 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u043e\u0439 \u043a\u043e\u0434\u0430"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>WYSIWYG-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0432 Next.js App Router \u0437\u0430\u0442\u0440\u0430\u0433\u0438\u0432\u0430\u0435\u0442 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0435\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. Quill \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 DOM \u0438 browser API, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0441\u0430\u043c \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0436\u0438\u0432\u0435\u0442 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435. \u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u043d\u0443\u0436\u043d\u043e \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Quill, \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438, \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0431\u0435\u0437 toolbar \u0438 \u0440\u0435\u0436\u0438\u043c\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<br \/>\u0415\u0441\u043b\u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Quill \u043a\u0430\u043a HTML, \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0438 viewer \u043c\u043e\u0433\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439\u00a0<code>contentHtml<\/code>. Editor \u043c\u0435\u043d\u044f\u0435\u0442 HTML \u0447\u0435\u0440\u0435\u0437\u00a0<code>onChange<\/code>. Autosave \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u043e\u0441\u043b\u0435 \u043f\u0430\u0443\u0437\u044b \u0432\u043e \u0432\u0432\u043e\u0434\u0435. Viewer \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0439 HTML \u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 Quill \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0434\u043b\u044f code blocks. \u0414\u043b\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0438 \u043a\u043e\u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f highlight.js.<\/p>\n<h3>Client-only \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440<\/h3>\n<p><code>use client<\/code>\u00a0\u0441\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 \u043d\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442, \u0447\u0442\u043e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u043f\u043e\u043a\u043e\u0439\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. Quill \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 DOM,\u00a0<code>window<\/code>\u00a0\u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 browser-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u0414\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 Client Component \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0430\u043c\u00a0<code>react-quill-new<\/code>\u00a0\u0447\u0435\u0440\u0435\u0437\u00a0<code>dynamic<\/code>\u00a0\u0441\u00a0<code>ssr: false<\/code>.<\/p>\n<p>Quill Syntax \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 highlight.js. \u0412 \u044d\u0442\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0434\u0443\u043b\u044c \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u00a0<code>hljs<\/code>\u00a0\u0432\u00a0<code>window<\/code>.<\/p>\n<pre><code class=\"typescript\">\/\/ src\/components\/editor\/NoteEditorClient.tsx\"use client\";import dynamic from \"next\/dynamic\";import hljs from \"highlight.js\";if (typeof window !== \"undefined\") {  window.hljs = hljs;}const ReactQuill = dynamic(  async () =&gt; (await import(\"react-quill-new\")).default,  {    ssr: false,  });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper\"><\/span><\/a><\/div><\/pre>\n<p>Next.js \u043d\u0435 \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442\u00a0<code>ReactQuill<\/code>\u00a0\u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435. \u0418\u043c\u043f\u043e\u0440\u0442 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0433\u0434\u0435 \u0443\u0436\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b DOM \u0438\u00a0<code>window<\/code>.<br \/>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u00a0<code>typeof window !== \"undefined\"<\/code>\u00a0\u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u043c\u0443\u00a0<code>hljs<\/code>. \u0421\u0430\u043c Quill \u043e\u0442\u0434\u0435\u043b\u0435\u043d \u043e\u0442 SSR \u0447\u0435\u0440\u0435\u0437\u00a0<code>dynamic<\/code>.<\/p>\n<h3>CSS Quill \u0438 highlight.js<\/h3>\n<p>Quill Snow \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f toolbar, editor area, \u0441\u043f\u0438\u0441\u043a\u043e\u0432, \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u0432 \u0438 code blocks. \u0418\u0445 \u0441\u0442\u0438\u043b\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0422\u0435\u043c\u0430 highlight.js \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e, \u0435\u0441\u043b\u0438 viewer \u0438 editor \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0434\u043d\u0443 \u0441\u0445\u0435\u043c\u0443 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0438.<\/p>\n<pre><code class=\"typescript\">\/\/ src\/app\/layout.tsximport \"react-quill-new\/dist\/quill.snow.css\";import \"highlight.js\/styles\/github-dark.css\";<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b Quill \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u0441\u0442\u0438\u043b\u0438\u00a0<code>.ql-toolbar<\/code>,\u00a0<code>.ql-container<\/code>\u00a0\u0438\u00a0<code>.ql-editor<\/code>. Viewer \u043f\u043e\u0437\u0436\u0435 \u0441\u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0447\u0430\u0441\u0442\u044c \u0442\u0435\u0445 \u0436\u0435 \u043a\u043b\u0430\u0441\u0441\u043e\u0432 \u0431\u0435\u0437 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043f\u0440\u0430\u0432\u0438\u043b \u0434\u043b\u044f \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e \u0442\u0435\u043a\u0441\u0442\u0430.<\/p>\n<h3>Toolbar \u0438 history<\/h3>\n<p>WYSIWYG-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u043e\u0431\u044b\u0447\u043d\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0435, \u0447\u0435\u043c bold \u0438 italic. \u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0438\u0436\u0435 \u0435\u0441\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0448\u0440\u0438\u0444\u0442, \u0446\u0432\u0435\u0442, \u0441\u043f\u0438\u0441\u043a\u0438, blockquote, inline code, code block, \u0441\u0441\u044b\u043b\u043a\u0438, \u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435 \u0438 \u043e\u0447\u0438\u0441\u0442\u043a\u0430 \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.<br \/>Undo \u0438 redo \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u043a history Quill.\u00a0<code>userOnly: true<\/code>\u00a0\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 history \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f.<\/p>\n<pre><code class=\"typescript\">const modules = {  syntax: true,  toolbar: {    container: [      [\"undo\", \"redo\"],      [{ header: [1, 2, 3, false] }, { font: [] }],      [\"bold\", \"italic\", \"underline\", \"strike\", \"code\"],      [{ script: \"sub\" }, { script: \"super\" }],      [{ color: [] }, { background: [] }],      [\"blockquote\", \"code-block\"],      [        { list: \"ordered\" },        { list: \"bullet\" },        { indent: \"-1\" },        { indent: \"+1\" },      ],      [{ align: [] }],      [\"link\"],      [\"clean\"],    ],    handlers: {      undo: function () {        this.quill.history.undo();      },      redo: function () {        this.quill.history.redo();      },    },  },  history: {    delay: 400,    maxStack: 80,    userOnly: true,  },  clipboard: {    matchVisual: false,  },} as const;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>syntax: true<\/code>\u00a0\u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 code blocks. Toolbar \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b undo \u0438 redo \u0432\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 history Quill.<br \/>\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043c\u043e\u0436\u043d\u043e \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0432\u043d\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c render React \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442\u00a0<code>modules<\/code>.<\/p>\n<h3>Controlled editor<\/h3>\n<p>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 editor \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 HTML \u0441\u043d\u0430\u0440\u0443\u0436\u0438 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0447\u0435\u0440\u0435\u0437\u00a0<code>onChange<\/code>. Quill \u043d\u0435 \u0437\u043d\u0430\u0435\u0442, \u0433\u0434\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0437\u0430\u043c\u0435\u0442\u043a\u0430 \u0438 \u043a\u0430\u043a\u0438\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u043f\u043e\u043f\u0430\u0434\u0435\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440.<\/p>\n<pre><code class=\"typescript\">export default function NoteEditorClient({  value,  onChange,}: {  value: string;  onChange: (nextHtml: string) =&gt; void;}) {  const handleChange = useCallback(    (content: string) =&gt; {      onChange(content);    },    [onChange]  );  return (    &lt;div className=\"wb-editor\"&gt;      &lt;ReactQuill        theme=\"snow\"        value={value}        onChange={handleChange}        modules={modules}      \/&gt;    &lt;\/div&gt;  );}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>value<\/code>\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 HTML.\u00a0<code>onChange<\/code>\u00a0\u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 HTML \u043f\u043e\u0441\u043b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u0435\u0433\u043e \u0432 \u0441\u043b\u043e\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<br \/>\u0422\u0430\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u043c\u0443 store, form state \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432\u043d\u0443\u0442\u0440\u0438 editor.<\/p>\n<h3>\u0421\u043c\u0435\u043d\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438<\/h3>\n<p>\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u0434\u043d\u043e\u0433\u043e editor \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0437\u0430\u043c\u0435\u0442\u043e\u043a. React \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u043c\u0435\u0436\u0434\u0443 render, \u0435\u0441\u043b\u0438 \u0435\u0433\u043e \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u0432 \u0434\u0435\u0440\u0435\u0432\u0435 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0430\u0441\u044c. Quill \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, selection \u0438 history.<\/p>\n<p>\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u0437\u0430\u043c\u0435\u0442\u043a\u0430 \u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0437\u0430\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c\u00a0<code>note<\/code>\u00a0\u0432 URL.<\/p>\n<pre><code class=\"typescript\">const sp = useSearchParams();const noteId = sp.get(\"note\") ?? \"\";const {  getNote,  updateNoteContent,  getNoteSaveInfo,} = useWorkbenchStore();const note = noteId  ? getNote(projectId, noteId)  : undefined;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435\u00a0<code>noteId<\/code>\u00a0\u0438\u0437 store \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u00a0<code>contentHtml<\/code>.<\/p>\n<p>Editor \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442\u0441\u044f \u0441\u00a0<code>key={<\/code><a href=\"http:\/\/note.id\" rel=\"noopener nofollow\"><code>note.id<\/code><\/a><code>}<\/code>.<\/p>\n<pre><code class=\"typescript\">&lt;NoteEditorClient  key={note.id}  value={note.contentHtml}  onChange={(nextHtml) =&gt;    updateNoteContent(note.id, nextHtml)  }\/&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041d\u043e\u0432\u044b\u0439\u00a0<code>key<\/code>\u00a0\u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 React \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 editor \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439. Selection, history \u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u043d\u0435 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u044f\u0442 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e.<br \/>\u041e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u00a0<code>value<\/code>\u00a0\u0434\u043b\u044f WYSIWYG-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0438\u043d\u043e\u0433\u0434\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e, \u0435\u0441\u043b\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u043d\u0435 React props. Remount \u043f\u043e id \u0443\u0431\u0438\u0440\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u0441\u043b\u043e\u0439 \u043c\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043c\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438.<\/p>\n<h3>HTML \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>Quill \u043e\u0442\u0434\u0430\u0435\u0442 HTML \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438. \u0422\u0435\u043a\u0441\u0442 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 store \u0431\u0435\u0437 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0447\u0442\u043e\u0431\u044b controlled\u00a0<code>value<\/code>\u00a0\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u043b \u0442\u0435\u043a\u0443\u0449\u0435\u043c\u0443 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u043c\u0443 editor.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f autosave.<\/p>\n<pre><code class=\"typescript\">const updateNoteContent = useCallback(  (noteId: Id, nextHtml: string) =&gt; {    setDb((prev) =&gt; ({      ...prev,      notes: prev.notes.map((note) =&gt;        note.id === noteId          ? { ...note, contentHtml: nextHtml }          : note      ),    }));    scheduleSaveNote(noteId, nextHtml);  },  [scheduleSaveNote]);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>contentHtml<\/code>\u00a0\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0432 store \u043d\u0430 \u043a\u0430\u0436\u0434\u043e\u043c\u00a0<code>onChange<\/code>. \u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043d\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0442\u043e\u0433\u043e \u0436\u0435 store.<br \/>\u0421\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0438\u0434\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e. Editor \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430.<\/p>\n<h3>Autosave<\/h3>\n<p>Autosave \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c server action \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 timer. \u041d\u043e\u0432\u043e\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043e\u0442\u043c\u0435\u043d\u044f\u0435\u0442 \u0435\u0449\u0435 \u043d\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 timer \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438.<\/p>\n<pre><code class=\"typescript\">const saveTimersRef =  React.useRef&lt;Map&lt;Id, number&gt;&gt;(new Map());const scheduleSaveNote = useCallback(  (noteId: Id, nextHtml: string) =&gt; {    const currentTimer =      saveTimersRef.current.get(noteId);    if (currentTimer) {      window.clearTimeout(currentTimer);    }    setNoteSaveInfoById((prev) =&gt; ({      ...prev,      [noteId]: {        status: \"saving\",        error: null,      },    }));    const timer = window.setTimeout(() =&gt; {      const updatedAt =        new Date().toISOString();      void updateNoteContentAction({        noteId,        contentHtml: nextHtml,        updatedAt,      });    }, 650);    saveTimersRef.current.set(noteId, timer);  },  [refreshFromServer]);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041a\u0430\u0436\u0434\u0430\u044f \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f HTML \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 650 \u043c\u0441. \u0417\u0430\u043f\u0440\u043e\u0441 \u0443\u0445\u043e\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043f\u0430\u0443\u0437\u044b \u0432\u043e \u0432\u0432\u043e\u0434\u0435.<br \/>Map \u0440\u0430\u0437\u0434\u0435\u043b\u044f\u0435\u0442 \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0440\u0430\u0437\u043d\u044b\u0445 \u0437\u0430\u043c\u0435\u0442\u043e\u043a. \u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043c\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u043d\u0435 \u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0434\u0438\u043d \u043e\u0431\u0449\u0438\u0439 debounce timer.<\/p>\n<h3>Server Action<\/h3>\n<p>Autosave \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u00a0<code>noteId<\/code>, HTML \u0438\u00a0<code>updatedAt<\/code>\u00a0\u0432 Server Action. \u0421\u0435\u0440\u0432\u0435\u0440 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 payload \u0447\u0435\u0440\u0435\u0437 Zod, \u043d\u0430\u0445\u043e\u0434\u0438\u0442 \u0437\u0430\u043c\u0435\u0442\u043a\u0443 \u0438 \u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e.<\/p>\n<pre><code class=\"typescript\">export async function updateNoteContentAction(  payload: unknown): Promise&lt;Result&gt; {  const parsed =    UpdateNoteContentPayloadSchema.safeParse(payload);  if (!parsed.success) {    return {      ok: false,      error: parsed.error.message,    };  }  const {    noteId,    contentHtml,    updatedAt,  } = parsed.data;  let projectId: string | null = null;  const res = await mutateDb((db) =&gt; {    const note =      db.notes.find((x) =&gt; x.id === noteId);    if (!note) return;    projectId = note.projectId;    note.contentHtml = contentHtml;    note.updatedAt = updatedAt;  });  if (res.ok &amp;&amp; projectId) {    revalidatePath(`\/p\/${projectId}`);  }  return res;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>Editor \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 API route \u0447\u0435\u0440\u0435\u0437\u00a0<code>fetch<\/code>. \u0417\u0430\u043f\u0438\u0441\u044c \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 Server Action.<br \/>\u041f\u043e\u0441\u043b\u0435 \u043c\u0443\u0442\u0430\u0446\u0438\u0438\u00a0<code>revalidatePath<\/code>\u00a0\u0438\u043d\u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0443\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430.<\/p>\n<h3>Saving, saved \u0438 error<\/h3>\n<p>\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 HTML \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043b\u0443\u0447\u0448\u0435 \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u0442\u044c \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u043e\u043b\u0435. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0432\u0438\u0434\u0435\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0442\u0435\u043a\u0441\u0442\u0430, \u043f\u043e\u043a\u0430 action \u0435\u0449\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u0432\u0435\u0442\u0430 store \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043c\u0435\u043d\u044f\u0435\u0442\u00a0<code>updatedAt<\/code>\u00a0\u0438 save state.<\/p>\n<pre><code class=\"typescript\">void updateNoteContentAction({  noteId,  contentHtml: nextHtml,  updatedAt,})  .then((result) =&gt; {    if (!result.ok) {      setNoteSaveInfoById((prev) =&gt; ({        ...prev,        [noteId]: {          status: \"error\",          error: result.error,        },      }));      void refreshFromServer();      return;    }    setDb((prev) =&gt; ({      ...prev,      notes: prev.notes.map((note) =&gt;        note.id === noteId          ? { ...note, updatedAt }          : note      ),    }));    setNoteSaveInfoById((prev) =&gt; ({      ...prev,      [noteId]: {        status: \"saved\",        lastSavedAt: updatedAt,        error: null,      },    }));  })  .catch(() =&gt; {    setNoteSaveInfoById((prev) =&gt; ({      ...prev,      [noteId]: {        status: \"error\",        error: \"Network error\",      },    }));    void refreshFromServer();  });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>updatedAt<\/code>\u00a0\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u00a0<code>ok<\/code>. \u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442 \u0437\u0430\u043c\u0435\u0442\u043a\u0443 \u0432\u00a0<code>error<\/code>\u00a0\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0447\u0438\u0442\u044b\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f.<\/p>\n<p>\u0421\u0442\u0430\u0442\u0443\u0441 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0440\u044f\u0434\u043e\u043c \u0441 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u043c \u0437\u0430\u043c\u0435\u0442\u043a\u0438.<\/p>\n<pre><code class=\"typescript\">&lt;div className=\"wb-tree-meta\"&gt;  \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e:{\" \"}  {new Date(note.updatedAt).toLocaleString(\"ru-RU\")}  {saveInfo.status === \"saving\" &amp;&amp;    \" \u2022 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u2026\"}  {saveInfo.status === \"saved\" &amp;&amp;    \" \u2022 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043e\"}  {saveInfo.status === \"error\" &amp;&amp;    \" \u2022 \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\"}&lt;\/div&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439\u00a0<code>contentHtml<\/code>. \u0421\u0442\u0440\u043e\u043a\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 server action.<\/p>\n<h3>Editor \u0438 viewer<\/h3>\n<p>\u0420\u0435\u0436\u0438\u043c \u0447\u0442\u0435\u043d\u0438\u044f \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u0434\u0430\u043d\u043d\u044b\u0445. \u041c\u043e\u0436\u043d\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c HTML Quill \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0438 \u0434\u043b\u044f editor, \u0438 \u0434\u043b\u044f viewer.<br \/>\u0412 workspace \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0440\u0435\u0436\u0438\u043c.<\/p>\n<pre><code class=\"typescript\">const [mode, setMode] =  useState&lt;\"edit\" | \"view\"&gt;(\"edit\");&lt;Button  variant=\"ghost\"  onClick={() =&gt;    setMode((mode) =&gt;      mode === \"edit\" ? \"view\" : \"edit\"    )  }&gt;  {mode === \"edit\"    ? \"\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\"    : \"\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440\"}&lt;\/Button&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0412 \u0440\u0435\u0436\u0438\u043c\u0435 edit \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 ReactQuill. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 view \u0442\u043e\u0442 \u0436\u0435\u00a0<code>contentHtml<\/code>\u00a0\u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443.<\/p>\n<pre><code class=\"typescript\">const previewHtml = useMemo(() =&gt; {  if (!note) return \"\";  return prepareHtmlForPreview(    note.contentHtml || \"\"  );}, [note]);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043d\u0443\u0436\u043d\u0430 \u0432 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u043c \u0438\u0437-\u0437\u0430 code blocks \u0438 \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u043e\u0439 Quill-\u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0438. \u041e\u0431\u044b\u0447\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u0433\u0440\u0430\u0444\u044b, \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438 \u0441\u043f\u0438\u0441\u043a\u0438 \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f HTML.<\/p>\n<h3>Viewer \u0441 Quill-\u0441\u0442\u0438\u043b\u044f\u043c\u0438<\/h3>\n<p>Quill \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 CSS \u0434\u043b\u044f\u00a0<code>.ql-editor<\/code>. Viewer \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043a\u043b\u0430\u0441\u0441 \u0431\u0435\u0437 toolbar \u0438 editable container.<\/p>\n<pre><code class=\"typescript\">&lt;div  key={note.id}  className=\"wb-viewer ql-snow\"&gt;  &lt;div    className=\"ql-editor\"    dangerouslySetInnerHTML={{      __html: previewHtml,    }}  \/&gt;&lt;\/div&gt;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041a\u043b\u0430\u0441\u0441\u00a0<code>ql-snow<\/code>\u00a0\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0442\u0435\u043c\u044b.\u00a0<code>.ql-editor<\/code>\u00a0\u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442 \u0442\u0438\u043f\u043e\u0433\u0440\u0430\u0444\u0438\u043a\u0443 Quill \u043a \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u043e\u043c\u0443 HTML.<br \/><code>dangerouslySetInnerHTML<\/code>\u00a0\u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 \u043a\u0430\u043a HTML. \u0415\u0441\u043b\u0438 \u0442\u0430\u043a\u043e\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u043e\u0442 \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430, \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0432\u043e\u0434\u043e\u043c \u043d\u0443\u0436\u0435\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u044d\u0442\u0430\u043f sanitization. \u0412 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u043a\u043e\u043d\u0442\u0443\u0440\u0435\u00a0<code>prepareHtmlForPreview<\/code>\u00a0\u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442 \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443, \u043d\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f sanitizer.<\/p>\n<h3>\u0420\u0430\u0437\u043c\u0435\u0442\u043a\u0430 code block<\/h3>\n<p>\u0424\u043e\u0440\u043c\u0430\u0442 code blocks \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0432\u0435\u0440\u0441\u0438\u0438 \u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Quill. \u0412 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u043e\u043c HTML \u043c\u043e\u0433\u0443\u0442 \u0432\u0441\u0442\u0440\u0435\u0442\u0438\u0442\u044c\u0441\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b\u00a0<code>.ql-code-block-container<\/code>, \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435\u00a0<code>.ql-code-block<\/code>\u00a0\u0438\u00a0<code>pre.ql-syntax<\/code>.<br \/>Viewer \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u044d\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u043a \u043e\u0431\u044b\u0447\u043d\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435\u00a0<code>pre &gt; code<\/code>, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 highlight.js.<\/p>\n<p>\u0414\u043b\u044f \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u043e\u043a \u0441\u043e\u0431\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0434\u0438\u043d \u0431\u043b\u043e\u043a.<\/p>\n<pre><code class=\"typescript\">doc  .querySelectorAll(\".ql-code-block-container\")  .forEach((container) =&gt; {    const blocks = Array.from(      container.querySelectorAll(        \".ql-code-block\"      )    );    const lines = blocks      .map((block) =&gt; block.textContent || \"\")      .join(\"\\n\");    if (!lines.trim()) return;    const pre =      doc.createElement(\"pre\");    highlightIntoPre(doc, pre, lines);    container.parentNode?.replaceChild(      pre,      container    );  });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439\u00a0<code>.ql-code-block<\/code>\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043e\u0434\u043d\u0443 \u0441\u0442\u0440\u043e\u043a\u0443. Viewer \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0438 \u0447\u0435\u0440\u0435\u0437\u00a0<code>\\n<\/code>\u00a0\u0438 \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043d\u0430\u00a0<code>pre<\/code>.<br \/>HTML \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0432 store. \u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0440\u0435\u0436\u0438\u043c\u0430 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430.<\/p>\n<h3>\u041d\u043e\u0432\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 code block<\/h3>\n<p>\u0414\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 Quill \u0443\u0436\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u00a0<code>pre<\/code>. Viewer \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u0430 \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439.<\/p>\n<pre><code class=\"typescript\">doc  .querySelectorAll(    \"pre.ql-syntax, pre[data-language]\"  )  .forEach((preEl) =&gt; {    const el = preEl as HTMLElement;    if (el.classList.contains(\"hljs\")) {      return;    }    const text = el.textContent || \"\";    if (!text.trim()) return;    highlightIntoPre(doc, el, text);  });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u00a0<code>hljs<\/code>\u00a0\u043d\u0435 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0443\u0436\u0435 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430.<\/p>\n<p>\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0442\u043a\u0430 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0434\u0438\u043d\u043e\u0447\u043d\u044b\u0439\u00a0<code>.ql-code-block<\/code>, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u043d\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430.<\/p>\n<pre><code class=\"typescript\">doc  .querySelectorAll(\".ql-code-block\")  .forEach((block) =&gt; {    if (      block.closest(        \".ql-code-block-container\"      )    ) {      return;    }    const text =      block.textContent || \"\";    if (!text.trim()) return;    const pre =      doc.createElement(\"pre\");    highlightIntoPre(doc, pre, text);    block.parentNode?.replaceChild(      pre,      block    );  });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>Viewer \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432 HTML \u0438 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u0438\u0445 \u043a \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435 \u043f\u0435\u0440\u0435\u0434 render.<\/p>\n<h3>DOMParser<\/h3>\n<p>\u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 viewer \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.\u00a0<code>DOMParser<\/code>\u00a0\u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 HTML \u0432\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 document, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0438 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c code blocks.<\/p>\n<pre><code class=\"typescript\">export function prepareHtmlForPreview(  html: string): string {  if (typeof document === \"undefined\") {    return html;  }  const parser = new DOMParser();  const doc = parser.parseFromString(    html,    \"text\/html\"  );  doc    .querySelectorAll(\"select.ql-ui\")    .forEach((el) =&gt; el.remove());  \/\/ \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 code blocks  return doc.body.innerHTML;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p><code>select.ql-ui<\/code>\u00a0\u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u043a \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u043e\u0439 \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0435 editor \u0438 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0447\u0442\u0435\u043d\u0438\u044f \u0443\u0434\u0430\u043b\u044f\u0435\u0442\u0441\u044f.<br \/>\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u043e\u0432\u0443\u044e HTML-\u0441\u0442\u0440\u043e\u043a\u0443. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439\u00a0<code>contentHtml<\/code>\u00a0\u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439.<\/p>\n<h3>\u041f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0430 \u0447\u0435\u0440\u0435\u0437 highlight.js<\/h3>\n<p>highlight.js \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0442\u0435\u043a\u0441\u0442 code block \u043f\u043e\u0441\u043b\u0435 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f Quill-\u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0438. \u041a\u043e\u0434 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u043e\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437\u00a0<code>textContent<\/code>, \u0430 \u0437\u0430\u0442\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044e\u044e \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 \u0434\u043b\u044f syntax highlighting.<\/p>\n<pre><code class=\"typescript\">const highlightIntoPre = (  doc: Document,  preEl: HTMLElement,  text: string) =&gt; {  preEl.classList.add(\"hljs\");  preEl.textContent = \"\";  const codeEl =    doc.createElement(\"code\");  codeEl.className = \"hljs\";  codeEl.textContent = text;  preEl.appendChild(codeEl);  try {    hljs.highlightElement(codeEl);  } catch {    \/\/ \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f plain text  }};<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438 \u043e\u0448\u0438\u0431\u043a\u0435 highlight.js \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u00a0<code>code<\/code>\u00a0\u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0441 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u043c \u0442\u0435\u043a\u0441\u0442\u043e\u043c. Viewer \u043d\u0435 \u0442\u0435\u0440\u044f\u0435\u0442 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0431\u043b\u043e\u043a\u0430.<br \/>\u042f\u0437\u044b\u043a \u0432 \u044d\u0442\u043e\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0435 \u044f\u0432\u043d\u043e \u043d\u0435 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044f. highlight.js \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0443 \u0441\u0432\u043e\u0438\u043c \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u043e\u043c.<\/p>\n<h3>CSS viewer<\/h3>\n<p>\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0438 \u0440\u0435\u0436\u0438\u043c \u0447\u0442\u0435\u043d\u0438\u044f \u0438\u043c\u0435\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u043f\u043e layout. Toolbar \u0432 viewer \u043d\u0435\u0442, \u0437\u0430\u0442\u043e \u0434\u043b\u0438\u043d\u043d\u044b\u0439 \u0442\u0435\u043a\u0441\u0442 \u0438 code blocks \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0442\u044c \u043f\u0440\u0430\u0432\u0443\u044e \u043f\u0430\u043d\u0435\u043b\u044c.<\/p>\n<pre><code class=\"javascript\">.wb-viewer .ql-editor {  height: 100%;  overflow-y: auto;  padding: 14px;  overflow-x: hidden;  overflow-wrap: anywhere;  word-break: break-word;  min-width: 0;}.wb-viewer .ql-editor pre {  max-width: 100%;  overflow-x: auto;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u041e\u0431\u044b\u0447\u043d\u044b\u0439 \u0442\u0435\u043a\u0441\u0442 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430. \u0423\u00a0<code>pre<\/code>\u00a0\u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 scroll.<\/p>\n<p>\u0414\u043b\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u043d \u0438 padding.<\/p>\n<pre><code class=\"typescript\">.wb-viewer pre.hljs {  background: #0b1220;  border-radius: 10px;  padding: 12px;  overflow-x: auto;  max-width: 100%;}.wb-viewer pre.hljs code.hljs {  background: transparent;  padding: 0;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>\u0424\u043e\u043d \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0438\u0442\u00a0<code>pre<\/code>, \u0430 highlight.js \u043e\u043a\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043a\u0435\u043d\u044b \u0432\u043d\u0443\u0442\u0440\u0438\u00a0<code>code<\/code>.<\/p>\n<p>\u0421\u043f\u0438\u0441\u043a\u0438 \u0442\u0430\u043a\u0436\u0435 \u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438. Quill CSS \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043c\u0430\u0440\u043a\u0435\u0440\u044b, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 viewer \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u0435\u00a0<code>disc<\/code>\u00a0\u0438\u00a0<code>decimal<\/code>.<\/p>\n<pre><code class=\"typescript\">.wb-viewer .ql-editor ul,.wb-viewer .ql-editor ol {  padding-left: 1.1rem !important;  margin: 0.5rem !important;  list-style-position: inside;}.wb-viewer .ql-editor ul &gt; li {  list-style: disc;}.wb-viewer .ql-editor ol &gt; li {  list-style: decimal;}.wb-viewer .ql-editor li::before {  content: none !important;}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><span class=\"sc-logo-wrapper icon-only\"><\/span><\/a><\/div><\/pre>\n<p>Editor \u0438 viewer \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0434\u043d\u0443 HTML-\u043c\u043e\u0434\u0435\u043b\u044c, \u043d\u043e \u0440\u0435\u0436\u0438\u043c \u0447\u0442\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 layout \u0438 \u0441\u043f\u0438\u0441\u043a\u043e\u0432.<\/p>\n<h3>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h3>\n<p>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441\u043e \u0441\u043c\u0435\u043d\u044b \u0437\u0430\u043c\u0435\u0442\u043e\u043a. \u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u043e\u0434\u043d\u0443 \u0437\u0430\u043c\u0435\u0442\u043a\u0443, \u0441\u0442\u0430\u0432\u0438\u043c \u043a\u0443\u0440\u0441\u043e\u0440 \u0432 editor, \u0437\u0430\u0442\u0435\u043c \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u043c \u0434\u0440\u0443\u0433\u0443\u044e \u0447\u0435\u0440\u0435\u0437\u00a0<code>?note=<\/code>. \u041d\u043e\u0432\u044b\u0439\u00a0<a href=\"http:\/\/note.id\" rel=\"noopener nofollow\"><code>note.id<\/code><\/a>\u00a0\u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 Quill \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u00a0<code>contentHtml<\/code>.<\/p>\n<p>Autosave \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0438\u0435\u0439 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0431\u0435\u0437 \u043f\u0430\u0443\u0437 \u0434\u043b\u0438\u043d\u043d\u0435\u0435 650 \u043c\u0441. \u0414\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f server action \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f\u00a0<code>\u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u2026<\/code>. \u041f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 \u043c\u0435\u043d\u044f\u044e\u0442\u0441\u044f\u00a0<code>updatedAt<\/code>\u00a0\u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u00a0<code>\u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043e<\/code>. \u041e\u0448\u0438\u0431\u043a\u0430 action \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \u0432\u00a0<code>\u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f<\/code>.<\/p>\n<p>Code block \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u0432 editor \u0438 viewer. \u0412 \u0437\u0430\u043c\u0435\u0442\u043a\u0443 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u043e\u043a \u043a\u043e\u0434\u0430, \u0437\u0430\u0442\u0435\u043c \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0440\u0435\u0436\u0438\u043c \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430. Quill-\u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0430 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u0442\u0441\u044f \u0432\u00a0<code>pre &gt; code<\/code>, highlight.js \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0443, \u0434\u043b\u0438\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0432\u043d\u0443\u0442\u0440\u0438 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0433\u043e scroll.<\/p>\n<p>\u0414\u043b\u044f \u043f\u0440\u0438\u043c\u0435\u0440\u043e\u0432 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u043f\u0440\u043e\u0435\u043a\u0442\u00a0<a href=\"https:\/\/workbench-ts.onrender.com\/\" rel=\"noopener nofollow\">Workbench<\/a>.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1101222\/\">https:\/\/habr.com\/ru\/articles\/1101222\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>WYSIWYG-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0432 Next.js App Router \u0437\u0430\u0442\u0440\u0430\u0433\u0438\u0432\u0430\u0435\u0442 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u043b\u043e\u0435\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. Quill \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 DOM \u0438 browser API, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0441\u0430\u043c \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0436\u0438\u0432\u0435\u0442 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435. \u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u043d\u0443\u0436\u043d\u043e \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Quill, \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438, \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0431\u0435\u0437 toolbar \u0438 \u0440\u0435\u0436\u0438\u043c\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.\u0415\u0441\u043b\u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Quill \u043a\u0430\u043a HTML, \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0438 viewer \u043c\u043e\u0433\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439\u00a0contentHtml. Editor \u043c\u0435\u043d\u044f\u0435\u0442 HTML \u0447\u0435\u0440\u0435\u0437\u00a0onChange. Autosave \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u043e\u0441\u043b\u0435 \u043f\u0430\u0443\u0437\u044b \u0432\u043e \u0432\u0432\u043e\u0434\u0435. Viewer \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0439 HTML \u0438 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 Quill \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0434\u043b\u044f code blocks. \u0414\u043b\u044f \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0438 \u043a\u043e\u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f highlight.js.Client-only \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440use client\u00a0\u0441\u0430\u043c \u043f\u043e \u0441\u0435\u0431\u0435 \u043d\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442, \u0447\u0442\u043e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u043f\u043e\u043a\u043e\u0439\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0439 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. Quill \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 DOM,\u00a0window\u00a0\u0438 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 browser-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438. \u0414\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 Client Component \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0430\u043c\u00a0react-quill-new\u00a0\u0447\u0435\u0440\u0435\u0437\u00a0dynamic\u00a0\u0441\u00a0ssr: false.Quill Syntax \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 highlight.js. \u0412 \u044d\u0442\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0434\u0443\u043b\u044c \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u00a0hljs\u00a0\u0432\u00a0window.\/\/ src\/components\/editor\/NoteEditorClient.tsx&#187;use client&#187;;import dynamic from &#171;next\/dynamic&#187;;import hljs from &#171;highlight.js&#187;;if (typeof window !== &#171;undefined&#187;) {  window.hljs = hljs;}const ReactQuill = dynamic(  async () =&gt; (await import(&#171;react-quill-new&#187;)).default,  {    ssr: false,  });Next.js \u043d\u0435 \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442\u00a0ReactQuill\u00a0\u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435. \u0418\u043c\u043f\u043e\u0440\u0442 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0433\u0434\u0435 \u0443\u0436\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b DOM \u0438\u00a0window.\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u00a0typeof window !== &#171;undefined&#187;\u00a0\u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u043c\u0443\u00a0hljs. \u0421\u0430\u043c Quill \u043e\u0442\u0434\u0435\u043b\u0435\u043d \u043e\u0442 SSR \u0447\u0435\u0440\u0435\u0437\u00a0dynamic.CSS Quill \u0438 highlight.jsQuill Snow \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043a\u043b\u0430\u0441\u0441\u044b \u0434\u043b\u044f toolbar, editor area, \u0441\u043f\u0438\u0441\u043a\u043e\u0432, \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u0432 \u0438 code blocks. \u0418\u0445 \u0441\u0442\u0438\u043b\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u0422\u0435\u043c\u0430 highlight.js \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e, \u0435\u0441\u043b\u0438 viewer \u0438 editor \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0434\u043d\u0443 \u0441\u0445\u0435\u043c\u0443 \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0438.\/\/ src\/app\/layout.tsximport &#171;react-quill-new\/dist\/quill.snow.css&#187;;import &#171;highlight.js\/styles\/github-dark.css&#187;;\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b Quill \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u0441\u0442\u0438\u043b\u0438\u00a0.ql-toolbar,\u00a0.ql-container\u00a0\u0438\u00a0.ql-editor. Viewer \u043f\u043e\u0437\u0436\u0435 \u0441\u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0447\u0430\u0441\u0442\u044c \u0442\u0435\u0445 \u0436\u0435 \u043a\u043b\u0430\u0441\u0441\u043e\u0432 \u0431\u0435\u0437 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043f\u0440\u0430\u0432\u0438\u043b \u0434\u043b\u044f \u043e\u0431\u044b\u0447\u043d\u043e\u0433\u043e \u0442\u0435\u043a\u0441\u0442\u0430.Toolbar \u0438 historyWYSIWYG-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u043e\u0431\u044b\u0447\u043d\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0435, \u0447\u0435\u043c bold \u0438 italic. \u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0438\u0436\u0435 \u0435\u0441\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0448\u0440\u0438\u0444\u0442, \u0446\u0432\u0435\u0442, \u0441\u043f\u0438\u0441\u043a\u0438, blockquote, inline code, code block, \u0441\u0441\u044b\u043b\u043a\u0438, \u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435 \u0438 \u043e\u0447\u0438\u0441\u0442\u043a\u0430 \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f.Undo \u0438 redo \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u043a history Quill.\u00a0userOnly: true\u00a0\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 history \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f.const modules = {  syntax: true,  toolbar: {    container: [      [&#171;undo&#187;, &#171;redo&#187;],      [{ header: [1, 2, 3, false] }, { font: [] }],      [&#171;bold&#187;, &#171;italic&#187;, &#171;underline&#187;, &#171;strike&#187;, &#171;code&#187;],      [{ script: &#171;sub&#187; }, { script: &#171;super&#187; }],      [{ color: [] }, { background: [] }],      [&#171;blockquote&#187;, &#171;code-block&#187;],      [        { list: &#171;ordered&#187; },        { list: &#171;bullet&#187; },        { indent: &#171;-1&#187; },        { indent: &#171;+1&#187; },      ],      [{ align: [] }],      [&#171;link&#187;],      [&#171;clean&#187;],    ],    handlers: {      undo: function () {        this.quill.history.undo();      },      redo: function () {        this.quill.history.redo();      },    },  },  history: {    delay: 400,    maxStack: 80,    userOnly: true,  },  clipboard: {    matchVisual: false,  },} as const;syntax: true\u00a0\u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 code blocks. Toolbar \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u044b undo \u0438 redo \u0432\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 history Quill.\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043c\u043e\u0436\u043d\u043e \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0432\u043d\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c render React \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442\u00a0modules.Controlled editor\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 editor \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 HTML \u0441\u043d\u0430\u0440\u0443\u0436\u0438 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0447\u0435\u0440\u0435\u0437\u00a0onChange. Quill \u043d\u0435 \u0437\u043d\u0430\u0435\u0442, \u0433\u0434\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0437\u0430\u043c\u0435\u0442\u043a\u0430 \u0438 \u043a\u0430\u043a\u0438\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u043f\u043e\u043f\u0430\u0434\u0435\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440.export default function NoteEditorClient({  value,  onChange,}: {  value: string;  onChange: (nextHtml: string) =&gt; void;}) {  const handleChange = useCallback(    (content: string) =&gt; {      onChange(content);    },    [onChange]  );  return (    &lt;div className=&#187;wb-editor&#187;&gt;      &lt;ReactQuill        theme=&#187;snow&#187;        value={value}        onChange={handleChange}        modules={modules}      \/&gt;    &lt;\/div&gt;  );}value\u00a0\u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 HTML.\u00a0onChange\u00a0\u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 HTML \u043f\u043e\u0441\u043b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u0435\u0433\u043e \u0432 \u0441\u043b\u043e\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\u0422\u0430\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u043c\u0443 store, form state \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0432\u043d\u0443\u0442\u0440\u0438 editor.\u0421\u043c\u0435\u043d\u0430 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u0434\u043d\u043e\u0433\u043e editor \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0437\u0430\u043c\u0435\u0442\u043e\u043a. React \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u043c\u0435\u0436\u0434\u0443 render, \u0435\u0441\u043b\u0438 \u0435\u0433\u043e \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u0432 \u0434\u0435\u0440\u0435\u0432\u0435 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0430\u0441\u044c. Quill \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, selection \u0438 history.\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u0437\u0430\u043c\u0435\u0442\u043a\u0430 \u0432 \u0434\u0430\u043d\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0437\u0430\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c\u00a0note\u00a0\u0432 URL.const sp = useSearchParams();const noteId = sp.get(&#171;note&#187;) ?? &#171;&#187;;const {  getNote,  updateNoteContent,  getNoteSaveInfo,} = useWorkbenchStore();const note = noteId  ? getNote(projectId, noteId)  : undefined;\u041f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435\u00a0noteId\u00a0\u0438\u0437 store \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u00a0contentHtml.Editor \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442\u0441\u044f \u0441\u00a0key={note.id}.&lt;NoteEditorClient  key={note.id}  value={note.contentHtml}  onChange={(nextHtml) =&gt;    updateNoteContent(note.id, nextHtml)  }\/&gt;\u041d\u043e\u0432\u044b\u0439\u00a0key\u00a0\u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 React \u0440\u0430\u0437\u043c\u043e\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 editor \u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439. Selection, history \u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u043d\u0435 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u044f\u0442 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e.\u041e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u00a0value\u00a0\u0434\u043b\u044f WYSIWYG-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0438\u043d\u043e\u0433\u0434\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e, \u0435\u0441\u043b\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u043d\u0435 React props. Remount \u043f\u043e id \u0443\u0431\u0438\u0440\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u0441\u043b\u043e\u0439 \u043c\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043c\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438.HTML \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044fQuill \u043e\u0442\u0434\u0430\u0435\u0442 HTML \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438. \u0422\u0435\u043a\u0441\u0442 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 store \u0431\u0435\u0437 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u0447\u0442\u043e\u0431\u044b controlled\u00a0value\u00a0\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u043b \u0442\u0435\u043a\u0443\u0449\u0435\u043c\u0443 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u043c\u0443 editor.\u041f\u043e\u0441\u043b\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f autosave.const updateNoteContent = useCallback(  (noteId: Id, nextHtml: string) =&gt; {    setDb((prev) =&gt; ({      &#8230;prev,      notes: prev.notes.map((note) =&gt;        note.id === noteId          ? { &#8230;note, contentHtml: nextHtml }          : note      ),    }));    scheduleSaveNote(noteId, nextHtml);  },  [scheduleSaveNote]);contentHtml\u00a0\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0432 store \u043d\u0430 \u043a\u0430\u0436\u0434\u043e\u043c\u00a0onChange. \u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043d\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0442\u043e\u0433\u043e \u0436\u0435 store.\u0421\u0435\u0440\u0432\u0435\u0440\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0438\u0434\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e. Editor \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430.AutosaveAutosave \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c server action \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430. \u0414\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u0430\u043c\u0435\u0442\u043a\u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u0441\u044f \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 timer. \u041d\u043e\u0432\u043e\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043e\u0442\u043c\u0435\u043d\u044f\u0435\u0442 \u0435\u0449\u0435 \u043d\u0435 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 timer \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438.const saveTimersRef =  React.useRef&lt;Map&lt;Id, number&gt;&gt;(new Map());const scheduleSaveNote = useCallback(  (noteId: Id, nextHtml: string) =&gt; {    const currentTimer =      saveTimersRef.current.get(noteId);    if (currentTimer) {      window.clearTimeout(currentTimer);    }    setNoteSaveInfoById((prev) =&gt; ({      &#8230;prev,      [noteId]: {        status: &#171;saving&#187;,        error: null,      },    }));    const timer = window.setTimeout(() =&gt; {      const updatedAt =        new Date().toISOString();      void updateNoteContentAction({        noteId,        contentHtml: nextHtml,        updatedAt,      });    }, 650);    saveTimersRef.current.set(noteId, timer);  },  [refreshFromServer]);\u041a\u0430\u0436\u0434\u0430\u044f \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f HTML \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 650 \u043c\u0441. \u0417\u0430\u043f\u0440\u043e\u0441 \u0443\u0445\u043e\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043f\u0430\u0443\u0437\u044b \u0432\u043e \u0432\u0432\u043e\u0434\u0435.Map \u0440\u0430\u0437\u0434\u0435\u043b\u044f\u0435\u0442 \u0442\u0430\u0439\u043c\u0435\u0440\u044b \u0440\u0430\u0437\u043d\u044b\u0445 \u0437\u0430\u043c\u0435\u0442\u043e\u043a. \u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043c\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u043d\u0435 \u0437\u0430\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0438\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0434\u0438\u043d \u043e\u0431\u0449\u0438\u0439 debounce timer.Server ActionAutosave \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u00a0noteId, HTML \u0438\u00a0updatedAt\u00a0\u0432 Server Action. \u0421\u0435\u0440\u0432\u0435\u0440 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 payload \u0447\u0435\u0440\u0435\u0437 Zod, \u043d\u0430\u0445\u043e\u0434\u0438\u0442 \u0437\u0430\u043c\u0435\u0442\u043a\u0443 \u0438 \u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e.export async function updateNoteContentAction(  payload: unknown): Promise&lt;Result&gt; {  const parsed =    UpdateNoteContentPayloadSchema.safeParse(payload);  if (!parsed.success) {    return {      ok: false,      error: parsed.error.message,    };  }  const {    noteId,    contentHtml,    updatedAt,  } = parsed.data;  let projectId: string | null = null;  const res = await mutateDb((db) =&gt; {    const note =      db.notes.find((x) =&gt; x.id === noteId);    if (!note) return;    projectId = note.projectId;    note.contentHtml = contentHtml;    note.updatedAt = updatedAt;  });  if (res.ok &amp;&amp; projectId) {    revalidatePath(`\/p\/${projectId}`);  }  return res;}Editor \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 API route \u0447\u0435\u0440\u0435\u0437\u00a0fetch. \u0417\u0430\u043f\u0438\u0441\u044c \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 Server Action.\u041f\u043e\u0441\u043b\u0435 \u043c\u0443\u0442\u0430\u0446\u0438\u0438\u00a0revalidatePath\u00a0\u0438\u043d\u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0443\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430.Saving, saved \u0438 error\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 HTML \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043b\u0443\u0447\u0448\u0435 \u043d\u0435 \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u0442\u044c \u0432 \u043e\u0434\u043d\u043e\u043c \u043f\u043e\u043b\u0435. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0432\u0438\u0434\u0435\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0442\u0435\u043a\u0441\u0442\u0430, \u043f\u043e\u043a\u0430 action \u0435\u0449\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u0432\u0435\u0442\u0430 store \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043c\u0435\u043d\u044f\u0435\u0442\u00a0updatedAt\u00a0\u0438 save state.void updateNoteContentAction({  noteId,  contentHtml: nextHtml,  updatedAt,})  .then((result) =&gt; {    if (!result.ok) {      setNoteSaveInfoById((prev) =&gt; ({        &#8230;prev,        [noteId]: {          status: &#171;error&#187;,          error: result.error,        },      }));      void refreshFromServer();      return;    }    setDb((prev) =&gt; ({      &#8230;prev,      notes: prev.notes.map((note) =&gt;        note.id === noteId          ? { &#8230;note, updatedAt }          : note      ),    }));    setNoteSaveInfoById((prev) =&gt; ({      &#8230;prev,      [noteId]: {        status: &#171;saved&#187;,        lastSavedAt: updatedAt,        error: null,      },    }));  })  .catch(() =&gt; {    setNoteSaveInfoById((prev) =&gt; ({      &#8230;prev,      [noteId]: {        status: &#171;error&#187;,        error: &#171;Network error&#187;,      },    }));    void refreshFromServer();  });updatedAt\u00a0\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u00a0ok. \u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442 \u0437\u0430\u043c\u0435\u0442\u043a\u0443 \u0432\u00a0error\u00a0\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u0447\u0438\u0442\u044b\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f.\u0421\u0442\u0430\u0442\u0443\u0441 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0440\u044f\u0434\u043e\u043c \u0441 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u043c \u0437\u0430\u043c\u0435\u0442\u043a\u0438.&lt;div className=&#187;wb-tree-meta&#187;&gt;  \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e:{&#187; &#171;}  {new Date(note.updatedAt).toLocaleString(&#171;ru-RU&#187;)}  {saveInfo.status === &#171;saving&#187; &amp;&amp;    &#187; \u2022 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u2026&#187;}  {saveInfo.status === &#171;saved&#187; &amp;&amp;    &#187; \u2022 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043e&#187;}  {saveInfo.status === &#171;error&#187; &amp;&amp;    &#187; \u2022 \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f&#187;}&lt;\/div&gt;\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439\u00a0contentHtml. \u0421\u0442\u0440\u043e\u043a\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-500883","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/500883","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=500883"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/500883\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=500883"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=500883"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=500883"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}