<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jake</title>
    <description>The latest articles on DEV Community by jake (@jakexkim).</description>
    <link>https://dev.to/jakexkim</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3838932%2F7985cd45-39fb-42e3-bfc4-8acbf05226d2.png</url>
      <title>DEV Community: jake</title>
      <link>https://dev.to/jakexkim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jakexkim"/>
    <language>en</language>
    <item>
      <title>Generate PPTX in Express.js with PaperJSX</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:30:49 +0000</pubDate>
      <link>https://dev.to/jakexkim/generate-pptx-in-expressjs-with-paperjsx-453m</link>
      <guid>https://dev.to/jakexkim/generate-pptx-in-expressjs-with-paperjsx-453m</guid>
      <description>&lt;p&gt;PaperJSX generates PowerPoint files from JSON inside Express route handlers with &lt;span&gt;zero native dependencies&lt;/span&gt;. Install the package, add a POST route, accept a JSON body, call &lt;code&gt;generate()&lt;/code&gt;, and return the buffer with PPTX MIME headers. The complete API endpoint is &lt;span&gt;15 lines&lt;/span&gt; of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;

&lt;p&gt;This guide is Express-specific. Not on Express? See the framework-agnostic hub, &lt;a href="https://dev.to/blog/generate-pptx-from-any-data-source"&gt;generate PPTX from any data source&lt;/a&gt;, or the &lt;a href="https://dev.to/blog/google-sheets-to-pptx"&gt;Google Sheets to PPTX&lt;/a&gt; walkthrough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two packages. No postinstall scripts, no native compilation. PaperJSX has &lt;span&gt;zero native dependencies&lt;/span&gt; — it runs anywhere Express runs, including Docker containers, PM2 clusters, and traditional VPS deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Minimal Express route
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="report.pptx"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening on :3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/pptx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"slides":[{"elements":[{"type":"text","value":"Hello from Express","style":{"fontSize":36,"bold":true}}]}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; test.pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;test.pptx&lt;/code&gt; in PowerPoint, Keynote, or Google Slides. The file is valid OOXML — no &lt;a href="https://dev.to/blog/pptx-repair-dialog-fix"&gt;repair dialog&lt;/a&gt;, no corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you add charts and tables?
&lt;/h2&gt;

&lt;p&gt;The JSON body can include any element type PaperJSX supports. Here is a request body that produces a slide with a bar chart and a data table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slides&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 revenue by region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fontSize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chartType&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;categories&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;series&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;values&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2800&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$4.2M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+10%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$3.1M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+7%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$2.8M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+27%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chart is native and editable — recipients can click it in PowerPoint and modify the data. For combo charts (bar + line on the same axis), set &lt;code&gt;"chartType": "combo"&lt;/code&gt; and add a &lt;code&gt;"type"&lt;/code&gt; field per series. Extended features across all four formats require Pro ($199/mo). For the supported schema, see the &lt;a href="https://paperjsx.com/docs/packages/pptx" rel="noopener noreferrer"&gt;PPTX documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you generate from database data?
&lt;/h2&gt;

&lt;p&gt;In production, PPTX schemas are assembled from database queries — not hardcoded JSON. Here is a route that generates a multi-slide deck from a list of regions stored in a database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;import { db } from "./db.mjs";

app.get("/api/report", async (req, res) =&amp;gt; {
  const regions = await db.query("SELECT * FROM regions");

  const doc = {
    slides: [
      // title slide
      {
        elements: [
          { type: "text", value: "Q3 report",
            style: { fontSize: 36, bold: true } }
        ]
      },
      // one slide per region
      ...regions.map(r =&amp;gt; ({
        elements: [
          { type: "text", value: r.name,
            style: { fontSize: 24, bold: true } },
          {
            type: "chart",
            chartType: "bar",
            data: {
              categories: ["Q1", "Q2", "Q3"],
              series: [{ name: "Revenue", values: r.quarterly }]
            }
          }
        ]
      }))
    ]
  };

  const buffer = await generate(doc);
  res.set({
    "Content-Type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    "Content-Disposition": 'attachment; filename="q3-report.pptx"',
  });
  res.send(buffer);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you add PDF, DOCX, and XLSX?
&lt;/h2&gt;

&lt;p&gt;The same JSON body can produce any of PaperJSX's four output formats. Add a &lt;code&gt;format&lt;/code&gt; query parameter and switch the generator. This is the same pattern used in the &lt;a href="https://dev.to/blog/multi-format-document-generation"&gt;multi-format tutorial&lt;/a&gt; and the &lt;a href="https://dev.to/blog/generate-pdf-nextjs"&gt;Next.js PDF tutorial&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPptx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toDocx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPdf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toXlsx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-xlsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pptx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toPptx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toDocx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.wordprocessingml.document&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toPdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;xlsx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toXlsx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/generate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`attachment; filename="report.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client calls &lt;code&gt;POST /api/generate?format=pdf&lt;/code&gt; with the same JSON body to get a PDF. One codebase, one data structure, four output formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request body size&lt;/td&gt;
&lt;td&gt;Increase Express's JSON limit for schemas with base64 images: &lt;code&gt;app.use(express.json({ limit: '10mb' }))&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;A 10-slide deck generates in ~200ms. Set &lt;code&gt;req.setTimeout(30000)&lt;/code&gt; only for very large documents with many embedded images.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;generate()&lt;/code&gt; is CPU-bound and stateless. Use PM2 cluster mode or Node.js &lt;code&gt;cluster&lt;/code&gt; module to utilize multiple cores.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input validation&lt;/td&gt;
&lt;td&gt;Validate the JSON schema before passing to &lt;code&gt;generate()&lt;/code&gt;. At minimum: verify &lt;code&gt;slides&lt;/code&gt; is a non-empty array and each element has a &lt;code&gt;type&lt;/code&gt; field.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;Wrap &lt;code&gt;generate()&lt;/code&gt; in try/catch. Invalid schemas throw descriptive errors. Return 400 with the error message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Document generation endpoints should be authenticated. Use your existing Express auth middleware (JWT, session, API key).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start generating PPTX in Express — &lt;a href="https://paperjsx.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;read the quick start&lt;/a&gt;, explore the &lt;a href="https://paperjsx.com/docs/packages/pptx" rel="noopener noreferrer"&gt;PPTX reference&lt;/a&gt;, or see the &lt;a href="/docs/hosted/quick-start"&gt;hosted API quick start&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generate PPTX in Hono on Bun with PaperJSX</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:30:48 +0000</pubDate>
      <link>https://dev.to/jakexkim/generate-pptx-in-hono-on-bun-with-paperjsx-11fb</link>
      <guid>https://dev.to/jakexkim/generate-pptx-in-hono-on-bun-with-paperjsx-11fb</guid>
      <description>&lt;p&gt;Hono has tens of millions of &lt;a href="https://www.npmjs.com/package/hono" rel="nofollow noopener noreferrer"&gt;weekly npm downloads&lt;/a&gt; and runs on every JavaScript runtime. PaperJSX generates PPTX, PDF, DOCX, and XLSX from JSON with zero native dependencies. Together: one API endpoint that generates PowerPoint decks on Bun, Node.js, Deno, or Cloudflare Workers — same code, any runtime. This tutorial starts with Bun, then shows how to deploy the same code to other runtimes with zero changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Hono + Bun?
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://www.npmjs.com/package/hono" rel="nofollow noopener noreferrer"&gt;Hono's npm page&lt;/a&gt;, it is "a small, simple, and ultrafast web framework built on Web Standards." Per &lt;a href="https://www.npmjs.com/package/hono" rel="nofollow noopener noreferrer"&gt;its npm page&lt;/a&gt;, it receives roughly &lt;span&gt;47 million weekly downloads&lt;/span&gt; (as of July 2026). According to &lt;a href="https://www.pkgpulse.com/blog/express-vs-hono-2026" rel="nofollow noopener noreferrer"&gt;PkgPulse&lt;/a&gt;, Hono is the recommended default for new greenfield projects in 2026, replacing Express for new API development.&lt;/p&gt;

&lt;p&gt;Hono's architecture makes it ideal for document generation endpoints: zero dependencies, sub-14KB bundle, Web Standard Request/Response objects, and the same code deploys to Bun, Node.js, Deno, Cloudflare Workers, and AWS Lambda. PaperJSX shares the same philosophy — zero native dependencies, pure JavaScript, any runtime.&lt;/p&gt;

&lt;p&gt;Not on Hono? The framework-agnostic pattern lives in &lt;a href="https://dev.to/blog/generate-pptx-from-any-data-source"&gt;generate PPTX from any data source&lt;/a&gt;, and Express users should start with the &lt;a href="https://dev.to/blog/generate-pptx-express"&gt;Express PPTX tutorial&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun create hono@latest doc-api
&lt;span class="c"&gt;# Select: bun&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;doc-api
bun add @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doc-api running on Bun + Hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/generate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Accept an optional format (default: pptx)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing schema.slides&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="deck.pptx"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Generation-Time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run dev
&lt;span class="c"&gt;# Started server at http://localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "schema": {
      "slides": [
        {
          "elements": [
            { "type": "text", "value": "Q3 Report",
              "style": { "fontSize": 36, "bold": true } },
            { "type": "chart", "chartType": "bar",
              "data": {
                "categories": ["NA", "EMEA", "APAC"],
                "series": [
                  { "name": "Revenue", "values": [4200, 3100, 2800] }
                ]
              }
            }
          ]
        }
      ]
    }
  }'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; deck.pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;deck.pptx&lt;/code&gt; in PowerPoint. One slide with a title and a native editable bar chart. The &lt;code&gt;X-Generation-Time&lt;/code&gt; header shows how long generation took — typically 20–50ms on Bun for a simple deck.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you serve multiple formats?
&lt;/h2&gt;

&lt;p&gt;Accept a &lt;code&gt;format&lt;/code&gt; parameter to generate PPTX, PDF, DOCX, or XLSX from the same JSON schema. This is the same &lt;a href="https://dev.to/blog/multi-format-document-generation"&gt;multi-format pattern&lt;/a&gt; adapted for Hono.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPptx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPdf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toDocx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toXlsx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-xlsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pptx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toPptx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toPdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toDocx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.wordprocessingml.document&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;xlsx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toXlsx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;xlsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/generate/:format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`attachment; filename="output.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;POST /generate/pptx&lt;/code&gt;, &lt;code&gt;POST /generate/pdf&lt;/code&gt;, &lt;code&gt;POST /generate/docx&lt;/code&gt;, or &lt;code&gt;POST /generate/xlsx&lt;/code&gt; — one API, four formats, same JSON schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you deploy to other runtimes?
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;src/index.ts&lt;/code&gt; deploys to every Hono-supported runtime. Only the entry point and deployment command change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Run command&lt;/th&gt;
&lt;th&gt;Deploy&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bun run dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker, Fly.io, Railway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx @hono/node-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any Node host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deno&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deno run --allow-net src/index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deno Deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wrangler dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wrangler deploy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;SAM local&lt;/td&gt;
&lt;td&gt;SAM deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vercel dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vercel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Node.js, add the &lt;code&gt;@hono/node-server&lt;/code&gt; adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hono/node-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cloudflare Workers, &lt;code&gt;export default app&lt;/code&gt; is already the correct entry point — Workers use the same &lt;code&gt;fetch&lt;/code&gt; handler that Hono exports by default. PaperJSX's PPTX engine runs within Workers' size limits. For larger format packages (PDF, DOCX, XLSX), use Bun or Node.js where bundle size is not constrained.&lt;/p&gt;

&lt;p&gt;This runtime portability is why Hono is the recommended framework for PaperJSX API endpoints. Write once, deploy anywhere — the same code that runs on Bun locally deploys to Cloudflare Workers in production, or to a Node.js container, or to &lt;a href="https://dev.to/blog/generate-pptx-hono-bun"&gt;Supabase Edge Functions&lt;/a&gt; (which also uses Web Standard APIs via Deno).&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison with Express
&lt;/h2&gt;

&lt;p&gt;For Express users migrating to Hono, the API is familiar but cleaner. See the &lt;a href="https://dev.to/blog/generate-pptx-express"&gt;Express PPTX tutorial&lt;/a&gt; for the Express version — the Hono version is shorter, type-safe, and deploys to more runtimes without adapter libraries.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Hono&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Community types (&lt;code&gt;@types/express&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Native (built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtimes&lt;/td&gt;
&lt;td&gt;Node.js only&lt;/td&gt;
&lt;td&gt;Bun, Node, Deno, CF Workers, Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;~2 MB (with deps)&lt;/td&gt;
&lt;td&gt;14 KB (zero deps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File response&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;res.set()&lt;/code&gt; + &lt;code&gt;res.send()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new Response(buffer, { headers })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PaperJSX integration&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Works (same code, more runtimes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start generating documents with Hono — &lt;a href="/blog/generate-pptx-from-json"&gt;read the PPTX quickstart&lt;/a&gt;, explore &lt;a href="/blog/multi-format-document-generation"&gt;multi-format generation&lt;/a&gt;, or see the &lt;a href="/blog/generate-pptx-express"&gt;Express tutorial&lt;/a&gt; for comparison.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generate PPTX from Google Sheets data with PaperJSX</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:31:16 +0000</pubDate>
      <link>https://dev.to/jakexkim/generate-pptx-from-google-sheets-data-with-paperjsx-2k3d</link>
      <guid>https://dev.to/jakexkim/generate-pptx-from-google-sheets-data-with-paperjsx-2k3d</guid>
      <description>&lt;p&gt;Read data from a Google Sheet, transform it into a PaperJSX JSON schema, and generate a PowerPoint deck with native editable charts — all in a single Node.js script. No copy-paste, no Google Slides, no manual formatting. The output is a real &lt;code&gt;.pptx&lt;/code&gt; file that opens in Microsoft PowerPoint. Run it once manually, schedule it with cron, or trigger it from a webhook. Total setup: &lt;span&gt;15 minutes&lt;/span&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem does this solve?
&lt;/h2&gt;

&lt;p&gt;Sales teams maintain pipeline data in Google Sheets. Every week, a manager needs a PowerPoint deck summarizing the numbers. The current workflow: open the sheet, copy cells, paste into PowerPoint, manually create charts, fix formatting, save, email. This takes 30–60 minutes every week and produces inconsistent results.&lt;/p&gt;

&lt;p&gt;The automated workflow: a Node.js script reads the Google Sheet, builds a PaperJSX JSON schema with charts and tables, generates a PPTX, and saves it to a shared drive or emails it. The script runs in under 5 seconds. The output is consistent every time.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set up Google Sheets API credentials?
&lt;/h2&gt;

&lt;p&gt;For server-side automation, use a Google Cloud service account — no OAuth consent screen, no user login, no browser interaction.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://developers.google.com/workspace/sheets/api/quickstart/nodejs" rel="nofollow noopener noreferrer"&gt;Google Sheets API Node.js quickstart&lt;/a&gt;, the setup requires three steps: enable the Google Sheets API in your Google Cloud project, create a service account with a JSON key, and share the target spreadsheet with the service account's email address (e.g., &lt;code&gt;sheets-reader@your-project.iam.gserviceaccount.com&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Save the downloaded JSON key as &lt;code&gt;credentials.json&lt;/code&gt; in your project directory. Do not commit this file to version control — add it to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Install dependencies
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;google-spreadsheet google-auth-library @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three packages: &lt;a href="https://www.npmjs.com/package/google-spreadsheet" rel="nofollow noopener noreferrer"&gt;google-spreadsheet&lt;/a&gt; (clean wrapper around the Google Sheets API), &lt;code&gt;google-auth-library&lt;/code&gt; (service account authentication), and &lt;code&gt;@paperjsx/json-to-pptx&lt;/code&gt; (PPTX generation from JSON).&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you read data from the sheet?
&lt;/h2&gt;

&lt;p&gt;Assume a Google Sheet called "Q3 Sales" with this structure:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Region | Q1 | Q2 | Q3 | Growth&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NA | 3200 | 3800 | 4200 | 10.5%&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EMEA | 2400 | 2900 | 3100 | 6.9%&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;APAC | 1600 | 2200 | 2800 | 27.3%&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleSpreadsheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google-spreadsheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JWT&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google-auth-library&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHEET_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1abc...your-sheet-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_SERVICE_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googleapis.com/auth/spreadsheets.readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readSalesData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHEET_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sheetsByTitle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 Sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRows&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;google-spreadsheet&lt;/code&gt; package returns rows as objects with column headers as keys. We extract the data we need and convert numeric strings to numbers with &lt;code&gt;Number()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Transform into PaperJSX schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="c1"&gt;// Slide 1: title&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 2026 sales report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Generated &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; from Google Sheets`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#6B6960&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;

      &lt;span class="c1"&gt;// Slide 2: bar chart — revenue by region&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue by region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;chartType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;

      &lt;span class="c1"&gt;// Slide 3: data table&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Regional breakdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;growth&lt;/span&gt;
            &lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;headerBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1A1A18&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;headerColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FFFFFF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transform function maps spreadsheet rows to PaperJSX elements. Charts use &lt;code&gt;data.map()&lt;/code&gt; to extract arrays from the row objects — &lt;code&gt;categories&lt;/code&gt; from the region column, &lt;code&gt;values&lt;/code&gt; from the quarterly columns. This is the same data structure used in the &lt;a href="https://dev.to/blog/generate-pptx-from-json"&gt;JSON-to-PPTX quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Generate the PPTX
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readSalesData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./read-sheet.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildDeckSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./build-schema.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readSalesData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Read &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; regions from Google Sheets`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`q3-sales-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.pptx`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Generated: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOGLE_SERVICE_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sheets-reader@your-project.iam.gserviceaccount.com
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOGLE_PRIVATE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-----BEGIN PRIVATE KEY-----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

node generate.mjs
&lt;span class="c"&gt;# Read 3 regions from Google Sheets&lt;/span&gt;
&lt;span class="c"&gt;# Generated: q3-sales-2026-04-12.pptx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;q3-sales-2026-04-12.pptx&lt;/code&gt; in PowerPoint. It contains three slides: a title slide with the generation date, a bar chart comparing Q2 and Q3 revenue by region (native and editable), and a styled data table with all quarterly figures and growth rates. The chart is a real PowerPoint chart — click it to see the data table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete script
&lt;/h2&gt;

&lt;p&gt;Here is the entire pipeline in one file for easy copy-paste.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleSpreadsheet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google-spreadsheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JWT&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google-auth-library&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHEET_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_SHEET_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Authenticate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_SERVICE_ACCOUNT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;n/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.googleapis.com/auth/spreadsheets.readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Read Google Sheet&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHEET_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sheetsByIndex&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRows&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Build PaperJSX schema&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue by region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;chartType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Breakdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;q3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Generate PPTX&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`report-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.pptx`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✓ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; regions, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; slides)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you schedule it with cron or webhooks?
&lt;/h2&gt;

&lt;p&gt;Run the script every Monday morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Every&lt;/span&gt; &lt;span class="nx"&gt;Monday&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="nx"&gt;AM&lt;/span&gt;
&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nx"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="nx"&gt;sheets&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;pptx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or trigger it from a Google Apps Script when the sheet changes — add a webhook that calls your API endpoint, which runs the generation script and saves the output to Google Drive or sends it via email. For the API route pattern, see the &lt;a href="https://dev.to/blog/generate-pptx-express"&gt;Express tutorial&lt;/a&gt; or the &lt;a href="https://dev.to/blog/generate-pdf-nextjs"&gt;Next.js tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same pipeline works with other data sources. Replace the Google Sheets read with a database query (&lt;a href="https://dev.to/blog/generate-pdf-nextjs"&gt;see the invoice tutorial&lt;/a&gt;), an API response, or a CSV file. PaperJSX does not care where the data comes from — it only cares about the JSON schema.&lt;/p&gt;

&lt;p&gt;Start automating spreadsheet-to-deck workflows — &lt;a href="/blog/generate-pptx-from-json"&gt;read the PPTX quickstart&lt;/a&gt;, explore &lt;a href="/blog/multi-format-document-generation"&gt;multi-format generation&lt;/a&gt;, or see &lt;a href="https://paperjsx.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generate PPTX from Airtable, Notion, or PostgreSQL</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:31:14 +0000</pubDate>
      <link>https://dev.to/jakexkim/generate-pptx-from-airtable-notion-or-postgresql-7jl</link>
      <guid>https://dev.to/jakexkim/generate-pptx-from-airtable-notion-or-postgresql-7jl</guid>
      <description>&lt;p&gt;One pattern, any data source. Read records from Airtable, Notion, or PostgreSQL. Transform rows into a PaperJSX JSON schema. Generate a PPTX with charts. The data source changes; the generation code doesn't. This article shows the same quarterly report deck built from three different data sources — proving the pattern is data-source-agnostic. For Google Sheets, see the &lt;a href="/blog/google-sheets-to-pptx"&gt;dedicated tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The pattern has three steps: &lt;strong&gt;Read&lt;/strong&gt; (fetch records from the data source), &lt;strong&gt;Transform&lt;/strong&gt; (map records to a PaperJSX JSON schema), and &lt;strong&gt;Generate&lt;/strong&gt; (produce the file). Steps 2 and 3 are identical regardless of data source. Only Step 1 changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you read from Airtable?
&lt;/h2&gt;

&lt;p&gt;According to &lt;a href="https://www.npmjs.com/package/airtable" rel="nofollow noopener noreferrer"&gt;Airtable's official npm package&lt;/a&gt;, "the Airtable API is your own RESTful API for your base. You will use your table names to address tables, column names to access data stored in those columns."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;airtable @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Airtable&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;airtable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. READ from Airtable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Airtable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AIRTABLE_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AIRTABLE_BASE_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Grid view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. TRANSFORM to JSON schema&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. GENERATE&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you read from Notion?
&lt;/h2&gt;

&lt;p&gt;Notion's &lt;code&gt;@notionhq/client&lt;/code&gt; package queries databases and returns structured property objects. Each property (title, number, select, date) has a specific type that you extract from the API response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @notionhq/client @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@notionhq/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. READ from Notion database&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTION_DATABASE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;plain_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Revenue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Growth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. TRANSFORM + 3. GENERATE (identical to Airtable)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you read from PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;Direct database queries with the &lt;code&gt;pg&lt;/code&gt; package. No API wrapper, no rate limits, fastest option for large datasets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;pg @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. READ from PostgreSQL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  SELECT region, SUM(amount) as revenue,
         ROUND(100.0 * (SUM(amount) - LAG(SUM(amount))
         OVER (ORDER BY region)) / LAG(SUM(amount))
         OVER (ORDER BY region), 1) as growth
  FROM orders
  WHERE created_at &amp;gt;= '2026-07-01'
  GROUP BY region ORDER BY revenue DESC
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. TRANSFORM + 3. GENERATE (identical)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report.pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The shared transform function
&lt;/h2&gt;

&lt;p&gt;All three integrations use the same &lt;code&gt;buildDeckSchema&lt;/code&gt; function. It is a pure function: data in, JSON schema out. No side effects, no data-source-specific logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDeckSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 2026 revenue report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue by region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;chartType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;}]&lt;/span&gt;
          &lt;span class="p"&gt;}},&lt;/span&gt;
      &lt;span class="p"&gt;]},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Breakdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; parameter is an array of objects with &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;revenue&lt;/code&gt;, and &lt;code&gt;growth&lt;/code&gt; properties. Whether those objects come from Airtable's &lt;code&gt;record.get()&lt;/code&gt;, Notion's &lt;code&gt;page.properties&lt;/code&gt;, PostgreSQL's &lt;code&gt;rows&lt;/code&gt;, or &lt;a href="https://dev.to/blog/google-sheets-to-pptx"&gt;Google Sheets&lt;/a&gt; — the schema doesn't care. This is the &lt;a href="https://dev.to/blog/json-layer-pattern-ai-document-generation"&gt;JSON layer pattern&lt;/a&gt; applied to data integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extend: add more data sources
&lt;/h2&gt;

&lt;p&gt;The pattern works with any data source that returns structured data. Replace Step 1 with your API client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Google Sheets&lt;/strong&gt; — see the &lt;a href="https://dev.to/blog/google-sheets-to-pptx"&gt;dedicated tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Supabase&lt;/strong&gt; — &lt;code&gt;supabase.from("table").select()&lt;/code&gt; (see the &lt;a href="https://dev.to/blog/generate-pptx-hono-bun"&gt;Edge Functions tutorial&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Stripe&lt;/strong&gt; — &lt;code&gt;stripe.invoices.list()&lt;/code&gt; (see the &lt;a href="https://dev.to/blog/generate-pdf-nextjs"&gt;Stripe invoice tutorial&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;REST API&lt;/strong&gt; — &lt;code&gt;fetch("https://api.example.com/data")&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;CSV file&lt;/strong&gt; — parse with &lt;code&gt;papaparse&lt;/code&gt;, transform rows&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;MongoDB&lt;/strong&gt; — &lt;code&gt;collection.find().toArray()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The transform function stays the same. The generate call stays the same. Only the data retrieval changes. For &lt;a href="https://dev.to/blog/multi-format-document-generation"&gt;batch generation&lt;/a&gt; at scale (10,000+ documents from a database), wrap the pattern with &lt;code&gt;p-limit&lt;/code&gt; or BullMQ.&lt;/p&gt;

&lt;p&gt;Start generating documents from your data source — &lt;a href="/blog/generate-pptx-from-json"&gt;PPTX quickstart&lt;/a&gt;, &lt;a href="/blog/google-sheets-to-pptx"&gt;Google Sheets integration&lt;/a&gt;, or &lt;a href="/blog/multi-format-document-generation"&gt;batch generation at scale&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generate PPTX from JSON in 5 minutes</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Thu, 16 Jul 2026 14:31:16 +0000</pubDate>
      <link>https://dev.to/jakexkim/generate-pptx-from-json-in-5-minutes-1306</link>
      <guid>https://dev.to/jakexkim/generate-pptx-from-json-in-5-minutes-1306</guid>
      <description>&lt;p&gt;PaperJSX converts a JSON object to a valid PowerPoint file with one function call. Install &lt;code&gt;@paperjsx/json-to-pptx&lt;/code&gt;, define your slides as a JSON schema with text, charts, images, and tables, call &lt;code&gt;generate()&lt;/code&gt;, and write the resulting buffer to disk or return it from an API endpoint. The entire flow takes under 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  Node.js 18 or later (LTS recommended)&lt;/li&gt;
&lt;li&gt;  npm, yarn, or pnpm&lt;/li&gt;
&lt;li&gt;  A text editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No native dependencies, no binary installs, no LibreOffice subprocess. PaperJSX is pure JavaScript — it runs anywhere Node.js runs, including Vercel Functions, AWS Lambda, Cloudflare Workers, Deno, and Bun.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @paperjsx/json-to-pptx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is &lt;span&gt;&amp;lt;5 MB&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;with zero native dependencies. Compare this to Aspose's per-format SDKs at 50–200 MB each, or wkhtmltopdf-based solutions that require headless browser binaries. According to a 2025 survey by Vercel, cold start time in serverless functions correlates directly with bundle size — lighter packages mean faster first-request latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Define slides as JSON
&lt;/h2&gt;

&lt;p&gt;A PaperJSX document is a plain JavaScript object. The root has a &lt;code&gt;slides&lt;/code&gt; array. Each slide has an &lt;code&gt;elements&lt;/code&gt; array. Each element has a &lt;code&gt;type&lt;/code&gt; and type-specific properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from PaperJSX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1A1A18&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generated from a JSON schema in Node.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#6B6960&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the minimum viable document — one slide, two text elements. The JSON is portable: you can store it in a database, generate it from a template, return it from an API, or have an AI agent produce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generate the PPTX
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from PaperJSX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output.pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done — output.pptx written&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node generate.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Open &lt;code&gt;output.pptx&lt;/code&gt; in PowerPoint, Keynote, Google Slides, or LibreOffice Impress. The file is valid OOXML — no repair dialog, no corruption.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;generate()&lt;/code&gt; function returns a &lt;code&gt;Buffer&lt;/code&gt; in Node.js. It does not write to disk automatically — you control where the output goes. This makes it trivial to return the PPTX from an HTTP endpoint, upload it to S3, or pipe it to a stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Serve from an API route
&lt;/h2&gt;

&lt;p&gt;In production, PPTX generation typically runs server-side behind an API endpoint. Here is a minimal Express route and a Next.js API route that both generate and return a PPTX file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/generate-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="report.pptx"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="report.pptx"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MIME type for PPTX files is &lt;code&gt;application/vnd.openxmlformats-officedocument.presentationml.presentation&lt;/code&gt;. Setting &lt;code&gt;Content-Disposition&lt;/code&gt; to &lt;code&gt;attachment&lt;/code&gt; triggers a download in the browser rather than trying to render the binary content inline.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add charts, images, and tables
&lt;/h2&gt;

&lt;p&gt;Text-only slides are a starting point. Production presentations need charts, images, and tables. Each is an element type in the JSON schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Charts
&lt;/h3&gt;

&lt;p&gt;PaperJSX generates native, editable OOXML charts — not images. According to internal benchmarks, a 10-slide deck with 5 charts generates in under &lt;span&gt;200ms&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;on a 2-core serverless function. Recipients can click the chart in PowerPoint and modify the data directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;chartType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expenses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1900&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;showLegend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported chart types on the free tier include &lt;code&gt;bar&lt;/code&gt;, &lt;code&gt;line&lt;/code&gt;, &lt;code&gt;pie&lt;/code&gt;, &lt;code&gt;scatter&lt;/code&gt;, and &lt;code&gt;area&lt;/code&gt;. Pro - Single ($79/mo) unlocks extended chart and document features for PPTX — capabilities that &lt;a href="https://dev.to/blog/python-pptx-vs-pptxgenjs-vs-paperjsx"&gt;python-pptx has been unable to deliver since 2017&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Images
&lt;/h3&gt;

&lt;p&gt;Images can be passed as file paths, URLs, or base64-encoded strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./logo.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported formats: PNG, JPEG, SVG, GIF, and WebP. SVG images are embedded as vector graphics and remain crisp at any zoom level. PaperJSX does not require images to be base64-encoded (unlike &lt;a href="https://github.com/bpampuch/pdfmake/issues/1905" rel="nofollow noopener noreferrer"&gt;pdfmake&lt;/a&gt;, which requires base64 for all images).&lt;/p&gt;

&lt;h3&gt;
  
  
  Tables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 Revenue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Q3 Growth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;North America&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$4.2M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+12%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$3.1M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+8%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APAC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$2.8M&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+22%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;headerBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#4580B0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;headerColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FFFFFF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;alternateRowFill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#F5F4F0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tables auto-size column widths based on content length. Unlike pdfmake, which has had an &lt;a href="https://github.com/bpampuch/pdfmake/issues/441" rel="nofollow noopener noreferrer"&gt;open issue for tables wider than the page width since 2015&lt;/a&gt;, PaperJSX handles overflow automatically by adjusting column proportions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete JSON schema reference
&lt;/h2&gt;

&lt;p&gt;The full PPTX schema is documented in the &lt;a href="https://dev.to/docs/packages/pptx"&gt;PPTX package guide&lt;/a&gt;. Here is a summary of all element types.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Element type&lt;/th&gt;
&lt;th&gt;Key properties&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;value, style (fontSize, bold, italic, color, align)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;src (path, URL, or base64), style (width, height, x, y)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shapeType (rect, ellipse, arrow), style (fill, stroke)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;headers, rows, style (headerBackground, alternateRowFill)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;chartType, data (categories, series), style (showLegend)&lt;/td&gt;
&lt;td&gt;Free (basic) / Pro (combo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every element also accepts an optional &lt;code&gt;position&lt;/code&gt; object with &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt;, and &lt;code&gt;height&lt;/code&gt; values in inches. If omitted, PaperJSX auto-layouts elements vertically with sensible margins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same JSON, four output formats
&lt;/h2&gt;

&lt;p&gt;The defining feature of PaperJSX is that the JSON schema is format-agnostic. The same object that produces a PPTX file can also produce a PDF, DOCX, or XLSX file — each from a separate &lt;code&gt;generate()&lt;/code&gt; function in a format-specific package.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPptx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pptx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toPdf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;toDocx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@paperjsx/json-to-docx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* same JSON schema */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pptx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;toPptx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;toPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;toDocx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without PaperJSX, the same multi-format output would require PptxGenJS for PPTX, pdfmake or PDFKit for PDF, the docx npm package for DOCX, and ExcelJS for XLSX — four separate libraries with four separate APIs, four separate data structures, and four separate sets of bugs. Public search-demand data shows developers looking for "json to pptx," "json to pdf," and "json to docx" at a combined volume of &lt;span&gt;600–1,400 searches/month&lt;/span&gt;, suggesting strong demand for a unified solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Using &lt;code&gt;require()&lt;/code&gt; instead of &lt;code&gt;import&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;"type": "module"&lt;/code&gt; to &lt;code&gt;package.json&lt;/code&gt; or use &lt;code&gt;.mjs&lt;/code&gt; extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PowerPoint repair dialog&lt;/td&gt;
&lt;td&gt;Invalid element structure in JSON&lt;/td&gt;
&lt;td&gt;Validate your JSON against the &lt;a href="/docs/packages/pptx"&gt;PPTX package guide&lt;/a&gt;. Most common cause: missing &lt;code&gt;type&lt;/code&gt; field on an element.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty slides&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;elements&lt;/code&gt; array is empty or missing&lt;/td&gt;
&lt;td&gt;Ensure every slide object has at least one element in its &lt;code&gt;elements&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts render as blank rectangles&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;series.values&lt;/code&gt; contains non-numeric data&lt;/td&gt;
&lt;td&gt;Ensure all values in the &lt;code&gt;values&lt;/code&gt; array are numbers, not strings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image not found&lt;/td&gt;
&lt;td&gt;Relative path resolves incorrectly in serverless&lt;/td&gt;
&lt;td&gt;Use absolute paths or base64-encoded images in serverless environments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start generating PPTX from JSON — &lt;a href="/docs/getting-started/quick-start"&gt;read the full quickstart&lt;/a&gt;, explore the &lt;a href="/docs/packages/pptx"&gt;PPTX package guide&lt;/a&gt;, or try the &lt;a href="/playground"&gt;playground&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why AI-generated PPTX triggers the repair dialog</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Thu, 16 Jul 2026 14:31:13 +0000</pubDate>
      <link>https://dev.to/jakexkim/why-ai-generated-pptx-triggers-the-repair-dialog-5glh</link>
      <guid>https://dev.to/jakexkim/why-ai-generated-pptx-triggers-the-repair-dialog-5glh</guid>
      <description>&lt;p&gt;If Microsoft's own OpenXML SDK — a purpose-built library maintained by the Office team — has produced files that trigger PowerPoint's repair dialog (&lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1226" rel="nofollow noopener noreferrer"&gt;issue #1226&lt;/a&gt;, &lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1955" rel="nofollow noopener noreferrer"&gt;issue #1955&lt;/a&gt;), an LLM generating raw OOXML has zero chance of doing better. This article explains the &lt;span&gt;seven technical causes&lt;/span&gt; of PPTX corruption, why sequential token generation is architecturally incompatible with OOXML's constraints, and the pattern that eliminates the problem entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is inside a PPTX file?
&lt;/h2&gt;

&lt;p&gt;A PPTX file is not a single document — it is a ZIP archive containing 15–40 interconnected files. Unzip any &lt;code&gt;.pptx&lt;/code&gt; and you will find a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;presentation.pptx/
├── [Content_Types].xml          ← declares every part's MIME type
├── _rels/
│   └── .rels                    ← root relationships
├── ppt/
│   ├── presentation.xml         ← slide list, size, settings
│   ├── _rels/
│   │   └── presentation.xml.rels ← maps rId → slide/master parts
│   ├── slides/
│   │   ├── slide1.xml           ← shapes, text, references
│   │   └── _rels/
│   │       └── slide1.xml.rels  ← slide → layout, images, charts
│   ├── slideLayouts/
│   │   └── slideLayout1.xml
│   ├── slideMasters/
│   │   └── slideMaster1.xml
│   ├── theme/
│   │   └── theme1.xml
│   └── charts/                  ← each chart embeds an Excel workbook
│       ├── chart1.xml
│       └── _rels/
│           └── chart1.xml.rels  ← references embedded .xlsx
├── ppt/embeddings/
│   └── Microsoft_Excel_Worksheet1.xlsx  ← chart data source
└── docProps/
    ├── app.xml
    └── core.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;.rels&lt;/code&gt; file maps relationship IDs (&lt;code&gt;rId1&lt;/code&gt;, &lt;code&gt;rId2&lt;/code&gt;, ...) to target parts. The &lt;code&gt;[Content_Types].xml&lt;/code&gt; file declares the MIME type of every part in the archive. The &lt;code&gt;presentation.xml&lt;/code&gt; file references slides by relationship ID, and each slide references its layout, master, images, and charts by relationship ID.&lt;/p&gt;

&lt;p&gt;PowerPoint validates this entire graph on open. If any reference is broken — an &lt;code&gt;rId&lt;/code&gt; pointing to a part that does not exist, a part not declared in &lt;code&gt;[Content_Types].xml&lt;/code&gt;, or XML elements in the wrong order — PowerPoint shows the repair dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven causes of the repair dialog
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Element ordering violations
&lt;/h3&gt;

&lt;p&gt;OOXML's XSD schema specifies that child elements must appear in a particular order within their parent. This is the most common and most subtle cause of corruption.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- CORRECT: sldLayoutIdLst before AlternateContent --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p:slideMaster&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p:cSld&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p:cSld&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p:sldLayoutIdLst&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p:sldLayoutIdLst&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mc:AlternateContent&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/mc:AlternateContent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p:slideMaster&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- WRONG: reversed order → repair dialog --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p:slideMaster&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p:cSld&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p:cSld&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mc:AlternateContent&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/mc:AlternateContent&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p:sldLayoutIdLst&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/p:sldLayoutIdLst&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p:slideMaster&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to &lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1226" rel="nofollow noopener noreferrer"&gt;OpenXML SDK issue #1226&lt;/a&gt;, this exact element ordering reversal happened in Microsoft's own SDK between versions 2.11 and 2.18. The &lt;code&gt;AlternateContent&lt;/code&gt; and &lt;code&gt;sldLayoutIdLst&lt;/code&gt; elements were swapped, causing repair dialogs for every generated file. If the official SDK maintained by the Office team gets element ordering wrong, the complexity of this constraint should be clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Orphaned relationship IDs
&lt;/h3&gt;

&lt;p&gt;Every reference between parts uses a relationship ID. A slide references its layout as &lt;code&gt;rId1&lt;/code&gt; in &lt;code&gt;slide1.xml.rels&lt;/code&gt;. If the slide XML contains &lt;code&gt;r:id="rId3"&lt;/code&gt; but &lt;code&gt;slide1.xml.rels&lt;/code&gt; has no entry for &lt;code&gt;rId3&lt;/code&gt;, PowerPoint cannot resolve the reference and triggers repair.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Missing content type declarations
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;[Content_Types].xml&lt;/code&gt; file at the root of the ZIP must declare every part. Add a chart to &lt;code&gt;ppt/charts/chart1.xml&lt;/code&gt; without adding &lt;code&gt;&amp;lt;Override PartName="/ppt/charts/chart1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/&amp;gt;&lt;/code&gt; to the content types file, and PowerPoint rejects the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Chart workbook mismatches
&lt;/h3&gt;

&lt;p&gt;PPTX charts embed a complete Excel workbook (&lt;code&gt;.xlsx&lt;/code&gt;) as their data source. According to &lt;a href="https://github.com/OfficeDev/Open-XML-SDK/issues/240" rel="nofollow noopener noreferrer"&gt;OpenXML SDK issue #240&lt;/a&gt;, streaming data to the embedded package part — even using the official SDK — corrupts the PowerPoint file. The chart XML (&lt;code&gt;chart1.xml&lt;/code&gt;) references cells by range, and those ranges must match the structure of the embedded workbook exactly. Any mismatch produces a repair dialog.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Invalid namespace prefixes
&lt;/h3&gt;

&lt;p&gt;OOXML uses multiple XML namespaces (&lt;code&gt;a:&lt;/code&gt; for Drawing, &lt;code&gt;p:&lt;/code&gt; for Presentation, &lt;code&gt;r:&lt;/code&gt; for Relationships, &lt;code&gt;mc:&lt;/code&gt; for Markup Compatibility). If a namespace prefix is used without declaration, or the wrong namespace URI is bound to a prefix, the file is invalid XML before PowerPoint even attempts to parse it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Duplicate IDs
&lt;/h3&gt;

&lt;p&gt;Slide IDs in &lt;code&gt;presentation.xml&lt;/code&gt;, shape IDs in each slide's shape tree, and relationship IDs in each &lt;code&gt;.rels&lt;/code&gt; file must be unique within their scope. Generating two slides with the same &lt;code&gt;id="256"&lt;/code&gt; or two shapes with the same &lt;code&gt;id="2"&lt;/code&gt; triggers validation failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Malformed ZIP structure
&lt;/h3&gt;

&lt;p&gt;OOXML files use ZIP as the container format. The ZIP archive must use the correct compression method (Deflate), must not include extraneous entries, and must end with a valid Central Directory record. Generating the ZIP incorrectly — wrong compression ratio, missing entries, or incorrect byte offsets — produces a file that PowerPoint cannot parse at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does even the official SDK get it wrong?
&lt;/h2&gt;

&lt;p&gt;The most compelling evidence that raw OOXML generation is fragile: Microsoft's own OpenXML SDK has a history of producing files that trigger the repair dialog.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1226" rel="nofollow noopener noreferrer"&gt;Issue #1226&lt;/a&gt; (Nov 2022):&lt;/strong&gt; Element ordering regression — &lt;code&gt;AlternateContent&lt;/code&gt; and &lt;code&gt;sldLayoutIdLst&lt;/code&gt; reversed in versions 2.11–2.18. Every file generated with the affected versions triggered repair. The fix was a single-line element reorder.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1571" rel="nofollow noopener noreferrer"&gt;Issue #1571&lt;/a&gt; (Nov 2023):&lt;/strong&gt; A developer creating a PPTX with tables using the SDK received the repair dialog, and after repair the presentation was empty — all content destroyed.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1955" rel="nofollow noopener noreferrer"&gt;Issue #1955&lt;/a&gt; (Aug 2025):&lt;/strong&gt; Creating a "simple blank pptx" with the SDK triggered the repair dialog. A blank file. With the official SDK.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/dotnet/Open-XML-SDK/issues/1948" rel="nofollow noopener noreferrer"&gt;Issue #1948&lt;/a&gt; (Jun 2025):&lt;/strong&gt; Adding a video element via the SDK corrupted the file. The video appeared after repair but the repair dialog triggered on every open.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/OfficeDev/Open-XML-SDK/issues/240" rel="nofollow noopener noreferrer"&gt;Issue #240&lt;/a&gt; (Aug 2017):&lt;/strong&gt; Streaming chart data to an embedded package part corrupted the PowerPoint file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are bugs in a &lt;strong&gt;purpose-built SDK&lt;/strong&gt; maintained by the team that designed the OOXML specification. The SDK has full access to the XSD schemas, comprehensive test suites, and years of production hardening. It still gets element ordering, relationship management, and embedded data wrong. A general-purpose LLM generating raw XML tokens has none of these safeguards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can't LLMs solve this?
&lt;/h2&gt;

&lt;p&gt;LLMs generate tokens sequentially — each token is predicted based on the tokens that came before it. OOXML requires &lt;strong&gt;global consistency&lt;/strong&gt; across 15–40 files in a ZIP archive. These are fundamentally incompatible requirements.&lt;/p&gt;

&lt;p&gt;Consider what an LLM would need to do to generate a valid PPTX with one chart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Generate &lt;code&gt;[Content_Types].xml&lt;/code&gt; declaring all parts that will exist — but it hasn't generated those parts yet&lt;/li&gt;
&lt;li&gt;  Generate &lt;code&gt;presentation.xml.rels&lt;/code&gt; with relationship IDs that will match references in slides not yet generated&lt;/li&gt;
&lt;li&gt;  Generate &lt;code&gt;slide1.xml&lt;/code&gt; with a chart reference (&lt;code&gt;r:id="rId2"&lt;/code&gt;) that matches an entry in &lt;code&gt;slide1.xml.rels&lt;/code&gt; not yet generated&lt;/li&gt;
&lt;li&gt;  Generate &lt;code&gt;chart1.xml&lt;/code&gt; with cell references (&lt;code&gt;Sheet1!$A$1:$B$4&lt;/code&gt;) that match the structure of an embedded workbook not yet generated&lt;/li&gt;
&lt;li&gt;  Generate the embedded &lt;code&gt;.xlsx&lt;/code&gt; workbook (itself a ZIP archive of XML files) with data that matches the chart's references&lt;/li&gt;
&lt;li&gt;  Assemble everything into a ZIP archive with correct byte offsets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every step requires information from future steps. The content types file must know about parts that do not yet exist. The relationship files must know about targets that have not been generated. The chart XML must reference cells in a workbook that has not been created. This is a forward-reference problem that sequential token generation cannot solve.&lt;/p&gt;

&lt;p&gt;This is not a model quality issue. GPT-6 and Claude 5 will have the same architectural limitation. Better models will produce more plausible XML — but "more plausible" is not "valid." A file that is 99.9% correct OOXML but has one misordered element still triggers the repair dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architectural solution
&lt;/h2&gt;

&lt;p&gt;The solution is a structured JSON layer: separate content (what the AI decides) from format compliance (what a rendering engine guarantees). The &lt;a href="https://dev.to/docs/getting-started/quick-start"&gt;quick start&lt;/a&gt; shows the boundary in code.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The AI produces JSON — slide count, text, chart data, table rows — and the rendering engine handles everything that requires global consistency: element ordering (XSD-compliant), relationship ID assignment (unique and cross-referenced), content type declarations (complete), embedded workbook creation (synchronized with chart XML), namespace declarations (correct URIs), and ZIP assembly (valid Deflate compression with correct Central Directory).&lt;/p&gt;

&lt;p&gt;If the JSON is structurally valid (correct types, required fields present), the output file is &lt;strong&gt;guaranteed&lt;/strong&gt; to open without the repair dialog. The engine's test suite runs against every PowerPoint version and the OOXML Validator extension. For working code, see the &lt;a href="https://dev.to/blog/generate-pptx-from-json"&gt;JSON-to-PPTX quickstart&lt;/a&gt; or the &lt;a href="https://dev.to/docs/packages/mcp-server"&gt;MCP server guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the broader architecture, read the &lt;a href="https://dev.to/docs/packages/pptx"&gt;PPTX package guide&lt;/a&gt; and &lt;a href="https://dev.to/docs/packages/mcp-server"&gt;MCP server guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Generate PPTX that never triggers repair — &lt;a href="/blog/generate-pptx-from-json"&gt;start with the quickstart&lt;/a&gt;, read the &lt;a href="/docs/packages/pptx"&gt;PPTX guide&lt;/a&gt;, or set up the &lt;a href="/docs/packages/mcp-server"&gt;MCP server&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pptx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Documentation Screenshot CI: The Scenario Manifest Pattern</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Thu, 16 Jul 2026 09:05:15 +0000</pubDate>
      <link>https://dev.to/jakexkim/documentation-screenshot-ci-the-scenario-manifest-pattern-42n9</link>
      <guid>https://dev.to/jakexkim/documentation-screenshot-ci-the-scenario-manifest-pattern-42n9</guid>
      <description>&lt;p&gt;A CI job that takes screenshots is not a screenshot system.&lt;/p&gt;

&lt;p&gt;It is a camera. Useful, fast, and very capable, but still a camera. The system begins when every screenshot has a scenario: the route, state, viewport, ready condition, capture target, output path, docs usage, and owner that explain why the image exists.&lt;/p&gt;

&lt;p&gt;That scenario manifest is the pattern that turns documentation screenshots from ad hoc files into maintained artifacts.&lt;/p&gt;

&lt;p&gt;Browser automation is already strong. &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright can capture pages and locators&lt;/a&gt;. &lt;a href="https://docs.github.com/en/actions/tutorials/store-and-share-data" rel="noopener noreferrer"&gt;GitHub Actions can run workflows and store artifacts&lt;/a&gt;. &lt;a href="https://github.com/simonw/shot-scraper" rel="noopener noreferrer"&gt;shot-scraper&lt;/a&gt; shows how approachable CLI-driven screenshot capture can be. The missing layer is usually the manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do CI screenshots become hard to trust?
&lt;/h2&gt;

&lt;p&gt;Most teams start with a script: start the app, open a URL, wait a bit, take a screenshot, upload the PNG. That works until someone asks ordinary maintenance questions. Which docs page uses this image? Which account state was captured? Why is it full-page instead of cropped? Who approves replacement? What changed since the previous approved asset?&lt;/p&gt;

&lt;p&gt;If the answer lives in a developer's memory, the screenshot is not maintainable. The manifest moves those answers into the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The manifest defines the artifact
&lt;/h2&gt;

&lt;p&gt;A scenario manifest should be boring and explicit. It does not need to describe every browser instruction. It needs to define the screenshot's contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;settings-api-keys-one-active-key&lt;/span&gt;
&lt;span class="na"&gt;docsPage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docs/api-keys/create.mdx&lt;/span&gt;
&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/settings/api-keys&lt;/span&gt;
&lt;span class="na"&gt;fixture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-workspace-one-active-key&lt;/span&gt;
&lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1440&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
&lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;light&lt;/span&gt;
&lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[data-docs-ready="api-keys"]'&lt;/span&gt;
&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[data-docs-shot="api-keys-panel"]'&lt;/span&gt;
&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;static/img/api-keys/create-key.png&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docs-platform&lt;/span&gt;
&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review-required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact shape can be JSON, YAML, TypeScript, or another structured format. The important thing is that the manifest is the source of truth, not the screenshot file name and not a comment inside a Playwright test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where should implementation details live?
&lt;/h2&gt;

&lt;p&gt;The manifest should not become a second programming language. Put policy and identity in the manifest. Put browser mechanics in the runner.&lt;/p&gt;

&lt;p&gt;The runner can translate the manifest into Playwright calls: create or select the fixture, navigate to the route, set viewport and theme, wait for the ready signal, capture the target, and write the image plus metadata. This keeps scenarios readable to non-specialists. A docs lead can review whether the scenario represents the right page. An engineer can maintain the runner. A product marketer can see what assets are connected to a launch flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you trigger screenshots by dependency or by panic?
&lt;/h2&gt;

&lt;p&gt;Screenshot sweeps happen when screenshots are disconnected from change. A manifest lets CI decide which screenshots are suspect, which is how teams &lt;a href="https://dev.to/blog/stop-doing-screenshot-sweeps-before-every-launch"&gt;stop doing screenshot sweeps before every launch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Track scenario dependencies in the manifest or in a nearby index. A docs page dependency might point to &lt;code&gt;docs/api-keys/create.mdx&lt;/code&gt;, a product dependency to &lt;code&gt;app/settings/api-keys/**&lt;/code&gt;, a fixture dependency to &lt;code&gt;fixtures/team-workspace-one-active-key.json&lt;/code&gt;, and a component dependency to the shared table or panel used in the screenshot. When one of those inputs changes, CI can run the related scenario. When unrelated code changes, the screenshot job can stay quiet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax" rel="noopener noreferrer"&gt;GitHub Actions workflow syntax&lt;/a&gt; supports path-based triggers and job conditions, but many teams eventually add a small dependency resolver so they can map changed files to scenario IDs. The resolver does not need to be clever. It only needs to prevent "run every screenshot for every commit" from becoming the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the generated image and the decision record
&lt;/h2&gt;

&lt;p&gt;A screenshot without metadata is hard to review. Once &lt;a href="https://dev.to/blog/your-docs-screenshot-is-a-build-artifact-now"&gt;your docs screenshot is a build artifact&lt;/a&gt;, it carries provenance. Store the generated image beside a small decision record that includes the scenario ID, commit SHA, workflow run ID, docs page, product route, fixture, viewport, output path, previous approved asset, diff status, and review state.&lt;/p&gt;

&lt;p&gt;GitHub Actions artifacts are useful for build-time storage. If the team wants longer-lived review, move the artifact into a review system or asset store after the CI run. Either way, do not force reviewers to reconstruct context from logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use review states instead of pass/fail only?
&lt;/h2&gt;

&lt;p&gt;Documentation screenshot changes are not only pass or fail.&lt;/p&gt;

&lt;p&gt;A changed screenshot may be correct because the product UI changed. An unchanged screenshot may still be wrong because the docs copy moved on. A failed capture may be a scenario problem, a fixture problem, or a real product bug.&lt;/p&gt;

&lt;p&gt;Use review states that match documentation work:&lt;br&gt;
generated, unchanged, changed, approved, rejected, docs edit needed, and scenario fix needed. This language keeps the workflow honest without pretending every screenshot difference is a broken test. CI can tell you what happened. People decide what belongs in the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a publishing rule per scenario
&lt;/h2&gt;

&lt;p&gt;Not every screenshot needs the same level of caution. The manifest should include a publishing policy.&lt;/p&gt;

&lt;p&gt;The default should be review required, especially for public setup flows, billing settings, security settings, and anything behind login. Internal test artifacts should never publish. Low-risk generated reference images can earn lighter rules later, but only after the team has enough history to know the scenario is stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can failures improve the manifest?
&lt;/h2&gt;

&lt;p&gt;The first screenshot CI failures are usually informative. A route needs seeded data. A ready signal is too vague. A capture target is unstable. A viewport does not match the docs layout. An animation or toast overlaps the target. Auth state expires. A feature flag differs from production.&lt;/p&gt;

&lt;p&gt;Do not treat these as one-off CI annoyances. Each failure should improve the manifest or runner. Add the fixture. Tighten the ready condition. Capture the locator instead of the full page. Pin the theme. Add an owner.&lt;/p&gt;

&lt;p&gt;This is how the screenshot system gets more reliable without pretending every product surface is deterministic on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Reshot to keep the manifest and review together
&lt;/h2&gt;

&lt;p&gt;Reshot is designed around this pattern: scenario manifests, generated capture artifacts, review decisions, and approved outputs.&lt;/p&gt;

&lt;p&gt;You can keep Playwright as the browser runner. You can keep GitHub Actions as the CI layer. Reshot keeps the screenshot from becoming an anonymous PNG. The workflow is straightforward: define the scenario, run it when dependencies change, store the generated image with metadata, review changed artifacts with context, and publish approved assets to the docs path.&lt;/p&gt;

&lt;p&gt;The scenario manifest is the handoff object. It gives Reshot enough context to show what changed, why it ran, who owns it, and whether it can replace the public docs image. That is how the manifest keeps &lt;a href="https://dev.to/visual-debt"&gt;visual debt&lt;/a&gt; from accumulating between releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright screenshots&lt;/a&gt; - Official page, full-page, buffer, and locator screenshot support.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax" rel="noopener noreferrer"&gt;GitHub Actions workflow syntax&lt;/a&gt; - Official workflow trigger and job configuration reference.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://docs.github.com/en/actions/tutorials/store-and-share-data" rel="noopener noreferrer"&gt;GitHub Actions workflow artifacts&lt;/a&gt; - Official artifact storage and sharing documentation.&lt;/li&gt;
&lt;li&gt;[C4] &lt;a href="https://github.com/simonw/shot-scraper" rel="noopener noreferrer"&gt;shot-scraper&lt;/a&gt; - Open source CLI project for repeatable website screenshots.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>documentation</category>
      <category>ci</category>
      <category>playwright</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Review a Signup Regression After a Next.js Release</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:31:32 +0000</pubDate>
      <link>https://dev.to/jakexkim/how-to-review-a-signup-regression-after-a-nextjs-release-53g1</link>
      <guid>https://dev.to/jakexkim/how-to-review-a-signup-regression-after-a-nextjs-release-53g1</guid>
      <description>&lt;p&gt;A signup regression after a Next.js release should not start with panic. It should start with a narrow review.&lt;/p&gt;

&lt;p&gt;The dangerous version is familiar: &lt;a href="https://dev.to/blog/your-next-js-deploy-was-green-did-signup-get-worse"&gt;a deploy ships green and signup quietly gets worse&lt;/a&gt;, and a few hours later signup looks weaker. Someone opens product analytics. Someone checks Sentry. Someone asks which pull request touched auth. Someone else wonders whether traffic changed. The team has evidence, but not a review.&lt;/p&gt;

&lt;p&gt;The useful question is smaller: did this deploy make the signup path worse, and what evidence supports the next action?&lt;/p&gt;

&lt;h2&gt;
  
  
  Which signup path should you name before opening dashboards?
&lt;/h2&gt;

&lt;p&gt;"Signup is down" is not specific enough.&lt;/p&gt;

&lt;p&gt;Signup may include a marketing page, account creation form, email verification, company profile, workspace creation, plan selection, invite acceptance, and first activation step. A Next.js release might affect only one of those pieces. If the review treats signup as one blob, the team can miss the actual regression.&lt;/p&gt;

&lt;p&gt;Start by naming the path precisely: &lt;code&gt;/signup&lt;/code&gt;, &lt;code&gt;/signup/company&lt;/code&gt;, &lt;code&gt;/auth/callback&lt;/code&gt;, &lt;code&gt;/join/[invite]&lt;/code&gt;, &lt;code&gt;/onboarding/workspace&lt;/code&gt;, or whatever route actually changed. Then name the success event. It should be a user outcome, not a page view. Account created, email verified, workspace created, invite accepted, or onboarding completed is stronger than "visited signup."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://posthog.com/docs/product-analytics/funnels" rel="noopener noreferrer"&gt;PostHog's funnel docs&lt;/a&gt; are useful here because they separate steps, drop-off, conversion rate, breakdowns, and historical trends. A funnel can show where people left. It still needs deploy context before it becomes a release review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you freeze the release context?
&lt;/h2&gt;

&lt;p&gt;Capture the deploy while it is still fresh:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What to record&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Commit SHA, deploy URL, deploy time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;Auth, form, validation, layout, tracking, copy, pricing, invite logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;The team or person responsible for the signup path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;The precise route or flow being reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;Signup completion, invite acceptance, verification success, workspace creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Before and after intervals with comparable traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;Product funnel, Sentry issue, route trace, logs, page analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Stable, watch, investigate, rollback candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://vercel.com/docs/deployments" rel="noopener noreferrer"&gt;Vercel's deployment docs&lt;/a&gt; matter because deployments carry concrete release context: generated URLs, environments, commits, logs, and project metadata. Use that context as the anchor. Do not let the review become "sometime after the release."&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you compare signup against itself?
&lt;/h2&gt;

&lt;p&gt;Do not start with all-site conversion. Signup can regress while the rest of the product looks normal. It can also look worse because a campaign sent colder traffic into the top of the funnel.&lt;/p&gt;

&lt;p&gt;Use the deploy timestamp as the anchor and compare a similar window before and after release. The window should match traffic shape where possible. A Monday morning release should not be compared casually against late Friday night traffic unless volume and source mix are stable.&lt;/p&gt;

&lt;p&gt;The comparison should answer the practical question without pretending one chart proves causality. Did the protected signup metric move after deploy? Did the PR touch code, content, routing, auth, pricing, flags, or validation on that path? Did traffic source, geography, device mix, or campaign exposure change enough to explain it? Did companion evidence point at the same route?&lt;/p&gt;

&lt;p&gt;The comparison should preserve uncertainty. A dip after deploy is not automatically caused by the deploy. A clean error dashboard is not automatically proof that signup is healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runtime evidence should you check second?
&lt;/h2&gt;

&lt;p&gt;After the path and metric are named, check runtime evidence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" rel="noopener noreferrer"&gt;Sentry's Next.js docs&lt;/a&gt; cover error monitoring, tracing, session replay, logs, and profiling for Next.js. In a signup review, Sentry is where you look for new exceptions on the signup route, failed server actions, hydration errors on the form, slow transactions around validation or workspace creation, and replay samples where users hit an error state.&lt;/p&gt;

&lt;p&gt;The runtime signal is strongest when it lines up with the product signal, because &lt;a href="https://dev.to/blog/posthog-shows-behavior-sentry-shows-breakage-deploy-review-needs-both"&gt;PostHog shows behavior while Sentry shows breakage and a deploy review needs both&lt;/a&gt;. "Signup completion dropped and validation exceptions appeared on &lt;code&gt;/signup/company&lt;/code&gt; after the validation PR" is a review. "Sentry has a new issue somewhere" is a lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check route and span evidence when the symptom is soft
&lt;/h2&gt;

&lt;p&gt;Signup regressions are not always loud. A route can become slower without throwing. A server action can retry. A third-party auth call can add latency. A dynamic route can fetch more than it did yesterday.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; shows default spans for route rendering, fetches, API routes, and metadata work, plus custom spans. That instrumentation helps explain soft regressions: a route render got slower, an auth callback made a slower external call, workspace creation moved from one backend call to several, or a fetch cache change added latency in the first signup step.&lt;/p&gt;

&lt;p&gt;The review does not need every span. It needs enough route evidence to explain whether the deploy plausibly changed the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check behavior and traffic context
&lt;/h2&gt;

&lt;p&gt;Product analytics can show the behavioral shape of the regression: more users start signup but fewer complete it, one step loses more people than usual, mobile moves while desktop stays stable, or a flagged cohort behaves differently. Vercel Web Analytics can add page-level context through visitor and page behavior. PostHog can add events, cohorts, flags, and funnel breakdowns. Use those signals to prevent false blame.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signup completion dropped after deploy, but traffic from a new campaign doubled and those visitors historically convert lower. Sentry is quiet. Mark watch and recheck with segmented traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is very different from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signup completion dropped after deploy, traffic mix is stable, and Sentry shows new validation failures on the same route. Mark investigate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Use four decision states
&lt;/h2&gt;

&lt;p&gt;The review should end with a state. Stable means signup stayed in range. Watch means signup moved but the evidence is mixed. Investigate means signup moved and the deploy plausibly touched it. Rollback candidate means signup moved and technical evidence points to the release. This language keeps the team from treating every dip as an incident or every clean error dashboard as a dismissal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why write the note while the evidence is fresh?
&lt;/h2&gt;

&lt;p&gt;A good signup regression note is short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deploy &lt;code&gt;9f3c&lt;/code&gt; changed company signup validation. &lt;code&gt;/signup/company&lt;/code&gt; completion dropped in the first stable post-deploy window. Traffic mix was normal. Sentry shows new validation errors on the same route. Marked investigate; owner is onboarding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A weak note is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signup looks down after the deploy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The weak note forces the next person to reconstruct the release. The strong note lets them act.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/product"&gt;TrueClara&lt;/a&gt; is built for that strong note. It keeps deploy metadata, path ownership, signup metrics, evidence links, and the decision together. Your analytics and monitoring tools remain the evidence sources. TrueClara keeps the signup review from dissolving into tabs.&lt;/p&gt;

&lt;p&gt;The next time signup moves after a Next.js release, do not start by asking which dashboard is right. Start with the path. Anchor it to the deploy. Layer the evidence. Record the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://posthog.com/docs/product-analytics/funnels" rel="noopener noreferrer"&gt;PostHog funnels&lt;/a&gt; - Product analytics documentation for funnel steps, conversion, breakdowns, and trends.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" rel="noopener noreferrer"&gt;Sentry Next.js docs&lt;/a&gt; - Sentry documentation for Next.js errors, traces, replay, logs, and profiling.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; - Official Next.js instrumentation guide for route, fetch, API, and custom spans.&lt;/li&gt;
&lt;li&gt;[C4] &lt;a href="https://vercel.com/docs/deployments" rel="noopener noreferrer"&gt;Vercel deployments&lt;/a&gt; - Vercel documentation for deploy methods, URLs, environments, and release metadata.&lt;/li&gt;
&lt;li&gt;[C5] &lt;a href="https://vercel.com/docs/analytics" rel="noopener noreferrer"&gt;Vercel Web Analytics&lt;/a&gt; - Vercel documentation for page-level visitor behavior.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>observability</category>
      <category>devops</category>
      <category>sentry</category>
    </item>
    <item>
      <title>PostHog Behavior, Sentry Breakage: Deploy Review Needs Both</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:30:42 +0000</pubDate>
      <link>https://dev.to/jakexkim/posthog-behavior-sentry-breakage-deploy-review-needs-both-2f2p</link>
      <guid>https://dev.to/jakexkim/posthog-behavior-sentry-breakage-deploy-review-needs-both-2f2p</guid>
      <description>&lt;p&gt;PostHog and Sentry are not interchangeable tools. They answer different questions.&lt;/p&gt;

&lt;p&gt;PostHog is where a team looks when it wants to understand product behavior: events, funnels, cohorts, feature flags, experiments, session replay, and conversion movement. Sentry is where a team looks when it wants to understand software failure: exceptions, traces, performance issues, releases, replays, and the technical context around a broken experience.&lt;/p&gt;

&lt;p&gt;After a deploy, both are useful. Neither one, alone, is the full release review.&lt;/p&gt;

&lt;p&gt;The question is not "PostHog or Sentry?" The better question is: did this deploy hurt the route we care about, and which evidence proves the next action?&lt;/p&gt;

&lt;h2&gt;
  
  
  What does each tool answer after a deploy?
&lt;/h2&gt;

&lt;p&gt;A Next.js release can damage a product path without creating a loud exception. It can also create errors without changing the customer outcome enough to matter. That is why deploy review gets messy when a team treats one dashboard as the source of truth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sentry.io/for/nextjs/" rel="noopener noreferrer"&gt;Sentry for Next.js&lt;/a&gt; is strong when the release causes runtime trouble. It can surface errors, performance spans, traces, replays, and release-adjacent debugging context. If a server action starts throwing, a hydration issue appears, or a route slows down, Sentry is often where the technical explanation begins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://posthog.com/docs/libraries/next-js" rel="noopener noreferrer"&gt;PostHog's Next.js docs&lt;/a&gt; are strong on product instrumentation. Events, feature flags, user behavior, and funnels help answer whether people completed the path after the release. If signup completion dipped or onboarding step two started losing users, product analytics is the right evidence source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vercel.com/docs/analytics" rel="noopener noreferrer"&gt;Vercel Analytics&lt;/a&gt; can explain page-level behavior and traffic changes. The &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; shows the instrumentation layer underneath routes, fetches, and custom spans.&lt;/p&gt;

&lt;p&gt;Each tool is useful. The deploy review is the layer that asks them all the same question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Best evidence source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Did users complete the path at the same rate?&lt;/td&gt;
&lt;td&gt;PostHog funnel or event analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the release introduce new runtime failures?&lt;/td&gt;
&lt;td&gt;Sentry issues, traces, and replays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did traffic mix or page behavior change?&lt;/td&gt;
&lt;td&gt;Vercel Analytics and acquisition context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did a route render, fetch, or backend call change?&lt;/td&gt;
&lt;td&gt;OpenTelemetry spans and traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the PR actually touch the path?&lt;/td&gt;
&lt;td&gt;Pull request diff and deploy metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why does deploy impact fall between dashboards?
&lt;/h2&gt;

&lt;p&gt;Most teams already have enough telemetry to see the symptoms. The hard part is keeping the symptoms attached to the release.&lt;/p&gt;

&lt;p&gt;A deploy goes out at 10:14. Signup dips by 11:00. Sentry shows a small increase in validation errors. PostHog shows a larger drop at one step. Vercel shows traffic was normal. The PR changed a validation rule. That is a useful story, and it mirrors the pattern in &lt;a href="https://dev.to/blog/your-next-js-deploy-was-green-did-signup-get-worse"&gt;a green Next.js deploy that quietly hurt signup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But if those facts live in separate tabs, the team has to assemble the story every time. One person pastes a Sentry link. Another posts a funnel screenshot. Someone asks which commit shipped. Someone else asks whether traffic changed. By the time the group agrees on the route, half the context is already stale.&lt;/p&gt;

&lt;p&gt;Deploy impact is not a dashboard category. It is an operating question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which release created the question?&lt;/li&gt;
&lt;li&gt;Which path could a customer feel?&lt;/li&gt;
&lt;li&gt;Which metric says the path got better or worse?&lt;/li&gt;
&lt;li&gt;Which evidence supports the movement?&lt;/li&gt;
&lt;li&gt;What decision did the team make?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TrueClara keeps that question as a case file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not force product analytics to explain engineering failure
&lt;/h2&gt;

&lt;p&gt;PostHog can show that signup completion dropped. It can show the step where users left. It can show whether a feature flag was involved. It can show cohorts, replays, and events around the behavior.&lt;/p&gt;

&lt;p&gt;But a product analytics tool is not the best place to inspect stack traces, slow spans, server-side exceptions, or release health. If the route moved because a server action started failing under a certain payload, Sentry or trace data should carry that investigation.&lt;/p&gt;

&lt;p&gt;The mistake is using the funnel as proof of cause. A funnel movement tells you something changed in behavior. It does not prove why.&lt;/p&gt;

&lt;p&gt;A useful review sentence sounds like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Signup completion dropped after deploy 9f3c. PostHog shows the loss at company validation. Sentry shows new validation exceptions on the same route. Investigate the validation change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence uses product analytics for behavior and error monitoring for failure. It does not make either tool do the other's job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not force error monitoring to explain product movement
&lt;/h2&gt;

&lt;p&gt;Sentry can show a clean release while a route still gets worse. A copy change can confuse users without throwing. A layout change can hide a button without causing a JavaScript exception. A pricing experiment can reduce upgrade completion with no runtime failure. An auth flow can become slower or more confusing before it breaks.&lt;/p&gt;

&lt;p&gt;In those cases, the absence of a Sentry incident is useful but not final. It narrows the investigation. It does not close the review.&lt;/p&gt;

&lt;p&gt;The route still needs product evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did users reach the same step?&lt;/li&gt;
&lt;li&gt;Did they complete the same action?&lt;/li&gt;
&lt;li&gt;Did the exposed audience change?&lt;/li&gt;
&lt;li&gt;Did a feature flag split the population?&lt;/li&gt;
&lt;li&gt;Did the traffic source change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A deploy can be technically clean and commercially worse. That is the point of reviewing the path, not only the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a deploy review packet
&lt;/h2&gt;

&lt;p&gt;The practical answer is not another giant dashboard. It is a packet that keeps the route, release, and evidence together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Packet field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy and PR&lt;/td&gt;
&lt;td&gt;Prevents vague "after the release" debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route or path&lt;/td&gt;
&lt;td&gt;Keeps the review tied to a customer experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected metric&lt;/td&gt;
&lt;td&gt;Defines what healthy means&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product evidence&lt;/td&gt;
&lt;td&gt;Shows behavior movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime evidence&lt;/td&gt;
&lt;td&gt;Shows failures, latency, traces, or replay context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic context&lt;/td&gt;
&lt;td&gt;Explains whether audience changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Turns evidence into action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The packet does not replace PostHog, Sentry, Vercel, or OpenTelemetry. It makes them answer the same release question.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you mark stable, watch, investigate, or rollback candidate?
&lt;/h2&gt;

&lt;p&gt;Use stable when the protected metric stays in range and companion evidence is quiet.&lt;/p&gt;

&lt;p&gt;Use watch when the path moved but the supporting evidence is mixed. Maybe traffic changed. Maybe the sample is thin. Maybe the movement is near the edge of normal variance. Watch is a real state because it prevents overreaction.&lt;/p&gt;

&lt;p&gt;Use investigate when the path moved and the deploy plausibly touched it. At that point the owner needs evidence links, not another meeting.&lt;/p&gt;

&lt;p&gt;Use rollback candidate when the route movement and technical evidence point to the same release. That still requires judgment, but the conversation becomes concrete: the release changed this path, the path got worse, and the evidence supports action. For a full walkthrough of that moment, see &lt;a href="https://dev.to/blog/the-deploy-shipped-the-route-got-worse-now-what"&gt;the deploy shipped, the route got worse, now what&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use TrueClara to keep the question intact
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/product"&gt;TrueClara&lt;/a&gt; is built for the space between product analytics and error monitoring. It does not ask a team to abandon either tool. It keeps deploy metadata, route ownership, path metrics, and evidence links attached to the release.&lt;/p&gt;

&lt;p&gt;That gives the team a release review object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what shipped&lt;/li&gt;
&lt;li&gt;what route it touched&lt;/li&gt;
&lt;li&gt;what behavior changed&lt;/li&gt;
&lt;li&gt;what PostHog showed&lt;/li&gt;
&lt;li&gt;what Sentry showed&lt;/li&gt;
&lt;li&gt;what Vercel or trace data explained&lt;/li&gt;
&lt;li&gt;what decision the team made&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostHog shows behavior. Sentry shows breakage. Vercel and OpenTelemetry add traffic and instrumentation context. TrueClara keeps the deploy question intact long enough for the team to answer it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who owns the release review?
&lt;/h2&gt;

&lt;p&gt;Tool choice is not the only reason deploy-impact reviews fail. Ownership is just as important.&lt;/p&gt;

&lt;p&gt;If product owns the funnel, engineering owns the errors, growth owns experiments, and platform owns deployments, the review can fall between teams. Everyone has a useful dashboard. Nobody owns the release question.&lt;/p&gt;

&lt;p&gt;Assign the owner by path, not by tool. If the deploy touched signup, the signup owner owns the review. If it touched checkout, the checkout owner owns the review. If it touched onboarding, the onboarding owner owns the review. Engineering still investigates technical evidence. Product still interprets behavior. The owner keeps the decision moving.&lt;/p&gt;

&lt;p&gt;A path owner should be able to say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this deploy touched my path&lt;/li&gt;
&lt;li&gt;this metric defines path health&lt;/li&gt;
&lt;li&gt;these evidence links matter&lt;/li&gt;
&lt;li&gt;this is stable, watch, investigate, or rollback candidate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That ownership model prevents the team from debating whether PostHog or Sentry is the "real" source. The real source is the review packet. The tools provide evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the answer is no action?
&lt;/h2&gt;

&lt;p&gt;A good deploy review often ends with no action. That is not wasted work.&lt;/p&gt;

&lt;p&gt;If the route stayed stable, the team learned that the release did not damage the protected path. If the route moved but traffic mix explains it, the team avoided a false incident. If product behavior changed but Sentry stayed quiet, the team learned to investigate UX or copy before chasing exceptions. If errors increased but the path stayed healthy, the team can prioritize cleanup without treating it as a product emergency.&lt;/p&gt;

&lt;p&gt;The value is not always a rollback. The value is a decision with evidence.&lt;/p&gt;

&lt;p&gt;That decision becomes part of release memory. The next time signup moves, the team can compare against prior reviews instead of starting from scratch. Over time, the route review becomes a habit: every important deploy leaves behind a short record of what happened to the customer path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://posthog.com/docs/libraries/next-js" rel="noopener noreferrer"&gt;PostHog Next.js docs&lt;/a&gt; - Product analytics, feature flag, and event instrumentation for Next.js apps.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://sentry.io/for/nextjs/" rel="noopener noreferrer"&gt;Sentry for Next.js&lt;/a&gt; - Error monitoring, performance monitoring, tracing, replay, and release debugging for Next.js.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://vercel.com/docs/analytics" rel="noopener noreferrer"&gt;Vercel Analytics&lt;/a&gt; - Page-level visitor analytics for Vercel applications.&lt;/li&gt;
&lt;li&gt;[C4] &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; - Official route, fetch, and span instrumentation guidance for Next.js.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>observability</category>
      <category>sentry</category>
      <category>devops</category>
    </item>
    <item>
      <title>CI Can Take Screenshots. It Should Not Publish Them Blindly.</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:30:37 +0000</pubDate>
      <link>https://dev.to/jakexkim/ci-can-take-screenshots-it-should-not-publish-them-blindly-51bb</link>
      <guid>https://dev.to/jakexkim/ci-can-take-screenshots-it-should-not-publish-them-blindly-51bb</guid>
      <description>&lt;p&gt;CI is a good place to generate product screenshots. It is a dangerous place to publish them without review.&lt;/p&gt;

&lt;p&gt;A browser can open the product, load a fixture, wait for the UI, capture the page, and store the result as a build artifact. &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; supports page screenshots, full-page screenshots, element screenshots, and buffers. &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; can run the job on pull requests, scheduled builds, or docs changes. Tools like &lt;a href="https://shot-scraper.datasette.io/" rel="noopener noreferrer"&gt;shot-scraper&lt;/a&gt; make website screenshot capture easy to script.&lt;/p&gt;

&lt;p&gt;The capture problem is manageable. The publishing problem is where teams get hurt.&lt;/p&gt;

&lt;p&gt;A CI screenshot can be technically fresh and still wrong for the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat screenshots like build artifacts
&lt;/h2&gt;

&lt;p&gt;A build artifact has provenance. It came from a commit, a workflow, an environment, and a set of inputs. A docs screenshot needs the same treatment.&lt;/p&gt;

&lt;p&gt;If a screenshot appears in public documentation, the team should be able to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which commit generated it?&lt;/li&gt;
&lt;li&gt;Which route did it capture?&lt;/li&gt;
&lt;li&gt;Which fixture or account state was loaded?&lt;/li&gt;
&lt;li&gt;Which viewport and theme were used?&lt;/li&gt;
&lt;li&gt;Which selector proved the page was ready?&lt;/li&gt;
&lt;li&gt;Which docs page uses the asset?&lt;/li&gt;
&lt;li&gt;Who approved the replacement?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that trail, the screenshot is just a generated PNG. It may be correct. It may be a loading state with nice pixels.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commit&lt;/td&gt;
&lt;td&gt;pull request head SHA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route&lt;/td&gt;
&lt;td&gt;\/settings\/api-keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture&lt;/td&gt;
&lt;td&gt;team workspace with one active key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Viewport&lt;/td&gt;
&lt;td&gt;1440x900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready condition&lt;/td&gt;
&lt;td&gt;data-docs-ready="api-keys"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture target&lt;/td&gt;
&lt;td&gt;API key table panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs usage&lt;/td&gt;
&lt;td&gt;docs\/api-keys\/create.mdx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review state&lt;/td&gt;
&lt;td&gt;approved, rejected, needs scenario fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This metadata is what makes CI screenshots safe to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate screenshot tests from docs screenshots
&lt;/h2&gt;

&lt;p&gt;Visual regression tests and documentation screenshots overlap, but they are not the same job.&lt;/p&gt;

&lt;p&gt;A visual regression test asks, "Did the UI change unexpectedly?" A docs screenshot asks, "Does this image still teach the product step correctly?"&lt;/p&gt;

&lt;p&gt;The same browser capture may support both jobs, but the review rules are different. A pixel difference can fail a regression test. In docs, a pixel difference may be the expected result of a product improvement. Conversely, a screenshot can pass a pixel threshold and still be bad documentation if it shows the wrong user state or crop.&lt;/p&gt;

&lt;p&gt;A clean CI design keeps the outputs separate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Visual regression result&lt;/td&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;Pass, fail, update baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs screenshot artifact&lt;/td&gt;
&lt;td&gt;Docs, product, or support owner&lt;/td&gt;
&lt;td&gt;Approve, reject, request scenario fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public docs asset&lt;/td&gt;
&lt;td&gt;Docs publisher&lt;/td&gt;
&lt;td&gt;Replace only after approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That separation prevents engineering test baselines from becoming public documentation by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make product state deterministic enough
&lt;/h2&gt;

&lt;p&gt;Screenshot CI fails when the product state depends on live data, random accounts, timestamps, or whatever a developer had in their local workspace.&lt;/p&gt;

&lt;p&gt;The fix is not always a full fake backend. Start with the state that matters for the screenshot. If the screenshot teaches API key creation, the page needs a workspace with a known key state. If it teaches billing settings, the page needs a known plan and payment state. If it teaches integration setup, the page needs connected and disconnected variants.&lt;/p&gt;

&lt;p&gt;Useful controls include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seeded demo workspaces&lt;/li&gt;
&lt;li&gt;mocked network responses for unstable external data&lt;/li&gt;
&lt;li&gt;fixed dates and time zones&lt;/li&gt;
&lt;li&gt;explicit feature flag values&lt;/li&gt;
&lt;li&gt;test users with known roles&lt;/li&gt;
&lt;li&gt;data attributes for capture targets and ready states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make the entire product deterministic. The goal is to make the screenshot state reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait for the UI you mean to capture
&lt;/h2&gt;

&lt;p&gt;A surprising number of bad screenshots are readiness bugs. The browser captured too early. A skeleton was still visible. A chart had not drawn. A toast covered the button. A font swapped after capture. A settings panel was still animating.&lt;/p&gt;

&lt;p&gt;The wait condition should be part of the scenario, not an invisible sleep.&lt;/p&gt;

&lt;p&gt;Weak:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wait 3000ms&lt;/li&gt;
&lt;li&gt;take full-page screenshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open route&lt;/li&gt;
&lt;li&gt;wait for account fixture to load&lt;/li&gt;
&lt;li&gt;wait for data-docs-ready="billing-settings"&lt;/li&gt;
&lt;li&gt;capture locator for the billing panel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Playwright gives teams the browser primitives. The scenario should express what "ready" means for the docs image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not auto-replace public docs on every run
&lt;/h2&gt;

&lt;p&gt;The safest default is: CI generates, humans approve, publishing replaces.&lt;/p&gt;

&lt;p&gt;Auto-replacement can work for low-risk internal references. It is risky for public product documentation, especially setup flows, compliance screens, billing pages, admin permissions, and integration guides. Those images carry trust. A wrong screenshot can make the docs look current while making the instruction worse.&lt;/p&gt;

&lt;p&gt;Use review states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generated&lt;/td&gt;
&lt;td&gt;CI produced the screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed&lt;/td&gt;
&lt;td&gt;The artifact differs from the current docs asset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs review&lt;/td&gt;
&lt;td&gt;A human must approve replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;The screenshot can publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejected&lt;/td&gt;
&lt;td&gt;The asset is wrong or docs need editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scenario fix needed&lt;/td&gt;
&lt;td&gt;The automation captured the wrong state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This gives the team a lane for screenshots instead of a folder of files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Reshot between CI and the docs repo
&lt;/h2&gt;

&lt;p&gt;Reshot keeps the screenshot scenario, CI artifact, review decision, and approved docs asset together. It does not replace Playwright. It does not replace your CI provider. It gives the screenshots a workflow.&lt;/p&gt;

&lt;p&gt;A practical setup looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define scenarios for screenshots that matter.&lt;/li&gt;
&lt;li&gt;Run the scenarios in CI when product or docs inputs change.&lt;/li&gt;
&lt;li&gt;Store generated artifacts with commit, route, fixture, viewport, and docs usage.&lt;/li&gt;
&lt;li&gt;Send changed screenshots to review.&lt;/li&gt;
&lt;li&gt;Publish approved assets back to the docs repo or asset store.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how CI becomes useful for documentation screenshots without becoming reckless.&lt;/p&gt;

&lt;p&gt;The browser can take the picture. The team still needs to decide whether the picture belongs in the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Name artifacts so people can debug them
&lt;/h2&gt;

&lt;p&gt;Screenshot artifacts often fail at the boring layer: naming. A folder called screenshots is not enough when a reviewer needs to know which product state produced an image.&lt;/p&gt;

&lt;p&gt;Use names that carry useful context without becoming unreadable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Naming part&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product area&lt;/td&gt;
&lt;td&gt;settings-api-keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scenario state&lt;/td&gt;
&lt;td&gt;one-active-key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Viewport&lt;/td&gt;
&lt;td&gt;desktop-1440&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme or locale&lt;/td&gt;
&lt;td&gt;light-en&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit or run id&lt;/td&gt;
&lt;td&gt;pr-1842&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A generated file like settings-api-keys_&lt;em&gt;one-active-key&lt;/em&gt;&lt;em&gt;desktop-1440&lt;/em&gt;_pr-1842.png is not beautiful, but it is debuggable. The reviewer can tell what it is before opening it. The CI logs, artifact store, and docs replacement step can all refer to the same object.&lt;/p&gt;

&lt;p&gt;Pair the file name with a small metadata record. The metadata should include the route, fixture, ready condition, capture target, docs page, and reviewer state. That turns a PNG into a traceable artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record common failure modes
&lt;/h2&gt;

&lt;p&gt;The first few CI screenshot failures will teach the team more than the happy path. Capture them as scenario fixes instead of one-off debugging.&lt;/p&gt;

&lt;p&gt;Common failure modes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;app captured before hydration finished&lt;/li&gt;
&lt;li&gt;test account missing required plan or role&lt;/li&gt;
&lt;li&gt;modal or toast covering the target element&lt;/li&gt;
&lt;li&gt;external integration state unavailable&lt;/li&gt;
&lt;li&gt;browser viewport different from docs layout&lt;/li&gt;
&lt;li&gt;animation or chart rendering after capture&lt;/li&gt;
&lt;li&gt;feature flag exposing a different UI than production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each failure should improve the scenario. Add a better ready signal. Seed the fixture. Mock the unstable response. Capture the locator instead of the full page. Pin the feature flag. The point is not to make screenshots flawless immediately. It is to make the workflow more deterministic each time it fails.&lt;/p&gt;

&lt;p&gt;This is why review is so important. Review does not only protect the docs. It improves the screenshot system itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright screenshots&lt;/a&gt; - Official screenshot APIs for pages, locators, full-page captures, and buffers.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://docs.github.com/actions" rel="noopener noreferrer"&gt;GitHub Actions docs&lt;/a&gt; - Workflow automation and artifacts in CI.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://shot-scraper.datasette.io/" rel="noopener noreferrer"&gt;shot-scraper docs&lt;/a&gt; - Scriptable website screenshot capture.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ci</category>
      <category>playwright</category>
      <category>screenshots</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Playwright Screenshots for Documentation: What to Automate and What to Review</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:30:37 +0000</pubDate>
      <link>https://dev.to/jakexkim/playwright-screenshots-for-documentation-what-to-automate-and-what-to-review-3h44</link>
      <guid>https://dev.to/jakexkim/playwright-screenshots-for-documentation-what-to-automate-and-what-to-review-3h44</guid>
      <description>&lt;p&gt;Playwright can take the screenshot. It cannot decide whether that screenshot still belongs in your docs.&lt;/p&gt;

&lt;p&gt;That distinction matters. &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright's screenshot guide&lt;/a&gt; covers the mechanics: full-page captures, element captures, buffers, clipping, image quality, and file output. Those primitives are exactly what a docs team needs to stop retaking the same product screenshot by hand.&lt;/p&gt;

&lt;p&gt;But documentation screenshots are not ordinary test artifacts. A product image in a setup guide is part of the instruction. It has to show the right user state, the right public UI, the right viewport, and enough surrounding context for a reader to complete a task. Automation helps only when it creates a reviewable artifact instead of a folder full of mystery PNGs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate repeatable product states first
&lt;/h2&gt;

&lt;p&gt;The best first candidates are screenshots where the product state is repeatable and the cost of staleness is high.&lt;/p&gt;

&lt;p&gt;Start with the screenshots that help users finish operational tasks: creating an API key, connecting an integration, changing a billing setting, configuring OAuth, exporting data, or checking an admin permission. These screenshots usually have a known route, a stable UI target, and a docs page that depends on them. They are much better candidates than launch collages, annotated marketing images, or one-off editorial visuals.&lt;/p&gt;

&lt;p&gt;Playwright's locator screenshot support is especially useful here because many docs images should show a panel, table, modal, or settings section rather than the entire browser window. Capturing a locator keeps the screenshot tied to the instructional object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the screenshot in a scenario manifest
&lt;/h2&gt;

&lt;p&gt;A Playwright test file can capture a page, but the docs team needs more than executable code. It needs a scenario record that explains why the screenshot exists.&lt;/p&gt;

&lt;p&gt;A useful scenario manifest contains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs page&lt;/td&gt;
&lt;td&gt;docs/integrations/github.mdx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product route&lt;/td&gt;
&lt;td&gt;/settings/integrations/github&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixture&lt;/td&gt;
&lt;td&gt;workspace with GitHub connected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Viewport&lt;/td&gt;
&lt;td&gt;desktop 1440x900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme&lt;/td&gt;
&lt;td&gt;light&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ready signal&lt;/td&gt;
&lt;td&gt;data-docs-ready="github-connected"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture target&lt;/td&gt;
&lt;td&gt;integration settings panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;static/img/integrations/github-connected.png&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Owner&lt;/td&gt;
&lt;td&gt;docs-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This manifest is not bureaucracy. It is the maintenance contract. When the UI changes, the reviewer can see what state the screenshot was supposed to show. When capture fails, the engineer can tell whether the route, fixture, selector, or docs path is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Playwright for readiness, not sleeps
&lt;/h2&gt;

&lt;p&gt;Documentation screenshots often fail in a boring way: the browser captures too early.&lt;/p&gt;

&lt;p&gt;A skeleton is still visible. A chart has not rendered. A toast covers the button. A font swaps after the screenshot. A menu animation has not finished. The screenshot looks plausible, but it teaches the wrong thing.&lt;/p&gt;

&lt;p&gt;Do not hide readiness in a long timeout. Express it in the scenario. The runner should navigate to the route, select the fixture, wait for the loaded state that matters, confirm the target element is visible, and only then capture the intended locator.&lt;/p&gt;

&lt;p&gt;Playwright's docs for screenshots show how simple the capture call can be. The hard part is the sentence before it: "This page is now in the state the docs need."&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate visual tests from docs screenshots
&lt;/h2&gt;

&lt;p&gt;Playwright also supports &lt;a href="https://playwright.dev/docs/test-snapshots" rel="noopener noreferrer"&gt;visual comparisons&lt;/a&gt;, and teams often ask whether docs screenshots should just be visual tests.&lt;/p&gt;

&lt;p&gt;They overlap, but they are not the same workflow.&lt;/p&gt;

&lt;p&gt;A pixel diff is useful evidence, not the final decision. If a button moved because the product shipped a better layout, the docs screenshot probably should change. If the image changed because the capture account lost access to a feature, the screenshot should not publish.&lt;/p&gt;

&lt;p&gt;Visual comparison asks whether the UI changed unexpectedly. Documentation screenshot review asks whether the image still teaches the step correctly. The same browser capture can feed both workflows, but the decision record should be separate. Visual testing protects UI behavior. Screenshot review protects public instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store artifacts with enough context
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/tutorials/store-and-share-data" rel="noopener noreferrer"&gt;GitHub Actions artifacts&lt;/a&gt; are a good place to store generated screenshots during CI. The important part is naming and metadata.&lt;/p&gt;

&lt;p&gt;A screenshot artifact should answer basic questions without asking the reviewer to inspect CI logs. It needs the commit, scenario, route, fixture, viewport, theme, current docs asset, and whether the candidate differs from the approved version.&lt;/p&gt;

&lt;p&gt;Use names that are readable in a CI artifact list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings-api-keys__one-active-key__desktop-1440__light__pr-1842.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file name does not need to carry everything. Pair it with a small JSON record containing route, owner, ready signal, docs path, and review status. That turns a Playwright output file into a docs artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review what automation cannot know
&lt;/h2&gt;

&lt;p&gt;Automation can detect that an image changed. It cannot know whether the change is useful, public, or misleading.&lt;/p&gt;

&lt;p&gt;Review is where docs, product, support, and engineering have different but useful perspectives. Engineering can tell whether the UI state is valid. Docs can tell whether the screenshot still supports the surrounding copy. Support can spot the states users misunderstand. Product can confirm whether the flow is ready to describe publicly. If a banner, toast, wrong account, unreleased flag, or bad crop appears in the image, the reviewer catches it before the docs inherit the mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish only after approval
&lt;/h2&gt;

&lt;p&gt;A safe pipeline has two distinct jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate screenshots from Playwright scenarios.&lt;/li&gt;
&lt;li&gt;Publish approved docs assets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not collapse those jobs just because CI can write to the docs repo. A technically fresh screenshot can still be wrong. It can show a test account, a transient experiment, a loading state, or a feature that has not launched.&lt;/p&gt;

&lt;p&gt;Use review states, but keep the state model simple. A screenshot is generated, changed, needs review, approved, rejected, or blocked on a scenario fix. That is enough to keep CI fast and publishing conservative without turning the workflow into a status taxonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Reshot as the artifact layer
&lt;/h2&gt;

&lt;p&gt;Reshot fits between Playwright and the docs repository. Playwright handles browser automation. GitHub Actions or another CI system runs the job. Reshot keeps the scenario, generated screenshot, metadata, review decision, and approved output together.&lt;/p&gt;

&lt;p&gt;That lets teams automate the repeatable parts without losing editorial judgment. A practical Reshot setup defines scenarios for the screenshots that matter, runs Playwright when a scenario input changes, stores the generated image with metadata, routes changed screenshots into review, and publishes only the approved asset back to the docs path.&lt;/p&gt;

&lt;p&gt;Playwright is the capture engine. Reshot is the place where the capture becomes a documentation artifact with state, ownership, and a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://playwright.dev/docs/screenshots" rel="noopener noreferrer"&gt;Playwright screenshots&lt;/a&gt; - Official screenshot primitives for page, full-page, buffer, and locator capture.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://playwright.dev/docs/test-snapshots" rel="noopener noreferrer"&gt;Playwright visual comparisons&lt;/a&gt; - Official guidance for snapshot and visual comparison testing.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://docs.github.com/en/actions/tutorials/store-and-share-data" rel="noopener noreferrer"&gt;GitHub Actions workflow artifacts&lt;/a&gt; - Official artifact upload, download, and retention guidance.&lt;/li&gt;
&lt;li&gt;[C4] &lt;a href="https://github.com/simonw/shot-scraper" rel="noopener noreferrer"&gt;shot-scraper&lt;/a&gt; - Open source CLI project for automated website screenshots.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>playwright</category>
      <category>documentation</category>
      <category>screenshots</category>
      <category>ci</category>
    </item>
    <item>
      <title>Next.js Observability Is Not Deploy Impact</title>
      <dc:creator>jake</dc:creator>
      <pubDate>Tue, 07 Jul 2026 14:30:43 +0000</pubDate>
      <link>https://dev.to/jakexkim/nextjs-observability-is-not-deploy-impact-361j</link>
      <guid>https://dev.to/jakexkim/nextjs-observability-is-not-deploy-impact-361j</guid>
      <description>&lt;p&gt;Next.js observability tells you what the application is doing. Deploy impact tells you whether the release changed a product path that matters.&lt;/p&gt;

&lt;p&gt;Those are related questions, but they are not the same question.&lt;/p&gt;

&lt;p&gt;A team can have Sentry installed, OpenTelemetry spans flowing, Vercel Web Analytics enabled, Speed Insights collecting Core Web Vitals, and product analytics events in place. The deploy can still leave everyone asking the same awkward question: did this release hurt signup, checkout, onboarding, invite acceptance, or another route customers actually feel?&lt;/p&gt;

&lt;p&gt;The missing layer is not more raw signal. It is the review that ties signal to a deploy, a route, a protected metric, and a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability gives you witnesses, not the case file
&lt;/h2&gt;

&lt;p&gt;The official &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; describes instrumentation for route rendering, fetches, API routes, metadata generation, and custom spans. That is useful engineering evidence. It can tell you that a route got slower, a fetch changed shape, or a server action became noisier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" rel="noopener noreferrer"&gt;Sentry's Next.js docs&lt;/a&gt; explain how to collect errors, performance data, replays, logs, and tracing context from a Next.js app. That is also useful evidence. It can tell you when a release introduced a new exception or made a transaction slower.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vercel.com/docs/analytics" rel="noopener noreferrer"&gt;Vercel Web Analytics&lt;/a&gt; gives page and visitor behavior. &lt;a href="https://vercel.com/docs/speed-insights" rel="noopener noreferrer"&gt;Vercel Speed Insights&lt;/a&gt; helps watch real-user performance signals. &lt;a href="https://posthog.com/docs/libraries/next-js" rel="noopener noreferrer"&gt;PostHog's Next.js docs&lt;/a&gt; cover product events and feature flags.&lt;/p&gt;

&lt;p&gt;All of those tools answer important questions. Error monitoring can show a runtime failure. Tracing can show a route, fetch, or handler changing shape. Web analytics can show page traffic and visitor behavior. Product analytics can show funnel movement. Deployment metadata can show what shipped and when.&lt;/p&gt;

&lt;p&gt;None of that automatically decides whether the product path got worse because of the release. Deploy impact starts when those witnesses are attached to the same release question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deploy question is narrower than the observability question
&lt;/h2&gt;

&lt;p&gt;Observability asks: what is happening in the system?&lt;/p&gt;

&lt;p&gt;Deploy impact asks: after this deploy, did this product path get worse?&lt;/p&gt;

&lt;p&gt;That narrower question is why the review should start with a route, not a dashboard. If a pull request touched &lt;code&gt;/signup/company&lt;/code&gt;, the first review target is signup completion and the evidence around that path. If it touched payment state, the first review target is checkout completion and payment failure context. If it changed auth middleware, the first review target may be login success, invitation acceptance, and protected route access.&lt;/p&gt;

&lt;p&gt;The route keeps the review honest. Without it, the team can waste time scanning broad charts until one of them looks interesting. With it, the team can move in a straight line: anchor the question to a deploy, name the path a user could feel, compare the path against a health metric, then decide whether the companion evidence supports action.&lt;/p&gt;

&lt;p&gt;The answer may still be "no action." That is fine. A reviewed stable path is useful release memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  A green deploy is only a shipping fact
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vercel.com/docs/deployments" rel="noopener noreferrer"&gt;Vercel's deployment docs&lt;/a&gt; are clear that every successful build creates a deployment with its own URL, and Git pushes or pull requests can trigger those deployments. That is the shipping layer.&lt;/p&gt;

&lt;p&gt;The product layer starts after the deployment exists.&lt;/p&gt;

&lt;p&gt;A successful build tells you the app was assembled and deployed. It does not tell you whether users completed signup at the same rate. It does not tell you whether the pricing page sent fewer people to checkout. It does not tell you whether a validation change increased retries without throwing an exception.&lt;/p&gt;

&lt;p&gt;This distinction matters because a team can mistake platform health for product health. The build may pass while signup gets slower. Sentry may stay quiet while checkout completion dips. Web Vitals may stay normal while an onboarding step becomes confusing. Product analytics may show a dip that is later explained by traffic mix rather than code.&lt;/p&gt;

&lt;p&gt;The review should preserve both facts: the deploy was technically green, and the path still needs evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a deploy impact packet
&lt;/h2&gt;

&lt;p&gt;A deploy impact packet is small enough to become habit. It is not a postmortem. It is not a dashboard replacement. It is the minimum object that keeps the release question from scattering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Commit, deployment URL, shipped time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;What changed and who owns it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/signup/company&lt;/code&gt;, &lt;code&gt;/checkout&lt;/code&gt;, &lt;code&gt;/onboarding/connect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protected metric&lt;/td&gt;
&lt;td&gt;Completion, successful submit, activation, payment success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before window&lt;/td&gt;
&lt;td&gt;Comparable traffic before release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After window&lt;/td&gt;
&lt;td&gt;First stable traffic window after release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence links&lt;/td&gt;
&lt;td&gt;Sentry issue, trace sample, PostHog funnel, Vercel page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Stable, watch, investigate, rollback candidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The packet is deliberately boring. Boring is useful when a chart is moving and everyone wants to jump to a theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch for the false comfort of complete instrumentation
&lt;/h2&gt;

&lt;p&gt;Instrumentation coverage can become its own trap. Once a team has enough traces, events, logs, and analytics, it is tempting to believe the answer is in there somewhere. It usually is. The problem is retrieval under pressure.&lt;/p&gt;

&lt;p&gt;If the team has not named the path, the search space is too large. If it has not named the deploy, every chart becomes a possible coincidence. If it has not named the decision state, the review ends as a Slack thread instead of an action.&lt;/p&gt;

&lt;p&gt;The decision language should stay plain. Stable means the protected metric stayed in range and evidence is quiet. Watch means the path moved, but traffic, sample size, or companion evidence is mixed. Investigate means the path moved and the deploy plausibly touched it. Rollback candidate means the path moved and technical evidence points to the same release.&lt;/p&gt;

&lt;p&gt;That standard does not require perfect causality. It requires enough evidence to choose the next action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what to review before the release
&lt;/h2&gt;

&lt;p&gt;The best post-deploy review starts in the pull request.&lt;/p&gt;

&lt;p&gt;If a PR touches a value route, add the post-deploy review target before merge: affected path &lt;code&gt;/signup/company&lt;/code&gt;, protected metric company signup completion, evidence to check in validation errors, signup funnel, and route traces, owner onboarding, review window first stable post-deploy traffic window. That short note changes the release habit. The team is no longer surprised after the chart moves. It already knows which path deserves attention.&lt;/p&gt;

&lt;p&gt;This is where TrueClara fits. It keeps the deploy, path, metric, evidence links, and decision together. Sentry remains the place to inspect errors and traces. PostHog remains the place to inspect events and funnels. Vercel remains the place to inspect deployments, page behavior, and performance. TrueClara keeps them attached to the product path the release may have changed.&lt;/p&gt;

&lt;p&gt;Observability tells you what the system emitted. Deploy impact tells you whether the release changed the path customers care about. Next.js teams need both, but they should not confuse one for the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[C1] &lt;a href="https://nextjs.org/docs/app/guides/open-telemetry" rel="noopener noreferrer"&gt;Next.js OpenTelemetry guide&lt;/a&gt; - Official Next.js guide for instrumentation, default spans, custom spans, testing, and deployment.&lt;/li&gt;
&lt;li&gt;[C2] &lt;a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" rel="noopener noreferrer"&gt;Sentry Next.js docs&lt;/a&gt; - Sentry setup and monitoring documentation for Next.js applications.&lt;/li&gt;
&lt;li&gt;[C3] &lt;a href="https://vercel.com/docs/analytics" rel="noopener noreferrer"&gt;Vercel Web Analytics&lt;/a&gt; - Vercel documentation for page and visitor analytics.&lt;/li&gt;
&lt;li&gt;[C4] &lt;a href="https://vercel.com/docs/speed-insights" rel="noopener noreferrer"&gt;Vercel Speed Insights&lt;/a&gt; - Vercel documentation for real-user performance measurements.&lt;/li&gt;
&lt;li&gt;[C5] &lt;a href="https://vercel.com/docs/deployments" rel="noopener noreferrer"&gt;Vercel deployments&lt;/a&gt; - Vercel deployment methods, URLs, environments, and deployment metadata.&lt;/li&gt;
&lt;li&gt;[C6] &lt;a href="https://posthog.com/docs/libraries/next-js" rel="noopener noreferrer"&gt;PostHog Next.js docs&lt;/a&gt; - PostHog setup guidance for Next.js, events, and feature flags.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>observability</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
