Building and deploying my Django portfolio felt like a major milestone.
The pages worked.
My projects were there.
The blog was running.
The website was live.
But that raised another question:
How will search engines actually understand what is on the website?
A working website and a discoverable website are not necessarily the same thing.
That led me to work on another part of the project that isn't as visually obvious as building pages or fixing deployment errors:
technical SEO.
Rather than treating SEO as something separate from development, I started implementing some of its foundations directly inside my Django application.
The Problem: A Browser Can Read My Site, but What About a Search Engine?
A visitor can navigate a portfolio naturally.
They see navigation such as:
Home
├── About
├── Services
├── Projects
├── Resume
├── Blog
└── Contact
They understand what those pages mean because of the interface.
A search engine doesn't interact with the site exactly like a human visitor.
It needs signals that help it discover URLs and understand what individual pages contain.
My portfolio also introduced another challenge: the blog.
Unlike a small static portfolio containing only a handful of fixed URLs, blog content creates individual article URLs.
Conceptually, the website now looks more like:
saddamshah.com/
│
├── about/
├── services/
├── projects/
├── resume/
├── contact/
│
└── blog/
├── article-one/
├── article-two/
└── future-articles/
As I publish more content, manually thinking about every URL becomes increasingly impractical.
I wanted Django to help manage this.
Step 1: Creating a Sitemap with Django
Django includes a sitemap framework, so I could generate the sitemap from the application instead of manually maintaining an XML file.
I added Django's sitemap functionality to the project:
INSTALLED_APPS = [
# ...
"django.contrib.sitemaps",
]
Then I separated the URLs into sitemap groups.
My project uses:
sitemaps = {
"static": StaticViewSitemap,
"blog": BlogSitemap,
}
This distinction is useful.
The portfolio contains two different kinds of content.
Static pages
These are URLs such as:
/
/about/
/services/
/projects/
/resume/
/contact/
Their routes are relatively stable.
Dynamic blog content
Blog articles are different.
Every new published article can create another URL.
Instead of manually editing the sitemap whenever I publish something, the sitemap can be generated from the application's content.
That is one of the advantages of handling SEO at the application level.
Step 2: Exposing sitemap.xml
After defining the sitemap configuration, I connected it through Django's URL routing.
The relevant route looks like this:
from django.contrib.sitemaps.views import sitemap
urlpatterns = [
# ...
path(
"sitemap.xml",
sitemap,
{"sitemaps": sitemaps},
name="sitemap",
),
]
Now the application can expose:
/sitemap.xml
instead of requiring me to manually create and update the XML document.
Conceptually, the sitemap provides search engines with something like:
<urlset>
<url>
<loc>https://saddamshah.com/</loc>
</url>
<url>
<loc>https://saddamshah.com/projects/</loc>
</url>
<url>
<loc>https://saddamshah.com/blog/...</loc>
</url>
</urlset>
The important part for me wasn't simply generating XML.
It was making URL discovery part of the Django application itself.
When the site's content grows, the infrastructure can grow with it.
Step 3: Adding robots.txt
I also wanted the site to give web crawlers basic instructions.
For this project, I implemented a small Django view that returns the robots configuration as plain text:
from django.http import HttpResponse
def robots_txt(request):
content = """User-agent: *
Allow: /
Disallow: /admin/
Sitemap: /sitemap.xml
"""
return HttpResponse(
content,
content_type="text/plain"
)
Then I exposed it through the URL configuration:
path(
"robots.txt",
robots_txt,
name="robots"
)
This gives the website:
/robots.txt
The response communicates three basic instructions:
User-agent: *
Allow: /
Disallow: /admin/
Sitemap: /sitemap.xml
The intention is straightforward.
Public website content can be crawled, the Django admin isn't intended as public content, and crawlers are given the location of the sitemap.
Sitemap vs. Robots.txt
Initially, it can be easy to think these files do the same job.
They don't.
I think about them like this:
SEARCH ENGINE
│
┌────────┴────────┐
│ │
robots.txt sitemap.xml
│ │
Crawling guidance URL discovery
│ │
└────────┬────────┘
│
WEBSITE
robots.txt provides crawler instructions.
sitemap.xml helps expose the URLs I want search engines to discover.
They complement each other.
Step 4: Making Page Metadata Flexible
URL discovery is only part of SEO.
Search engines also need context about individual pages.
A portfolio has pages with very different purposes.
For example:
Home → Developer positioning
Projects → Development work
Resume → Skills and experience
Blog → Technical content
Article → One specific technical topic
Using exactly the same page title and description everywhere would throw away useful context.
Django template inheritance provides a clean way to handle this.
The base template can define defaults:
<title>
{% block title %}
Saddam Shah | Full-Stack Django Developer
{% endblock %}
</title>
Individual pages can then override the value:
{% block title %}
Projects | Saddam Shah
{% endblock %}
The same principle can be used for descriptions.
<meta
name="description"
content="{% block meta_description %}
Full-Stack Django Developer building web applications,
APIs, and backend systems.
{% endblock %}"
>
Then a specific article can provide metadata relevant to that article rather than inheriting a generic portfolio description.
This is where Django's template system becomes particularly useful.
I don't need to duplicate the entire HTML <head> across every template.
I define the structure once and override the information that changes.
Step 5: Thinking Beyond Google
There is another place where page metadata matters:
social sharing.
When somebody shares a portfolio page or article, platforms may use metadata to determine the preview.
That includes information such as:
<meta property="og:title"
content="Page Title">
<meta property="og:description"
content="Page description">
<meta property="og:type"
content="website">
<meta property="og:image"
content="...">
For larger previews, Twitter/X metadata can follow the same idea:
<meta name="twitter:card"
content="summary_large_image">
<meta name="twitter:title"
content="Page Title">
<meta name="twitter:description"
content="Page description">
This was especially relevant for my blog.
I'm publishing technical articles and sharing them outside the website, including on LinkedIn.
That means the presentation of an article shouldn't begin only after someone clicks it.
The shared preview is part of the experience too.
Dynamic Content Changes the SEO Problem
The blog made me think about SEO differently.
With only five or six static pages, maintaining metadata manually is manageable.
But imagine the site after publishing:
10 articles
25 articles
50 articles
100 articles
Each article potentially has:
Title
Description
Slug
Featured image
Category
Content
URL
At that point, content management and SEO start overlapping.
The better question becomes:
How can the application use information it already has to generate meaningful metadata?
For example, conceptually:
<title>
{{ post.seo_title|default:post.title }}
</title>
<meta
name="description"
content="{{ post.meta_description }}"
>
Now SEO information can follow the article rather than requiring changes to the global template every time new content is published.
That is a much more scalable approach.
What This Changed About How I Think About Django
When I first started building Django projects, most of my attention naturally went toward things like:
Models
Views
URLs
Templates
Forms
Databases
Authentication
Those are fundamental.
But building an actual website pushed me to think about another layer:
Django Application
│
┌──────────────┼──────────────┐
│ │ │
Application Production Discoverability
Logic Deployment │
│ │ ┌─────┴─────┐
Models/Views Configuration Sitemap Metadata
Robots Social
A production website isn't just application logic.
It also has to communicate properly with systems outside the application.
Search engines are one of those systems.
What I Learned
The biggest lesson wasn't that every Django project needs a few SEO tags copied into its <head>.
It was that SEO can be treated as part of application architecture rather than an afterthought.
Django already knows:
- which URLs exist,
- which blog posts exist,
- what their titles are,
- what their slugs are,
- and what content is published.
Using that information programmatically is much better than maintaining everything manually.
I also learned that deploying a website isn't necessarily the finish line.
After asking:
"Does the website work?"
there are other useful questions:
Can users find it?
Can search engines discover its pages?
Does each page explain what it contains?
What happens when an article is shared?
Can the SEO structure grow as the content grows?
Those questions pushed my portfolio beyond simply being a collection of Django templates.
Building for More Than the Browser
My portfolio started as a way to demonstrate my development skills.
But building it as a real deployed application continues to expose areas that small local projects don't always force you to think about.
First came deployment.
Then production debugging.
Then external integrations.
And now discoverability.
Each layer has made the project more useful as a learning experience.
The goal isn't just to build something that renders correctly when I open it in my browser.
It's to understand the different systems a production web application needs to work with.
And that is becoming one of the most valuable parts of building the project.
I'm documenting the real technical decisions, problems, and lessons behind building my Django portfolio rather than only showing the finished UI.
You can explore the project at saddamshah.com.