> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Off with its Head! Headless Chrome as a Service decoded

> Headless Chrome is a headless browser configurable on Platform.sh. Compare screenshots and generate PDFs hassle-free today

export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud",
      "linkedin": "https://www.linkedin.com/in/laurent-arnoud-861b44121/"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["chadwcarlson"], date: "2019-06-12", image: "/images/posts/unknown/off-with-its-head-headless-Chrome-as-a-service/off-with-its-head-headless-Chrome-as-a-service.webp" }} />

<Tip>
  This post was originally published on the Platform.sh blog and reflects information from the time of publication.
</Tip>

We’re excited to share [Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome) as a service is now available on Platform.sh. When used in combination with the Node.js library [Puppeteer](https://github.com/GoogleChrome/puppeteer), you get a powerful, new automated testing tool that enables you to:

* Easily generate PDFs and screenshots of your application, including emulating your site’s appearance on mobile devices.
* Crawl each page of an application, and potentially compare screenshots between branches, to ensure new features don’t result in changes to your UI.
* Simulate user action. Puppeteer provides programmatic control over keyboard input, form completion, mouse position and clicks. So you could, for example, verify the checkout process of your eCommerce application—end-to-end.
* Monitor and verify that client-side Javascript and resource handling is working as you intended.

In this post, we'll show you how to configure Headless Chrome on a project, and then build an [ExpressJS](https://expressjs.com/) application that shows off some of the cool new capabilities your applications can have with a headless browser at its disposal.

## Configuring Headless Chrome

Headless Chrome can be configured on an existing Platform.sh project like any other of our maintained services. First, define the `chrome-headless` container in your `.platform/services.yaml`:

```yaml theme={null}
headless:
  type: chrome-headless:73
```

along with a complementary relationship definition in `.platform.app.yaml`:

```yaml theme={null}
relationships:
  headless: "headless:http"
```

Using [Puppeteer](https://github.com/GoogleChrome/puppeteer) with the Platform.sh [Config Reader](https://github.com/platformsh/config-reader-nodejs) library requires Node.js 10 or later

```yaml theme={null}
type: nodejs:10
```

and that both libraries are included in your `package.json` dependencies:

```json theme={null}
 "dependencies": {
   "platformsh-config": "^2.0.0",
   "puppeteer": "^1.14.0",
 }
```

If you're running a Node.js application, you're all set! Projects running with non-Node.js runtimes will also have to upgrade the current installation of Node.js, which can be done easily using the Node Version Manager and by following the instructions outlined in our [documentation](https://docs.platform.sh/languages/nodejs/nvm.html).

## Using Headless Chrome and Puppeteer

Now that we’ve configured Headless Chrome on Platform.sh, let's build something that uses Puppeteer!

### Generating PDFs

First, we're going to create a simple Node.js application using the [ExpressJS](https://expressjs.com/) framework that generates a PDF of any URL a user provides. We've already defined our `services.yaml` above, so let's define our application in `.platform.app.yaml`:

```yaml theme={null}
name: nodejs

type: nodejs:10

relationships:
  headless: "headless:http"

crons:
  cleanup:
    spec: "*/30 * * * *"
    cmd: rm pdfs/*

web:
  commands:
    start: "node index.js"

mounts:
  "/pdfs": "shared:files/pdfs"

disk: 512
```

We define the mount `/pdfs`, which will become the writable directory our generated PDFs will be saved to, and a
`cron` job that empties that directory every thirty minutes. We can also define a single upstream and redirect route for our application `nodejs` in `.platform/routes.yaml`:

```yaml theme={null}
"https://{default}/":
  id: main
  type: upstream
  upstream: "nodejs:http"

"https://www.{default}/":
  type: redirect
  to: "https://{default}/"
```

#### Writing the ExpressJS application

We start our application by running `index.js`, so let's write one now that uses the ExpressJS framework. It defines an application `app` that uses `public/` as a static source for the application's stylesheet and `express-rate-limit` to set limits on the number of requests users can make to the site. It also requires the local file `/examples/pdfs.js`, which will actually use Puppeteer to generate PDFs—but more on that later.

```js theme={null}
const fs = require("fs");
const uuidv4 = require("uuid/v4");
const express = require("express");
const rateLimit = require("express-rate-limit");
const platformsh = require("platformsh-config");

// Require locals
var pdfs = require("./examples/pdfs.js");

// Build the application
var app = express();

// Define static source for css
app.use(express.static(__dirname + "/public"));

// Set rate limits
app.set("trust proxy", 1);

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 50 // limit each IP to 50 requests per windowMs
});

// Apply to all requests
app.use(limiter);
```

Next, we can define what the front page will look like with a little HTML that provides an input field named `pdfURL`, where users can provide a website URL from which the PDF will be generated.

```js theme={null}
// Define the index route
app.get("/", (req, res) => {
  res.writeHead(200, { "Content-Type": "text/html" });
  res.write(`<html>
<head>
   <title>Headless Chrome on Platform.sh</title>
   <link rel="stylesheet" type="text/css" href="css/style.css" />
</head>
<body>

<h1>Headless Chrome on Platform.sh</h1>

<h2>Generate a PDF of a page</h2>

Click 'Submit' to generate a PDF of the <a href="/">Platform.sh website</a>, or paste in another URL.

</br></br>

<form method="get" action="/pdfs/result">
   <input type="text" name="pdfURL" value="https://platform.sh/">
   <input type="submit">
</form>
`);
  res.end(`</body></html>`);
});
```

We’ll have to define the route associated with the above form's action, `/pdfs/result`. It includes an async function that generates a random identifier, which will be a part of the output PDF file name, and then passes that identifier with the `pdfURL` to another function within `/examples/pdfs.js` called `makePDF()`. The resulting file is then downloaded within the user's browser.

```js theme={null}
// Define PDF result route
app.get("/pdfs/result", async function (req, res) {
  // Create a randomly generated ID number for the current PDF
  var pdfID = uuidv4();
  // Generate the PDF
  await pdfs.makePDF(req.query["pdfURL"], pdfID);
  // Define and download the file
  const file = `pdfs/${pdfID}.pdf`;
  res.download(file);
});
```

Finally, we use Config Reader to tell `app` which port it should be listening on:

```js theme={null}
// Get PORT and start the server
let config = platformsh.config();
app.listen(config.port, function () {
  console.log(`Listening on port ${config.port}`);
});
```

#### Generating PDFs with Puppeteer

Now that our base application is written, let's use Puppeteer to generate PDFs in `examples/pdfs.js`:

```js theme={null}
const puppeteer = require("puppeteer");
const platformsh = require("platformsh-config");
var exports = (module.exports = {});

// Create an async function
exports.makePDF = async function (url, pdfID) {
  try {
    // Connect to chrome-headless using pre-formatted puppeteer credentials
    let config = platformsh.config();
    const formattedURL = config.formattedCredentials("headless", "puppeteer");
    const browser = await puppeteer.connect({ browserURL: formattedURL });

    // Open a new page to the given url and create the PDF
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: "networkidle2" });
    await page.pdf({
      path: `pdfs/${pdfID}.pdf`,
      printBackground: true
    });
    await browser.close();

    return browser;
  } catch (e) {
    return Promise.reject(e);
  }
};
```

We can pass Config Reader's formatted credentials for Puppeteer as `browserURL` in `puppeteer.connect()` to connect to our Headless Chrome service container and instantiate a new headless browser. Then, we can use Puppeteer to create a new page, visit the user-provided URL, and call `page.pdf()` to generate a PDF of that site.

Here, we also call two parameters: `path`, saving the PDF with our unique identifier to the mount `pdfs/`, and `printBackground`, an optional parameter that will include the site's background images in the resulting file. Many more options are available within `page.pdf()`, so check the [documentation](https://pptr.dev/#?product=Puppeteer\&version=v1.17.0\&show=api-pagepdfoptions) for more details.

Our application can now receive a URL provided by a user and generate a PDF from it. Before we push to Platform.sh, we can add another interesting feature that comes with Headless Chrome: screenshots.

### Creating screenshots with Puppeteer

First, we can modify `.platform.app.yaml` with another mount where the screenshots will be saved

```yaml theme={null}
mounts:
  "/pdfs": "shared:files/pdfs"
  "/screenshots": "shared:files/screenshots"
```

and append our `cron` job to remove its files periodically, like we did with `pdfs`

```yaml theme={null}
crons:
  cleanup:
    spec: "*/30 * * * *"
    cmd: rm pdfs/* && rm screenshots/*
```

In a file called `examples/screenshots.js` we can connect to Headless Chrome as we did before, but, instead, use `page.screenshot()` to take a screenshot of a given URL and save it to the new mount `screenshots/` with its own unique name.

```js theme={null}
const puppeteer = require("puppeteer");
const devices = require("puppeteer/DeviceDescriptors");
const platformsh = require("platformsh-config");

var exports = (module.exports = {});

// Create an async function
exports.takeScreenshot = async function (
  url,
  screenshotID,
  emulateMobile = false
) {
  try {
    // Connect to chrome-headless using pre-formatted puppeteer credentials
    let config = platformsh.config();
    const formattedURL = config.formattedCredentials("headless", "puppeteer");
    const browser = await puppeteer.connect({ browserURL: formattedURL });

    // Open a new page to the given url and take the screenshot
    const page = await browser.newPage();
    // Emulate mobile device if selected
    if (emulateMobile) {
      await page.emulate(devices["iPhone 6"]);
    }
    await page.goto(url);
    await page.screenshot({
      fullPage: true,
      path: `screenshots/${screenshotID}.png`
    });

    await browser.close();

    return browser;
  } catch (e) {
    return Promise.reject(e);
  }
};
```

There are a few new things happening here. First, `page.screenshot()` comes with its own set of parameters that can be modified. `fullPage` is set to `true`, generating a PNG of the entire web page instead of just what’s seen in a browser window. You can change the default image format from PNG to JPEG, along with many other options described in the [`page.screenshot() documentation`](https://pptr.dev/#?product=Puppeteer\&version=v1.17.0\&show=api-pagescreenshotoptions).

Second, we've included a new parameter in our exported function `takeScreenshot`: `emulateMobile`. If no value is provided, `takeScreenshot()` will create a full page PNG of the given URL. Otherwise, Puppeteer will use its `DeviceDescriptors` module to modify `browser` so it emulates the view of the web page as it would appear on a mobile device using [`page.emulate()`](https://pptr.dev/#?product=Puppeteer\&version=v1.17.0\&show=api-pagepdfoptions), which, in this case, is an iPhone 6.

All we need to do to include this new feature in our application is to modify `index.js` to require the new module,

```js theme={null}
// Require locals
var pdfs = require("./examples/pdfs.js");
var screenshots = require("./examples/screenshots.js");
```

add a new form in our front page with an input field called `screenshotURL`,

```html theme={null}
<h2>Take a screenshot of a page</h2>

Click 'Submit' to create a screenshot of the <a href="/">Platform.sh website</a>, or paste in another URL.

</br></br>

<form method="get" action="/screenshots/result">
   <input type="text" name="screenshotURL" value="https://platform.sh/">
   <input type="submit">
   </br>
 <label class="checkbox"><input type="checkbox" name="emulateMobile" value=true> Emulate mobile device</label><br>
</form>
```

and define the route for its result that calls `takeScreenshot()`

```js theme={null}
// Define Screenshots result route
app.get("/screenshots/result", async function (req, res) {
  // Create a randomly generated ID number for the current screenshot
  var screenshotID = uuidv4();
  // Generate the screenshot
  await screenshots.takeScreenshot(
    req.query["screenshotURL"],
    screenshotID,
    req.query["emulateMobile"]
  );
  // Define and download the file
  const file = `screenshots/${screenshotID}.png`;
  res.download(file);
});
```

### Puppeteer & ExpressJS on Platform.sh: PDFs & Screenshot

Define all of the dependencies in a `package.json`

```json theme={null}
{
  "name": "chrome_headless",
  "version": "1.0.0",
  "description": "A simple example for taking screenshots and PDFs with Puppeteer and headless Chrome on Platform.sh",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Chad Carlson",
  "license": "MIT",
  "dependencies": {
    "platformsh-config": "^2.0.0",
    "puppeteer": "^1.14.0",
    "express": "^4.16.4",
    "uuid": "^3.3.2",
    "express-rate-limit": "^4.0.4"
  }
}
```

Run `npm install` to generate a `package-lock.json`, commit the changes, and push to an empty project on Platform.sh.

That's it! When the build process is finished, we’ll have an ExpressJS application that uses Headless Chrome and Puppeteer to generate:

* PDFs
* Full-page screenshots
* Screenshots that emulate what the site would look like on a mobile device

for any URL a user provides!

## Effortless Headless Chrome with Platform.sh

Platform.sh makes it easy to use and install Headless Chrome in your projects—without the need to install your own version of Chrome into the container as part of your builds. You can expand PDF generation to create a tool that generates your application’s invoices, or [crawl each page](https://github.com/GoogleChromeLabs/puppeteer-examples/blob/master/crawlsite.js) of an application on your production and development branches to compare screenshots, and build complete visual regression testing into your workflow, blocking merges that would cause unwanted changes or distortions to the UI.

You can find the source code for this post on [GitHub](https://github.com/platformsh/chrome-headless-demo). For more information about Puppeteer and Headless Chrome, you can check out these resources:

* [Live demo of this application](https://master-7rqtwti-vt64lpqxlrukk.eu-3.platformsh.site/)
* [Platform.sh Headless Chrome documentation](https://docs.platform.sh/configuration/services/headless-chrome.html)
* [Puppeteer GitHub](https://github.com/GoogleChrome/puppeteer)
* [Puppeteer documentation](https://pptr.dev/)
* [Getting Started with Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome)
