Snapshot any site with Django in 3 minutes ๐ผ๏ธ
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.htmlinsim/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.htmlinsim/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 runserverNow, 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).