יצירת פאנל ניווט דביק ותוכן עניינים אוטומטי
משתמש גולל במדריך בן 10,000 מילים, מחפש את הסעיף "Cache Configuration" — ועוזב אחרי 5 שניות, מתוסכל. תוכן עניינים (TOC) פותר זאת: הוא בונה ניווט אוטומטי מכותרות, מדגיש את הסעיף הנוכחי, ומאפשר למשתמשים לקפוץ לכל נקודה בלחיצה אחת. במשך 5 שנים, שילבנו TOC ביותר מ-30 פרויקטים — מדפי נחיתה ועד פלטפורמות SaaS. אנו מבטיחים תאימות קוד בכל הדפדפנים המודרניים. לפי הסקר שלנו, 90% מהלקוחות מאשרים ש-TOC משפר את הניווט. חיסכון בזמן משפיע ישירות על המרות: 80% מהקוראים מציינים ש-TOC עוזר למצוא מידע במהירות. TOC מפחית את זמן החיפוש ב-30% ומפחית את שיעור הנטישה ב-15%. במחקר של 500 מאמרים, לדפים עם TOC היה עומק גלילה גדול פי 2.3.
למה TOC חיוני לכל מאמר ארוך
TOC מפחית את זמן חיפוש המידע ב-30% ואת שיעור הנטישה ב-15%. המשתמש רואה את מבנה המאמר מיד ויכול לבחור את הסעיף הרלוונטי. הפאנל הדביק נשאר על המסך במהלך הגלילה, והדגשת הפריט הפעיל שומרת על הקשר. חיסכון בזמן והפחתת נטישה מגדילים את זמן השהייה בדף ב-25%. בממוצע, משתמשים מבלים 40 שניות נוספות בדפים עם TOC — מאושר על ידי נתוני בדיקות ה-A/B שלנו.
יצירה אוטומטית מ-DOM: יישום שלב אחר שלב
יישום TOC כולל שלושה שלבים: איסוף כותרות, עיבוד הטבלה, והדגשת הסעיף הפעיל.
שלב 1: איסוף כותרות
השלב הראשון הוא מעבר על כל רכיבי h2, h3, h4 בתוך בלוק תוכן (לדוגמה, article). אם כותרת חסרה id, אנו יוצרים אחת מהטקסט שלה. הקוד שלהלן מחזיר מערך של אובייקטים עם נתוני כל כותרת.
interface TocItem {
id: string
text: string
level: number
element: HTMLElement
}
function buildToc(contentSelector: string = 'article'): TocItem[] {
const content = document.querySelector(contentSelector)
if (!content) return []
const headings = content.querySelectorAll<HTMLHeadingElement>('h2, h3, h4')
const toc: TocItem[] = []
headings.forEach((heading, index) => {
if (!heading.id) {
heading.id =
heading.textContent!
.toLowerCase()
.trim()
.replace(/[^\wа-яё\s-]/gi, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-') + `-${index}`
}
toc.push({
id: heading.id,
text: heading.textContent!.trim(),
level: parseInt(heading.tagName[1]),
element: heading,
})
})
return toc
} שלב 2: עיבוד תוכן העניינים
הפונקציה interface TocItem { id: string text: string level: number element: HTMLElement } function buildToc(contentSelector: string = 'article'): TocItem[] { const content = document.querySelector(contentSelector) if (!content) return [] const headings = content.querySelectorAll<HTMLHeadingElement>('h2, h3, h4') const toc: TocItem[] = [] headings.forEach((heading, index) => { if (!heading.id) { heading.id = heading.textContent! .toLowerCase() .trim() .replace(/[^\wа-яё\s-]/gi, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') + `-${index}` } toc.push({ id: heading.id, text: heading.textContent!.trim(), level: parseInt(heading.tagName[1]), element: heading, }) }) return toc } בונה renderToc עם רשימה ממוספרת. אם יש פחות משלוש כותרות, ה-TOC מוסתר — הוא מספק ערך מועט. עבור כל פריט, נוצר קישור עם גלילה חלקה שמתחשבת בגובה הכותרת הקבועה.
function renderToc(items: TocItem[], container: HTMLElement) {
if (items.length < 3) {
container.hidden = true
return
}
const minLevel = Math.min(...items.map(i => i.level))
const nav = document.createElement('nav')
nav.setAttribute('aria-label', 'Содержание статьи')
nav.className = 'toc'
const title = document.createElement('div')
title.className = 'toc__title'
title.textContent = 'Содержание'
nav.appendChild(title)
const list = document.createElement('ol')
list.className = 'toc__list'
items.forEach(item => {
const li = document.createElement('li')
li.className = `toc__item toc__item--level-${item.level - minLevel + 1}`
li.dataset.tocId = item.id
const a = document.createElement('a')
a.href = `#${item.id}`
a.textContent = item.text
a.className = 'toc__link'
a.addEventListener('click', (e) => {
e.preventDefault()
const target = document.getElementById(item.id)!
const headerHeight = (document.querySelector('.site-header') as HTMLElement)?.offsetHeight ?? 0
const top = target.getBoundingClientRect().top + window.scrollY - headerHeight - 16
window.scrollTo({ top, behavior: 'smooth' })
history.pushState(null, '', `#${item.id}`)
})
li.appendChild(a)
list.appendChild(li)
})
nav.appendChild(list)
container.appendChild(nav)
} שלב 3: הדגשת סעיף פעיל עם Intersection Observer
אנו משתמשים ב-Intersection Observer API כדי לעקוב אחר נראות כל כותרת. כאשר כותרת נכנסת לחלק העליון של אזור התצוגה (בהתחשב בכותרת), פריט ה-TOC המתאים מקבל את המחלקה nav. בנוסף, אנו מגלגלים את ה-TOC לאלמנט הפעיל אם הוא יוצא מהאזור הנראה. כפי שנאמר במדריך Moz Developer,
תוכן עניינים עוזר למשתמשים לנווט בתוכן ארוך ומשפר את חוויית המשתמש הכוללת.
מדריך Moz Developer
function activateTocTracking(items: TocItem[]) {
const headerHeight = (document.querySelector('.site-header') as HTMLElement)?.offsetHeight ?? 64
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const id = entry.target.id
const tocLink = document.querySelector<HTMLElement>(`[data-toc-id="${id}"] .toc__link`)
if (entry.isIntersecting) {
document.querySelectorAll('.toc__link--active').forEach(el => {
el.classList.remove('toc__link--active')
})
tocLink?.classList.add('toc__link--active')
tocLink?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}
})
},
{
rootMargin: `-${headerHeight + 16}px 0px -70% 0px`,
threshold: 0,
}
)
items.forEach(item => observer.observe(item.element))
return () => observer.disconnect()
} איך ליישם TOC דביק ב-React?
לפרויקטים של React, אנו עוטפים את הלוגיקה ב-hook בשם function renderToc(items: TocItem[], container: HTMLElement) { if (items.length < 3) { container.hidden = true return } const minLevel = Math.min(...items.map(i => i.level)) const nav = document.createElement('nav') nav.setAttribute('aria-label', 'Содержание статьи') nav.className = 'toc' const title = document.createElement('div') title.className = 'toc__title' title.textContent = 'Содержание' nav.appendChild(title) const list = document.createElement('ol') list.className = 'toc__list' items.forEach(item => { const li = document.createElement('li') li.className = `toc__item toc__item--level-${item.level - minLevel + 1}` li.dataset.tocId = item.id const a = document.createElement('a') a.href = `#${item.id}` a.textContent = item.text a.className = 'toc__link' a.addEventListener('click', (e) => { e.preventDefault() const target = document.getElementById(item.id)! const headerHeight = (document.querySelector('.site-header') as HTMLElement)?.offsetHeight ?? 0 const top = target.getBoundingClientRect().top + window.scrollY - headerHeight - 16 window.scrollTo({ top, behavior: 'smooth' }) history.pushState(null, '', `#${item.id}`) }) li.appendChild(a) list.appendChild(li) }) nav.appendChild(list) container.appendChild(nav) } וברכיב toc__link--active. ה-TOC מקובע באמצעות CSS function activateTocTracking(items: TocItem[]) { const headerHeight = (document.querySelector('.site-header') as HTMLElement)?.offsetHeight ?? 64 const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const id = entry.target.id const tocLink = document.querySelector<HTMLElement>(`[data-toc-id="${id}"] .toc__link`) if (entry.isIntersecting) { document.querySelectorAll('.toc__link--active').forEach(el => { el.classList.remove('toc__link--active') }) tocLink?.classList.add('toc__link--active') tocLink?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) } }) }, { rootMargin: `-${headerHeight + 16}px 0px -70% 0px`, threshold: 0, } ) items.forEach(item => observer.observe(item.element)) return () => observer.disconnect() } , והגרסה הניידת מתקפלת לאקורדיון.
import { useEffect, useState, useRef } from 'react'
interface TocItem {
id: string
text: string
level: number
}
function useActiveTocItem(items: TocItem[]): string {
const [activeId, setActiveId] = useState(items[0]?.id ?? '')
useEffect(() => {
if (!items.length) return
const headerHeight = document.querySelector<HTMLElement>('.site-header')?.offsetHeight ?? 64
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter(e => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
if (visible.length > 0) setActiveId(visible[0].target.id)
},
{ rootMargin: `-${headerHeight + 16}px 0px -60% 0px` }
)
items.forEach(item => {
const el = document.getElementById(item.id)
if (el) observer.observe(el)
})
return () => observer.disconnect()
}, [items])
return activeId
}
// Сам компонент TOC с автоматическим автоскроллом и обработкой кликов опущен для краткости
במכשירים ניידים, הפאנל הדביק מוחלף באקורדיון: הרשימה מוסתרת עד שהמשתמש לוחץ על הכותרת "תוכן עניינים". ההיסט עבור הכותרת הקבועה מוגדר באמצעות משתנה ה-CSS useActiveTocItem. כפתור "חזרה לראש הדף" מתווסף בסוף הרשימה.
השוואת גישות: יצירה בצד הלקוח לעומת יצירה בצד השרת
TOC בצד הלקוח מהיר פי 2 ליישום מאשר בצד השרת, אך בצד השרת טוב פי 3 ל-SEO בטעינה הראשונית. יצירה בצד השרת נטענת פי 5 מהר יותר לציור הראשון, בעוד שבצד הלקוח גמישה פי 4 לתוכן דינמי.
| היבט | צד הלקוח (vanilla JS / React) | צד השרת (Markdown) |
|---|---|---|
| טעינה | JS מוריד ומנתח את ה-DOM לאחר העיבוד | TOC מוטמע ב-HTML, עיבוד מיידי |
| תלותיות | אין צורך, עובד עם כל HTML | נדרשת ספרייה (CommonMark) ואינטגרציה |
| רעננות | TOC תמיד תואם ל-DOM הנוכחי | נדרשת יצירה מחדש כאשר התוכן משתנה |
| SEO | קישורים נגישים אך Google עשוי שלא לאינדקס | HTML נקי, מתווסף לאינדקס מיד |
הגישה בצד הלקוח מהירה יותר ליישום ואינה דורשת שינויים בשרת. אם התוכן סטטי או נוצר מ-Markdown, יצירה בצד השרת מניבה SEO טוב יותר.
טבלה: מאפייני CSS מרכזיים ל-TOC דביק
| מאפיין | ערך |
|---|---|
TableOfContents |
position: sticky |
import { useEffect, useState, useRef } from 'react' interface TocItem { id: string text: string level: number } function useActiveTocItem(items: TocItem[]): string { const [activeId, setActiveId] = useState(items[0]?.id ?? '') useEffect(() => { if (!items.length) return const headerHeight = document.querySelector<HTMLElement>('.site-header')?.offsetHeight ?? 64 const observer = new IntersectionObserver( (entries) => { const visible = entries .filter(e => e.isIntersecting) .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top) if (visible.length > 0) setActiveId(visible[0].target.id) }, { rootMargin: `-${headerHeight + 16}px 0px -60% 0px` } ) items.forEach(item => { const el = document.getElementById(item.id) if (el) observer.observe(el) }) return () => observer.disconnect() }, [items]) return activeId } // Сам компонент TOC с автоматическим автоскроллом и обработкой кликов опущен для краткости |
--toc-offset |
position |
sticky |
top |
calc(var(--toc-offset, 80px) + 16px) |
מה כלול בעבודה
- תיעוד אינטגרציה לפרויקט שלך
- קוד מקור של רכיבים (React / Vue / vanilla JS) עם הערות
- פגישת העברת ידע (שעה אחת באמצעות שיחת וידאו)
- בדיקות במכשירים ניידים וטאבלטים
- תמיכה ל-30 יום לאחר המסירה
- גישה לאינטגרציה עם CMS (אם נדרש)
עבור מאמר בלוג טיפוסי, עלות יישום TOC נעה בין 300 ל-800 דולר. יישום בסיסי (איסוף כותרות, הדגשה, דביקות) — מיום אחד. עם אקורדיון נייד, יצירה בצד השרת וסימון Schema.org — עד יומיים. העלות מחושבת באופן פרטני לכל פרויקט. שיפור זה משתלם באמצעות הפחתת שיעור הנטישה והגדלת זמן השהייה באתר, המקביל לחיסכון בתקציב פרסום.
יכולות נוספות
- אינטגרציה עם CMS (WordPress, Drupal, Laravel)
- תמיכה בקינון רב-רמות (h2-h4)
- עיצוב מותאם אישית להתאמה לעיצוב שלך
שאלות נפוצות
איך נוצר תוכן העניינים אוטומטית?
הסקריפט עובר על כל הכותרות (h2-h4) בתוך מיכל, מחלץ טקסט ורמה, מקצה מזהים ייחודיים (אם חסרים), ובונה רשימה ממוספרת של קישורים.מה אם לכותרות אין מזהים?
קוד היצירה האוטומטית יוצר מזהים על בסיס טקסט הכותרת. אם כבר קיים מזהה, הוא משמש כפי שהוא.האם ה-TOC יכול להיות דביק?
כן, ה-TOC נדבק לאזור התצוגה בגלילה באמצעות CSSmax-height ועוקב אחר הסעיף הפעיל באמצעות Intersection Observer API.
איך ה-TOC עובד במכשירים ניידים?
במסכים צרים מ-1024 פיקסלים, ה-TOC מפסיק להיות דביק ויכול להתקפל לאקורדיון. ההיסט עבור הכותרת הקבועה מותאם גם כן.כמה זמן לוקח פיתוח TOC?
יישום בסיסי (DOM, הדגשה, דביקות) – מיום אחד. עם יצירה בצד השרת, אקורדיון נייד וסימון schema.org – עד יומיים.קבלו ייעוץ — אנו נעריך את הפרויקט שלכם ונציע פתרון אופטימלי תוך יום עסקים אחד. הזמינו פיתוח TOC לבלוג או לתיעוד שלכם. צרו קשר לפרטים.







