تخفیف عضویت ویژه تا 6 آبان فعال شد

اتصال قالب HTML آماده به جنگو؛ از static تا template inheritance

امیرحسین بیگدلو 10 ساعت قبل
اتصال قالب HTML آماده به جنگو؛ از static تا template inheritance

خیلی از پروژه‌های جنگو اینطور شروع می‌شوند: بک‌اند آماده است، مدل‌ها و ویوها کار می‌کنند، و حالا یک قالب HTML آماده دارید که باید به پروژه وصل شود. فایل index.html را داخل پوشه templates کپی می‌کنید، سرور را اجرا می‌کنید و با یک صفحه‌ی سفید و بی‌استایل روبرو می‌شوید. نه CSS لود شده، نه تصاویر، نه فونت‌ها.

مشکل از قالب نیست. جنگو فایل‌های استاتیک را به روش خودش مدیریت می‌کند و تا وقتی این روش را نشناسید، هر قالب آماده‌ای همین رفتار را خواهد داشت. در این مقاله قدم به قدم یک قالب HTML آماده (با تمرکز روی قالب‌های راست‌چین فارسی) را به یک پروژه جنگو ۵ وصل می‌کنیم، آن را به base.html و بخش‌های قابل استفاده مجدد می‌شکنیم و در نهایت برای محیط production آماده‌اش می‌کنیم.

 

# قالب آماده معمولا چه شکلی است؟

بیشتر قالب‌های HTML آماده، چه رایگان و چه خریداری شده، ساختاری شبیه به این دارند:

my-template/
├── index.html
├── about.html
├── contact.html
└── assets/
├── css/
│ ├── bootstrap.rtl.min.css
│ └── style.css
├── js/
│ └── main.js
├── fonts/
│ └── vazirmatn/
└── images/

داخل فایل‌های HTML هم مسیرها به شکل نسبی نوشته شده‌اند:

<link rel="stylesheet" href="assets/css/style.css">
<img src="assets/images/logo.png" alt="لوگو">
<script src="assets/js/main.js"></script>

این مسیرها وقتی فایل را مستقیم در مرورگر باز می‌کنید درست کار می‌کنند، چون مرورگر آن‌ها را نسبت به محل فایل HTML پیدا می‌کند. اما در جنگو آدرس صفحه مثلا /about/ است و مرورگر دنبال /about/assets/css/style.css می‌گردد که وجود ندارد. کار ما این است که این مسیرها را به سیستم static جنگو بسپاریم.

قالب آماده قبل از اتصال به جنگو

صفحه اصلی قالب Techlab در جنگو قبل از اصلاح مسیرهای static؛ CSS، تصاویر و فونت‌ها لود نشده‌اند. قالب Techlab بعد از کپی مستقیم index.html در templates: همه درخواست‌های assets/ خطای 404 می‌گیرند.

 

# ساخت پروژه و ساختار پوشه ها

یک پروژه جنگو و یک اپ ساده بسازید:

python -m venv venv
source venv/bin/activate # در ویندوز: venv\Scripts\activate
pip install django
django-admin startproject config .
python manage.py startapp pages

اپ pages را به INSTALLED_APPS اضافه کنید. سپس دو پوشه در ریشه پروژه (کنار manage.py) بسازید:

project/
├── manage.py
├── config/
├── pages/
├── templates/
│ ├── base.html
│ ├── partials/
│ └── pages/
└── static/
├── css/
├── js/
├── fonts/
└── images/

محتویات پوشه assets قالب را مستقیما داخل static کپی کنید و فایل‌های HTML را فعلا کنار بگذارید. برای قالبی که بین چند اپ مشترک است، پوشه‌های سطح پروژه انتخاب بهتری هستند. اگر فایلی فقط مخصوص یک اپ است، می‌توانید آن را در pages/static/pages/ و pages/templates/pages/ قرار دهید. پوشه‌ی تکراری pages در این مسیرها اضافه نیست؛ جلوی تداخل اسم فایل‌ها بین اپ‌های مختلف را می‌گیرد.

 

# تنظیمات settings.py

حالا باید به جنگو بگوییم این پوشه‌ها کجا هستند:

# config/settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"

LANGUAGE_CODE = "fa-ir"
TIME_ZONE = "Asia/Tehran"

این سه تنظیم static معمولا با هم اشتباه گرفته می‌شوند:

✔️ STATIC_URL پیشوند آدرسی است که فایل‌ها در مرورگر با آن نمایش داده می‌شوند، مثلا /static/css/style.css.

✔️ STATICFILES_DIRS لیست پوشه‌هایی است که شما فایل‌های استاتیک را در آن‌ها نگه می‌دارید (علاوه بر پوشه static داخل هر اپ).

✔️ STATIC_ROOT پوشه‌ای است که دستور collectstatic همه فایل‌ها را در آن جمع می‌کند تا در production سرو شوند. هیچوقت دستی در آن فایل نگذارید.

دقت کنید که STATIC_ROOT نباید داخل STATICFILES_DIRS باشد. اگر هر دو را روی یک پوشه بگذارید، جنگو خطای ImproperlyConfigured می‌دهد.

 

# اصلاح مسیرها با تگ static

حالا index.html قالب را به templates/pages/home.html منتقل کنید. اولین خط فایل باید بارگذاری تگ static باشد و تمام مسیرهای assets باید با این تگ نوشته شوند:

{% load static %}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="{% static 'css/bootstrap.rtl.min.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<img src="{% static 'images/logo.png' %}" alt="لوگو">
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

مسیر داخل تگ static نسبت به ریشه پوشه static نوشته می‌شود، پس assets/ دیگر در آن نیست. چرا آدرس را مستقیم /static/css/style.css ننویسیم؟ چون اگر روزی STATIC_URL را به یک CDN تغییر دهید، یا از ManifestStaticFilesStorage استفاده کنید که اسم فایل‌ها را hash دار می‌کند، فقط تگ static آدرس درست را تولید می‌کند.

همان صفحه بعد از اجرای اسکریپت پایین و اصلاح url() های داخل style: بدون هیچ تغییری در فایل‌های CSS.

همان صفحه بعد از اصلاح مسیرها با تگ static؛ استایل، تصاویر و فونت فارسی درست نمایش داده می‌شوند.

در قالب‌هایی با ده‌ها صفحه، اصلاح دستی مسیرها خسته کننده است. یک اسکریپت کوچک پایتون این کار را برایتان انجام می‌دهد:

# fix_paths.py
import re
import sys
from pathlib import Path

PATTERN = re.compile(r'(src|href)="(?:\./)?assets/([^"]+)"')

for file in Path(sys.argv[1]).glob("*.html"):
    text = file.read_text(encoding="utf-8")
    new_text = PATTERN.sub(lambda m: f'{m[1]}="{{% static \'{m[2]}\' %}}"', text)
    if "{% load static %}" not in new_text:
    new_text = "{% load static %}\n" + new_text
    file.write_text(new_text, encoding="utf-8")
    print(f"fixed: {file.name}")

>>> python fix_paths.py templates/pages

قبل از اجرا از فایل‌ها نسخه پشتیبان بگیرید و بعد از اجرا خروجی را چک کنید، به خصوص مسیرهایی که داخل data-src یا style=“background-image: url(…)” نوشته شده‌اند و این الگو آن‌ها را نمی‌گیرد.

 

+ به فایل های css دست نزنید

یک سوال رایج این است که آدرس‌های داخل CSS، مثل فونت‌ها و تصاویر پس زمینه، را هم باید با تگ static بنویسیم یا نه. جواب منفی است. تگ static فقط در تمپلیت‌های جنگو کار می‌کند و فایل CSS تمپلیت نیست. خبر خوب این است که لازم هم نیست:

/* static/css/style.css */
@font-face {
font-family: "Vazirmatn";
src: url("../fonts/vazirmatn/Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}

مسیرهای داخل CSS نسبت به خود فایل CSS حساب می‌شوند. تا وقتی ساختار پوشه‌های css و fonts را همانطور که در قالب بوده نگه دارید، همه چیز درست کار می‌کند. پس هنگام کپی کردن assets، ساختار پوشه‌ها را به هم نریزید.

 

# نکات مخصوص قالب های راستچین

 

اگر با قالب فارسی کار می‌کنید، چند مورد را چک کنید:

✔️ تگ html باید lang=“fa” و dir=“rtl” داشته باشد. بدون dir، جهت متن و چیدمان فلکس‌ها به هم می‌ریزد.

✔️ اگر قالب از بوت‌استرپ ۵ استفاده می‌کند، مطمئن شوید نسخه bootstrap.rtl.min.css لود شده و نه نسخه معمولی. بوت‌استرپ ۵ نسخه RTL رسمی دارد و کلاس‌هایی مثل ms-* و me-* در آن به صورت خودکار برعکس می‌شوند.

✔️ فونت فارسی را به صورت محلی در static/fonts نگه دارید. وابستگی به CDN خارجی برای فونت، در شرایطی که دسترسی به سرویس‌های خارجی محدود است، باعث می‌شود سایت با فونت پیشفرض سیستم نمایش داده شود.

✔️ اعداد و تاریخ‌هایی که از دیتابیس می‌آیند انگلیسی هستند. برای تاریخ شمسی از کتابخانه‌ای مثل jdatetime یا django-jalali استفاده کنید و برای تبدیل اعداد یک template filter ساده بنویسید.

اگر هنوز قالبی انتخاب نکرده‌اید، قالب‌هایی که از اول راست‌چین طراحی شده‌اند کار را خیلی ساده‌تر می‌کنند، چون برعکس کردن دستی یک قالب انگلیسی معمولا بیشتر از خود اتصال به جنگو وقت می‌گیرد. برای نمونه می‌توانید از قالب‌های HTML راست‌چین آماده با فونت فارسی محلی استفاده کنید که با همین روش این مقاله به جنگو وصل می‌شوند. تصاویر این مقاله هم با یکی از همین قالب‌ها (قالب Techlab) و یک پروژه واقعی جنگو ۵ گرفته شده‌اند.

 

# ساخت base.html با template inheritance

حالا home.html کار می‌کند، اما about.html و contact.html هم همان head، هدر و فوتر را دارند. اگر فردا یک لینک به منو اضافه شود، باید همه فایل‌ها را تغییر دهید. اینجا template inheritance به کمک می‌آید.

بخش‌های مشترک را به templates/base.html منتقل کنید و جاهایی را که در هر صفحه فرق می‌کنند با block مشخص کنید:

{% load static %}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}سایت من{% endblock %}</title>
<meta name="description" content="{% block description %}{% endblock %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.rtl.min.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
{% include "partials/header.html" %}

<main>
{% block content %}{% endblock %}
</main>

{% include "partials/footer.html" %}

<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

حالا home.html فقط محتوای خودش را دارد:

{% extends "base.html" %}
{% load static %}

{% block title %}صفحه اصلی | {{ block.super }}{% endblock %}

{% block content %}
<section class="hero">
<img src="{% static 'images/hero.jpg' %}" alt="">
<h1>به سایت ما خوش آمدید</h1>
</section>
{% endblock %}

{% block extra_js %}
<script src="{% static 'js/slider.js' %}"></script>
{% endblock %}

سه نکته مهم در این کد وجود دارد:

✔️ تگ extends باید اولین تگ تمپلیت باشد. هر چیزی قبل از آن باعث خطا یا رفتار غیرمنتظره می‌شود.

✔️ تگ load در تمپلیت والد به فرزند به ارث نمی‌رسد. اگر در home.html از static استفاده می‌کنید، باید دوباره load static بنویسید. فراموش کردن این خط یکی از رایج‌ترین خطاهاست.

✔️ متغیر block.super محتوای همان بلاک در والد را برمی‌گرداند. در مثال بالا عنوان صفحه می‌شود «صفحه اصلی | سایت من».

بلاک‌های extra_css و extra_js به شما اجازه می‌دهند فایل‌هایی که فقط در یک صفحه لازم هستند، مثل اسکریپت اسلایدر، را فقط در همان صفحه لود کنید و بقیه صفحات را سنگین نکنید.

 

# جدا کردن بخش های آماده با include

هدر و فوتر را از قالب جدا کنید و در templates/partials/ قرار دهید. در هدر به جای لینک‌های ثابت مثل about.html از تگ url استفاده کنید:

{# templates/partials/header.html #}
{% load static %}
<header class="site-header">
<a href="{% url 'pages:home' %}">
<img src="{% static 'images/logo.png' %}" alt="لوگو">
</a>
<nav>
{% with current=request.resolver_match.url_name %}
<a href="{% url 'pages:home' %}" class="{% if current == 'home' %}active{% endif %}">خانه</a>
<a href="{% url 'pages:about' %}" class="{% if current == 'about' %}active{% endif %}">درباره ما</a>
<a href="{% url 'pages:contact' %}" class="{% if current == 'contact' %}active{% endif %}">تماس</a>
{% endwith %}
</nav>
</header>

با request.resolver_match.url_name لینک صفحه فعلی کلاس active می‌گیرد، کاری که در قالب HTML خام باید در هر فایل دستی انجام می‌شد. برای کار کردن این کد، context processor مربوط به request باید فعال باشد که در تنظیمات پیشفرض جنگو فعال است.

برای بخش‌های تکراری مثل کارت محصول یا کارت مقاله هم می‌توانید include را با متغیر استفاده کنید:

{% for post in posts %}
    {% include "partials/post_card.html" with post=post only %}
{% endfor %}

کلمه only باعث می‌شود تمپلیت include شده فقط به متغیرهایی که صریحا فرستاده‌اید دسترسی داشته باشد. این کار وابستگی‌های پنهان را از بین می‌برد و بخش آماده را واقعا قابل استفاده مجدد می‌کند.

 

# ویوها و url ها

حالا صفحات را به ویو و URL وصل می‌کنیم. برای صفحات ساده TemplateView کافی است:

# pages/urls.py
from django.urls import path
from django.views.generic import TemplateView

app_name = "pages"

urlpatterns = [
    path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
    path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"),
    path("contact/", TemplateView.as_view(template_name="pages/contact.html"), name="contact"),
]

# config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("pages.urls")),
]

وقتی محتوا باید از دیتابیس بیاید، به جای متن ثابت قالب از context استفاده کنید:

# pages/views.py
from django.shortcuts import render
from blog.models import Post

def home(request):
    posts = Post.objects.filter(is_published=True).order_by("-created")[:6]
    return render(request, "pages/home.html", {"posts": posts})

روش کار همین است: هر جایی از قالب که چند بار تکرار شده، یک حلقه for و یک بخش آماده می‌شود و هر متن ثابتی که باید قابل تغییر باشد، یک متغیر.

 

# آماده سازی برای production

در حالت DEBUG = True، دستور runserver فایل‌های استاتیک را به صورت خودکار سرو می‌کند. به محض اینکه DEBUG را False کنید، CSS ها دوباره ناپدید می‌شوند و این طبیعی است. جنگو برای سرو فایل استاتیک در production طراحی نشده و این کار باید به وب سرور یا کتابخانه‌ای مثل whitenoise سپرده شود.

ابتدا فایل‌ها را جمع آوری کنید:

$ python manage.py collectstatic

اگر از Nginx استفاده می‌کنید، کافی است آدرس /static/ را به پوشه STATIC_ROOT بدهید:

location /static/ {
alias /path/to/project/staticfiles/;
expires 30d;
}

اگر نمی‌خواهید Nginx را درگیر کنید (مثلا روی پلتفرم‌های PaaS یا داخل Docker)، whitenoise ساده‌ترین راه است:

$ pip install whitenoise
# config/settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... بقیه middleware ها
]

STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

میدل‌ویر whitenoise باید دقیقا بعد از SecurityMiddleware قرار بگیرد. تنظیم STORAGES از جنگو ۴.۲ به بعد جایگزین STATICFILES_STORAGE شده است. این storage دو کار انجام می‌دهد: فایل‌ها را فشرده می‌کند و به اسم آن‌ها یک hash اضافه می‌کند (مثلا style.4f2a1c.css) تا بتوانید کش طولانی مدت بگذارید و بعد از هر تغییر، مرورگر کاربران نسخه جدید را بگیرد.

 

# خطاهای رایج و حل آنها

 

خطای Invalid block tag ‘static’

خط load static را در بالای همان تمپلیت ننوشته‌اید. یادتان باشد load به ارث نمی‌رسد و هر فایل include شده هم باید خودش آن را داشته باشد.

 

CSS در محیط توسعه لود نمی‌شود (خطای 404)

مسیر داخل تگ static را با ساختار پوشه مقایسه کنید. دستور زیر نشان می‌دهد جنگو فایل را کجا پیدا می‌کند:

$ python manage.py findstatic css/style.css

اگر چیزی پیدا نشد، STATICFILES_DIRS یا مسیر فایل اشتباه است. همچنین مطمئن شوید django.contrib.staticfiles در INSTALLED_APPS هست.

ساختار پوشه‌های پروژه جنگو و خروجی دستور findstatic

ساختار پروژه نمونه و خروجی findstatic که نشان می‌دهد جنگو style.css را از پوشه static پیدا کرده است.

 

خطای ValueError: Missing staticfiles manifest entry

با storage مانیفست دار، قبل از اجرا باید collectstatic را اجرا کرده باشید. اگر اجرا کرده‌اید، احتمالا اسم فایل در تمپلیت با فایل واقعی فرق دارد.

 

خطای The file could not be found هنگام collectstatic

در قالب‌های آماده رایج است. یک فایل CSS به تصویر یا فونتی با url() اشاره کرده که در پکیج قالب وجود ندارد، یا فایل .map کتابخانه‌ای که حذف شده. storage مانیفست دار همه url() ها را بررسی می‌کند و با یک فایل گمشده متوقف می‌شود. یا فایل را اضافه کنید یا ارجاع را از CSS حذف کنید. خط sourceMappingURL انتهای فایل‌های min شده هم معمولا مقصر است.

 

فونت فارسی نمایش داده نمی‌شود

در تب Network مرورگر وضعیت فایل woff2 را چک کنید. اگر 404 است، مسیر نسبی داخل @font-face با ساختار پوشه‌ها نمی‌خواند. اگر فایل لود شده اما فونت اعمال نشده، font-family در CSS با اسم تعریف شده در @font-face یکی نیست.

 

تغییرات CSS دیده نمی‌شود

مرورگر نسخه قبلی را کش کرده است. در محیط توسعه با Ctrl+Shift+R صفحه را رفرش کنید. در production با storage مانیفست دار این مشکل خودبخود حل می‌شود.

 

# نتیجه گیری

اتصال قالب HTML آماده به جنگو سه مرحله اصلی دارد: فایل‌های assets را با حفظ ساختار پوشه‌ها به static منتقل کنید و مسیرها را با تگ static بنویسید، بخش‌های مشترک را در base.html و partial ها قرار دهید و صفحات را با extends و block بسازید، و در نهایت با collectstatic و whitenoise یا Nginx پروژه را برای production آماده کنید.

اگر این مراحل را یک بار با دقت انجام دهید، قالب شما فقط یک پوسته ثابت نیست. به ساختاری تبدیل می‌شود که اضافه کردن صفحه جدید به آن چند خط کد بیشتر نیست، و تغییر یک لینک در منو فقط در یک فایل انجام می‌شود.

نویسنده: تیم RTLHTML، طراح قالب‌های HTML راست‌چین برای وب فارسی (rtlhtml.ir)

مطالب مشابه



مونگارد