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:

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).

Subscribe to my free newsletter

Get updates on AI, software, and business.