Mock Screenshot feature of SaaS applications

While working on a SaaS application for a client recently, I came across an interesting feature, where the application lets you to take screenshots of the entire page and sends them to the requested user via email either in the Image or PDF format. I was intrigued by this feature and thought I’d share it with you all.

Here is how you go about it:

STEP 1:The setup

We are going to need a backend. I have designed mine using Node.js and Express.js to take the screenshot and then mail it along with the frontend. The Backend and Frontend can be found in the repos attached.

STEP-2: The UI:

Honestly, the UI of a SasS application can be quite boring as it just displays a bunch of boring data consisting of metric cards, goal scorecards, charts etc.

So, to make this blog a little interesting the UI displays a collection of Pokemon cards, because who doesn’t like Pokemons, right?

STEP-3: The backend

Now that the UI is present, let’s build the backend.

It is going to be quite simple. The backend will just have a route which would be called when the “Export Screenshot” button from the frontend is clicked.

const express = require(“express”)
let cors = require(“cors”)

const { sendEmail } = require(“./helpers/helper”)

const app = express()
app.use(cors())
app.use(express.json())

app.get(“/”, (req, res) => {
res.send(“backend home route”)
})

app.post(“/send-screenshot”, async (req, res) => {
const { receiversEmail } = req.body

try {
await sendEmail(receiversEmail)
res.status(200).send(“Email sent successfully”)
} catch (err) {
res.status(400).send(“Error in sending the email with screenshot”)
}
})

app.listen(4000, () => {
console.info(“Backend is running on port 4000”)
})

STEP-4: The Screenshot

The frontend and backend are now in place. Let’s use the Screenshot API’s query builder to design a query for the screenshot.

Here, I have designed a query in order to get a high-resolution full-page screenshot of the current page.

Let me take a second to explain the options that I am using.

  • Full page screenshot – This implies the entire page, including the scrollable area.
  • Retina – This would capture the screenshot in high resolution
  • Lazy loading – This makes sure that all the content is loaded before the screenshot is taken.
  • Fresh screenshot – The screenshot.api captures all the screenshots for you on their server. So, I’m using this just to make sure that I get a new screenshot every time.
    • Alternatively, you can use “Destroy screenshot” feature which makes sure that your screenshot is not cached on their server in case the data displayed is sensitive
  • Load event – A lot of nuances lie within this feature and using it correctly would save a lot of time. By default, it is set to “load” but imagine a scenario where the page has a lot of images. Obviously, those images would take some time to load up. If you want to make sure that all images on the page show up in the screenshot, we need to use the “networkidle” option. This essentially means that the API is going to wait until all the network calls are completed to take the screenshot.

Most importantly, if you want to take screenshot of a public website, it follows two additional steps: “Block ads” and “no Cookie banners”.

Finally, the query would look something like this

https://shot.screenshotapi.net/screenshot?token=%3cYOUR_API_TOKEN%3e&url=%3cFORNTEND_URL%3e&full_page=true&fresh=true&output=image&
file_type=jpeg&lazy_load=true&retina=true&wait_for_event=networkidle

PS. For the frontend URL ngrok can be used.

STEP-5: The Email:

We are going to use nodemailer for sending the screenshot. The screenshot.api would send back the JSON response which would contain the screenshot key that contains the URL of the screenshot.

Now, for emailing the image we need to first fetch the image, write it to the disk using the fs module and then send it using nodemailer. Attaching the code to the below:

const nodemailer = require("nodemailer")
const axios = require("axios")
const fs = require("fs")

const { SCREENSHOT_API_TOKEN } = require("./credentials")
const path = require("path")

const takeScreenshot = async () => {
  try {
    var query = "https://shot.screenshotapi.net/screenshot"
    let url = "<FRONTEND_URL>"
    query += `?token=${SCREENSHOT_API_TOKEN}&url=${url}&full_page=true&fresh=true&output=image&file_type=jpeg&lazy_load=true&retina=true&wait_for_event=networkidle`
    const response = await axios.get(query)

    console.info(JSON.stringify(response.data))

    const imageStream = await axios.get(screenshotURL, {
      responseType: "stream",
    })
    return imageStream
  } catch (err) {
    console.error("\nError while taking the screenshot", err)
    throw err
  }
}

const sendEmail = async (receiversEmail) => {
  try {
    let mailerConfig = {
      host: "smtp.gmail.com",
      port: 587,
      secure: false, // true for 465, false for other ports
      auth: {
        user: "<GMAIL_ID>", // user
        pass: "<APP_PASSWORD>", // password
      },
    }

    let transporter = nodemailer.createTransport(mailerConfig)

    const imageStream = await takeScreenshot()

    const imagePath = path.join(__dirname, "..", "output", "screenshot.png")
    imageStream.data
      .pipe(fs.createWriteStream(imagePath))
      .on("finish", () => {
        // send mail with defined transport object
        let info = await transporter.sendMail({
          from: "<SENDER'S EMAIL ADDRESS>", // sender address
          to: `${receiversEmail}`, // list of receivers
          subject: "Screenshot requested", // Subject line,
          attachment: [
            {
              filename: imagePath,
              content: imageBuffer,
              encoding: "base64",
            },
          ],
          text: "Hello! find the screenshot that you requested attached", // plain text body
          html: "<b>Hello! find the screenshot that you requested attached</b>", // html body
        })
      })
      .on("error", (err) => {
        console.error("Stream closed with following error: ", err)
      })
    return true
  } catch (err) {
    console.error("\nError in sending the email", err)
    throw err
  }
}

module.exports = {
  sendEmail,
}

Finally, if you want to use your Gmail account then you need to generate an app password. You can find more details on this right here.