HTML to PDF using a Chrome puppet in the cloud

HTML to PDF using a Chrome puppet in the cloud

<strong>Originally published by </strong><a href="https://medium.com/@keith.coughtrey" target="_blank">Keith Coughtrey</a><strong> </strong><em>at&nbsp;</em><a href="https://medium.com/@keith.coughtrey/html-to-pdf-using-a-chrome-puppet-in-the-cloud-de6e6a0dc6d7" target="_blank"><em>Medium</em></a>

I’m going to take you through the process of setting up a headless chrome browser that you can run on AWS and use an API to do most of the things a browser can do. Our target for today is to have chrome navigate to a URL, wait for the page to fully-load and then create a PDF.

The chromium team have released the headless chrome node API Puppeteer.

https://github.com/GoogleChrome/puppeteer

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

There is also a really useful site where you can go and try puppeteer: https://try-puppeteer.appspot.com/. There sample code they provide to create a pdf looks like this:

const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
  await page.pdf({
    path: 'hn.pdf',
    format: 'letter'
  });
await browser.close();

The API being used above is very well documented here. Looking at page.pdf we see that the function takes and array of options and returns a promise which resolves with a PDF buffer. The options give you a good deal of control. You can set a path to save the pdf if you don’t want to consume the buffer, control headers, footers and page formatting, among other things.

Building and deploying to AWS

Before we get started you will need node8.10 and npm installed on your machine and you will need an AWS account to deploy your code to. AWS Lambda has a reasonably generous free tier — see AWS Lambda Pricing

Serverless

I’m going to use the serverless framework, which I find to be the easiest way to deploy to AWS. If you haven’t used serverless before, start by installing the cli:

npm install -g serverless

You then need to set up your AWS credentials:

Once you’ve finished the setup, create your project.

serverless create --template aws-nodejs --path ./lambda-puppeteer

This will create the lambda-puppeteer folder containing a basic javascript lambda deployment project.

My preference is to use typescript rather than plain javascript so we will convert the project to typescript below. The serverless template aws-nodejs-typescript could be used above but it creates a project that misses out a number of useful comments and it includes webpack, which we don’t need.

cd lambda-puppeteer

The serverless.yml files contains all the configuration necessary to deploy you project and the template creates a project that can be deployed and tested straightaway.

serverless deploy -v

Now test your function and look at the logs with these commands:

serverless invoke -f hello -l
serverless logs -f hello -t

Chromium and puppeteer core

Lambda has a 50Mb deployment limit (unless using layers) but the community has provided an easy way to deploy everything needed in a package of about 35Mb. We will use this library to get the chromium dependencies we need:

https://github.com/alixaxel/chrome-aws-lambda

Initialise node package manager:

npm init

Just accept the defaults for the project setup.

Add chromium:

npm i chrome-aws-lambda --save

and puppeteer-core, which is a version of Puppeteer that doesn’t download Chromium by default:

npm i puppeteer-core --save

Using typescript

There are a number of ways to configure your project for typescript such as using the serverless-plugin-typescript. In this case we’re going to manually convert the project in five steps:

  1. install typescript
npm i --save-dev typescript

  1. rename handler.js to handler.ts

  2. install node types:

npm i @types/node

  1. Add a tsconfig.json file with the following content:
{
	  "compilerOptions": {
	    "lib": ["es6"],
	    "module": "commonjs",
	    "noImplicitReturns": true,
	    "outDir": "lib",
	    "sourceMap": true,
	    "target": "es6",
	    "skipLibCheck": true
	  },
	  "compileOnSave": true,
	  "include": [
	    "*.ts"
	  ]
	}

  1. Add these two scripts to package.json:
"scripts": {
  "build": "tsc",
  "deploy": "npm run build && serverless deploy",
  ...
},

Here we’ve added a deploy command that will compile typescript and do a serverless deploy. You could also run tests as part of the deploy by defining a test script and changing deploy to npm run build && npm run test && serverless deploy.

Implementing the service

Our pdf service will have the following interface:

export interface PdfService {
  getPdf(url: string): Promise<Buffer>;
}

We expose a single function that accepts a URL parameter and returns a promise of a Buffer containing the PDF of the content of the URL.

Create a file named pdf-service.ts and add the interface code above to it.

The implementation of the interface looks like this:

import chromium = require('chrome-aws-lambda');
	import puppeteer = require('puppeteer-core');
	

	export class ChromePdfService implements PdfService {
	  public async getPdf(url: string): Promise<Buffer> {
	    console.log(`Generating PDF for ${url}`);
	

	    let browser = null;
	    try {
	      browser = await puppeteer.launch({
	        args: chromium.args,
	        defaultViewport: chromium.defaultViewport,
	        executablePath: await chromium.executablePath,
	        headless: chromium.headless,
	      });
	

	      const page = await browser.newPage();
	

	      await page.goto(url, {
	        waitUntil: ['networkidle0', 'load', 'domcontentloaded'],
	      });
	      const result = await page.pdf({
	        printBackground: true,
	        format: 'A4',
	        displayHeaderFooter: false,
	      });
	      console.log(`buffer size = ${result.length}`);
	      return result;
	    } catch (error) {
	      throw new Error(`Failed to PDF url ${url} Error: ${JSON.stringify(error)}`);
	    } finally {
	      if (browser !== null) {
	        await browser.close();
	      }
	    }
	  }
	}

Add the implementation code above to pdf-service.ts so that it contains both the interface and the implementation.

This code expands on the simple example near the beginning of this post. One thing to note is the waitUntil options I have included. This setting determines when to consider navigation has succeeded and it defaults to load. When you specify an array of event strings, navigation is considered to be successful after all events have been fired.

  • load - consider navigation to be finished when the load event is fired.
  • domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.
  • networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.

So capturing the pdf does not proceed until the last of these three have completed.

Wiring up to an https endpoint

To make our service callable, we change the handler code to:

import { ChromePdfService, PdfService } from './pdf-service';
	

	const pdfService: PdfService = new ChromePdfService();
	

	module.exports.pdfReport = async (event, context, callback) => {
	  console.log(`pdfReport request ${JSON.stringify(event, null, 4)}`);
	  const url = event.query.url;
	  const buffer = await pdfService.getPdf(url);
	  callback(null, buffer.toString('base64'));
	};

Here we convert the buffer returned from our PdfService to a base64 string.

Finally, we add an https endpoint /pdf to call our function by replacing the functions section of serverless.yml with:

functions:
  pdfReport:
    handler: lib/handler.pdfReport
    events:
     - http:
        path: pdf
        method: get
        integration: lambda

Note that the handler path of lib matches the outDir specified in tsconfig.json above.

Deploy your service using the deploy script we defined in package.json:

npm run deploy

After the deployment has finished we can call our pdf service by going to the url allocated by the serverless deploy, for example:

https://<your project id and region>.amazonaws.com/dev/pdf?url=https://example.com

If all is well, this should return a long base64 text response. If we use an online base64 to pdf converter (eg base64.guru) to convert the text of the response to a pdf we can see the result.

Returning application/pdf

By changing some settings in API gateway you can have your endpoint return the correct Content-Type to be displayed as a PDF. There is a serverless plugin that is meant to automate these settings:

https://www.npmjs.com/package/serverless-plugin-custom-binary

I wasn’t able to get it to work but it may work for you. However, I was able to make the change manually following these instructions, but it’s not ideal to have configuration outside of your serverless deployment.

Adding header and footer

You can add your own HTML markup to create custom page headers and footers. One thing to note is that none of the stylesheets from the page are available so any styling needs to be done inline.

The header and footer markup can contain the following classes used to inject printing values into them:

  • date formatted print date
  • title document title
  • url document location
  • pageNumber current page number
  • totalPages total pages in the document

Here’s an example of adding a footer containing page numbers:

 const result = await page.pdf({
	        printBackground: true,
	        format: 'A4',
	        displayHeaderFooter: true,
	        footerTemplate: `
	        <div style="font-size:10px; margin-left:20px;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>
	        `,
	        margin: {
	          top: '20px',
	          right: '20px',
	          bottom: '50px',
	          left: '20px',
	        },
	      });

This is how it looks on the page:

Of course using page.pdf is just one example of the many things you can do with chrome using the puppeteer API.

That completes today’s post. Remember to delete your AWS resources when you’ve finished using serverless remove .

In my next post we’ll add PDF password protection using a command-line tool and in the third post of the series I will cover calling the PDF service from an AWS step function.

Render HTML with Vanilla JavaScript and lit-html

Render HTML with Vanilla JavaScript and lit-html

Sometimes you need to render HTML elements on a web page. And like Goldilocks' search for "just right", you have to try a few techniques before you find the right one. Using a framework may be too hard. Using pure HTML and the DOM API may be too soft. What you need is something in the middle that is just right. Is lit-html "just right"? Let's find out.

Sometimes you need to render HTML elements on a web page. And like Goldilocks' search for "just right", you have to try a few techniques before you find the right one. Using a framework may be too hard. Using pure HTML and the DOM API may be too soft. What you need is something in the middle that is just right. Is lit-html "just right"? Let's find out.

First, I'll show how this all works. Then at the end of this article, I'll explain everything you need to get started with lit-html to try this for yourself.

When you're done, you can push your HTML app with lit-html to the cloud to see it in all of its glory! I included a link to a free Azure trial, so you can try it yourself.

Resources:

The Sample App

Here is the app I'll demonstrate in this article. It fetches a list of heroes and renders them when you click the button. It also renders a progress indicator while it is fetching.

What's the Value of lit-html

When you focus on rendering content, and nothing else, lit-html is a good fit. It works closely with the DOM to render content, and refresh it in an optimal manner. The docs can provide you with more details, but the basic code for lit-html looks like this.

// Credit: As seen in official docs https://lit-html.polymer-project.org/guide/getting-started

// Import lit-html
import { html, render } from 'lit-html';

// Define a template
const myTemplate = name =>
  html`
    <p>Hello ${name}</p>
  `;

// Render the template to the document
render(myTemplate('World'), document.body);

You import lit-html, define a template, then render it to the DOM. That's it!

Rendering HTML

A progress bar is fairly basic. There is some HTML, and we show it when needed and hide it when it is not required. While we could use a template, or innerHTML, or the DOM API for this, let's see what this would look like with lit-html.

First, we get a reference to the element in the DOM where the progress bar will appear.

Then we define the template. This code looks and feels like JSX (or TSX). The advantage here is that you can write the HTML. You wrap the HTML in a template string (notice the back-tick character is used and not a single quote). Template strings allow you to span lines and insert variables where needed (we'll see this soon). The magic that makes this work is the html tag that precedes the template string. The html tag is what tells lit-html that you are about to define a template.

Next, we compile the template and pass those results to lit-html's render function, which places the results in the DOM. Finally, we hide or show the progress bar as needed.

function showProgress(show = true) {
  const container = document.getElementById('progress-placeholder');

  const template: () => TemplateResult = () => html`
    <progress class="progress is-medium is-info" max="100"></progress>
  `;
  const result = template();
  render(result, container);

  container.style.display = show ? 'block' : 'none';
}

Now you can run this showProgress function any time you want to show the progress bar.

Note that when a template is re-rendered, the only part that is updated is the data that changed. If no data changed, nothing is updated.

Rendering HTML with Dynamic Values

The progress bar does not change each time it is rendered. You will have situations where you want your HTML to change. For example, you may have a message area on your web app that shows a styled message box with a title and a message. The title and message will change every time you show the message area. Now you have dynamic values.

The HTML is defined with a template string, so it is trivial to add a variable into it. Notice the code below adds a title and text into the template, using the ${data.title} and ${data.text} syntax, respectively.

Then the template is compiled and rendered were needed.

When this template is re-rendered, the only part that is updated is the data that changed. In this case, that's the title and text.

function showMessage(text: string, title = 'Info') {
  const template: (data: any) => TemplateResult = (data: Message) => html`
    <div id="message-box" class="message is-info">
      <h3 class="message-header">${data.title}</h3>
      <p class="message-body">${data.text}</p>
    </div>
  `;

  const el = document.getElementById('message-placeholder');
  const result = template({ title, text });
  render(result, el);

  el.style.visibility = !!text ? 'visible' : 'hidden';
}

Rendering a List

Things get a little more real when we render a list. Let's think about that for a moment. A list requires that we have a plan if there is data and a backup plan if there is no data. A list requires that we render the same thing for each row, and we don't know how many rows we have. A list requires that we pass different values for each row, too. Then we have to take the rows and wrap them in a container such as a <ul> or a <table>.

So there is a little more logic here, regardless of whether we use lit-html or any other technique. Let's explore how the replaceHeroList function renders the rows using lit-html.

function replaceHeroList(heroes?: Hero[]) {
 const heroPlaceholder = document.querySelector('.hero-list');

 // Define the template
 let template: () => TemplateResult;

 if (heroes && heroes.length) {
   // Create the template for every hero row
   template = createList();
 } else {
   // Create the template with a simple "not found" message
   template = () =>
     html`
       <p>heroes not found</p>
     `;
 }

 // Compile the template
 const result = template();

 // Render the template
 render(result, heroPlaceholder);

Notice that when there are heroes, we call the createList function. This function begins by creating an array of TemplateResult. So for every hero in the heroes array, we define a template that represents the <li> containing the HTML that displays that respective hero.

Then we create another template that contains the <ul> and embeds the array of hero templates. It's pretty cool that we can embed templates like this! Finally, we return it all and let the logic compile the templates and render them.

function createList() {
  // Create an array of the templates for each hero
  const templates: TemplateResult[] = heroes.map(hero => {
    return html`
      <li>
        <div class="card">
          <div class="card-content">
            <div class="content">
              <div class="name">${hero.name}</div>
              <div class="description">${hero.description}</div>
            </div>
          </div>
        </div>
      </li>
    `;
  });

  // Create a template that includes the hero templates
  const ulTemplate: () => TemplateResult = () =>
    html`
      <ul>
        ${templates}
      </ul>
    `;
  return ulTemplate;
}

Summary

When you want to render HTML, lit-html is a fast and light-weight option. Is it better than using templates and the DOM API? You'll have to decide what is best for you. But the real story here is that you have another great option to consider when determining the right tool for your job.

Prologue

You can also get editor help with your lit-html templates. Notice the image below shows the syntax highlighting for the HTML template!

Setup

You can install the lit-html package with npm.

npm install lit-html

Alternately you can load it directly from the unpkg.com CDN

import { html, render } from 'https://unpkg.com/lit-html?module';

You have a choice here. npm is my preference, but feel 100% free to use the CDN if that suits you.

TypeScript and lit-html

You only need to include the library for lit-html and you're done. But I like to use TypeScript, and I absolutely recommend enabling your tooling to work great with typeScript and lit-html.

Let me be very clear here - you do not need TypeScript. I choose to use it because it helps identify mistakes while I write code. If you don't want TypeScript, you can opt to use plain JavaScript.

Here are the steps to make TypeScript and lit-html light up together:

  1. Install TypeScript support for lit-html
  2. Configure your tsconfig.json file
  3. Install the VS Code extension for lit-html

Run this command to install the plugin and typescript, as development dependencies to your project.

npm install --save-dev typescript-lit-html-plugin typescript

Edit your tsconfig.json by adding the following to your compilerOptions section.

"compilerOptions": {
  "plugins": [
    {
      "name": "typescript-lit-html-plugin"
    }
  ]
}

Finally, install the VS Code extension for lit-html.

Now you get syntax highlighting for all of your lit-html templates!

HTML 5 tutorial - How to view Google Maps in the HTML Page?

HTML 5 tutorial - How to view Google Maps in the HTML Page?

In this HTML 5 tutorial, we will show you how to view Google Maps in the HTML page in just a few lines of HTML tags, CSS, and Javascript codes.

An example of view Google Maps in HTML page from scratch complete with setup Google Maps Javascript API

We will start this tutorial with the setup of Google Maps API in the Google Developer Console. After that, we will display or view Google Maps in the HTML page using the created Google API Key. Configuration and layout will be done using a little** Javascript** and CSS codes.

Setup Google Maps API Key

To view Google Maps in the HTML page using Javascript API we need a Google Maps API key. The Google Maps JavaScript API lets you customize maps with your own content and imagery for display on web pages and mobile devices. The Maps JavaScript API features four basic map types (roadmap, satellite, hybrid, and terrain) which you can modify using layers and styles, controls and events, and various services and libraries.

Next, open your browser the go to the Google Developer Console. You will be taken to this Google Developer Console page.

Just scroll the default opened projects then it will take to this dialog.

Click the "New Project" button then it will take to this New Google Project page.

Fill the project name and leave other fields as default then click the "Create" button and it will take to the Google Developer Console home page with default opened project. Select again the project then choose the newly created Google project and click the "OK" button. It will take to the new Google project dashboard without any APIs enabled.

Click the "Enable API" button then it will take to this Google APIs library page.

Find and choose Maps Javascript API then it will take to this Maps Javascript API page.

Just click on the "Enable" button then it will take back to the Maps Javascript API dashboard.

Click on Credentials link then it will take to the Credentials page.

Click on "+ Create Credentials" link then choose "API KEY" and it will take to the API KEY dialog. Copy and paste the newly created API KEY to your Notepad or Text Editor for using in the HTML page.

View Google Maps in HTML Page

Now, we will create an HTML page from scratch starting by creating a new HTML file in your project folder. You can create the HTML file using your text editor, IDE, or terminal/command line. Next, open and edit that HTML file then adds these HTML tags that just contain a div for viewing Google Maps.

<!DOCTYPE html>
<html>
  <head>
    <title>HTML Google Maps</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>

As you can see, we declare an HTML 5 page by defining HTML using this tag . Next, include the Google Maps Javascript API before the closing of tag.

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
    async defer></script>

In that Google Maps Javascript API calls include a callback to "initMap" function. Add a Javascript code after or before that Google Maps Javascript API calls.

<script>
  var map;
  function initMap() {
    map = new google.maps.Map(document.getElementById('map'), {
      center: {lat: 53.430938, lng: -2.960841},
      zoom: 17
    });
  }
</script>

Just using the HTML page and Javascript function will not display Google maps in the browser. For that, you have determined the size of MAP DIV using CSS. Add the CSS or STYLE codes before the end of the tag.

<style>
    #map {
      height: 100%;
    }
    html, body {
      height: 100%;
      margin: 0;
      padding: 0;
    }
</style>

Now, you can see the Google Maps in the HTML page like this.

If you see a Grey Google Maps view like ours, that means, your Google developers account to reach the limits of API quota's and it needs to upgrade.

That it's, an example of View Google Maps in HTML Page. You can find the full source code in our GitHub.

Learn HTML - HTML Crash Course For Absolute Beginners

In this course, I wanted to introduce the basic concepts of developing an HTML website, these concepts are the basic backbone used in every website, let it be from your small freelancer portfolio website to those large websites like uDemy, Facebook, Instagram, Google...

Everyone uses HTML, I designed this course and made it specifically HTML, so that you can master this amazing markup language and refer to this course every time you have something you need to do with HTML.

I covered all the HTML tags, HTML definitions, HTML5 tags, in addition to that, I went over some basic concepts of CSS and Javascript. I really hope that you enjoy this course and let me know if you have any questions.


Thanks for watching

If you liked this post, share it with all of your programming buddies!

Follow me on Facebook | Twitter

Further reading

The Web Developer Bootcamp

Build Responsive Real World Websites with HTML5 and CSS3

Advanced CSS and Sass: Flexbox, Grid, Animations and More!

Web Design for Beginners: Real World Coding in HTML & CSS

Foundation CSS Framework - Crash Course for Beginners

Animating SVG with CSS

The CSS Handbook: a handy guide to CSS for developers

11 Amazing CSS You Shouldn’t Miss

How to build a reusable card modal using Vue.js and Tailwind CSS