DEV Community

Daniel Feldroy
Daniel Feldroy

Posted on • Updated on

Django Slug + ID URLs: Copying Dev.to's URL pattern

One of the things I like about dev.to is their URL design. It combines a slug with a hashed value representing an internal representative of some kind of index value to ensure uniqueness. You can see an example of it in my post "Autodocumenting Makefiles" featured in the URL "https://dev.to/feldroy/autodocumenting-makefiles-175b".

Let's break apart that URL:

  1. feldroy is my company name.
  2. autodocumenting-makefiles is the slug and it's based off the article title.
  3. 175b is a hashed value that is either stored in an indexed character field or broken down by the router into a numeric primary key.

Here is another way of looking at their URL design:

/<org-or-username>/<slugified-title>/<hashed-id>
Enter fullscreen mode Exit fullscreen mode

Let's see how we can implement a simplified version of this technique using Django.

Our Version of the URL

We're going with a simpler version of the Dev.to implementation. Our implementation will use the database primary key to ensure uniqueness instead of the hashed value relied on by Dev.to.

/<username>/<slugified-title>/<primary-key>/
Enter fullscreen mode Exit fullscreen mode

Okay, now that we've determined our URL design, let's build it!

The Model

Store the data!

# articles/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

class Article(models.Model):

    title = models.CharField(_("Title"), max_length=100)
    slug = models.CharField(_("Slug"), max_length=100)
    author = models.ForeignKey(settings.AUTH_USER_MODEL, 
        on_delete=models.CASCADE)
    # More fields...
Enter fullscreen mode Exit fullscreen mode

The Form

Collect and validate the data!

# articles/forms.py
from django import forms

from .models import Article

class ArticleForm(forms.ModelForm):

    class Meta:
        model = Article
        fields = ('title', ) # more fields
Enter fullscreen mode Exit fullscreen mode

The Views

Now that we have the model and form, let's build the views:

# articles/views.py
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils import slugify
from django.views.generic import CreateView, DetailView, UpdateView

from .forms import ArticleForm
from .models import Article


class ArticleCreateView(LoginRequiredMixin, CreateView):

    model = Article
    form_class = ArticleForm

    def form_valid(self, form):
        # Save the data to an article object - 
        #   this hasn't yet saved to the database.
        article = form.save(commit=False)
        article.slug = slugify(article.title)
        article.author = self.request.user
        # Save again - this time to the database
        article.save()
        return super().form_valid(form)


class ArticleUpdateView(LoginRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm

    def get_object(self):
        # We get records by primary key, which ensures that
        # changes in the title or slug doesn't break links
        return get_object_or_404(Article,
            id=self.kwargs['pk'],
            author__username=self.kwargs['username'],
            author=self.request.user
        )

    def form_valid(self, form):
        # Update the slug if the title has changed.
        article = form.save(commit=False)
        article.slug = slugify(article.title)
        article.save()
        return super().form_valid(form)        


class ArticleDetailView(DetailView):
    model = Article

    def get_object(self):
        # We get records by primary key, which ensures that
        # changes in the title or slug doesn't break links
        return get_object_or_404(Article,
            id=self.kwargs['pk'],
            author__username=self.kwargs['username']
        )        
Enter fullscreen mode Exit fullscreen mode

The URLs

Let's route this into our urls:

# articles/urls.py
from django.urls import path

from articles import views

urlpatterns = [
    path(route='/new/',
        view=views.ArticleCreateView.as_view(),
        name='create',
    ),
    path(route='/<slug:username>/<slug:slug>/<int:pk>/edit/',
        view=views.ArticleUpdateView.as_view(),
        name='update',
    ), 
    path(route='/<slug:username>/<slug:slug>/<int:pk>/',
        view=views.ArticleDetailView.as_view(),
        name='detail',
    ),       
]
Enter fullscreen mode Exit fullscreen mode

And in the project's root config, we add in this:

# config/urls.py or wherever you stick the project's root urls.py
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    # Django Admin, change this URL
    path('two-scoops-of-django-is-awesome', admin.site.urls),
    # Articles management
    path('', include('articles.urls', namespace='article')),
    # More URLS here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# There's certainly more URLs down here
Enter fullscreen mode Exit fullscreen mode

Add templates and there it is, a Django example that follows the Dev.to URL design!

Going Forward

Experienced Django users will know that the slugification logic in the form_valid methods properly belongs in the Article model's save() method, rather than the views. That's the kind of thing we discuss in Two Scoops of Django.

Speaking of which, if you want to learn all kinds of advanced tricks and tips, take a look at me and my wife's my ice-cream themed book and course on Django Best Practices.

Image of Daniel and Audrey holding Two Scoops of Django 3.x

Top comments (1)

Collapse
 
vigo profile image
Uğur "vigo" Özyılmazel

<3