ScreenshotNeo

BlogHow-to

How to Render an HTML Dashboard to PDF on a Schedule

Build a browser script that prints a dashboard to PDF, then schedule it with GitHub Actions. Configure readiness, layout, credentials, storage, and failure handling.

By the ScreenshotNeo team4 October 202611 min read

To render an HTML dashboard to PDF on a schedule, use a browser automation script to load the dashboard, wait until its data is ready, and print it to PDF. Then have a scheduler run that script at the cadence you need. Puppeteer and Playwright both support PDF generation; this guide uses Puppeteer with GitHub Actions as a concrete example.

The browser script and the scheduler are separate pieces. Your dashboard’s authentication, readiness signal, timezone, and PDF destination determine the production details, so make those choices explicit before relying on the export.

1. Choose the browser and scheduler

Use the browser framework that fits your project’s runtime and API familiarity. Puppeteer documents the flow of launching a browser, navigating to a page, calling page.pdf(), and closing the browser. Playwright also supports page PDF generation with options for paper size, margins, backgrounds, and related print settings. Neither is universally better; compare the framework you already maintain and the layout options your report needs.

This example uses Node.js, Puppeteer, and GitHub Actions. A scheduler can run the same script on a virtual machine, a container platform, or another cron-capable workflow runner. GitHub Actions is one option, not a guarantee of exact-time execution.

2. Create a PDF script with an explicit readiness check

Install Puppeteer in a Node.js project:

npm install puppeteer

Save this as render-dashboard.js. It takes the dashboard URL from an environment variable, waits for a dashboard-specific ready selector, and writes the PDF to a path supplied by the workflow. Replace #dashboard-ready with an element your application shows only after its data has loaded.

const fs = require('node:fs/promises');
const puppeteer = require('puppeteer');

async function main() {
  const url = process.env.DASHBOARD_URL;
  const outputPath = process.env.PDF_PATH || 'dashboard.pdf';
  const readySelector = process.env.READY_SELECTOR || '#dashboard-ready';

  if (!url) throw new Error('Set DASHBOARD_URL');
  if (!/^https?:\/\//i.test(url)) throw new Error('DASHBOARD_URL must use http or https');

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    page.setDefaultNavigationTimeout(60_000);
    page.setDefaultTimeout(60_000);

    // Supply authentication only if your dashboard requires it.
    if (process.env.DASHBOARD_USER && process.env.DASHBOARD_PASSWORD) {
      await page.authenticate({
        username: process.env.DASHBOARD_USER,
        password: process.env.DASHBOARD_PASSWORD,
      });
    }

    const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
    if (!response || !response.ok()) {
      throw new Error(`Dashboard navigation failed: ${response ? response.status() : 'no response'}`);
    }

    // Prefer an application-owned signal that means data and charts are ready.
    await page.waitForSelector(readySelector, { visible: true });

    // Optional app-specific condition; expose this flag once chart rendering is complete.
    if (process.env.WAIT_FOR_DASHBOARD_FLAG === 'true') {
      await page.waitForFunction(() => window.dashboardRenderComplete === true);
    }

    await page.pdf({
      path: outputPath,
      format: process.env.PDF_FORMAT || 'A4',
      landscape: process.env.PDF_LANDSCAPE === 'true',
      printBackground: true,
      preferCSSPageSize: true,
      displayHeaderFooter: false,
      margin: { top: '12mm', right: '10mm', bottom: '12mm', left: '10mm' },
      timeout: 60_000,
    });

    await fs.access(outputPath);
    console.log(`Wrote ${outputPath}`);
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

page.authenticate() covers HTTP basic authentication. For a login form, use the application’s supported login flow or a dedicated report credential, then verify that navigation actually reaches the dashboard. Do not put passwords in the script or commit them to the repository.

The script checks the HTTP response and readiness selector before printing. A successful navigation alone does not prove the dashboard’s asynchronous data finished loading. Add an application-owned signal, such as a ready element or JavaScript flag, after the relevant charts and tables finish rendering.

3. Set print styling and PDF layout

Puppeteer PDF output uses print CSS by default. If the dashboard has a print stylesheet, use it to hide navigation, set page breaks, and make wide charts fit. If the report should retain screen styles instead, emulate screen media before calling page.pdf():

await page.emulateMediaType('screen');

Place that line after the readiness check and before page.pdf(). Print rendering may change colors. For exact colors, the Puppeteer documentation points to CSS -webkit-print-color-adjust; use it selectively because preserving every screen color can make printed pages use more ink.

Relevant PDF settings in the example:

  • format: a paper size such as A4 or Letter. You can instead define width and height for a custom page size.
  • landscape: use landscape for wide dashboards, then inspect whether labels remain readable.
  • margin: adjust whitespace around content; use small margins only if the dashboard layout needs them.
  • printBackground: include background colors and graphics. Set it to false for a lighter, more printer-friendly document.
  • preferCSSPageSize: let CSS @page size rules control the page size when the site defines them.
  • displayHeaderFooter: enable built-in header/footer templates if you need printed page metadata. Puppeteer supports template fields such as page number and URL; test their layout with your report.

Long dashboards can split charts or tables across pages; wide ones can be clipped or scaled. Add print-specific CSS such as break-inside: avoid to important chart containers and deliberate @page rules, then review representative PDFs. Browser PDF output is paginated, so it will not always match a continuous screenshot.

4. Schedule the script with GitHub Actions

Commit the script and a package lockfile. Add a workflow at .github/workflows/dashboard-pdf.yml:

name: Scheduled dashboard PDF

on:
  schedule:
    - cron: '17 7 * * *'
  workflow_dispatch:

jobs:
  render:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
      DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
      DASHBOARD_PASSWORD: ${{ secrets.DASHBOARD_PASSWORD }}
      PDF_PATH: dashboard.pdf
      READY_SELECTOR: '#dashboard-ready'
      PDF_FORMAT: A4
      PDF_LANDSCAPE: 'true'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: node render-dashboard.js
      - uses: actions/upload-artifact@v4
        with:
          name: dashboard-pdf
          path: dashboard.pdf
          if-no-files-found: error

Add DASHBOARD_URL and, if needed, the credentials as repository or environment secrets in GitHub. This example stores the resulting PDF as a workflow artifact. If you need a longer retention period or delivery to a team, add a separately configured upload or delivery step for your chosen destination.

The cron expression above runs daily at 07:17 UTC. GitHub Actions also supports an IANA timezone setting on scheduled workflows. Check the current workflow syntax before using a timezone field in a repository, and account for daylight-saving changes if your desired schedule follows local clock time.

GitHub documents a minimum scheduled interval of five minutes. Scheduled workflows run from the latest commit on the default branch. GitHub also warns scheduled events can be delayed during periods of high load, especially near the start of an hour, and queued jobs can be dropped. Choosing a minute other than zero can avoid a common busy period, but does not make execution exact. Public repository schedules are automatically disabled after 60 days without repository activity. For a strict delivery deadline, choose a scheduler whose documented guarantees meet that requirement.

5. Decide access, output, and operational behavior

  • Authentication: use a least-privilege reporting account where possible. Keep credentials in the runner’s secret store and rotate them through your normal process. Avoid putting sensitive tokens in URLs, where they may appear in logs.
  • PDF destination: workflow artifacts are convenient for retrieval from a run. For a durable report, add a storage or delivery integration and define retention, access control, and naming rules.
  • Retries: a transient dashboard or runner failure can be retried, but retries may create duplicate uploads or notifications. Make the destination naming or delivery step safe to repeat.
  • Monitoring: make the workflow fail when navigation, readiness, or PDF creation fails. Review failed runs and consider a separate notification mechanism appropriate to your team.
  • Data exposure: a PDF may contain confidential dashboard data. Limit artifact access and retention, and avoid printing information the report recipients should not see.
  • Dependencies: use a lockfile and update the browser automation package deliberately. Browser rendering can change with browser versions, so keep a representative output for visual review when you make upgrades.

6. Playwright alternative

If your project already uses Playwright, keep that runtime rather than introducing another browser stack. The equivalent core flow is to launch Chromium, navigate, wait for your dashboard’s readiness condition, and call page.pdf():

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const page = await browser.newPage();
    const response = await page.goto(process.env.DASHBOARD_URL, {
      waitUntil: 'domcontentloaded',
      timeout: 60_000,
    });
    if (!response || !response.ok()) throw new Error('Dashboard navigation failed');
    await page.waitForSelector('#dashboard-ready', { state: 'visible', timeout: 60_000 });
    await page.pdf({
      path: 'dashboard.pdf',
      format: 'A4',
      landscape: true,
      printBackground: true,
      margin: { top: '12mm', right: '10mm', bottom: '12mm', left: '10mm' },
    });
  } finally {
    await browser.close();
  }
})();

Playwright’s PDF API includes paper format, margins, background printing, and header/footer options. Its documentation notes that PDF generation uses print CSS; select screen media if that better matches the intended result. Follow the official API documentation for the installed version’s exact options.

7. Performance, reliability, and cost

Each scheduled run starts a browser, loads the dashboard and its assets, waits for rendering, and produces a PDF. Keep the page focused on the report, avoid unnecessary browser tabs, and close the browser in a finally block as shown. Set navigation, selector, job, and scheduler timeouts so a hung dashboard does not occupy a runner indefinitely.

Use the narrowest readiness condition that still guarantees report completeness. Waiting for all network connections to stop can be unreliable on dashboards that poll or keep live connections open. Puppeteer’s networkidle2 navigation option is an example, not a universal signal that application data is ready. A short fixed delay is easy but can either waste runtime or print stale data; an app-owned ready selector or flag is usually more predictable.

The main cost drivers are scheduled run frequency, browser execution time, and the storage or delivery system you choose. GitHub Actions usage is subject to the repository’s applicable plan and current billing rules; check the platform’s current documentation for your account. The cited scheduling documentation establishes cadence behavior, not a price or exact timing guarantee. Avoid claiming a fixed run duration until you measure it with your own dashboard.

8. Troubleshooting

Symptom Likely cause Fix
PDF shows a login page Credentials are missing, expired, or the dashboard uses a login form instead of HTTP basic auth. Check the run’s final page URL and status. Configure the supported login flow or a report-specific access method, and keep secrets in the runner’s secret store.
Charts or KPI values are missing The page loaded before asynchronous dashboard data or chart rendering completed. Wait for an application-owned readiness selector or flag that is set after data and charts finish. Do not rely on navigation completion alone.
Navigation timeout The page is slow, unreachable from the runner, or keeps network activity open. Check network access and response status. Use domcontentloaded followed by a specific readiness check; raise timeouts only when the dashboard legitimately needs longer.
Fonts differ or text shifts Web fonts had not loaded, were inaccessible, or print styling uses different font rules. Check font requests and readiness, and inspect print CSS. Puppeteer PDF generation waits for fonts by default, but a font that fails to load cannot be used.
Background colors are absent Background printing is disabled or print CSS removes backgrounds. Set printBackground: true and inspect the print stylesheet. Use -webkit-print-color-adjust when exact colors are needed.
Colors or layout differ from the screen PDF generation uses print media by default. Use a print stylesheet for a document layout, or call page.emulateMediaType('screen') before printing to retain screen CSS.
Wide charts are clipped or tiny Portrait paper, fixed-width elements, or print scaling does not suit the dashboard. Try landscape or a custom page size, adjust chart print styles and margins, and inspect a representative export at actual size.
Rows or charts split across pages Content exceeds the printable page height and has no print break rules. Add print-specific break rules to suitable containers and verify that avoiding a break does not create excessive whitespace.
Workflow has no scheduled runs The workflow is not on the default branch, the cron is invalid, or a public repository schedule was disabled after 60 days without activity. Check the default branch, workflow syntax, and repository activity; use manual dispatch to confirm the job itself runs.
PDF artifact is missing The script wrote to another path or failed before PDF creation. Set an explicit output path, check the script log and failure status, and keep if-no-files-found: error so the artifact step signals missing output.
Run starts later than the cron minute Scheduled events may be delayed under load. Allow for delay, avoid minute zero when practical, and use a scheduler with suitable documented guarantees if timing is critical.

Or skip the browser setup

ScreenshotNeo can render a URL to PDF with one GET request, so you do not need to maintain a browser script for the capture step. Add your schedule and destination around the request. See the ScreenshotNeo API documentation for PDF options and request parameters.

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

For a private dashboard, configure its supported authentication and access parameters from the API documentation, and test that the capture sees the intended report. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots per month are free with no card, with paid plans starting at $5 for 3,000. Schedule your request with the scheduler you already use, then sign up for the free plan.

FAQ

Can I export a dashboard every five minutes?

GitHub Actions documents five minutes as its shortest scheduled interval. Its schedule can still be delayed or dropped under load, so do not treat that cadence as exact-time execution.

Will a PDF always look like the dashboard in my browser?

No. PDF generation applies print CSS by default and paginates content. Use screen media when appropriate and test page breaks, colors, and chart sizing with the actual dashboard.

Can I use the same workflow for a dashboard behind single sign-on?

Only if the runner can complete the dashboard’s supported authentication flow. The example’s HTTP basic auth is not a general single sign-on solution; use an approved report credential or authentication flow for your application.

Does GitHub Actions guarantee the PDF will arrive at a precise time?

No. Its documentation describes possible schedule delays and dropped queued work. Use a scheduler with timing guarantees that match your delivery requirement.

Sources