ScreenshotNeo

BlogHTML to image & PDF

How to Create PDF Files from HTML in Go

Create browser-rendered PDFs from HTML in Go with chromedp, or run conversions through Gotenberg. Includes runnable code, print settings, troubleshooting, and tradeoffs.

By the ScreenshotNeo team4 October 202610 min read

Use chromedp to control headless Chrome from Go, wait for the HTML to finish rendering, call Chrome’s Page.printToPDF command, and write the returned bytes to a .pdf file. Chrome does the rendering; chromedp is the Go control client. If you would rather submit documents to a separate conversion service, Gotenberg provides Chromium-backed HTTP endpoints for HTML files and hosted URLs.

This guide covers both approaches, print settings, readiness, deployment, and common failures. For the browser protocol options, see the Page API; for chromedp setup and its PDF example, see the chromedp project and official examples.

1. Create a PDF with chromedp

The basic flow is: create a browser context, navigate to a page, wait for the content you need, print to PDF, then save the bytes. The following example is runnable for a publicly reachable URL. It waits for the document body and uses a timeout so a stalled navigation or render does not block indefinitely.

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/chromedp/cdproto/page"
    "github.com/chromedp/chromedp"
)

func main() {
    if err := createPDF("https://example.com", "output.pdf"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func createPDF(targetURL, outputPath string) error {
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    // Bound browser startup, navigation, readiness and printing.
    ctx, cancelTimeout := context.WithTimeout(ctx, 90*time.Second)
    defer cancelTimeout()

    var pdf []byte
    err := chromedp.Run(ctx,
        chromedp.Navigate(targetURL),
        chromedp.WaitReady("body", chromedp.ByQuery),
        chromedp.ActionFunc(func(ctx context.Context) error {
            data, _, err := page.PrintToPDF().
                WithPrintBackground(true).
                WithPreferCSSPageSize(true).
                Do(ctx)
            if err != nil {
                return fmt.Errorf("print page to PDF: %w", err)
            }
            pdf = data
            return nil
        }),
    )
    if err != nil {
        return fmt.Errorf("render %q: %w", targetURL, err)
    }
    if len(pdf) == 0 {
        return fmt.Errorf("browser returned an empty PDF")
    }
    if err := os.WriteFile(outputPath, pdf, 0o644); err != nil {
        return fmt.Errorf("write %q: %w", outputPath, err)
    }
    return nil
}

Initialize the module and install the dependency with the normal Go module workflow:

go mod init example.com/htmltopdf
go get github.com/chromedp/chromedp
go run .

The example enables background printing and prefers CSS page dimensions. Those choices are explicit because protocol defaults do not enable background printing and do not prefer CSS page size. Remove or change these settings if they do not fit your document.

HTML templates and local assets

For an HTML template generated in your Go process, make it available to Chrome in a way that preserves asset resolution. Common approaches are serving the document from a temporary local HTTP server or writing an HTML file and navigating Chrome to its file URL. Relative stylesheet, font and image paths must resolve from Chrome’s point of view; an in-memory HTML string alone does not make relative assets available.

If you serve a temporary page, bind to loopback, use a unique path or port, and stop the server after capture. If the page uses JavaScript to populate content, wait for an application-specific signal such as a report container becoming visible or a known “render complete” attribute. Waiting for body only confirms that an element exists; it does not prove that asynchronous data, fonts, images or charts are ready.

2. Control page size, margins and print output

Chrome’s print command accepts layout options through the generated page.PrintToPDF builder. Choose the paper, orientation and margins deliberately, especially for invoices, reports and wide tables.

Setting What it controls Practical guidance
Paper width and height PDF page dimensions in inches Set for a non-default paper size. For standard paper, CSS @page can define dimensions when CSS size is preferred.
Landscape Page orientation Use for wide content; check that columns still fit.
Margins Top, bottom, left and right printable space Set values that leave room for content and any header/footer.
Scale Content scale on the page Adjust only after checking paper size, margins and CSS layout.
Print background Background colors and images Enable when the design relies on them; it is disabled by default in the cited API options.
Prefer CSS page size Whether CSS @page dimensions take precedence Enable when the document stylesheet defines its intended paper size. The protocol option defaults to false.
Page ranges Which pages to include Ranges are one-based. Validate ranges against the actual page count.
Header/footer HTML Printed header and footer templates Use when page numbers or document labels are needed; account for their space in the margins.
Tagged PDF and document outline Accessibility structure and outline generation Enable when the consuming workflow needs these outputs; both options default to false in the cited API options.

A representative call with explicit paper dimensions and margins looks like this:

data, _, err := page.PrintToPDF().
    WithLandscape(false).
    WithPaperWidth(8.5).
    WithPaperHeight(11).
    WithMarginTop(0.5).
    WithMarginBottom(0.5).
    WithMarginLeft(0.5).
    WithMarginRight(0.5).
    WithPrintBackground(true).
    WithPreferCSSPageSize(true).
    Do(ctx)

Builder method availability follows the version of github.com/chromedp/cdproto in your module. Consult that version’s generated Page API when setting additional fields such as ranges, scale, header/footer templates, tagged output or outlines. CSS also affects pagination: use print styles such as @media print, @page, and page-break rules, then inspect output containing long tables, large images and multi-page sections.

3. Make rendering readiness explicit

Browser navigation completion is not always the same as “the document is ready to print.” A page may still be fetching data, rendering a client-side application, loading a web font, or decoding images. Choose a readiness condition that matches the document:

  • Static HTML: waiting for navigation and a document element may be enough when all styles and assets load directly.
  • Client-rendered page: wait for a stable selector or application-provided ready signal after data and layout are complete.
  • Known delay: a fixed delay can help with a page that has no readiness signal, but it is less reliable and may waste time or still be too short.
  • External assets: verify that the browser can reach the asset hosts and that failed requests do not leave missing fonts or images.

Prefer a selector or app-ready signal over an arbitrary sleep. If you own the page, expose a predictable marker after rendering is complete. Treat timeouts as errors and return enough context to identify the target URL and failed stage.

4. Use Gotenberg when conversion belongs in a service

Gotenberg packages document conversion behind a Docker-based HTTP API. This shifts browser deployment and conversion into a separately operated service; the Go program submits a request and receives a PDF. Its Chromium routes support uploading HTML and assets or converting a hosted URL. See the HTML conversion documentation and Gotenberg introduction.

Convert an HTML file and assets

The HTML conversion route expects a multipart upload with a file named index.html. Supporting files can be uploaded alongside it. First start Gotenberg using the deployment instructions in its official documentation, then submit the form:

curl --fail-with-body \
  -F 'files=@index.html' \
  -F 'files=@styles.css' \
  -F 'printBackground=true' \
  http://localhost:3000/forms/chromium/convert/html \
  -o output.pdf

Use filenames and relative references consistently so the HTML resolves its uploaded assets. For production, configure the service and its network access according to your environment rather than exposing an unauthenticated conversion endpoint publicly.

Convert a hosted URL from Go

Gotenberg’s URL route is POST /forms/chromium/convert/url; the URL field is required. A Go client can submit multipart form data and save the response:

package main

import (
    "context"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/textproto"
    "os"
    "strings"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
    defer cancel()

    var body strings.Builder
    writer := multipart.NewWriter(&body)
    field, err := writer.CreateFormField("url")
    if err != nil {
        panic(err)
    }
    if _, err := io.WriteString(field, "https://example.com"); err != nil {
        panic(err)
    }
    if err := writer.Close(); err != nil {
        panic(err)
    }

    req, err := http.NewRequestWithContext(ctx, http.MethodPost,
        "http://localhost:3000/forms/chromium/convert/url", strings.NewReader(body.String()))
    if err != nil {
        panic(err)
    }
    req.Header.Set("Content-Type", writer.FormDataContentType())

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        message, _ := io.ReadAll(resp.Body)
        panic(fmt.Sprintf("Gotenberg returned %s: %s", resp.Status, message))
    }
    out, err := os.Create("output.pdf")
    if err != nil {
        panic(err)
    }
    defer out.Close()
    if _, err := io.Copy(out, resp.Body); err != nil {
        panic(err)
    }
}

// Keep imports used if extending the form with file parts.
var _ = textproto.MIMEHeader{}

The example keeps the upload shape visible; the unused textproto import and sentinel can be omitted as written. A shorter version of the import list is preferable in a production file.

Gotenberg documents print background behavior, paper layout, waiting options and controls for failed resources or HTTP status codes. Its documented defaults include US Letter dimensions, 0.39-inch margins and backgrounds off. Specify settings when defaults are unsuitable, and consult the deployed version’s documentation for exact field names and supported controls. The URL conversion route does not accept file://; use the HTML upload route for local files.

5. Choose the deployment model

Question chromedp in the Go process Gotenberg service
Where does Chrome run? With the application runtime, which needs access to a compatible browser. In the separately deployed Docker-based conversion service.
How is conversion called? Go browser actions and direct DevTools Protocol options. HTTP multipart requests to conversion endpoints.
When does it fit? When the app needs direct browser control and can own browser setup. When the team wants a distinct conversion API boundary.
What needs operational attention? Browser installation, process lifecycle, cancellation, fonts, network access and resource limits. Service deployment, service access, uploaded assets, network access and request handling.

Another Go browser driver is go-rod/rod, whose official examples include PDF methods. It is reasonable to compare its API if selecting a DevTools Protocol driver. For a browser-free-from-your-Go-process architecture, Gotenberg is an HTTP service, but it still uses Chromium to render the document.

6. Troubleshooting

Symptom Likely cause Fix
Chrome cannot start No compatible browser binary, missing runtime dependencies, or an unsuitable container setup. Install/provide Chrome or Chromium for the deployment. The chromedp project points to its headless-shell image as an option for headless deployments.
Navigation or printing times out The page is slow, blocked on a resource, waiting on client-side work, or the timeout is too short. Set a context deadline appropriate to the workload; identify a readiness selector; check network access and failed resources.
PDF is blank or content is missing Printing started before application rendering finished, or content is hidden in print styles. Wait for an app-ready signal; inspect @media print rules and the rendered page before printing.
Styles, fonts or images are absent Relative paths do not resolve, remote assets are inaccessible, or assets have not loaded. Serve or upload assets at resolvable paths; check browser network access and wait for required content.
Background colors are missing Background printing was not enabled. Call WithPrintBackground(true) or set the corresponding Gotenberg form option.
CSS paper size is ignored CSS page size preference is disabled, or CSS @page rules are missing or overridden. Enable CSS page-size preference and check the print stylesheet.
Content is clipped or scaled unexpectedly Paper dimensions, margins, orientation, scale and CSS widths conflict. Set paper and margins intentionally, check landscape mode, then review print CSS and scale.
Gotenberg rejects the URL request The URL field is absent or the input is a file:// URL. Supply a reachable HTTP(S) URL, or upload local HTML through the HTML conversion endpoint.
Gotenberg output omits uploaded assets Asset files were not uploaded or the HTML references do not match their uploaded names. Include supporting files in the multipart request and align relative asset paths.
Output file is empty or truncated The response was not checked, the body was not fully copied, or a write failed. Check the browser error and PDF byte length; for HTTP, validate status before streaming the full response and handle write errors.

7. Performance, reliability and cost

The cited project and protocol documentation do not provide a suitable benchmark, so do not estimate conversion speed from these examples. Measure with your own representative HTML, fonts, images, page lengths and concurrency. Browser startup, page rendering, external requests and PDF output size all affect the work a deployment must handle.

For reliability, use deadlines, propagate cancellation, return stage-specific errors, and monitor browser or conversion-service failures. Test documents with realistic long tables, page breaks, large images and missing resources. A lost or terminated browser connection cancels the chromedp context, so callers should treat that as a failed conversion and decide whether a retry is safe. Retries can repeat page-side requests, so avoid assuming conversion is side-effect free for arbitrary URLs.

Cost depends on how you run the browser or service: the reviewed sources do not establish a per-conversion price. Account for the compute and operational resources of the chosen deployment, and compare that with any hosted service you evaluate using its own published terms. Limit concurrency and input sizes according to the capacity you observe; these are operational controls rather than documented chromedp performance guarantees.

8. Or skip the browser setup

If your goal is a PDF of a hosted page rather than a Go-owned HTML document, ScreenshotNeo can return a PDF from one GET request. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf

ScreenshotNeo removes cookie banners, newsletter popups and chat widgets before capture. Bot checks, blank pages and failed loads are never billed, and responses identify page verdict and billing status in headers. Its MCP server lets AI agents use take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo is a website capture API, so use the Go and Gotenberg approaches above when you need to render your own HTML template and control its print styles.

Sign up free for 1,000 screenshots a month, with no card.

FAQ

Does chromedp convert HTML itself?

No. It drives a Chrome-compatible browser through the Chrome DevTools Protocol; the browser renders the page and produces the PDF.

Can I convert an HTML string without hosting it?

The printing mechanism is the same, but the browser still needs a document URL or another content-loading method. Choose a method that also makes relative assets resolvable, such as serving the HTML and assets locally.

Can Gotenberg convert a local file URL?

No. Its Chromium URL route rejects file://. Upload local HTML and its supporting assets through the HTML conversion endpoint.

Which option should I start with?

Start with chromedp when the Go service should directly control Chrome. Start with Gotenberg when conversion should sit behind a separate HTTP service operated by your team.