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

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 ๐Ÿ’ก

Subscribe to my free newsletter

Get updates on AI, software, and business.