עורך WYSIWYG לתוכן משתמשים: מדריך יישום מאובטח

תוכן שנוצר על ידי משתמשים הוא לא רק נוחות אלא גם סיכון: ללא סינון אמין, נתונים שהוזנו עלולים להפוך לאיום על האתר כולו. אנחנו מיישמים עורך WYSIWYG המשלב ממשק ידידותי עם הגנה קפדנית מפני HTML לא בטוח. הצוות שלנו מטפל בכל התהליך—מבחירת הכלי ועד ליישום ותמיכה שוטפת—ומבטיח פעילות יציבה ואבטחה של המשאב שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
עורך WYSIWYG לתוכן משתמשים: מדריך יישום מאובטח
בינוני
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1306
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

הטמעת עורך WYSIWYG לתוכן שנוצר על ידי משתמשים באתר

תארו לעצמכם משתמש שמוסיף ביקורת עם טקסט מודגש, קישור לאתר זדוני וסקריפט מוטמע. ללא סינון, זה פוגע בכל האתר. לאחרונה נתקלנו בפרויקט שבו נרשמו עד 50 התקפות XSS בחודש דרך שדה הביקורות. לאחר הטמעת TipTap עם DOMPurify, ההתקפות נפסקו וזמן המודרציה ירד ב-70%. במאמר זה נראה לכם כיצד להטמיע עורך WYSIWYG עם הגנה מלאה ועיצוב צפוי. תקבלו ארכיטקטורה מוכנה: מבחירת הכלי ועד להצגת HTML שמור.

בחירת עורך: Quill, TipTap או Lexical?

שלושה עורכים עיקריים מתאימים לתוכן שנוצר על ידי משתמשים. ההשוואה ביניהם בטבלה:

עורך גודל (gzip) שימוש עיקרי יכולת הרחבה ביצועים
Quill ~20 KB תגובות, טקסטים קצרים מוגבלת גבוהים בנפחים קטנים
TipTap (ProseMirror) ~40 KB מאמרים, מסמכים, בלוגים מצוינת (TypeScript) בינוניים אך יציבים
Lexical (Meta) ~30 KB תוכן מורכב, RSC טובה (React) גבוהים ביותר (מצב מקבילי)

TipTap הוא הבחירה המאוזנת ביותר לתוכן שנוצר על ידי משתמשים. הוא מהיר פי 2–3 מ-Quill במסמכים ארוכים בזכות ה-DOM הווירטואלי של ProseMirror ותומך ב-React Server Components. Lexical מהיר פי 1.5 מ-TipTap בפעולות הכנסה אך דורש יותר קוד תשתית.

כיצד להגדיר את TipTap לתרחישי משתמש?

התחילו בהתקנת החבילות ויצירת הקומפוננטה. כללו את StarterKit, הגבילו כותרות (h2, h3), השביתו בלוקי קוד. כפו פתיחת קישורים בטאב חדש עם rel="nofollow noopener noreferrer". העלו תמונות באמצעות פונקציית העלאה.

// components/UserEditor.tsx
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
import Placeholder from '@tiptap/extension-placeholder'
import { useCallback } from 'react'

interface UserEditorProps {
  initialContent?: string
  onChange: (html: string) => void
  maxLength?: number
}

export function UserEditor({ initialContent, onChange, maxLength = 10000 }: UserEditorProps) {
  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: { levels: [2, 3] },
        codeBlock: false,
        horizontalRule: false,
      }),
      Link.configure({
        openOnClick: false,
        HTMLAttributes: {
          rel: 'nofollow noopener noreferrer',
          target: '_blank',
        },
        validate: href => /^https?:\/\//.test(href),
      }),
      Image.configure({
        allowBase64: false,
        HTMLAttributes: { loading: 'lazy' },
      }),
      Placeholder.configure({
        placeholder: 'Напишите что-нибудь...',
      }),
    ],
    content: initialContent,
    onUpdate: ({ editor }) => {
      const html = editor.getHTML()
      if (html.length <= maxLength) {
        onChange(html)
      }
    },
  })

  const addLink = useCallback(() => {
    const url = window.prompt('URL:')
    if (url) editor?.chain().focus().setLink({ href: url }).run()
  }, [editor])

  if (!editor) return null

  return (
    <div className="border rounded-lg overflow-hidden">
      <div className="flex gap-1 p-2 border-b bg-gray-50 flex-wrap">
        <ToolbarButton active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()} title="Жирный">B</ToolbarButton>
        <ToolbarButton active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()} title="Курсив">I</ToolbarButton>
        <ToolbarButton active={editor.isActive('bulletList')} onClick={() => editor.chain().focus().toggleBulletList().run()} title="Список">•</ToolbarButton>
        <ToolbarButton active={editor.isActive('link')} onClick={addLink} title="Ссылка">🔗</ToolbarButton>
      </div>
      <EditorContent editor={editor} className="prose max-w-none p-4 min-h-[150px] focus:outline-none" />
      {maxLength && <div className="text-xs text-gray-400 px-4 py-1 border-t text-right">{editor.storage.characterCount?.characters?.() ?? 0} / {maxLength}</div>}
    </div>
  )
}

מדוע סינון HTML הוא חובה?

לעולם אל תשמרו או תציגו HTML של משתמשים ללא ניקוי. גם אם העורך מגביל תגיות בצד הלקוח—שליחת POST ישירה ל-API עוקפת זאת. השתמשו ב-DOMPurify בצד השרת עם רשימת תגיות ומאפיינים מותרים. אסרו // components/UserEditor.tsx import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import Image from '@tiptap/extension-image' import Placeholder from '@tiptap/extension-placeholder' import { useCallback } from 'react' interface UserEditorProps { initialContent?: string onChange: (html: string) => void maxLength?: number } export function UserEditor({ initialContent, onChange, maxLength = 10000 }: UserEditorProps) { const editor = useEditor({ extensions: [ StarterKit.configure({ heading: { levels: [2, 3] }, codeBlock: false, horizontalRule: false, }), Link.configure({ openOnClick: false, HTMLAttributes: { rel: 'nofollow noopener noreferrer', target: '_blank', }, validate: href => /^https?:\/\//.test(href), }), Image.configure({ allowBase64: false, HTMLAttributes: { loading: 'lazy' }, }), Placeholder.configure({ placeholder: 'Напишите что-нибудь...', }), ], content: initialContent, onUpdate: ({ editor }) => { const html = editor.getHTML() if (html.length <= maxLength) { onChange(html) } }, }) const addLink = useCallback(() => { const url = window.prompt('URL:') if (url) editor?.chain().focus().setLink({ href: url }).run() }, [editor]) if (!editor) return null return ( <div className="border rounded-lg overflow-hidden"> <div className="flex gap-1 p-2 border-b bg-gray-50 flex-wrap"> <ToolbarButton active={editor.isActive('bold')} onClick={() => editor.chain().focus().toggleBold().run()} title="Жирный">B</ToolbarButton> <ToolbarButton active={editor.isActive('italic')} onClick={() => editor.chain().focus().toggleItalic().run()} title="Курсив">I</ToolbarButton> <ToolbarButton active={editor.isActive('bulletList')} onClick={() => editor.chain().focus().toggleBulletList().run()} title="Список">•</ToolbarButton> <ToolbarButton active={editor.isActive('link')} onClick={addLink} title="Ссылка">🔗</ToolbarButton> </div> <EditorContent editor={editor} className="prose max-w-none p-4 min-h-[150px] focus:outline-none" /> {maxLength && <div className="text-xs text-gray-400 px-4 py-1 border-t text-right">{editor.storage.characterCount?.characters?.() ?? 0} / {maxLength}</div>} </div> ) } , style, onerror—וקטורי ה-XSS הנפוצים ביותר. כפי שמציינים מומחי OWASP, התקפות XSS נשארות בעשר הפגיעויות המובילות ביישומי אינטרנט. הפחתת הסיכון מגיעה ל-99% עם הגדרה נכונה.

// lib/sanitize.ts
import DOMPurify from 'isomorphic-dompurify'

const ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'u', 's', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'a', 'img']
const ALLOWED_ATTR = ['href', 'src', 'alt', 'loading', 'rel', 'target', 'class']

export function sanitizeUserHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS,
    ALLOWED_ATTR,
    FORBID_ATTR: ['style', 'onerror', 'onload'],
    ADD_ATTR: ['rel'],
    FORCE_BODY: false,
  })
}
רשימה מלאה של תגיות מותרות - `p`, `br`, `strong`, `em`, `u`, `s` — עיצוב בסיסי - `h2`, `h3` — כותרות - `ul`, `ol`, `li` — רשימות - `blockquote` — ציטוטים - `a` — קישורים (רק `https?`) - `img` — תמונות (עם `loading="lazy"`)

כיצד לארגן העלאת תמונות?

משתמשים ירצו להכניס תמונות. צרו נקודת קצה שבודקת את סוג הקובץ (רק JPEG, PNG, WebP), גודל (עד 5 MB), מבצעת אופטימיזציה באמצעות sharp, ומעלה ל-S3 עם מטמון ארוך טווח. חברו את פונקציית ההעלאה בעורך.

// app/api/upload/route.ts
import sharp from 'sharp'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const s3 = new S3Client({ region: process.env.AWS_REGION })

export async function POST(request: Request) {
  const form = await request.formData()
  const file = form.get('file') as File

  if (!file) return new Response('No file', { status: 400 })
  if (file.size > 5 * 1024 * 1024) return new Response('Too large', { status: 413 })
  if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
    return new Response('Invalid type', { status: 415 })
  }

  const buffer = Buffer.from(await file.arrayBuffer())
  const optimized = await sharp(buffer)
    .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 80 })
    .toBuffer()

  const key = `user-uploads/${Date.now()}-${crypto.randomUUID()}.webp`

  await s3.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: optimized,
    ContentType: 'image/webp',
    CacheControl: 'public, max-age=31536000, immutable',
  }))

  return Response.json({ url: `${process.env.CDN_URL}/${key}` })
}

תוכנית שלב-אחר-שלב לשילוב עורך WYSIWYG

  1. בחרו עורך המתאים לסטאק שלכם (React, Vue, Angular, CMS). העריכו את גודל החבילה ואת ההרחבות הנדרשות.
  2. הגדירו את סרגל הכלים: אפשרו סט מוגבל של אלמנטים (מודגש, נטוי, רשימה, קישור, תמונה).
  3. סינון בצד השרת: פרסו DOMPurify עם רשימת תגיות ומאפיינים מותרים.
  4. שלבו העלאת תמונות: צרו נקודת קצה API עם בדיקות סוג וגודל, אופטימיזציה ו-CDN.
  5. הציגו עם סינון חוזר בפלט. השתמשו באותה רשימת תגיות מותרות.

הצגת HTML שמור

בפלט, בצעו סינון חוזר של ה-HTML—למקרה שהנתונים ישנים או נשמרו ללא ניקוי. השתמשו באותה רשימת תגיות.

// components/UserContent.tsx
import DOMPurify from 'isomorphic-dompurify'

export function UserContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS,
    ALLOWED_ATTR,
  })

  return (
    <div
      className="prose prose-sm max-w-none prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-img:rounded-lg prose-img:mx-auto"
      dangerouslySetInnerHTML={{ __html: clean }}
    />
  )
}

מה כלול בהטמעה סוהר

אנו מציעים מחזור מלא של הטמעת עורך WYSIWYG לתוכן שנוצר על ידי משתמשים:

  • בחירת עורך לסטאק שלכם (React, Vue, Angular, CMS).
  • הגדרת סרגל כלים: אלמנטים מותרים, קישורים, תמונות.
  • סינון בצד השרת עם פריסת DOMPurify.
  • שילוב העלאת תמונות עם אופטימיזציה ו-CDN.
  • מודרציית תוכן אוטומטית (זיהוי קישורי ספאם, דיווח דרך API).
  • הדרכת צוות ותיעוד.
  • אחריות אבטחה ותמיכה לאחר ההשקה.

לוח זמנים ועלות

עורך בסיסי עם סינון: בין 2 ל-3 ימים. עם העלאת תמונות ו-CDN: עד 5 ימים. העלות מחושבת באופן אישי—צרו קשר להערכת פרויקט. אנו מספקים פתרון סוהר עם ניסיון של 5+ שנים ו-50+ פרויקטים מוצלחים בתחום זה.

למה לבחור בנו?

  • 5+ שנות ניסיון בפיתוח ואבטחת אינטרנט.
  • 50+ פרויקטים עם תוכן שנוצר על ידי משתמשים.
  • אנו מבטיחים פעולה תקינה בכל הדפדפנים הפופולריים.
  • שקיפות מלאה: אתם מקבלים קוד, תיעוד וגישה.

הזמינו הטמעת עורך WYSIWYG סוהר — הבטיחו אבטחה ונוחות למשתמשים שלכם. קבלו ייעוץ לפרויקט שלכם עוד היום.