מצב טיפוסי: לקוח מבקש לא רק עמוד "אודותינו", אלא קטלוג שירותים מלא עם מחירים, תעריפים וסינון. בלוקים מוכנים לא מתאימים — הם צריכים שדות מותאמים אישית, טאבים בניהול וניתוב משלהם. הפתרון הוא מודלי Page מותאמים אישית. השתמשנו בגישה זו בכל פרויקט Wagtail במשך למעלה מ-5 שנים וצברנו יותר מ-30 מקרי הצלחה. לדוגמה, חנות מקוונת דרשה 7 סוגי עמודים שונים עם שדות ויחסים ייחודיים — הפיתוח כולו ארך 4 ימים, והעורכים קיבלו ממשק נקי עם טאבים. בממוצע, פרויקט מורכב דורש 5–10 סוגי עמודים. שדות Wagtail הסטנדרטיים (כותרת, slug, גוף) אינם מכסים את הדרישות: אם דוחסים תעריפים, מפרטים וביקורות לתוך RichTextField אחד, הניהול הופך לסיוט. בפרויקט אחד, עורכים בילו 20 דקות בעריכת עמוד בודד — לאחר יישום מודלים מותאמים אישית, הזמן ירד ל-2 דקות. הפחתה של 90%.
כיצד מודלי Page מותאמים אישית מקלים על חיי העורך?
עורכים זקוקים לממשק ניהול מובן. לשם כך, אנו משתמשים ב-TabbedInterface עם אובייקטים של ObjectList. הדוגמה שלהלן מציגה מודל ServicePage עם שלושה טאבים. שימו לב ל-InlinePanel עבור רשומות קשורות ול-pricing_panels להצגת מחירים.
from django.db import models
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField, StreamField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TabbedInterface, ObjectList
from wagtail.search import index
from modelcluster.fields import ParentalKey
class ServicePage(Page):
intro = models.CharField(max_length=500, blank=True)
body = RichTextField(blank=True)
icon = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
)
price_from = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True)
search_fields = Page.search_fields + [
index.SearchField('intro'),
index.SearchField('body'),
index.FilterField('price_from'),
]
content_panels = Page.content_panels + [
FieldPanel('intro'),
FieldPanel('icon'),
FieldPanel('body'),
InlinePanel('features', label='Особенности'),
]
promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('search_description'),
], heading='SEO'),
]
pricing_panels = [
FieldPanel('price_from'),
InlinePanel('pricing_tiers', label='Тарифы'),
]
edit_handler = TabbedInterface([
ObjectList(content_panels, heading='Содержание'),
ObjectList(pricing_panels, heading='Цены'),
ObjectList(promote_panels, heading='SEO'),
])
parent_page_types = ['services.ServicesIndexPage']
subpage_types = []
class Meta:
verbose_name = 'Service Page'
class ServiceFeature(Orderable):
page = ParentalKey(ServicePage, on_delete=models.CASCADE, related_name='features')
title = models.CharField(max_length=255)
text = models.TextField(blank=True)
icon = models.CharField(max_length=50, blank=True)
panels = [
FieldPanel('title'),
FieldPanel('text'),
FieldPanel('icon'),
]
למה RoutablePage עדיף על עמודים נפרדים?
אם אתם צריכים בלוג עם קטגוריות ותגיות, ניתן ליצור מודלי Page נפרדים לכל ישות, או להשתמש ב-from django.db import models from wagtail.models import Page, Orderable from wagtail.fields import RichTextField, StreamField from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TabbedInterface, ObjectList from wagtail.search import index from modelcluster.fields import ParentalKey class ServicePage(Page): intro = models.CharField(max_length=500, blank=True) body = RichTextField(blank=True) icon = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) price_from = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True) search_fields = Page.search_fields + [ index.SearchField('intro'), index.SearchField('body'), index.FilterField('price_from'), ] content_panels = Page.content_panels + [ FieldPanel('intro'), FieldPanel('icon'), FieldPanel('body'), InlinePanel('features', label='Особенности'), ] promote_panels = [ MultiFieldPanel([ FieldPanel('slug'), FieldPanel('seo_title'), FieldPanel('search_description'), ], heading='SEO'), ] pricing_panels = [ FieldPanel('price_from'), InlinePanel('pricing_tiers', label='Тарифы'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Содержание'), ObjectList(pricing_panels, heading='Цены'), ObjectList(promote_panels, heading='SEO'), ]) parent_page_types = ['services.ServicesIndexPage'] subpage_types = [] class Meta: verbose_name = 'Service Page' class ServiceFeature(Orderable): page = ParentalKey(ServicePage, on_delete=models.CASCADE, related_name='features') title = models.CharField(max_length=255) text = models.TextField(blank=True) icon = models.CharField(max_length=50, blank=True) panels = [ FieldPanel('title'), FieldPanel('text'), FieldPanel('icon'), ] . השוו: עם 10 קטגוריות ו-20 תגיות, עמודים נפרדים ידרשו 30 מודלים. RoutablePage אחד עם דקורטורים של BlogIndexPage מחליף את כולם, חותך את הקוד בחצי ומפשט את התחזוקה. לפי תיעוד Wagtail, RoutablePage מאפשר לעמוד אחד לטפל במספר תצוגות URL.
from wagtail.contrib.routable_page.models import RoutablePage, path, re_path
class BlogIndexPage(RoutablePage, Page):
intro = RichTextField(blank=True)
content_panels = Page.content_panels + [FieldPanel('intro')]
subpage_types = ['blog.BlogPost']
@path('')
def index_view(self, request):
posts = BlogPost.objects.live().child_of(self).order_by('-first_published_at')
from django.core.paginator import Paginator
paginator = Paginator(posts, 12)
return self.render(request, context_overrides={
'posts': paginator.get_page(request.GET.get('page')),
})
@path('category/<str:category_slug>/')
def category_view(self, request, category_slug):
category = get_object_or_404(BlogCategory, slug=category_slug)
posts = BlogPost.objects.live().child_of(self).filter(categories=category)
return self.render(request, context_overrides={
'posts': posts,
'category': category,
}, template='blog/blog_category.html')
@path('tag/<str:tag>/')
def tag_view(self, request, tag):
posts = BlogPost.objects.live().child_of(self).filter(tags__slug=tag)
return self.render(request, context_overrides={
'posts': posts,
'tag': tag,
})
@re_path(r'^archive/(\d{4})/$')
def year_archive(self, request, year):
posts = BlogPost.objects.live().child_of(self).filter(date__year=year)
return self.render(request, context_overrides={
'posts': posts,
'year': year,
}) כיצד להתאים אישית תמונות?
לצורך עמידה בדרישות SEO ונגישות, אנו מרחיבים את מודל התמונה הסטנדרטי, ומוסיפים שדות @path ו-from wagtail.contrib.routable_page.models import RoutablePage, path, re_path class BlogIndexPage(RoutablePage, Page): intro = RichTextField(blank=True) content_panels = Page.content_panels + [FieldPanel('intro')] subpage_types = ['blog.BlogPost'] @path('') def index_view(self, request): posts = BlogPost.objects.live().child_of(self).order_by('-first_published_at') from django.core.paginator import Paginator paginator = Paginator(posts, 12) return self.render(request, context_overrides={ 'posts': paginator.get_page(request.GET.get('page')), }) @path('category/<str:category_slug>/') def category_view(self, request, category_slug): category = get_object_or_404(BlogCategory, slug=category_slug) posts = BlogPost.objects.live().child_of(self).filter(categories=category) return self.render(request, context_overrides={ 'posts': posts, 'category': category, }, template='blog/blog_category.html') @path('tag/<str:tag>/') def tag_view(self, request, tag): posts = BlogPost.objects.live().child_of(self).filter(tags__slug=tag) return self.render(request, context_overrides={ 'posts': posts, 'tag': tag, }) @re_path(r'^archive/(\d{4})/$') def year_archive(self, request, year): posts = BlogPost.objects.live().child_of(self).filter(date__year=year) return self.render(request, context_overrides={ 'posts': posts, 'year': year, }) . זה מאפשר לעורכים למלא טקסט חלופי חובה ולציין את מחבר התמונה.
# core/models.py from wagtail.images.models import AbstractImage, AbstractRendition, Image
class CustomImage(AbstractImage):
alt_text = models.CharField(max_length=255, blank=True)
credit = models.CharField(max_length=255, blank=True)
admin_form_fields = Image.admin_form_fields + [
'alt_text',
'credit',
]
@property
def default_alt_text(self):
return self.alt_text or self.title
class CustomRendition(AbstractRendition):
image = models.ForeignKey(CustomImage, on_delete=models.CASCADE, related_name='renditions')
class Meta:
unique_together = [('image', 'filter_spec', 'focal_point_key')] get_context — העברת נתונים נוספים
המתודה alt_text מאפשרת להעשיר את התבנית בנתונים מטבלאות אחרות, פרמטרי GET או טפסים. היא נקראת בעת רינדור העמוד.
def get_context(self, request):
context = super().get_context(request)
context['related_posts'] = BlogPost.objects.live().exclude(
pk=self.pk
).filter(
categories__in=self.categories.all()
).distinct().order_by('-first_published_at')[:3]
context['comment_form'] = CommentForm()
context['current_tag'] = request.GET.get('tag')
return context השוואת גישות לבניית מודלי Page
| גישה | שימוש | מורכבות | מהירות תחזוקה |
|---|---|---|---|
| שדות Page בסיסיים | עמודים פשוטים (אודות, צור קשר) | נמוכה | גבוהה |
| שדות מותאמים אישית + פאנלים | עמודי שירות, קטלוגים | בינונית | בינונית |
| StreamField | עמודי נחיתה גמישים, עריכה | בינונית | נמוכה |
| RoutablePage | בלוגים, קטעים מסוננים | גבוהה | גבוהה |
| תמונות מותאמות אישית | אתרי מדיה, ארכיוני תמונות | נמוכה | גבוהה |
| סוג שדה | דוגמת שימוש | לוח ניהול |
|---|---|---|
credit | כותרת, תיאור קצר | # core/models.py from wagtail.images.models import AbstractImage, AbstractRendition, Image class CustomImage(AbstractImage): alt_text = models.CharField(max_length=255, blank=True) credit = models.CharField(max_length=255, blank=True) admin_form_fields = Image.admin_form_fields + [ 'alt_text', 'credit', ] @property def default_alt_text(self): return self.alt_text or self.title class CustomRendition(AbstractRendition): image = models.ForeignKey(CustomImage, on_delete=models.CASCADE, related_name='renditions') class Meta: unique_together = [('image', 'filter_spec', 'focal_point_key')] |
get_context | תוכן ראשי | def get_context(self, request): context = super().get_context(request) context['related_posts'] = BlogPost.objects.live().exclude( pk=self.pk ).filter( categories__in=self.categories.all() ).distinct().order_by('-first_published_at')[:3] context['comment_form'] = CommentForm() context['current_tag'] = request.GET.get('tag') return context |
CharField | בלוקים גמישים (טקסט, תמונות, וידאו) | FieldPanel |
RichTextField | קישור לתמונה או קטגוריה | FieldPanel עם בורר |
StreamField | רשומות קשורות (תעריפים, מאפיינים) | StreamFieldPanel |
בפרויקט אמיתי לחנות מקוונת עם קטלוג של 10,000 מוצרים, פיתחנו 7 סוגי עמודים: CatalogPage, ProductPage, BrandPage, PromotionPage, ReviewPage, FAQPage ו-ContactPage. עבור ProductPage השתמשנו ב-StreamField עם בלוקי תיאור, טבלאות מפרט וקרוסלות תמונות. CatalogPage נבנה על RoutablePage עם סינון לפי קטגוריות, מחיר ותגיות. הפיתוח ארך 4 ימים, העורכים הוכשרו בשעה. זמן פרסום מוצר חדש ירד פי 10.
תהליך העבודה על מודלי Page מותאמים אישית
- ניתוח — אנו לומדים את מבנה התוכן של הלקוח, קובעים שדות ויחסים נחוצים.
- עיצוב — אנו מציירים דיאגרמת ER של המודלים, מתכננים את ממשק העורך (טאבים, פאנלים מובנים).
- יישום — אנו כותבים מודלים של Django, מגדירים
ForeignKey,FieldPanelו-InlinePanel. - בדיקות — אנו בודקים גרסאות, טיוטות, זרימת פרסום וביצועים.
- פריסה — אנו משחררים לסביבת ייצור, מגדירים אינדוקס לחיפוש.
מה כלול בתוצאה
- קוד מקור של מודלי Page מותאמים אישית עם תיעוד בעברית.
- לוח ניהול עם טאבים, אימות שדות והשלמה אוטומטית.
- אינדוקס מוגדר לחיפוש באתר.
- תמיכה בגרסאות ו-Workflow לאישור פרסום.
- הכשרת עורכים (שעה מקוונת) והוראות כתובות.
לוח זמנים ואחריות
פיתוח של 5–7 מודלי Page עם RoutablePage ו-InlinePanel אורך 3–6 ימים. העלות מחושבת באופן אישי לפי מורכבות. אנו נותנים אחריות ל-6 חודשים על כל המודלים: כל תקלות מתוקנות בחינם. גישה זו חוסכת עד 40% מהתקציב בהשוואה ליצירת עמודים נפרדים.
נעריך את הפרויקט שלכם ביום אחד — פשוט צרו קשר. הניסיון שלנו: יותר מ-5 שנים עם Wagtail, יותר מ-30 פרויקטים מוצלחים. קבלו ייעוץ — נספר לכם כיצד מודלי Page מותאמים אישית יכולים לפתור את המשימה שלכם.







