<?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: Tanglement</title>
    <description>The latest articles on DEV Community by Tanglement (@tanglement).</description>
    <link>https://dev.to/tanglement</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%2F4161956%2Fd5304f06-697d-407f-aa9c-7b65e81c4090.png</url>
      <title>DEV Community: Tanglement</title>
      <link>https://dev.to/tanglement</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanglement"/>
    <language>en</language>
    <item>
      <title>A Hemingway Editor alternative that works offline</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:31:36 +0000</pubDate>
      <link>https://dev.to/tanglement/a-hemingway-editor-alternative-that-works-offline-54l7</link>
      <guid>https://dev.to/tanglement/a-hemingway-editor-alternative-that-works-offline-54l7</guid>
      <description>&lt;p&gt;Hemingway Editor highlights long sentences, adverbs and passive voice. If you want the same checks in a browser, offline, there are options.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Highlights for hard and very hard sentences.&lt;/li&gt;
&lt;li&gt;Adverbs, passive voice and wordy phrases.&lt;/li&gt;
&lt;li&gt;A grade level that updates as you type.&lt;/li&gt;
&lt;li&gt;Your text staying on your computer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Readability grade checker
&lt;/h2&gt;

&lt;p&gt;A readability grade estimates the school grade needed to read a text easily. Long sentences and long words raise it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to lower it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Split long sentences.&lt;/li&gt;
&lt;li&gt;Swap long words for short ones where meaning stays the same.&lt;/li&gt;
&lt;li&gt;Cut wordy phrases ("in order to" becomes "to").&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Find passive voice in your writing
&lt;/h2&gt;

&lt;p&gt;Passive voice hides who did the action: "the report was sent" instead of "Sam sent the report". It is not wrong, but too much makes text slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to find it by hand
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Look for a form of "be" (is, was, were, been) followed by a past participle (sent, made, taken).&lt;/li&gt;
&lt;li&gt;Ask "by whom?". If the answer is missing or comes after "by", it is passive.&lt;/li&gt;
&lt;li&gt;Rewrite with the doer first.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/readability-editor/" rel="noopener noreferrer"&gt;Readability Editor&lt;/a&gt;. A Hemingway Editor alternative: find hard sentences, adverbs, passive voice and wordy phrases. Runs in your browser and works offline.&lt;/p&gt;

</description>
      <category>writing</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Export a WhatsApp chat to PDF</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:20:35 +0000</pubDate>
      <link>https://dev.to/tanglement/export-a-whatsapp-chat-to-pdf-2m9j</link>
      <guid>https://dev.to/tanglement/export-a-whatsapp-chat-to-pdf-2m9j</guid>
      <description>&lt;p&gt;WhatsApp has no "save as PDF" button. It can export a chat as a text file and a folder of photos. From there you can make a PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: export the chat from WhatsApp
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;iPhone: open the chat, tap the name at the top, then Export Chat. Choose Attach Media to include photos.&lt;/li&gt;
&lt;li&gt;Android: open the chat, tap ⋮, then More, then Export chat, then Include media.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the .zip to your computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free way: open the text and print it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Unzip the export and open the .txt file in a text editor.&lt;/li&gt;
&lt;li&gt;Print it and choose Save as PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works, but you get plain lines of text. Photos are separate files, and long chats are hard to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Print a WhatsApp chat with photos
&lt;/h2&gt;

&lt;p&gt;Printing the exported text gives you words only. To print the photos where they were sent, you need to put the two back together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Export the chat with media (Attach Media on iPhone, Include media on Android) and unzip it.&lt;/li&gt;
&lt;li&gt;Print the .txt file.&lt;/li&gt;
&lt;li&gt;Print the photos you need, and match them to the messages by the file names in the text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fine for a few photos. For a long chat it takes a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save a WhatsApp chat as a record for court or a dispute
&lt;/h2&gt;

&lt;p&gt;If you need to keep a chat as a record, keep the original export and make a readable copy with dates and page numbers. This page is not legal advice; ask your lawyer what your court accepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Export the chat from WhatsApp with media (iPhone: tap the name, Export Chat, Attach Media. Android: ⋮, More, Export chat, Include media).&lt;/li&gt;
&lt;li&gt;Keep the .zip exactly as WhatsApp made it. Do not edit it.&lt;/li&gt;
&lt;li&gt;Write down when and from which phone you exported it.&lt;/li&gt;
&lt;li&gt;For a readable copy, open the .txt and print it to PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Screenshots are another option, but they take hours for long chats and can cut off dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/whatsapp-chat-pdf/" rel="noopener noreferrer"&gt;WhatsApp Chat to PDF&lt;/a&gt;. Turn a WhatsApp chat export into a PDF that looks like the chat, with names, dates, times and photos. Runs in your browser.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>pdf</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to rebuild a mileage log for past trips</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:09:34 +0000</pubDate>
      <link>https://dev.to/tanglement/how-to-rebuild-a-mileage-log-for-past-trips-5d18</link>
      <guid>https://dev.to/tanglement/how-to-rebuild-a-mileage-log-for-past-trips-5d18</guid>
      <description>&lt;p&gt;If you did not keep a mileage log, you can still rebuild one from records you already have. A rebuilt log is weaker than one kept at the time, so keep the records you used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the trips already are
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Maps Timeline, if Location History was on: each drive with its date and distance.&lt;/li&gt;
&lt;li&gt;Your calendar: meetings with an address show where you went and when.&lt;/li&gt;
&lt;li&gt;Receipts, invoices and job notes: dates and client addresses.&lt;/li&gt;
&lt;li&gt;Service records with odometer readings, for your total miles in the year.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free way: a spreadsheet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make columns for date, start, destination, business purpose and miles.&lt;/li&gt;
&lt;li&gt;Go through your calendar and receipts, one row per trip.&lt;/li&gt;
&lt;li&gt;Look up each distance in Google Maps and type it in.&lt;/li&gt;
&lt;li&gt;Add up business miles and total miles at the end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works, but a year of trips can take a full day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make an IRS mileage log from Google Maps Timeline
&lt;/h2&gt;

&lt;p&gt;If Location History was on, Google Maps Timeline already holds your drives with dates and distances. You can turn that into a mileage log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: export your Timeline
&lt;/h3&gt;

&lt;p&gt;Timeline is now stored on your phone. Export it from the Google Maps app's Timeline settings, or use the Location History files from Google Takeout. Copy the file (Timeline.json or location-history.json) to your computer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the log needs
&lt;/h3&gt;

&lt;p&gt;IRS Publication 463 (chapter 5) asks a car-use record to show the date, the destination, the business purpose and the miles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way: read the Timeline by hand
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Timeline on your phone day by day.&lt;/li&gt;
&lt;li&gt;Copy each drive's date, places and distance into a spreadsheet.&lt;/li&gt;
&lt;li&gt;Add the business purpose for each trip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works for a few weeks of driving. For a full year it is slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mileage log template for taxes
&lt;/h2&gt;

&lt;p&gt;A mileage log for taxes is a list of trips. IRS Publication 463 (chapter 5) asks a car-use record to show the date, the destination, the business purpose and the miles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free template layout
&lt;/h3&gt;

&lt;p&gt;Make a spreadsheet with these columns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Date&lt;/li&gt;
&lt;li&gt;Start (for example Home)&lt;/li&gt;
&lt;li&gt;Destination&lt;/li&gt;
&lt;li&gt;Business purpose&lt;/li&gt;
&lt;li&gt;Miles (or start and end odometer)&lt;/li&gt;
&lt;li&gt;Type: business, commute or personal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the end of the year, add up business miles and total miles. Business miles divided by total miles is your business-use percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/mileage-log-rebuilder/" rel="noopener noreferrer"&gt;Mileage Log Rebuilder&lt;/a&gt;. Rebuild a mileage log for trips you already took, from Google Maps Timeline, your calendar or a list. Runs in your browser.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>freelancing</category>
      <category>taxes</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Geotag photos from a GPX track</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:58:34 +0000</pubDate>
      <link>https://dev.to/tanglement/geotag-photos-from-a-gpx-track-3e53</link>
      <guid>https://dev.to/tanglement/geotag-photos-from-a-gpx-track-3e53</guid>
      <description>&lt;p&gt;If you carried a phone or GPS logger, its track says where you were at each moment. A geotagger matches each photo's time to the track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export the track as GPX (most GPS apps can).&lt;/li&gt;
&lt;li&gt;Check your camera clock: if it was off by minutes, the places will be off too.&lt;/li&gt;
&lt;li&gt;Match photos to the track, with a time offset if needed.&lt;/li&gt;
&lt;li&gt;Check a few photos on a map.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Add GPS location to many photos at once
&lt;/h2&gt;

&lt;p&gt;Photos from a camera without GPS have no location. You can add one to many photos at once, either one place for all of them or from a GPS track.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way with ExifTool
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install ExifTool.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;exiftool -GPSLatitude=40.7 -GPSLatitudeRef=N -GPSLongitude=74.0 -GPSLongitudeRef=W *.jpg&lt;/code&gt; in the photo folder.&lt;/li&gt;
&lt;li&gt;Check one photo in a map app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works, but it is a command line tool and you need to look up coordinates yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/batch-geotagger/" rel="noopener noreferrer"&gt;Batch Geotagger&lt;/a&gt;. Add GPS to hundreds of photos at once, from a map pin or a GPX track. Runs in your browser.&lt;/p&gt;

</description>
      <category>photography</category>
      <category>gps</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>TikTok, Reels and Shorts safe zones in pixels</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:47:33 +0000</pubDate>
      <link>https://dev.to/tanglement/tiktok-reels-and-shorts-safe-zones-in-pixels-44kh</link>
      <guid>https://dev.to/tanglement/tiktok-reels-and-shorts-safe-zones-in-pixels-44kh</guid>
      <description>&lt;p&gt;TikTok puts buttons, captions and your name on top of your video. Text under them is hard to read. The safe zone is the area that stays clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe zone on a 1080 x 1920 video
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Keep key text at least 240 px from the top.&lt;/li&gt;
&lt;li&gt;Keep it at least 660 px from the bottom.&lt;/li&gt;
&lt;li&gt;Keep it 120 px from the left and 120 px from the right.&lt;/li&gt;
&lt;li&gt;Put an overlay on a track above your video while editing, then turn it off before export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These come from a third-party measurement of the platform's own guide image; the platform does not publish them as text. Source: &lt;a href="https://adkit.so/tools/safe-zones/tiktok" rel="noopener noreferrer"&gt;https://adkit.so/tools/safe-zones/tiktok&lt;/a&gt;. Platforms change their layout, so check the date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instagram Reels safe zone
&lt;/h2&gt;

&lt;p&gt;Instagram Reels puts buttons, captions and your name on top of your video. Text under them is hard to read. The safe zone is the area that stays clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe zone on a 1080 x 1920 video
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Keep key text at least 269 px from the top.&lt;/li&gt;
&lt;li&gt;Keep it at least 672 px from the bottom.&lt;/li&gt;
&lt;li&gt;Keep it 65 px from the left and 65 px from the right.&lt;/li&gt;
&lt;li&gt;Put an overlay on a track above your video while editing, then turn it off before export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are Meta's numbers. Source: &lt;a href="https://www.facebook.com/business/ads-guide/update/video/instagram-reels" rel="noopener noreferrer"&gt;https://www.facebook.com/business/ads-guide/update/video/instagram-reels&lt;/a&gt;. Platforms change their layout, so check the date.&lt;/p&gt;

&lt;h2&gt;
  
  
  YouTube Shorts safe zone
&lt;/h2&gt;

&lt;p&gt;YouTube Shorts puts buttons, captions and your name on top of your video. Text under them is hard to read. The safe zone is the area that stays clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe zone on a 1080 x 1920 video
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Keep key text at least 288 px from the top.&lt;/li&gt;
&lt;li&gt;Keep it at least 672 px from the bottom.&lt;/li&gt;
&lt;li&gt;Keep it 48 px from the left and 192 px from the right.&lt;/li&gt;
&lt;li&gt;Put an overlay on a track above your video while editing, then turn it off before export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These come from a third-party measurement of the platform's own guide image; the platform does not publish them as text. Source: &lt;a href="https://adkit.so/tools/safe-zones/youtube" rel="noopener noreferrer"&gt;https://adkit.so/tools/safe-zones/youtube&lt;/a&gt;. Platforms change their layout, so check the date.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/safe-zone-guides/" rel="noopener noreferrer"&gt;Safe Zone Overlays&lt;/a&gt;. Safe-zone overlays for TikTok, Reels, Shorts and Stories. Put one above your video and keep text clear of the app's buttons.&lt;/p&gt;

</description>
      <category>video</category>
      <category>tiktok</category>
      <category>design</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to use a .cube LUT in Blender</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:36:32 +0000</pubDate>
      <link>https://dev.to/tanglement/how-to-use-a-cube-lut-in-blender-35j7</link>
      <guid>https://dev.to/tanglement/how-to-use-a-cube-lut-in-blender-35j7</guid>
      <description>&lt;p&gt;Blender's compositor has no node that reads a .cube file. The built-in route is to add the LUT to an OpenColorIO config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free way: an OCIO look
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Copy Blender's colormanagement folder somewhere you can edit.&lt;/li&gt;
&lt;li&gt;Add your .cube file and a Look entry that points to it in config.ocio.&lt;/li&gt;
&lt;li&gt;Start Blender with the OCIO environment variable set to your config.&lt;/li&gt;
&lt;li&gt;Pick the look under Color Management &amp;gt; Look.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It works but every machine, and every render farm node, needs the same config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Film emulation LUTs for DaVinci Resolve, Premiere and Final Cut
&lt;/h2&gt;

&lt;p&gt;A LUT changes colour only. It can give you the colour of a film stock. It cannot add grain, halation or gate weave.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use a .cube LUT
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;DaVinci Resolve: put the file in the LUT folder (Project Settings &amp;gt; Color Management &amp;gt; Open LUT Folder), refresh, then apply it to a node.&lt;/li&gt;
&lt;li&gt;Premiere Pro: Lumetri Color &amp;gt; Creative &amp;gt; Look &amp;gt; Browse.&lt;/li&gt;
&lt;li&gt;Final Cut Pro: Effects &amp;gt; Custom LUT, then choose the file.&lt;/li&gt;
&lt;li&gt;Apply it after your basic correction, then lower its strength if it is too much.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to add film grain in Blender
&lt;/h2&gt;

&lt;p&gt;Film grain is fine noise that changes every frame. In Blender you add it after the render, in the compositor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Make a noise image (a Noise texture rendered to an image, or a grain scan you are allowed to use).&lt;/li&gt;
&lt;li&gt;In the compositor, add it with a Mix node set to Overlay or Soft Light, at low strength.&lt;/li&gt;
&lt;li&gt;For moving grain, use an image sequence or offset the texture per frame with a driver.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Grain that does not move looks like dirt on the lens. Render farms also need the grain image packed into the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/blender-film-look/" rel="noopener noreferrer"&gt;Film Look&lt;/a&gt;. One-click film stocks for Blender: grain, halation, LUTs and grading in the compositor. Runs on every GPU.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>colorgrading</category>
      <category>vfx</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Blender camera animation: dolly, orbit, shake and dolly zoom</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:25:32 +0000</pubDate>
      <link>https://dev.to/tanglement/blender-camera-animation-dolly-orbit-shake-and-dolly-zoom-2hbn</link>
      <guid>https://dev.to/tanglement/blender-camera-animation-dolly-orbit-shake-and-dolly-zoom-2hbn</guid>
      <description>&lt;p&gt;A dolly moves the camera toward or away from the subject. An orbit circles around it. Both are easy to set up by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free way
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Dolly: keyframe the camera's location at the start, move it closer, keyframe again.&lt;/li&gt;
&lt;li&gt;Orbit: add an Empty at the subject, parent the camera to it, add a Track To constraint to the subject, then keyframe the Empty's Z rotation.&lt;/li&gt;
&lt;li&gt;Smooth the motion in the Graph Editor (easing in and out).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Changing the move later means editing keyframes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blender camera shake
&lt;/h2&gt;

&lt;p&gt;Handheld shake is small random movement. Blender can add it to any animated value with a Noise modifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Keyframe the camera's rotation once so it has curves.&lt;/li&gt;
&lt;li&gt;In the Graph Editor, select the rotation curves and add a Noise modifier.&lt;/li&gt;
&lt;li&gt;Lower Strength and raise Scale until it looks handheld, not shaky.&lt;/li&gt;
&lt;li&gt;Use the modifier's frame range to limit it to part of the shot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Dolly zoom (Vertigo effect) in Blender
&lt;/h2&gt;

&lt;p&gt;A dolly zoom keeps the subject the same size while the background grows or shrinks. You move the camera and change the focal length at the same time, in opposite directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Point the camera at the subject with a Track To constraint.&lt;/li&gt;
&lt;li&gt;Keyframe location and focal length at the start.&lt;/li&gt;
&lt;li&gt;Move the camera closer and lower the focal length until the subject is the same size, keyframe both.&lt;/li&gt;
&lt;li&gt;Check the middle frames; you may need extra keys to keep the size steady.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/blender-camera-moves/" rel="noopener noreferrer"&gt;Camera Moves&lt;/a&gt;. One-click camera moves on your own camera. Fully adjustable, stackable, and removable in one click.&lt;/p&gt;

</description>
      <category>blender</category>
      <category>3d</category>
      <category>animation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Create Google Calendar events from Google Sheets</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:14:31 +0000</pubDate>
      <link>https://dev.to/tanglement/create-google-calendar-events-from-google-sheets-5b7o</link>
      <guid>https://dev.to/tanglement/create-google-calendar-events-from-google-sheets-5b7o</guid>
      <description>&lt;p&gt;Google Calendar can import a CSV file. Each row becomes an event. It is a one-way, one-time import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free way: CSV import
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Make columns named Subject, Start Date, Start Time, End Date, End Time, All Day Event, Description, Location.&lt;/li&gt;
&lt;li&gt;File &amp;gt; Download &amp;gt; CSV.&lt;/li&gt;
&lt;li&gt;In Google Calendar: Settings &amp;gt; Import &amp;amp; export &amp;gt; Import, pick the file and the calendar.&lt;/li&gt;
&lt;li&gt;Check a few events; the times use your calendar's time zone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Importing the same file twice makes duplicates, and later edits in the sheet do not reach the calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bulk add events to Google Calendar
&lt;/h2&gt;

&lt;p&gt;Typing events one by one is slow. A spreadsheet lets you fill many rows at once, then send them to the calendar together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Put one event per row: title, date, start time, end time.&lt;/li&gt;
&lt;li&gt;Use the CSV import in Google Calendar for a one-time load (see &lt;a href="https://tanglement.ai/sheets-to-calendar/guides/create-google-calendar-events-from-google-sheets/" rel="noopener noreferrer"&gt;this guide&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;For big lists, split into smaller files; a bad row can stop the import.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sync Google Sheets and Google Calendar
&lt;/h2&gt;

&lt;p&gt;A sync means a change in the sheet updates the event that already exists, instead of making a new one. That needs the event ID stored next to each row.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing it with a script
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add a hidden column for the event ID.&lt;/li&gt;
&lt;li&gt;On each run: rows with no ID get a new event and the ID is written back; rows with an ID update that event.&lt;/li&gt;
&lt;li&gt;Decide how a delete works, for example a Status column.&lt;/li&gt;
&lt;li&gt;Add a time-driven trigger.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/sheets-to-calendar/" rel="noopener noreferrer"&gt;Sheets to Calendar&lt;/a&gt;. Turn spreadsheet rows into Google Calendar events and keep both in sync. Preview first, no account needed.&lt;/p&gt;

</description>
      <category>googlesheets</category>
      <category>googlecalendar</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sync Airtable to Google Sheets automatically</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:03:31 +0000</pubDate>
      <link>https://dev.to/tanglement/sync-airtable-to-google-sheets-automatically-a9a</link>
      <guid>https://dev.to/tanglement/sync-airtable-to-google-sheets-automatically-a9a</guid>
      <description>&lt;p&gt;To keep a sheet in step with Airtable you need something that re-reads Airtable on a schedule. The common options are an automation service, a script, or an add-on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free way: a script with the Airtable API
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a personal access token in Airtable (Builder hub &amp;gt; Personal access tokens) with read access to your base.&lt;/li&gt;
&lt;li&gt;In Apps Script, call &lt;code&gt;https://api.airtable.com/v0/BASE_ID/TABLE&lt;/code&gt; with the token in an Authorization header.&lt;/li&gt;
&lt;li&gt;Follow the &lt;code&gt;offset&lt;/code&gt; value until it is gone; Airtable returns 100 records a page.&lt;/li&gt;
&lt;li&gt;Write the rows, then add a time-driven trigger.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will also need to turn linked record IDs into names and handle field renames yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to export Airtable to Google Sheets
&lt;/h2&gt;

&lt;p&gt;Airtable can download any view as a CSV file, and Google Sheets can import CSV. That works for a one-time copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way: CSV export and import
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In Airtable, open the view you want. Filters and sort apply to the download.&lt;/li&gt;
&lt;li&gt;Click the view menu next to the view name and choose Download CSV.&lt;/li&gt;
&lt;li&gt;In Google Sheets, File &amp;gt; Import &amp;gt; Upload, pick the CSV, and choose Replace current sheet or Insert new sheet.&lt;/li&gt;
&lt;li&gt;Repeat every time the data changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Linked records come out as text, attachments as file links, and nothing updates on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Airtable linked records in Google Sheets
&lt;/h2&gt;

&lt;p&gt;Through the API, a linked record field is a list of record IDs like recXXXXXXXXXXXXXX. A CSV download shows the primary field text instead, but loses the link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning IDs into names yourself
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Import the linked table too, with its record ID in one column.&lt;/li&gt;
&lt;li&gt;In the main sheet, split the ID list into separate cells.&lt;/li&gt;
&lt;li&gt;Use XLOOKUP or VLOOKUP against the linked table to get each name.&lt;/li&gt;
&lt;li&gt;Join the names back with TEXTJOIN if you want them in one cell.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/airtable-to-sheets/" rel="noopener noreferrer"&gt;Airtable to Sheets&lt;/a&gt;. Import Airtable bases, tables and views into Google Sheets: linked records, attachments, hourly refresh, two-way push.&lt;/p&gt;

</description>
      <category>airtable</category>
      <category>googlesheets</category>
      <category>nocode</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Make a timesheet from Google Calendar in Google Sheets</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:52:30 +0000</pubDate>
      <link>https://dev.to/tanglement/make-a-timesheet-from-google-calendar-in-google-sheets-4b1o</link>
      <guid>https://dev.to/tanglement/make-a-timesheet-from-google-calendar-in-google-sheets-4b1o</guid>
      <description>&lt;p&gt;Your calendar already knows when you worked. A timesheet is those events, for one week or month, with hours added up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export the week or month to a sheet (start, end, title, hours).&lt;/li&gt;
&lt;li&gt;Round each event the way you bill: up to 15 minutes, for example.&lt;/li&gt;
&lt;li&gt;Group by day or week with a pivot table.&lt;/li&gt;
&lt;li&gt;Remove events you declined or never answered.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Google Calendar hours report by client
&lt;/h2&gt;

&lt;p&gt;If you track client work in Google Calendar, you can bill from it. You need a way to tag each event with a client and add up the hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Pick a rule: one event colour per client, or put the client name in brackets at the start of the title.&lt;/li&gt;
&lt;li&gt;Export the events to a sheet with their duration in hours.&lt;/li&gt;
&lt;li&gt;Add a client column (from the colour or the bracket text).&lt;/li&gt;
&lt;li&gt;Make a pivot table: rows = client, values = SUM of hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check for overlapping events. Two events at the same time count those minutes twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to export Google Calendar to Google Sheets
&lt;/h2&gt;

&lt;p&gt;Google Calendar has no export to Sheets button. Its built-in export gives you an .ics file, which Sheets cannot open as rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Free way: an Apps Script
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In a new sheet, Extensions &amp;gt; Apps Script.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;CalendarApp.getDefaultCalendar().getEvents(start, end)&lt;/code&gt; for the date range you want.&lt;/li&gt;
&lt;li&gt;For each event, write title, start, end and (end minus start) in hours.&lt;/li&gt;
&lt;li&gt;Run it and allow calendar access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch time zones: the script uses the script's time zone, which may not match your sheet or calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I made for this
&lt;/h2&gt;

&lt;p&gt;I make and sell &lt;a href="https://tanglement.ai/calendar-export-sheets/" rel="noopener noreferrer"&gt;Export Calendar to Sheets&lt;/a&gt;. Export Google Calendar events to Sheets for any date range you pick. Hours by client or event colour, plus a Review tab that catches billing errors.&lt;/p&gt;

</description>
      <category>googlesheets</category>
      <category>productivity</category>
      <category>freelancing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Google Takeout photos show the wrong date, and how to fix them</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:46:39 +0000</pubDate>
      <link>https://dev.to/tanglement/why-google-takeout-photos-show-the-wrong-date-and-how-to-fix-them-mdo</link>
      <guid>https://dev.to/tanglement/why-google-takeout-photos-show-the-wrong-date-and-how-to-fix-them-mdo</guid>
      <description>&lt;p&gt;You exported Google Photos with Takeout, opened the folders, and every photo says it was taken today. The photos are fine. The dates are just not inside the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Google Photos keeps the date you see in the app in its own database. Takeout exports that as a separate &lt;code&gt;.json&lt;/code&gt; file next to each photo, with &lt;code&gt;photoTakenTime&lt;/code&gt; and &lt;code&gt;geoData&lt;/code&gt; inside. Most photo apps ignore those JSON files, so they fall back to the file's download date.&lt;/p&gt;

&lt;p&gt;Matching photos to their JSON is harder than it looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Newer exports name them &lt;code&gt;photo.jpg.supplemental-metadata.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Long names get cut to 46 characters, so the JSON name no longer matches.&lt;/li&gt;
&lt;li&gt;Duplicates are numbered differently: &lt;code&gt;photo(1).jpg&lt;/code&gt; goes with &lt;code&gt;photo.jpg(1).json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A photo and its JSON can end up in different zip parts.&lt;/li&gt;
&lt;li&gt;Album photos are exported again for every album.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fixing it for free with ExifTool
&lt;/h2&gt;

&lt;p&gt;If you're comfortable on the command line, ExifTool can copy the date in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exiftool &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; %s &lt;span class="nt"&gt;-tagsfromfile&lt;/span&gt; &lt;span class="s2"&gt;"%d/%F.supplemental-metadata.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"-DateTimeOriginal&amp;lt;PhotoTakenTimeTimestamp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"-GPSLatitude&amp;lt;GeoDataLatitude"&lt;/span&gt; &lt;span class="s2"&gt;"-GPSLongitude&amp;lt;GeoDataLongitude"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-ext&lt;/span&gt; jpg &lt;span class="nt"&gt;-overwrite_original&lt;/span&gt; &lt;span class="s2"&gt;"Takeout/Google Photos"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once per naming pattern (&lt;code&gt;%F.json&lt;/code&gt;, &lt;code&gt;%F.supplemental-metadata.json&lt;/code&gt;), unzip every part into one folder first, and check a few photos before deleting the originals. The truncated and renumbered names need extra handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it in the browser
&lt;/h2&gt;

&lt;p&gt;I built ExportFixer for people who don't want to script this. You drop all the Takeout zips in, it matches every naming variant, writes the date and GPS into each photo and video, removes the album duplicates and sorts everything by year and month. It runs in your browser tab and nothing is uploaded. The first 200 files are free: &lt;a href="https://tanglement.ai/export-fixer/google-takeout-photos-wrong-date/" rel="noopener noreferrer"&gt;https://tanglement.ai/export-fixer/google-takeout-photos-wrong-date/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also handles Snapchat Memories and iCloud Photos exports, which have the same problem.&lt;/p&gt;

&lt;p&gt;Disclosure: I make it and sell it.&lt;/p&gt;

</description>
      <category>photography</category>
      <category>tutorial</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Pull every page of a paginated API into Google Sheets (Apps Script)</title>
      <dc:creator>Tanglement</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:46:38 +0000</pubDate>
      <link>https://dev.to/tanglement/pull-every-page-of-a-paginated-api-into-google-sheets-apps-script-5g6g</link>
      <guid>https://dev.to/tanglement/pull-every-page-of-a-paginated-api-into-google-sheets-apps-script-5g6g</guid>
      <description>&lt;p&gt;Most APIs give you one page of results at a time, often 100 rows, plus a way to ask for the next page. If you want all of it in a Google Sheet, you have to follow those pages until they run out. Here is a small Apps Script that does that, and the things that usually break it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the paging style
&lt;/h2&gt;

&lt;p&gt;Check the API docs for one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page number:&lt;/strong&gt; &lt;code&gt;?page=2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offset and limit:&lt;/strong&gt; &lt;code&gt;?offset=200&amp;amp;limit=100&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor:&lt;/strong&gt; the response has a &lt;code&gt;next_cursor&lt;/code&gt; (Stripe calls it &lt;code&gt;starting_after&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next URL:&lt;/strong&gt; the response body has a &lt;code&gt;next&lt;/code&gt; link&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link header:&lt;/strong&gt; the HTTP &lt;code&gt;Link&lt;/code&gt; header has &lt;code&gt;rel="next"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A working loop (cursor style)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;importAll&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;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSheet&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="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/items?limit=100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;amp;cursor=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&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="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;getContentText&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nx"&gt;cursor&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;next_cursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&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;clearContents&lt;/span&gt;&lt;span class="p"&gt;();&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;getRange&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&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;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="s1"&gt;created_at&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="nx"&gt;rows&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="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&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="mi"&gt;1&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="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValues&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;muteHttpExceptions&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;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_KEY&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&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;getResponseCode&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="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Utilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&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;i&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="k"&gt;continue&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;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API kept failing: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&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;For page numbers, replace the cursor with a counter and stop when a page comes back empty. For a Link header, read &lt;code&gt;res.getHeaders()['Link']&lt;/code&gt; and pull out the &lt;code&gt;rel="next"&lt;/code&gt; URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that break it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writing one row at a time.&lt;/strong&gt; It is very slow. Collect everything in an array and call &lt;code&gt;setValues&lt;/code&gt; once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; Retry on 429 with a growing wait, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 6-minute limit.&lt;/strong&gt; Apps Script stops long runs. For big pulls, save progress in Script Properties and continue on a time trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys in the code.&lt;/strong&gt; Keep the API key in Script Properties, not in the script.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you'd rather not write it
&lt;/h2&gt;

&lt;p&gt;I made a sheet that does all of this with no code: six paging styles, OAuth, retries, and an &lt;code&gt;=IMPORTJSON()&lt;/code&gt; function. There is a free copy you can try: &lt;a href="https://tanglement.ai/sheets-api-connector/" rel="noopener noreferrer"&gt;https://tanglement.ai/sheets-api-connector/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I make it and sell the Pro version.&lt;/p&gt;

</description>
      <category>googlesheets</category>
      <category>javascript</category>
      <category>api</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
