<?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: Waseem Al-Dmeiri</title>
    <description>The latest articles on DEV Community by Waseem Al-Dmeiri (@waseemaldmeiri).</description>
    <link>https://dev.to/waseemaldmeiri</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862038%2Feca10bd7-c1f1-4155-8b2b-52edd2ccc06f.jpeg</url>
      <title>DEV Community: Waseem Al-Dmeiri</title>
      <link>https://dev.to/waseemaldmeiri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/waseemaldmeiri"/>
    <language>en</language>
    <item>
      <title>Datetime Is Tech Debt You Didn't Know You Had</title>
      <dc:creator>Waseem Al-Dmeiri</dc:creator>
      <pubDate>Sat, 18 Apr 2026 09:43:28 +0000</pubDate>
      <link>https://dev.to/waseemaldmeiri/datetime-is-tech-debt-you-didnt-know-you-had-3884</link>
      <guid>https://dev.to/waseemaldmeiri/datetime-is-tech-debt-you-didnt-know-you-had-3884</guid>
      <description>&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Almost every developer has a datetime story. An appointment showing up an hour late. A report aggregating the wrong day's data. A notification firing at 3am. It works fine in development, ships, and then a user in a different timezone files a bug you can't immediately reproduce.&lt;/p&gt;

&lt;p&gt;A 2025 empirical study of real-world bugs in open-source software found that &lt;strong&gt;53.6% of datetime bugs are timezone-related&lt;/strong&gt;, and &lt;strong&gt;27.8%&lt;/strong&gt; stem specifically from mixing naive and timezone-aware datetime objects incorrectly. These are the result of treating datetime as a trivial detail.&lt;/p&gt;

&lt;p&gt;Datetime is one of the most common forms of silent tech debt. It's easy to defer because it appears to work until it doesn't. By the time it fails, it's in production for a user in a country you didn't test for. By then, you're stuck refactoring entire columns in a live production database.&lt;/p&gt;

&lt;p&gt;This post is the reference I wish I'd had earlier — terminology, mental models, and the decisions that actually matter. Treat it as a cheat sheet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Terminology (Get This Right First)
&lt;/h2&gt;

&lt;p&gt;Before anything else, let's align on words. These get conflated constantly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UTC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The universal time reference. Every other timezone is an offset from it. Not a timezone itself — a standard.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Offset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A fixed shift from UTC. &lt;code&gt;+04:00&lt;/code&gt; means 4 hours ahead. &lt;code&gt;−05:00&lt;/code&gt; means 5 hours behind.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timezone (TZ)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A named region with rules — including DST. &lt;code&gt;Asia/Dubai&lt;/code&gt; is always UTC+4. &lt;code&gt;America/Toronto&lt;/code&gt; is UTC−5 in winter, UTC−4 in summer. An offset is a snapshot; a timezone is the full rulebook.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wall clock time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What the clock on the wall shows in a given place. &lt;code&gt;09:00&lt;/code&gt; in Dubai. Meaningless without knowing where.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Naive datetime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A datetime with no timezone or offset attached. &lt;code&gt;2024-06-01T09:00:00&lt;/code&gt;. Could mean anything.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aware datetime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A datetime with offset attached. &lt;code&gt;2024-06-01T09:00:00+04:00&lt;/code&gt;. Unambiguous.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ISO 8601&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The international standard for representing datetimes as strings. What you should always use for transfer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unix timestamp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seconds (or milliseconds) since &lt;a href="https://en.wikipedia.org/wiki/Epoch_(computing)" rel="noopener noreferrer"&gt;Epoch&lt;/a&gt;: &lt;code&gt;1970-01-01T00:00:00Z&lt;/code&gt;  Always UTC. Useful for math, not for display.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Daylight Saving Time. The reason offset ≠ timezone — the same timezone can have different offsets at different times of year.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  UTC: The One Ring
&lt;/h2&gt;

&lt;p&gt;UTC is the reference point everything else orbits. It doesn't observe DST. It doesn't change. When you know a moment in UTC, you can derive the correct wall clock time anywhere in the world by applying the local offset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpkywmzspcemfsl6b0gj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpkywmzspcemfsl6b0gj.png" alt="UTC example" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store in UTC. Display in local.&lt;/strong&gt; That's the whole principle. Everything else is implementation detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  ISO 8601: The Transfer Format
&lt;/h2&gt;

&lt;p&gt;When moving a datetime between systems — from backend to frontend, from database to API, from one language to another — you need a string format both sides understand. That format is ISO 8601.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2024-06-01T09:00:00          ← naive (no offset) — avoid this
2024-06-01T09:00:00Z         ← UTC (Z = zero offset)
2024-06-01T09:00:00+00:00    ← UTC (explicit offset)
2024-06-01T13:00:00+04:00    ← Dubai local time
2024-06-01T05:00:00−04:00    ← Toronto local time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last four all represent &lt;strong&gt;the same moment in time&lt;/strong&gt;. A system that parses ISO 8601 correctly will handle all of them identically — converting to UTC internally and displaying in the user's local timezone.&lt;/p&gt;

&lt;p&gt;The naive format (no offset) is the one to avoid. It looks like a datetime but carries no timezone information. Every system that receives it has to guess — and they won't all guess the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Postgres Handles This
&lt;/h2&gt;

&lt;p&gt;Postgres has two datetime types that look similar but behave completely differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;th&gt;Timezone-aware?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Calendar date only (&lt;code&gt;2024-06-01&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time of day only (&lt;code&gt;09:00:00&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Date + time, verbatim, no conversion&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamptz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Date + time, converted to UTC internally&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs1oaa1x5fcgf0yvzirhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs1oaa1x5fcgf0yvzirhc.png" alt="timestamp without tz vs timestamptz" width="800" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default to &lt;code&gt;timestamptz&lt;/code&gt; for every datetime column.&lt;/strong&gt; The overhead is negligible. &lt;code&gt;timestamp&lt;/code&gt; is appropriate only when you can guarantee the writer and reader will always be in the same timezone — a constraint that rarely holds as products grow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't Split Date and Time Into Separate Columns
&lt;/h2&gt;

&lt;p&gt;This one is counterintuitive but important.&lt;/p&gt;

&lt;p&gt;A common pattern is storing date and time separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;appointment_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- "2024-06-01"&lt;/span&gt;
&lt;span class="n"&gt;appointment_time&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- "09:00:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It seems clean. But it breaks down across timezones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem: a "day" is not the same moment everywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a client books at &lt;code&gt;09:00 Dubai time&lt;/code&gt; on &lt;code&gt;June 1st&lt;/code&gt;, that's &lt;code&gt;05:00 UTC&lt;/code&gt; on June 1st. But for a user in Toronto viewing the same record, it's &lt;code&gt;01:00 on June 1st&lt;/code&gt; — still June 1st, fine. But what if the appointment was at &lt;code&gt;02:00 Dubai time&lt;/code&gt;? That's &lt;code&gt;22:00 UTC on May 31st&lt;/code&gt;. The date changes depending on where you're standing.&lt;/p&gt;

&lt;p&gt;When you store date and time as separate fields, you lose the ability to reconstruct the correct moment in time without knowing the business's timezone — information that isn't in those columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1to5vq72y0ja0dwad36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1to5vq72y0ja0dwad36.png" alt="date time two clolumns vs timestamptz one coloumn" width="800" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a single &lt;code&gt;timestamptz&lt;/code&gt; column.&lt;/strong&gt; It stores the full moment — date, time, and UTC equivalent — atomically. You can always extract the date or time for display on the client.&lt;/p&gt;




&lt;h2&gt;
  
  
  When &lt;code&gt;date&lt;/code&gt; Alone Is Correct
&lt;/h2&gt;

&lt;p&gt;There are cases where a plain &lt;code&gt;date&lt;/code&gt; column is genuinely right: when the value is &lt;strong&gt;inherently timezone-independent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Birthdate&lt;/strong&gt; is the canonical example. If someone was born on June 1st, that's true regardless of what timezone you're in when you ask. It's a calendar fact, not a moment in time. Storing it as &lt;code&gt;timestamptz&lt;/code&gt; would actually introduce a problem — you'd need to decide which timezone to use, and converting it could shift the date.&lt;/p&gt;

&lt;p&gt;Same for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public holidays (&lt;code&gt;2024-12-25&lt;/code&gt; is Christmas everywhere)&lt;/li&gt;
&lt;li&gt;Contract effective dates (&lt;code&gt;2024-07-01&lt;/code&gt; a policy takes effect)&lt;/li&gt;
&lt;li&gt;Expiry dates on a license or document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test: &lt;em&gt;"Does the answer change depending on where you are when you ask?"&lt;/em&gt; If no — use &lt;code&gt;date&lt;/code&gt;. If yes — use &lt;code&gt;timestamptz&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76c3hijgx6yxvqf592jg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F76c3hijgx6yxvqf592jg.png" alt="Timestamptz dates cheatsheat" width="757" height="845"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write path (always):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send an offset-aware ISO 8601 string: &lt;code&gt;2024-06-01T13:00:00+04:00&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never send a naive string to a &lt;code&gt;timestamptz&lt;/code&gt; column — Postgres will assume UTC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read path (always):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;code&gt;timestamptz&lt;/code&gt;: the string comes back with &lt;code&gt;+00:00&lt;/code&gt; — parse it, then convert to the user's local timezone before display&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;timestamp&lt;/code&gt; / &lt;code&gt;date&lt;/code&gt;: the string comes back with no offset — treat as-is, no conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store datetime as a plain string&lt;/li&gt;
&lt;li&gt;Split a moment in time across separate date + time columns&lt;/li&gt;
&lt;li&gt;Assume the server timezone matches the user timezone&lt;/li&gt;
&lt;li&gt;Skip &lt;code&gt;.toLocal()&lt;/code&gt; (or equivalent) on display&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One Last Thing: Offset vs Timezone
&lt;/h2&gt;

&lt;p&gt;These are not the same, and the distinction matters if your product operates across DST boundaries.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;+04:00&lt;/code&gt; is an offset — a fixed number. &lt;code&gt;Asia/Dubai&lt;/code&gt; is a timezone — a named ruleset that includes DST transitions.&lt;/p&gt;

&lt;p&gt;Dubai doesn't observe DST, so &lt;code&gt;Asia/Dubai&lt;/code&gt; is always &lt;code&gt;+04:00&lt;/code&gt;. But &lt;code&gt;America/Toronto&lt;/code&gt; is &lt;code&gt;−05:00&lt;/code&gt; in winter and &lt;code&gt;−04:00&lt;/code&gt; in summer. If you hardcode the offset, you'll be wrong half the year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store and reason in named IANA timezones&lt;/strong&gt; (&lt;code&gt;Asia/Dubai&lt;/code&gt;, &lt;code&gt;Europe/Berlin&lt;/code&gt;, &lt;code&gt;America/Toronto&lt;/code&gt;). Use the offset only at the moment of conversion, derived from the IANA name at that specific point in time.&lt;/p&gt;




&lt;p&gt;Datetime isn't hard. But it requires deliberate thought — the kind most developers postpone until users are in two countries and support tickets start rolling in. Get it right from the start and it stays invisible, which is exactly where infrastructure should be.&lt;/p&gt;

</description>
      <category>timezones</category>
      <category>datetime</category>
      <category>cheatsheet</category>
      <category>teachdebt</category>
    </item>
    <item>
      <title>Automating Roadmap.sh into NotebookLM</title>
      <dc:creator>Waseem Al-Dmeiri</dc:creator>
      <pubDate>Sun, 05 Apr 2026 09:21:59 +0000</pubDate>
      <link>https://dev.to/waseemaldmeiri/automating-roadmapsh-into-notebooklm-132i</link>
      <guid>https://dev.to/waseemaldmeiri/automating-roadmapsh-into-notebooklm-132i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you're a developer, you probably know &lt;a href="https://roadmap.sh" rel="noopener noreferrer"&gt;roadmap.sh&lt;/a&gt;. It's the gold standard for figuring out what to learn, packed with community-curated articles and videos.&lt;/p&gt;

&lt;p&gt;Then there's &lt;a href="https://notebooklm.google.com" rel="noopener noreferrer"&gt;NotebookLM&lt;/a&gt;. For me, this is the "holy grail" of studying. You feed it a few links, and it generates these incredibly fun, informative "Deep Dive" podcasts. I started listening to them on my daily commute, and honestly, I've never absorbed complex tech topics faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; The Backend roadmap has over &lt;strong&gt;130 nodes&lt;/strong&gt;. Each node has about 7 resources.&lt;br&gt;
Doing the math? That's nearly 1,000 links to manually copy-paste.&lt;/p&gt;

&lt;p&gt;I tried doing it by hand for about ten minutes before realizing I'd be there all week. So, I did what any developer would do: &lt;strong&gt;I automated it&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/WaseemAldemeri" rel="noopener noreferrer"&gt;
        WaseemAldemeri
      &lt;/a&gt; / &lt;a href="https://github.com/WaseemAldemeri/roadmap-to-notebooklm" rel="noopener noreferrer"&gt;
        roadmap-to-notebooklm
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Populate Google NotebookLM with learning resources from any roadmap.sh roadmap — one notebook per topic, ready for AI-generated     podcasts, flashcards, and quizzes.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;roadmap-to-notebooklm&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Automatically populate &lt;a href="https://notebooklm.google.com" rel="nofollow noopener noreferrer"&gt;Google NotebookLM&lt;/a&gt; with learning resources from any &lt;a href="https://roadmap.sh" rel="nofollow noopener noreferrer"&gt;roadmap.sh&lt;/a&gt; roadmap.&lt;/p&gt;

  
    

    &lt;span class="m-1"&gt;roadmap-to-notebooklm.mp4&lt;/span&gt;
  

  

  


&lt;p&gt;The pipeline extracts every topic from a roadmap, finds the community-curated YouTube videos and articles linked to each one, creates a dedicated NotebookLM notebook per topic, and uploads all the sources into it. Once your notebooks are loaded, a small CLI lets you fuzzy-search through them and fire off study material generation (podcasts, flashcards, quizzes, etc.) in bulk — without copy-pasting prompts everywhere.&lt;/p&gt;
&lt;p&gt;Notebook creation and source ingestion is automated using &lt;a href="https://github.com/nazrulworld/notebooklm-py" rel="noopener noreferrer"&gt;notebooklm-py&lt;/a&gt;, a community-built Python package for the NotebookLM API.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Defaults to the Backend roadmap.&lt;/strong&gt; To use it with any other roadmap.sh roadmap, see the &lt;a href="https://github.com/WaseemAldemeri/roadmap-to-notebooklm#configuration" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt; section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;roadmap.sh GitHub repo
        │
        ▼
 get_resources.py        ← scrapes topics + resource links → &amp;lt;roadmap&amp;gt;_resources.json
        │
        ▼
 create_notebooks.py     ← creates one NotebookLM notebook per topic, uploads all sources
        │
        ▼
 generate_study_material_for_notebook.py   ← interactive CLI to generate study&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/WaseemAldemeri/roadmap-to-notebooklm" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Seeing it in Action
&lt;/h2&gt;



&lt;p&gt;&lt;a href="https://notebooklm.google.com/notebook/09feeef9-b604-46b5-84dc-e6d49e64ee6f" rel="noopener noreferrer"&gt;Example of a generated notebook about gRPC&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built the Bridge
&lt;/h2&gt;

&lt;p&gt;I wanted a simple, three-stage pipeline that didn't over-engineer the problem. I used a community-built wrapper called &lt;a href="https://github.com/teng-lin/notebooklm-py" rel="noopener noreferrer"&gt;notebooklm-py&lt;/a&gt; to handle the heavy lifting with Google's internal API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuqgzkx284tbpwlfdrsyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuqgzkx284tbpwlfdrsyh.png" alt="Image Top down graph of the process flow" width="548" height="888"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Scraper (&lt;code&gt;get_resources.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;First, I needed the data. I targeted the open-source repo behind roadmap.sh. The script recursively digs through their JSON structures and markdown files to find every curated YouTube video and article link.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Heavy Lifting (&lt;code&gt;create_notebooks.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. The script logs into NotebookLM (reusing a local session) and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a dedicated notebook for every single topic.&lt;/li&gt;
&lt;li&gt;Generates a "context" markdown file so the AI knows &lt;em&gt;why&lt;/em&gt; it's looking at these links.&lt;/li&gt;
&lt;li&gt;Uploads the resources automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: Google doesn't love it when you create 130 notebooks in 2 seconds, so I added some deliberate &lt;code&gt;asyncio.sleep&lt;/code&gt; pauses to keep things civil.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Interactive CLI (&lt;code&gt;generate_study_material.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Since you can't (and shouldn't) generate 130 podcasts at once, I built a snappy CLI. It uses &lt;code&gt;InquirerPy&lt;/code&gt; for &lt;strong&gt;fuzzy searching&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When I want to study "Redis" or "gRPC," I just type a few letters, select the topic, and hit Enter. The script triggers the generation on Google's servers, and by the time I've grabbed a coffee, my study materials are ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;I've learned more in the last few weeks using this setup than I have in months of "doom-scrolling" documentation.&lt;/p&gt;

&lt;p&gt;If you want to set this up for yourself (it works for Frontend, DevOps, or any other roadmap too), check out the &lt;a href="https://github.com/WaseemAldemeri/roadmap-to-notebooklm" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Originally published at &lt;a href="https://dmeiri.dev/blog/roadmap-to-notebooklm/" rel="noopener noreferrer"&gt;demeri.dev&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>notebooklm</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
