اگر امروز یک الگوی path("<str:slug>/", ...) را در انتهای URLconf خود داشته باشید ولی آن را قبل از include اپ‌های دیگر قرار دهید، احتمالاً تمام مسیرهای /panel/، /api/ و /admin/ شما به‌جای اینکه به اپ‌های مربوطه برسند، به‌عنوان slug تفسیر می‌شوند و کاربر با خطای 404 مواجه می‌شود؛ چون ترتیب URLها در جنگو، تقدیر پروژه‌ی شما را تعیین می‌کند.

چرا ترتیب URLها این‌قدر مهم است؟

در یکی از پروژه‌های فروشگاهی که چند سال پیش روی آن کار می‌کردم، تیم توسعه یک اپ آماری جدید به سایت اضافه کرد و URL آن را زیر /panel/stat/ قرار داد. چند روز بعد از استقرار، تیم پشتیبانی با شکایت مواجه شد: «وقتی روی لینک آمار کلیک می‌کنیم، صفحه‌ی 404 می‌گیریم.» بررسی دقیق نشان داد که URLconf پروژه، اپ آماری را بعد از یک الگوی slug قرار داده. یعنی /panel/stat/ به‌عنوان یک slug برای پست‌ها تفسیر می‌شد و چون پستی با آن slug وجود نداشت، 404 می‌داد.

این تجربه نشان می‌دهد که ترتیب URLها، یک مسئله‌ی ظریف است که در پروژه‌های بزرگ به یک بحران تبدیل می‌شود. سه دلیل اصلی:

دلیل اول، URLResolver خطی عمل می‌کند. جنگو URLها را به‌ترتیب بررسی می‌کند و اولین الگویی که مطابقت کند، انتخاب می‌شود. یعنی ترتیب، بخشی از منطق برنامه است.

دلیل دوم، الگوی catch-all خیلی راحت اضافه می‌شود. یک خط path("<str:slug>/", ...) کافی است تا تمام مسیرهای یک‌بخشی به آن هدایت شوند.

دلیل سوم، خطا دیر کشف می‌شود. اگر /panel/ شما بعد از catch-all قرار گرفته باشد، تا زمانی که کسی به آن URL مراجعه نکند، متوجه خطا نمی‌شوید.

در URLconf جنگو، ترتیب یک قرارداد است، نه یک سلیقه. اگر ترتیب را جدی نگیرید، رفتار پروژه‌ی شما غیرقابل پیش‌بینی می‌شود.

این اهمیت، در ساختار پروژه‌های آماری مثل اپ جنگو برای ردیابی بازدیدکننده به‌طور مستقیم دیده می‌شود. اگر لایه‌ی URL به‌درستی طراحی نشود، تمام مسیرهای اپ‌های جانبی به یک بی‌نظمی تبدیل می‌شوند. برای درک عمیق‌تر این موضوع، پیشنهاد می‌کنم ابتدا ساخت پنل ادمین سفارشی با کارت‌های quick view را مطالعه کنید، چون URLهای پنل، بخشی از همین معماری است.

URLResolver در جنگو چطور کار می‌کند؟

برای درک درست مسئله، باید بفهمید که جنگو چطور URLها را resolve می‌کند. فرآیند به این شکل است:

مرحله‌ی اول، دریافت درخواست. وقتی یک درخواست HTTP دریافت می‌شود، جنگو مسیر درخواست را از request.path_info استخراج می‌کند.

مرحله‌ی دوم، پیمایش URLconf. جنگو از ROOT_URLCONF شروع می‌کند و به‌ترتیب urlpatterns را بررسی می‌کند.

مرحله‌ی سوم، تطبیق. برای هر الگو، جنگو چک می‌کند که آیا مسیر درخواست با آن مطابقت دارد یا نه. اولین الگویی که مطابقت کند، برنده است.

مرحله‌ی چهارم، بازگشت view. اگر الگو شامل یک include باشد، جنگو به URLconf آن اپ می‌رود و فرآیند را تکرار می‌کند. اگر یک view باشد، همان view بازگردانده می‌شود.

نکته‌ی حیاتی: این فرآیند کاملاً خطی است. جنگو الگوها را به‌ترتیب بررسی می‌کند و اولین تطبیق را انتخاب می‌کند. اگر الگوی catch-all قبل از یک include دیگر قرار بگیرد، آن include هرگز اجرا نمی‌شود.

# نمونه‌ی مشکل‌دار

urlpatterns = [
    path("", HomeView.as_view(), name="home"),
    path("<str:slug>/", PostDetailView.as_view(), name="post"),  # ← مشکل اینجاست
    path("panel/stat/", analytics_views.quick_view, name="stat"),
]

در این کد، URL /panel/stat/ به الگوی <str:slug>/ می‌خورد، چون آن الگو قبل از panel/stat/ قرار گرفته. نتیجه: viewی که صدا زده می‌شود PostDetailView است، نه quick_view.

مفهوم URL Routing در ویکی‌پدیا توضیح داده شده است، ولی رفتار دقیق جنگو در این زمینه، به الگوی regex یا converter وابسته است.

الگوی catch-all؛ شمشیر دولبه

الگوی catch-all یک الگوی عمومی است که هر مسیری را می‌گیرد. ساده‌ترین نوع آن، path("<str:slug>/", ...) است. انواع پیشرفته‌تر شامل re_path(r"^.*/$", ...) یا path("<path:rest>", ...) هستند.

سه دلیل که چرا الگوی catch-all استفاده می‌شود:

دلیل اول، الگوی URL ساده برای کاربر. به‌جای /post/my-article/، از /my-article/ استفاده می‌شود.

دلیل دوم، سازگاری با ساختار سایت‌های دیگر. بسیاری از سایت‌های وردپرسی از این الگو استفاده می‌کنند.

دلیل سوم، SEO بهتر. URLهای کوتاه‌تر، در نتایج جستجو بهتر عمل می‌کنند.

ولی این الگو، سه خطر جدی دارد.

خطر اول، بلعیدن مسیرهای دیگر. اگر بعد از آن مسیر دیگری اضافه کنید، آن مسیر هرگز اجرا نمی‌شود.

خطر دوم، رفتار غیرقابل پیش‌بینی در افزودن اپ جدید. هر اپی که بعداً اضافه کنید، باید URL آن را قبل از catch-all قرار دهید، وگرنه کار نمی‌کند.

خطر سوم، پیچیدگی در دیباگ. وقتی یک URL اشتباه resolve می‌شود، باید بدانید که کدام الگو آن را گرفته.

الگوپوششخطر تداخلکاربرد
<str:slug>/فقط یک بخشمتوسطپست‌های سطح اول
<slug:slug>/فقط slug معتبرکمپست‌های سطح اول
<path:rest>کل مسیرزیادSPA fallback
re_path(r"^.*$")همه چیزبسیار زیاداشتباه؛ توصیه نمی‌شود

توصیه‌ی من: از <slug:slug>/ استفاده کنید، چون slug فقط شامل حروف و اعداد و خط تیره است و مسیرهایی مثل panel یا admin می‌توانند مشکل‌ساز شوند، ولی slug با space یا اسلش مشکل ندارد.

سناریوی واقعی؛ وقتی /panel/ به 404 می‌خورد

بیایید یک سناریوی واقعی را بررسی کنیم. فرض کنید URLconf شما این‌طور است:

from django.contrib import admin
from django.urls import path
from contents import views as contents_views
from analytics import views as analytics_views


urlpatterns = [
    path("robots.txt", ...),
    path("site.webmanifest", ...),
    path("panel/", admin.site.urls),
    path("news/", ...),
    path("", views.HomeView.as_view(), name="home"),
    path("blog/", ..., name="archive"),
    path("post/category/<str:slug>/", ..., name="category"),
    path("category/<str:slug>/", ..., name="category"),
    path("post/tag/<str:slug>/", ..., name="tag"),
    path("tag/<str:slug>/", ..., name="tag"),
    path("about/", ..., name="about"),
    path("newsletter/subscribe/", ..., name="newsletter"),
    path("post/<str:slug>/", ..., name="post"),
    path("<str:slug>/", ..., name="post"),  # ← مشکل اصلی
]

حالا تصور کنید که یک اپ آماری با URL زیر اضافه می‌کنید:

urlpatterns = [
    # ...
    path("", include("analytics.urls")),  # analytics.urls شامل "panel/stat/"
]

اما این include را بعد از path("<str:slug>/", ...) قرار می‌دهید. نتیجه: URL /panel/stat/ به الگوی catch-all می‌خورد، چون آن الگو قبل از include قرار گرفته. بنابراین، post_detail با slug="panel" اجرا می‌شود و چون پستی با آن slug وجود ندارد، 404 برمی‌گرداند.

خطای دقیقی که کاربر می‌بیند:

Page not found (404)
No Post matches the given query.
Request URL: https://example.com/panel/stat/
Raised by: contents.views.post_detail
The current path, panel/stat/, matched the last one.

این پیام به شما می‌گوید که مسیر فعلی به «آخرین» الگو خورده است. یعنی همه‌ی الگوهای قبل از catch-all تطبیق نکردند.

خطای 404 در جنگو، همیشه یعنی «URL پیدا نشد». گاهی یعنی «URL پیدا شد ولی محتوایی با آن پارامترها وجود ندارد». تفکیک این دو، اولین قدم دیباگ است.

راه‌حل اول؛ ترتیب درست URLها

ساده‌ترین راه‌حل، ترتیب درست URLها است: الگوهای خاص قبل از الگوهای عمومی قرار بگیرند.

urlpatterns = [
    # ۱. مسیرهای سیستمی
    path("robots.txt", ...),
    path("site.webmanifest", ...),
    path("admin/", admin.site.urls),

    # ۲. مسیرهای اپ‌ها
    path("", include("analytics.urls")),
    path("panel/", include("panel.urls")),
    path("news/", include("news.urls")),
    path("blog/", include("blog.urls")),

    # ۳. مسیرهای خاص
    path("post/category/<str:slug>/", ..., name="category"),
    path("post/tag/<str:slug>/", ..., name="tag"),
    path("post/<str:slug>/", ..., name="post"),
    path("about/", ..., name="about"),
    path("newsletter/subscribe/", ..., name="newsletter"),

    # ۴. مسیرهای عمومی
    path("", HomeView.as_view(), name="home"),
    path("blog/", ..., name="archive"),

    # ۵. الگوی catch-all در آخر
    path("<slug:slug>/", PostDetailView.as_view(), name="post"),
]

این ترتیب، از قانون ساده‌ای پیروی می‌کند: از خاص به عام. مسیرهای دقیق‌تر (مثل panel/stat/) قبل از مسیرهای عمومی‌تر (مثل <slug:slug>/) قرار می‌گیرند.

نکته‌ی مهم: این ترتیب باید در کل پروژه رعایت شود، نه فقط در یک اپ. اگر با الگوی اپ جنگو برای ردیابی بازدیدکننده کار کرده باشید، می‌دانید که در پروژه‌های چنداپی، ترتیب URLها یک تصمیم معماری است، نه یک جزئیات.

راه‌حل دوم؛ پیشوندگذاری روی slug

اگر می‌خواهید مسیرهای پست‌ها ساده بمانند ولی تداخلی هم نداشته باشند، می‌توانید یک پیشوند اضافه کنید:

urlpatterns = [
    # ...
    path("p/<slug:slug>/", PostDetailView.as_view(), name="post"),
]

این تغییر، URL پست‌ها را از /my-article/ به /p/my-article/ تغییر می‌دهد. این رویکرد، سه مزیت دارد:

مزیت اول، حذف کامل تداخل. URL /panel/stat/ دیگر به الگوی catch-all نمی‌خورد، چون catch-all با p/ شروع می‌شود.

مزیت دوم، امکان اضافه‌کردن اپ‌های جدید بدون تغییر ترتیب. هر اپ جدیدی که اضافه کنید، URL آن با catch-all تداخل ندارد.

مزیت سوم، ساختار روشن. کاربر با دیدن /p/ می‌فهمد که این یک صفحه‌ی پست است.

نقطه‌ی ضعف این رویکرد، URL طولانی‌تر است. برای پروژه‌های SEO-محور، این موضوع ممکن است مهم باشد. ولی در عمل، تفاوت /my-article/ و /p/my-article/ ناچیز است.

راه‌حل سوم؛ الگوهای محدودکننده با re_path

رویکرد سوم، استفاده از re_path با یک الگوی محدودکننده است. به‌جای اینکه هر رشته‌ای به‌عنوان slug پذیرفته شود، فقط الگوهای خاص پذیرفته می‌شوند.

from django.urls import re_path


urlpatterns = [
    # ...
    re_path(
        r"^(?P<slug>[a-z0-9-]+)/$",
        PostDetailView.as_view(),
        name="post",
    ),
]

این regex، فقط slugهایی را می‌پذیرد که شامل حروف کوچک، اعداد و خط تیره باشند. بنابراین، مسیرهایی مثل /panel/stat/ که شامل اسلش هستند، دیگر به این الگو نمی‌خورند.

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

مزیت اول، کنترل دقیق. شما دقیقاً تعیین می‌کنید که چه الگویی به‌عنوان slug پذیرفته شود.

مزیت دوم، جلوگیری از خطاهای غیرمنتظره. مسیرهایی مثل /api/v1/users/ که شامل اسلش هستند، به catch-all نمی‌خورند.

مزیت سوم، سازگاری با SEO. می‌توانید regex را طوری تنظیم کنید که فقط slugهای معتبر را بپذیرد.

نقطه‌ی ضعف، پیچیدگی regex است. اگر به regex مسلط نیستید، این رویکرد می‌تواند خودش خطا ایجاد کند. اگر با الگوی تشخیص بات از کاربر انسانی کار کرده باشید، می‌دانید که regex ابزار قدرتمندی است که نیازمند دقت بالا است.

راه‌حل چهارم؛ استفاده از converter سفارشی

جنگو از سال ۲.۰ به بعد، converters را معرفی کرده است. یک converter، یک کلاس است که منطق تطبیق یک الگوی URL را کپسوله می‌کند.

# config/converters.py

class ArticleSlugConverter:
    regex = "[a-z0-9-]{3,60}"

    def to_python(self, value):
        return value

    def to_url(self, value):
        return value

و در URLconf:

from django.urls import path, register_converter
from config.converters import ArticleSlugConverter

register_converter(ArticleSlugConverter, "article_slug")


urlpatterns = [
    # ...
    path(
        "<article_slug:slug>/",
        PostDetailView.as_view(),
        name="post",
    ),
]

این رویکرد، چهار مزیت دارد.

مزیت اول، بازاستفاده. converter را می‌توانید در چند URLconf مختلف استفاده کنید.

مزیت دوم، تست‌پذیری. regex درون converter قابل تست مستقل است.

مزیت سوم، خوانایی. URLconf شما معنادارتر می‌شود.

مزیت چهارم، انعطاف. می‌توانید converter را با منطق پیچیده‌تر ترکیب کنید.

نکته‌ی مهم: converter سفارشی می‌تواند شامل توابع to_python و to_url باشد. اگر منطق slug شما شامل تبدیل خاصی است (مثل کوتاه کردن یا نرمال‌سازی)، این توابع می‌توانند آن را انجام دهند.

APPEND_SLASH و تعاملش با slug

APPEND_SLASH یک تنظیم در جنگو است که به‌طور پیش‌فرض True است. وقتی این تنظیم فعال باشد، اگر کاربری URL بدون اسلش انتهایی را وارد کند، جنگو یک redirect 301 به URL با اسلش انجام می‌دهد.

این رفتار، در تعامل با catch-all می‌تواند مشکل‌ساز شود.

# APPEND_SLASH = True (پیش‌فرض)
# کاربر /about را می‌زند → redirect به /about/
# کاربر /panel/stat را می‌زند → redirect به /panel/stat/

این رفتار به‌ظاهر بی‌ضرر است، ولی دو مشکل ایجاد می‌کند.

مشکل اول، redirect بی‌فایده. اگر URL شما واقعاً بدون اسلش درست است (مثل APIها)، این redirect بی‌فایده است.

مشکل دوم، تعامل با catch-all. اگر catch-all شما اسلش انتهایی را می‌پذیرد، redirect ممکن است به آن بخورد.

راه‌حل: در APIها و URLهای خاص، APPEND_SLASH = False بگذارید یا از re_path با اسلش صریح استفاده کنید.

# در URLconf APIها
urlpatterns = [
    path("api/v1/users/", ...),  # همیشه با اسلش
    path("api/v1/users", ...),   # بدون اسلش، بدون redirect
]

مفهوم URL Normalization در ویکی‌پدیا توضیح داده شده است، ولی رفتار دقیق APPEND_SLASH در جنگو، به تنظیمات و الگوهای URL وابسته است.

namespace و اپ‌های چندگانه

در پروژه‌های چنداپی، استفاده از namespace یکی از بهترین راهکارها برای جلوگیری از تداخل URL است.

# analytics/urls.py
app_name = "analytics"

urlpatterns = [
    path("panel/stat/", views.quick_view, name="quick_view"),
    # ...
]


# config/urls.py
urlpatterns = [
    path("", include("analytics.urls")),
    path("shop/", include("shop.urls")),
    path("blog/", include("blog.urls")),
]

نکته‌ی مهم: namespace به شما اجازه می‌دهد که در تمپلیت‌ها و ویوها به URLها با نام یکتا ارجاع دهید. مثلاً {% url "analytics:quick_view" %} همیشه به همان URL مشخص اشاره می‌کند.

این کار، در پروژه‌های بزرگ بسیار مفید است. اگر با الگوی انتقال منطق از services.py به templatetags کار کرده باشید، می‌دانید که namespace، بخشی از انضباط URL است.

تداخل با APIها و اپ‌های ثالث

یکی از بزرگ‌ترین چالش‌های URL در جنگو، تعامل با APIها و اپ‌های ثالث است. این اپ‌ها اغلب URLهای خاص خودشان را دارند و اگر ترتیب درست نباشد، به catch-all می‌خورند.

چند اپ ثالث که URLهایشان ممکن است با catch-all تداخل کند:

اپ اول، Django REST Framework. اگر از DRF استفاده می‌کنید، URLهای API ممکن است با catch-all تداخل کنند.

اپ دوم، Django admin. URLهای /admin/ باید قبل از catch-all قرار بگیرند.

اپ سوم، django-allauth. URLهای احراز هویت مثل /accounts/login/.

اپ چهارم، django-debug-toolbar. اگر فعال باشد، URLهای خاصی مثل /__debug__/ اضافه می‌کند.

راه‌حل، همیشه یکسان است: اپ‌های ثالث را قبل از catch-all قرار دهید و مطمئن شوید که URLهایشان با / انتهایی مطابقت دارد.

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("api.urls")),
    path("accounts/", include("allauth.urls")),

    # ...

    path("<slug:slug>/", PostDetailView.as_view(), name="post"),
]

دیباگ خطاهای 404؛ از کجا شروع کنیم؟

وقتی با خطای 404 مواجه می‌شوید، باید بتوانید تشخیص دهید که دقیقاً کدام الگو مسئول است. چند تکنیک برای دیباگ:

تکنیک اول، خواندن پیام خطا. پیام خطای جنگو به شما می‌گوید که مسیر فعلی به کدام الگو خورده است.

The current path, panel/stat/, matched the last one.

عبارت «the last one» به شما می‌گوید که این URL به آخرین الگو در URLconf خورده است.

تکنیک دوم، نمایش URLconf. با کامند show_urls می‌توانید کل URLconf پروژه را ببینید.

python manage.py show_urls

این کامند، نیاز به نصب django-extensions دارد.

تکنیک سوم، shell تعاملی. با django.urls.resolve می‌توانید ببینید که یک URL خاص به کدام view خورده است.

from django.urls import resolve

match = resolve("/panel/stat/")
print(match.func)
print(match.kwargs)

اگر match.func نشان دهد که view اشتباهی انتخاب شده، مشکل از ترتیب URLها است.

تکنیک چهارم، تست‌های خودکار. برای هر URL مهم، یک تست بنویسید که بررسی کند به view درست resolve می‌شود.

from django.urls import resolve


def test_panel_stat_url_resolves():
    match = resolve("/panel/stat/")
    assert match.func.__name__ == "quick_view"

اگر با الگوی نوشتن Middleware سفارشی در جنگو کار کرده باشید، می‌دانید که تست‌های ساده‌ای مثل این، در طول زمان از بروز مشکلات جلوگیری می‌کنند.

معماری URL برای پروژه‌های بزرگ

در پروژه‌های بزرگ، معماری URL باید از چند اصل پیروی کند.

اصل اول، ترتیب از خاص به عام. URLهای دقیق‌تر باید قبل از URLهای عمومی‌تر قرار بگیرند.

اصل دوم، namespace برای هر اپ. هر اپ باید namespace اختصاصی خودش را داشته باشد.

اصل سوم، پیشوند مشخص برای هر اپ. URLهای هر اپ باید با یک پیشوند مشخص شروع شوند.

اصل چهارم، اجتناب از catch-all در root. اگر می‌خواهید catch-all داشته باشید، آن را در یک پیشوند خاص قرار دهید.

# ساختار پیشنهادی

urlpatterns = [
    # سیستم
    path("admin/", admin.site.urls),
    path("robots.txt", ...),
    path("sitemap.xml", ...),

    # APIها
    path("api/v1/", include("api.v1.urls")),
    path("api/v2/", include("api.v2.urls")),

    # اپ‌ها
    path("panel/", include("panel.urls")),
    path("shop/", include("shop.urls")),
    path("blog/", include("blog.urls")),
    path("analytics/", include("analytics.urls")),

    # صفحات اصلی
    path("", HomeView.as_view(), name="home"),
    path("about/", AboutView.as_view(), name="about"),
    path("contact/", ContactView.as_view(), name="contact"),

    # catch-all در آخر، با پیشوند
    path("p/<slug:slug>/", PostDetailView.as_view(), name="post"),
]

این ساختار، سه مزیت کلیدی دارد. اول، هر URL جای مشخص خودش را دارد. دوم، اضافه‌کردن URLهای جدید ساده است. سوم، دیباگ خطاها ساده‌تر است.

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

تست URLها و رفتار catch-all

تست URLها یکی از بخش‌های نادیده گرفته شده در اکثر پروژه‌ها است. سه سطح تست را در نظر بگیرید.

سطح اول، تست resolve. برای هر URL مهم، بررسی کنید که به view درست resolve می‌شود.

import pytest
from django.urls import resolve


@pytest.mark.parametrize("url,expected_view", [
    ("/panel/stat/", "quick_view"),
    ("/panel/stat/online/", "online"),
    ("/admin/", "index"),
    ("/api/v1/users/", "UserList"),
    ("/about/", "AboutView"),
    ("/my-article/", "PostDetailView"),
])
def test_urls_resolve_correctly(url, expected_view):
    match = resolve(url)
    assert match.func.__name__ == expected_view

سطح دوم، تست HTTP. با django.test.Client، درخواست واقعی بفرستید و بررسی کنید که پاسخ درست برمی‌گردد.

@pytest.mark.django_db
def test_panel_stat_returns_200(client, admin_user):
    client.force_login(admin_user)
    response = client.get("/panel/stat/")
    assert response.status_code == 200

سطح سوم، تست catch-all. بررسی کنید که catch-all فقط مسیرهایی که باید را می‌گیرد.

@pytest.mark.django_db
def test_catchall_does_not_break_panel(client, admin_user):
    client.force_login(admin_user)

    response = client.get("/panel/stat/")
    assert response.status_code == 200
    assert "آمار امروز" in response.content.decode()

این سه سطح تست، به شما اجازه می‌دهند که در طول زمان، تغییرات URLconf را با اطمینان اعمال کنید.

anti-patternهای رایج در طراحی URL

در بازبینی پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

۱. catch-all در ابتدای URLconf. اگر catch-all را قبل از اپ‌ها قرار دهید، تمام اپ‌ها غیرفعال می‌شوند.

۲. استفاده از re_path(r"^.*$", ...). این الگو هر مسیری را می‌گیرد و تقریباً همیشه اشتباه است.

۳. عدم استفاده از namespace. بدون namespace، URLها در تمپلیت‌ها و ویوها مبهم می‌شوند.

۴. پیشوندگذاری اشتباه روی slug. اگر پیشوند p/ داشته باشید ولی URLهای API هم با p/ شروع شوند، تداخل ایجاد می‌شود.

۵. عدم تست URLها. بدون تست، هر تغییر URLconf ممکن است بی‌سروصدا مشکل ایجاد کند.

۶. APPEND_SLASH ناهماهنگ. اگر APPEND_SLASH فعال باشد ولی URLها اسلش نداشته باشند، redirectهای بی‌فایده ایجاد می‌شود.

۷. عدم مدیریت تداخل با اپ‌های ثالث. اپ‌های ثالث URLهای خاص خودشان را دارند که باید قبل از catch-all قرار بگیرند.

۸. استفاده از slug به‌جای slug معتبر. <str:slug>/ هر رشته‌ای را می‌پذیرد، ولی <slug:slug>/ فقط slug معتبر.

۹. عدم بررسی خطاهای 404. اگر خطای 404 روی یک URL مهم دارید، باید بلافاصله برطرف شود. لاگ‌های 404 را رصد کنید.

۱۰. عدم استفاده از converter سفارشی. برای URLهای پیچیده، converter سفارشی انتخاب بهتری از regex است.

۱۱. قرار دادن API بعد از catch-all. اگر APIها بعد از catch-all باشند، هرگز اجرا نمی‌شوند.

۱۲. عدم مستندسازی URLconf. در پروژه‌های بزرگ، یک مستند ساده از URLها به تیم کمک می‌کند.

پرسش‌های پرتکرار درباره‌ی تداخل URL در جنگو

چطور بفهمم که یک URL به کدام view resolve می‌شود؟ با django.urls.resolve در shell یا با کامند show_urls.

آیا باید catch-all را کاملاً حذف کنم؟ نه، ولی باید در آخر URLconf و با پیشوند مشخص باشد.

چطور URLهای اپ‌های ثالث را از catch-all مستثنا کنم؟ با قرار دادن آن‌ها قبل از catch-all در URLconf.

آیا باید از <slug:slug> یا <str:slug> استفاده کنم؟ <slug:slug> برای پست‌ها مناسب‌تر است، چون فقط الگوهای معتبر را می‌پذیرد.

چطور با APPEND_SLASH مقابله کنم؟ در APIها، APPEND_SLASH = False بگذارید یا از re_path با اسلش صریح استفاده کنید.

آیا namespace روی عملکرد تأثیر دارد؟ نه، namespace فقط یک نام‌گذاری است. ولی برای سازماندهی، مفید است.

چطور با تداخل URLهای فارسی مقابله کنم؟ از URLهای انگلیسی استفاده کنید و فقط محتوای صفحه را فارسی کنید.

آیا باید در URLconf کامنت بگذارم؟ در پروژه‌های بزرگ، بله. یک کامنت کوتاه در هر بخش، دیباگ را ساده می‌کند.

چطور URLهای قدیمی را با redirect حفظ کنم؟ از RedirectView یا اپ django-redirects استفاده کنید.

آیا باید URLهای API را با prefix مشخص کنم؟ بله، /api/v1/ یکی از استانداردها است.

چطور URLهای بدون اسلش را پشتیبانی کنم؟ با path("my-page", ...) به‌جای path("my-page/", ...) یا با re_path و اسلش اختیاری.

آیا باید URLهای قدیمی را نگه دارم؟ بله، برای چند ماه. با redirect 301 به URL جدید.

چطور URLها را برای SEO بهینه کنم؟ با استفاده از کلمات کلیدی در URL، کوتاه بودن و ساختار سلسله‌مراتبی.

چطور با URLهای خیلی طولانی مقابله کنم؟ URLها را کوتاه نگه دارید. برای پارامترهای اضافی، از query string استفاده کنید.

آیا باید URLها را به حروف کوچک تبدیل کنم؟ بله، همیشه. URLها باید lowercase باشند.

نگاهی از منظر مهندس پلتفرم در مقیاس بزرگ

در مقیاس بزرگ، طراحی URL تبدیل به یک مسئله‌ی معماری می‌شود. سه مفهوم بنیادین را باید بازتعریف کنید.

مفهوم اول، URL به‌عنوان API. URLها، خودشان یک API هستند. هر تغییر در ساختار URL، یک breaking change محسوب می‌شود. باید نسخه‌بندی URL را جدی بگیرید. مفهوم API Versioning در ویکی‌پدیا توضیح داده شده است.

مفهوم دوم، routing توزیع‌شده. در مقیاس بزرگ، ممکن است URLها بین چند سرویس تقسیم شوند. یک API Gateway می‌تواند URLهای مختلف را به سرویس‌های مختلف هدایت کند. این معماری، در سیستم‌های میکروسرویس استاندارد است.

مفهوم سوم، observability. در مقیاس بزرگ، باید بدانید که هر URL چقدر ترافیک می‌گیرد. این داده، برای بهینه‌سازی و ظرفیت‌سنجی ضروری است. اگر با الگوی ساخت پنل ادمین سفارشی با کارت‌های quick view کار کرده باشید، می‌دانید که این نوع اندازه‌گیری، بخشی از انضباط پلتفرم است.

نکته‌ی آخر: در مقیاس بزرگ، URLconf یک سند زنده است که باید به‌طور مداوم بازبینی و مستندسازی شود. اگر URLconf شما بیش از ۱۰۰ خط است، باید آن را به چند فایل URLconf در اپ‌های مختلف تقسیم کنید.

پرسشی که در پایان باید پاسخ دهید

قبل از اینکه URLconf پروژه‌ی خود را نهایی کنید، یک پرسش را از خودتان بپرسید: «اگر امروز یک اپ جدید به پروژه اضافه کنم، چقدر طول می‌کشد تا URL آن را بدون تداخل اضافه کنم؟» اگر پاسخ شما «چند دقیقه» است، یعنی URLconf شما به‌درستی طراحی شده است. اگر پاسخ شما «چند ساعت» است، یعنی به یک بازنگری در ساختار URL نیاز دارید.

طراحی URL، در نهایت یک تصمیم معماری است که به نگهداشت‌پذیری و مقیاس‌پذیری پروژه‌ی شما گره خورده است. اگر این تجربه را در پروژه‌ی خودتان داشته‌اید — مثلاً جایی که یک URL به‌اشتباه resolve شده یا جایی که اضافه‌کردن یک اپ جدید، کل URLconf را به هم ریخته — برایم جالب است بدانید. مخصوصاً اگر راه‌حل خاصی برای یک سناریوی خاص پیدا کرده‌اید، چون همان راه‌حل‌ها می‌توانند به خواننده‌ی بعدی کمک کنند. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.