Snapshot any site with Django in 3 minutes 🖼️

Photo of Tom Dekan
by Tom Dekan
Updated: Fri 03 May 2024

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" style="cursor: pointer">
    <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 build your Django frontend even faster?

I want to release high-quality products as soon as possible. Probably like you, I want to make my Django product ideas become reality as soon as possible.

That's why I'm building Photon Designer - an entirely visual editor for building Django frontend at the speed that light hits your eyes 💡

Let's get visual.

Do you want to create beautiful frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).
Copied link to clipboard 📋

Made with care by Tom Dekan

© 2024 Photon Designer