<?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: DesignClarity</title>
    <description>The latest articles on DEV Community by DesignClarity (@designclarity).</description>
    <link>https://dev.to/designclarity</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%2F4145097%2F6818023c-0046-4b0c-9034-184eef91f9fc.png</url>
      <title>DEV Community: DesignClarity</title>
      <link>https://dev.to/designclarity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/designclarity"/>
    <language>en</language>
    <item>
      <title>The QA Script That Checks 8,556 PDF Links (and What It Caught)</title>
      <dc:creator>DesignClarity</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:17:01 +0000</pubDate>
      <link>https://dev.to/designclarity/the-qa-script-that-checks-8556-pdf-links-and-what-it-caught-19kp</link>
      <guid>https://dev.to/designclarity/the-qa-script-that-checks-8556-pdf-links-and-what-it-caught-19kp</guid>
      <description>&lt;h2&gt;
  
  
  A link you never click is a bug your customer finds
&lt;/h2&gt;

&lt;p&gt;I generated an 871-page hyperlinked PDF planner with Python — 8,556 internal link annotations across 730 daily pages, 105 weekly spreads, 24 monthly calendars, and a persistent tab bar. The render pass is only half the job. The other half is a verification script that re-opens the finished PDF and checks every single link, because a link you never click is a bug your customer finds.&lt;br&gt;
This is the QA pass from the companion piece on generating the planner. Same build, second half of the story.&lt;/p&gt;
&lt;h2&gt;
  
  
  Check 1: the document opens and counts match
&lt;/h2&gt;

&lt;p&gt;Before anything else, the script opens the built PDF and asserts the basics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pypdf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PdfReader&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner-2026-2027.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;871&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected 871 pages, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also runs text extraction on a sample of page types — home, month, day, week, goal, notes — to confirm the document opens cleanly and the text layer is intact. A corrupted render fails here before wasting time on 8,556 links. Cheap check, run first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: every link annotation resolves
&lt;/h2&gt;

&lt;p&gt;This is the core loop. Every page's annotations are walked, every link-type annotation is resolved to its destination page, and anything pointing nowhere — or anywhere outside the document — is recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;broken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;annot&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Annots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;annot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;obj&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Subtype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Dest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_destination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page_idx&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;page_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;broken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; links checked, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;broken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; broken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the shipped build: &lt;strong&gt;8,556 checked, 0 broken.&lt;/strong&gt; The number is the whole point — a human spot-checking "a few links" on an 871-page document is theater. The script is the actual test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: semantic spot-checks (not just "resolves")
&lt;/h2&gt;

&lt;p&gt;A link can resolve to a &lt;em&gt;valid&lt;/em&gt; page and still be wrong. March 14's calendar cell landing on March 15's page is a valid link and a broken product. So the script does semantic checks: extract text from the destination page and confirm it contains the expected date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dest_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resolved_idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dest_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;March 14, 2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cell landed on wrong page: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same treatment for the tab bar (each tab lands on its section's real first page) and for a sample of daily pages. Resolution checks catch crashes; semantic checks catch wrongness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: the week chain
&lt;/h2&gt;

&lt;p&gt;The 105 weekly spreads form a prev/next chain across two year boundaries — the highest-risk area for off-by-one errors. The script walks the full chain: week N's "next" link must land on week N+1's page, including the December 2026 → January 2027 handoff. First-week and last-week edge cases get explicit assertions. Calendar code loves to break at boundaries; the QA knows where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 5: static-file guarantee
&lt;/h2&gt;

&lt;p&gt;The finished file is asserted to be a static PDF: no JavaScript actions, no external URIs, no network calls. A planner that phones home is a support nightmare and a trust problem. The check is a few lines and it runs on every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script caught
&lt;/h2&gt;

&lt;p&gt;Honest answer: on the shipped build it caught &lt;strong&gt;nothing&lt;/strong&gt; — the QA result was PASS, 8,556/8,556, zero broken. The script earned its keep earlier in development, where the checks it now runs automatically are exactly the failure modes that bit during layout changes: named destinations are used instead of raw page numbers precisely because inserting a page during development used to silently repoint hundreds of links. The QA pass is the reason that class of bug can't ship again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script can't check
&lt;/h2&gt;

&lt;p&gt;The remaining gap is stated plainly in the product's own README: &lt;strong&gt;on-device testing in GoodNotes and Notability hasn't happened yet.&lt;/strong&gt; pypdf says every link resolves; what it can't tell you is how a specific annotation app renders or handles 8,556 annotations on a real tablet. Desktop viewers forgive sins that annotation apps don't. That test is next, and the product copy doesn't claim it passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;h2&gt;
  
  
  For template-style digital products, the QA script &lt;em&gt;is&lt;/em&gt; part of the product. The render code makes the thing; the verify code is what lets you sell it with a straight face. Write the checks first, run them on every build, and disclose what they don't cover.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;DesignClarity builds original digital products with code-first workflows. More of the factory's builds: &lt;a href="https://designclarity.github.io" rel="noopener noreferrer"&gt;https://designclarity.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Generated an 871-Page Hyperlinked PDF Planner With Python — 8,556 Links, Zero Broken</title>
      <dc:creator>DesignClarity</dc:creator>
      <pubDate>Sun, 27 Sep 2026 05:35:22 +0000</pubDate>
      <link>https://dev.to/designclarity/i-generated-an-871-page-hyperlinked-pdf-planner-with-python-8556-links-zero-broken-3emn</link>
      <guid>https://dev.to/designclarity/i-generated-an-871-page-hyperlinked-pdf-planner-with-python-8556-links-zero-broken-3emn</guid>
      <description>&lt;h2&gt;
  
  
  The problem with digital planners
&lt;/h2&gt;

&lt;p&gt;A hyperlinked PDF planner lives or dies on its links. Every monthly calendar date must jump to the right daily page. Every tab bar must land on the right section. Do it by hand in InDesign and you're placing thousands of link rectangles manually — and one fat-fingered destination ruins the product.&lt;/p&gt;

&lt;p&gt;I generate mine with code instead. The latest build: &lt;strong&gt;871 pages, 730 daily pages, 8,556 internal link annotations, verified zero broken.&lt;/strong&gt; Here's the approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: render + verify as separate passes
&lt;/h2&gt;

&lt;p&gt;Two passes, two libraries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Render pass&lt;/strong&gt; — draw every page and place every link annotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA pass&lt;/strong&gt; — re-open the finished PDF and mechanically verify every link resolves to a real page, plus semantic spot-checks (does the March 14 cell land on the March 14 page?).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Separating them matters. The render pass can have a systematic off-by-one that &lt;em&gt;looks&lt;/em&gt; fine; only an independent verification pass catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core trick: date → page index math
&lt;/h2&gt;

&lt;p&gt;Everything hangs off one function: given a date, which 0-based page index is its daily page? Once that's right, every calendar cell, week header, and tab bar is just arithmetic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="n"&gt;START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;daily_page_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_daily_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;0-based PDF page index of this date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s daily page.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;first_daily_page&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layout plan (page indices decided up front, before drawing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;P_COVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;P_HOME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;P_YEAR_2026&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;P_YEAR_2027&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;FIRST_MONTH_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;            &lt;span class="c1"&gt;# 24 monthly spreads follow
&lt;/span&gt;&lt;span class="n"&gt;FIRST_WEEK_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;        &lt;span class="c1"&gt;# 105 weekly spreads follow
&lt;/span&gt;&lt;span class="n"&gt;FIRST_DAILY_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;105&lt;/span&gt; &lt;span class="c1"&gt;# 730 daily pages follow
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Placing links: named destinations, not raw page numbers
&lt;/h2&gt;

&lt;p&gt;In the real build I used &lt;strong&gt;named destinations&lt;/strong&gt; (&lt;code&gt;bookmarkPage&lt;/code&gt; + &lt;code&gt;linkAbsolute&lt;/code&gt;) rather than raw page numbers, so inserting or removing a page during development never silently repoints hundreds of links. Named destinations are the single biggest robustness win: links survive layout edits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# When drawing March 14, 2026's daily page:
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bookmarkPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily-2026-03-14&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# When drawing the March 2026 calendar grid, over the "14" cell:
&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linkAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily-2026-03-14&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;thickness&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tab bar: one function, every page
&lt;/h2&gt;

&lt;p&gt;A persistent tab bar (Home / 2026 / 2027 / Months / Goals / Notes) appears on all 871 pages — one drawing function, six named-destination links, same coordinates everywhere. Six tabs × 871 pages = 5,226 of the 8,556 links come from this one function. Write it once, verify it once.&lt;/p&gt;

&lt;h2&gt;
  
  
  QA pass: trust nothing
&lt;/h2&gt;

&lt;p&gt;The verification script re-opens the built PDF with pypdf and checks: (1) page count exactly 871; (2) every link annotation resolves — 8,556 checked, 0 broken; (3) semantic spot-checks — extract text from the destination page and confirm it contains the expected date; (4) week-chain integrity across all 105 weeks including year boundaries; (5) static-file check — no JavaScript, no external URIs, fully offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File size:&lt;/strong&gt; 2.5 MB for 871 pages — vector drawing beats rasterized pages by an order of magnitude, and text stays selectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page size:&lt;/strong&gt; 768 × 1024 pt, portrait tablet-first, matches GoodNotes/Notability import expectations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fonts:&lt;/strong&gt; stick to the 14 base PDF fonts or embed your own. A missing font on the buyer's device is a support ticket you can't debug remotely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on-device.&lt;/strong&gt; Desktop PDF viewers forgive sins that annotation apps don't. (Still on my own checklist — automated QA passed, device testing is next.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why generate instead of design?
&lt;/h2&gt;

&lt;p&gt;Speed of iteration. Extending the planner from one year to two was a constant change and a re-run — not a month of manual layout. The QA script re-verified all 8,556 links in seconds. For template-style digital products, code is the design tool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;DesignClarity builds original digital products with code-first workflows. More of the factory's builds: &lt;a href="https://designclarity.github.io" rel="noopener noreferrer"&gt;https://designclarity.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pdf</category>
      <category>automation</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
