Snapshot any site with Django in 3 minutes 🖼️

Published: October 27, 2023

There will be 4 steps. With the final product that we'll make in 3 minutes, you could:

  • 💵 Expand it and sell it as a product (e.g., Screenshotlayer)
  • Create visual content for your blog posts by capturing live website previews
  • Create OG social images to improve your sharing on social media (See my guide on how to add OG images: Create a open graph social image generator with Django 🌐)

We'll use a headless browser to visit a url, take a screenshot, and then render that screenshot into our page.

Here's what our final product will look like. Let's start 🏇🏿

Optional video tutorial (featuring me 🏇🏿) below:

1. Setup

1.1 Install the requirements and create a new Django app

  • In your terminal:
pip install Django selenium Pillow webdriver_manager
django-admin startproject core .
python manage.py startapp sim

1.2 Install Google Chrome

The code is setup for Google Chrome. You can use Brave or Safari, but you'll need to modify the code (only 4-5 lines to change).

1.3 Update your  settings.py

  • Register our sim app by adding it to INSTALLED_APPS
INSTALLED_APPS = [
# ...
'sim',
]`

2. Add our urls

  • Update core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include("sim.urls")),
]
  • Update sim/urls.py
from django.urls import path
from sim.views import CaptureView, capture_page
urlpatterns = [
path('capture/', capture_page, name='capture_page'),
path('capture-image/', CaptureView.as_view(), name='capture_image'),
]

3. Add our views to take the screenshot

  • Add this to sim/views.py:
import os
from django.http import HttpResponse
from django.shortcuts import render
from django.views import View
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
class CaptureView(View):
def post(self, request, *args, **kwargs) -> HttpResponse:
url = request.POST.get('url')
print(f'{url = }')
if url:
return self.capture_website(url)
else:
return HttpResponse('No url provided', status=400)
def capture_website(self, url: str) -> HttpResponse:
"""
Visits the url to take a screenshot.
"""
chrome_options = Options()
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
print(f'Getting image for {url = }')
driver.set_window_size(1920, 1080)
# Create a path for our screenshot file.
static_dir = os.path.join('sim', 'static', 'sim')
os.makedirs(static_dir, exist_ok=True)
screenshot_path = os.path.join(static_dir, 'screenshot.png')
driver.save_screenshot(screenshot_path)
driver.quit()
return render(self.request, 'preview.html')
def capture_page(request) -> HttpResponse:
"""
Renders the initial page.
"""
return render(request, 'capture.html')

4. Add templates to render the result

  • Create templates folder at sim/templates/
  • Create preview.html in sim/templates/ containing:
{% load static %}
<style>
#preview-link img {
border-radius: 15px;
width: 80%;
margin: auto;
}
#preview-link {
display: flex;
justify-content: center;
}
</style>
<a
id="download-link"
href="{% static 'sim/screenshot.png' %}"
download="screenshot.png"
>Download Image</a
>
<a id="preview-link" href="{% static 'sim/screenshot.png' %}" target="_blank">
<img
id="screenshot"
src="{% static 'sim/screenshot.png' %}"
alt="Website Screenshot"
/>
</a>
  • Create capture.html in sim/templates/ containing:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Website Capture</title>
<script
src="https://unpkg.com/htmx.org@1.9.6"
integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"
crossorigin="anonymous"
></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f0f0f0;
}
h1 {
font-weight: normal;
}
form {
margin-bottom: 20px;
}
input,
button {
padding: 10px;
margin: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
button:disabled {
background-color: #ccc;
}
#loading {
display: none;
}
#preview[loading] #loading {
display: block;
}
#preview[loading] img,
#preview[loading] p,
#preview[loading] a {
display: none;
}
#preview {
text-align: center;
}
.htmx-indicator {
opacity: 0;
transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator {
opacity: 1;
}
.htmx-request.htmx-indicator {
opacity: 1;
}
#spinner {
position: fixed;
margin: auto;
}
#spinner:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 80px;
height: 80px;
margin-top: -40px;
margin-left: -40px;
border: 4px solid #f1cbcb;
border-top-color: transparent;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<h1>Screenshot any website</h1>
<form
hx-post="/capture-image/"
hx-trigger="submit"
hx-swap="innerHTML"
hx-target="#preview"
hx-indicator="#spinner"
>
{% csrf_token %}
<input
type="url"
name="url"
required
placeholder="Enter URL"
value="{{ url }}"
/>
<button type="submit">Capture</button>
</form>
<div id="spinner" class="spinner htmx-indicator"></div>
<div id="preview">
<!-- Content will be replaced by the server response -->
</div>
</body>
</html>

Run your Django server

python manage.py runserver

Now, you can visit the page at http://127.0.0.1:8000/capture/ , submit a url, and receive the screenshot back automatically.

Finished 🎉 Here some ideas to extend this

  • Deploy this online and sell it as a service 💵. If you do, I'd recommend using serverless functions when deploying this: each request will take a few seconds. Here's my guide on how to add serverless functions with Django as simply as possible: How to add serverless functions to Django in 6 minutes 🧠

  • Add mobile device previews by adapting the code to capture mobile views (You'll only need to change one line of the above code).

P.S Want to ship better features with AI?
Join my free weekly newsletter

Each week, I share bite-sized learnings and AI news that matters - so you can build better software in practice.

No spam guaranteed · Unsubscribe whenever