Connect: Django
Connect a Django site: one small app with the views, routes and templates. Works with any supported Django version.
This is the simple way: we send finished HTML with the SEO done, and your site shows it in its own design. Using a coding agent? Copy the prompt from your dashboard's Integration page instead: it has your values filled in and the same steps for this framework.
How it works
- The key comes from the environment.
BLOGSFORSEO_API_KEYis read on the server and sent asAuthorization: Bearer <key>. It's never in the code, the templates or the browser. - Every visit asks our API. The pages are rendered on each request, never saved at build time, so new posts, edits and removals show up on their own. The one rule for caching: nothing may keep our API's answer for more than 5 minutes. If you add a cache, keep it at 300 seconds or less, and check your CDN rules on
/blogand/blog-sitemap.xmltoo. - A 404 from us is a 404 from you. An unknown or removed post must answer a real 404, not an empty page. Any other error fails the request, so your framework shows its normal error page.
Match the routes to your post URL pattern in the dashboard: these use /blog and /blog/{slug}. In this guide, {API} means https://blogsforseo.com/api/v1.
Set it up
Uses requests. Add the settings, then the helper, views and routes to an app called blog.
# settings.py
import os
BLOGSFORSEO_API_URL = os.environ.get("BLOGSFORSEO_API_URL", "https://blogsforseo.com").rstrip("/") + "/api/v1"
BLOGSFORSEO_API_KEY = os.environ["BLOGSFORSEO_API_KEY"] # Fails at startup when it's missing.
BLOGSFORSEO_INDEXNOW_KEY = "<your IndexNow key>" # Not secret.
# blog/blogsforseo.py
import requests
from django.conf import settings
from django.http import Http404
def fetch(path, params=None):
"""Asks the Content API on every request. A 404 from the API is this page's 404."""
response = requests.get(
settings.BLOGSFORSEO_API_URL + path,
params=params,
headers={"Authorization": "Bearer " + settings.BLOGSFORSEO_API_KEY},
timeout=10,
)
if response.status_code == 404:
raise Http404("Not found")
response.raise_for_status()
return response
# blog/views.py
from urllib.parse import quote
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from .blogsforseo import fetch
def page_number(request):
try:
return max(1, int(request.GET.get("page", "1")))
except ValueError:
return 1
def blog_index(request):
data = fetch("/html/posts", {"page": page_number(request)}).json()
return render(request, "blog/index.html", data)
def blog_post(request, slug):
data = fetch("/html/posts/" + quote(slug)).json()
return render(request, "blog/post.html", data)
def blog_sitemap(request):
xml = fetch("/sitemap", {"format": "xml"}).content
return HttpResponse(xml, content_type="application/xml")
def llms_txt(request):
return HttpResponse(fetch("/llms.txt").text, content_type="text/plain; charset=utf-8")
def indexnow_key(request):
return HttpResponse(settings.BLOGSFORSEO_INDEXNOW_KEY, content_type="text/plain")
# blog/urls.py (include it from your root urls.py with path("", include("blog.urls")))
from django.conf import settings
from django.urls import path, re_path
from . import views
urlpatterns = [
path("blog", views.blog_index),
re_path(r"^blog/(?P<slug>[a-z0-9-]+)$", views.blog_post), # Other slug shapes are a 404 without asking the API.
path("blog-sitemap.xml", views.blog_sitemap),
path("llms.txt", views.llms_txt),
path(settings.BLOGSFORSEO_INDEXNOW_KEY + ".txt", views.indexnow_key),
]
Templates, in your own base layout. head and body are already escaped by us, so mark them safe:
<!-- blog/templates/blog/post.html; blog/index.html is the same without the head block -->
{% extends "base.html" %}
{% block head %}{{ head|safe }}{% endblock %}
{% block content %}{{ body|safe }}{% endblock %}
Your base.html needs a {% block head %}{% endblock %} inside <head>; leave out your own <title> on post pages. Then add the sitemap line from your dashboard (Sitemap: https://<your domain>/blog-sitemap.xml) to your robots.txt, wherever you serve it.
Caching: these views fetch on every request. Don't wrap them in cache_page for longer than 300 seconds, and if you use Django's whole-site cache middleware, keep CACHE_MIDDLEWARE_SECONDS at 300 or less.
The advanced way
Fetch /posts and /posts/{slug} instead of the /html endpoints, and build the page from the fields in the Content API reference, following the advanced guide. bodyHtml is already sanitised: print it raw. When you put jsonLd in a <script type="application/ld+json">, escape <:
json.dumps(post["jsonLd"]).replace("<", r"\u003c")
Check it
Run these against your site (local or live). -A GPTBot asks the way an AI crawler does, without JavaScript:
curl -A GPTBot https://<your domain>/blog # lists posts, or "No posts yet"
curl -A GPTBot https://<your domain>/blog/<a post slug> # <h1>, article text, canonical link, application/ld+json
curl -i https://<your domain>/blog/not-a-real-post # HTTP 404
curl https://<your domain>/blog-sitemap.xml # the same posts as GET {API}/sitemap
curl https://<your domain>/llms.txt # plain text
curl https://<your domain>/<your IndexNow key>.txt # just the key
curl https://<your domain>/robots.txt # has the Sitemap: line
Then check it stays fresh: the number of posts on /blog matches total from the API. Finish with the simple guide's dashboard steps: check the post address, run the health check and connect Search Console.