<?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: Brandon Brown</title>
    <description>The latest articles on DEV Community by Brandon Brown (@brandonb927).</description>
    <link>https://dev.to/brandonb927</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%2F17928%2Fa24e2b52-4d76-414e-bf20-334641666550.jpg</url>
      <title>DEV Community: Brandon Brown</title>
      <link>https://dev.to/brandonb927</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brandonb927"/>
    <language>en</language>
    <item>
      <title>The Best VSCode Extensions for Web Development in 2020</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Mon, 13 Jul 2020 07:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/the-best-vscode-extensions-for-web-development-in-2020-27c8</link>
      <guid>https://dev.to/brandonb927/the-best-vscode-extensions-for-web-development-in-2020-27c8</guid>
      <description>&lt;p&gt;I’ve been using &lt;a href="https://code.visualstudio.com/"&gt;Visual Studio Code&lt;/a&gt; (VSCode for short) as my editor of choice for what feels like a &lt;a href="https://twitter.com/brandonb927/status/895105158979268608?s=20"&gt;very long time&lt;/a&gt;. Ever since I &lt;a href="https://twitter.com/brandonb927/status/593532993026699266?s=20"&gt;first set eyes on the project&lt;/a&gt; the project and the community has intrigued me.&lt;/p&gt;

&lt;p&gt;My primary use case for VSCode is web development and over the years I’ve amassed a very finely tuned environment and extensions list that comes with me to every computer I use. What follows is a list of the best VSCode extensions that I have installed and personally used, along with an explanation of the value each extension provides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings Sync
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;number one extension&lt;/em&gt; you need to install after you’ve installed VSCode. Settings Sync allows you to sync your VSCode settings to a private GitHub gist so that you always have your setup backed up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync"&gt;https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: There is going to be a built-in “Settings Sync” &lt;a href="https://code.visualstudio.com/docs/editor/settings-sync"&gt;eventually&lt;/a&gt; so this project may become unmaintained in time, however for right now it works amazingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Manager
&lt;/h3&gt;

&lt;p&gt;The second most important extension if you work on multiple repositories or projects. Project Manager gives you a menu where you can quickly move between projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager"&gt;https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  File Icons
&lt;/h3&gt;

&lt;p&gt;The stock file icons are kinda meh, so add some pizzazz to Explorer sidebar with the File Icons extension.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=file-icons.file-icons"&gt;https://marketplace.visualstudio.com/items?itemName=file-icons.file-icons&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open In Browser
&lt;/h3&gt;

&lt;p&gt;If you work with HTML files a lot, this one is for you: simply open the command palette and type “open in browser” to open your HTML file in your default web browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=techer.open-in-browser"&gt;https://marketplace.visualstudio.com/items?itemName=techer.open-in-browser&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tomorrow Theme Kit
&lt;/h3&gt;

&lt;p&gt;Theme your editor with one of the best syntax and colour schemes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.Theme-TomorrowKit"&gt;https://marketplace.visualstudio.com/items?itemName=ms-vscode.Theme-TomorrowKit&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  File Size
&lt;/h3&gt;

&lt;p&gt;Adds a simple file size indicator to the status bar for the current file. This should really be baked into the editor by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=mkxml.vscode-filesize"&gt;https://marketplace.visualstudio.com/items?itemName=mkxml.vscode-filesize&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Word count
&lt;/h3&gt;

&lt;p&gt;Adds a simple word count indicator to the status bar for the current file that updates as you type. Another extension that should really be built-in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount"&gt;https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate Action
&lt;/h3&gt;

&lt;p&gt;I can’t believe this isn’t built-in… but this extension adds a “Duplicate” action to the right-click context in the sidebar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=mrmlnc.vscode-duplicate"&gt;https://marketplace.visualstudio.com/items?itemName=mrmlnc.vscode-duplicate&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH
&lt;/h3&gt;

&lt;p&gt;If you frequently ssh into machines, this extension allows you to connect to a remote host over ssh directly in VSCode! This is handy when editing config files where you don’t want to be forced to use an unconfigured version of nano or vim on the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=chrmarti.ssh"&gt;https://marketplace.visualstudio.com/items?itemName=chrmarti.ssh&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Excel Viewer
&lt;/h3&gt;

&lt;p&gt;Sometimes you just need a better way to view tabular data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=GrapeCity.gc-excelviewer"&gt;https://marketplace.visualstudio.com/items?itemName=GrapeCity.gc-excelviewer&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Pull Requests
&lt;/h3&gt;

&lt;p&gt;Manage your GitHub Pull Requests directly within VSCode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github"&gt;https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Regex tester
&lt;/h3&gt;

&lt;p&gt;Inline regex testing within VSCode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=chrmarti.regex"&gt;https://marketplace.visualstudio.com/items?itemName=chrmarti.regex&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Emojisense
&lt;/h3&gt;

&lt;p&gt;You’ve likely used the “colon-word-colon” format for emoji in many pieces of software, now you can use that same familiar emoji format within VSCode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=bierner.emojisense"&gt;https://marketplace.visualstudio.com/items?itemName=bierner.emojisense&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run On Save
&lt;/h3&gt;

&lt;p&gt;Need to run some arbitrary code whenever you save a file? RunOnSave is the extension for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave"&gt;https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG Viewer
&lt;/h3&gt;

&lt;p&gt;Sometimes you just need to view what an SVG renders like instead of editing the code of said SVG.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=cssho.vscode-svgviewer"&gt;https://marketplace.visualstudio.com/items?itemName=cssho.vscode-svgviewer&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bracket Pair Colorizer 2
&lt;/h3&gt;

&lt;p&gt;Sometimes code gets hairy, deeply nested, and overall visually difficult to grok. Fortunately, Bracket Pair Colorizer can help with making sense of your “eight-level-deep if-statement mixed with ten functions and callbacks”.&lt;/p&gt;

&lt;p&gt;Make sure you install v2 as it comes with a significant speed improvement as it uses the built-in VSCode bracket parsing engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2"&gt;https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Toggle
&lt;/h3&gt;

&lt;p&gt;Need to toggle different settings in VSCode based on a keyboard shortcut? Now you can do that. I personally used this extension to toggle my environments for “coding for work and pleasure” and “teaching code to a class of students” which changes my window zoom level and the theme for my editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=rebornix.toggle"&gt;https://marketplace.visualstudio.com/items?itemName=rebornix.toggle&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Spell Checker
&lt;/h3&gt;

&lt;p&gt;A spell checker for code and other documents in VSCode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker"&gt;https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open in GitHub
&lt;/h3&gt;

&lt;p&gt;Also works for BitBucket&lt;a href="https://marketplace.visualstudio.com/items?itemName=ziyasal.vscode-open-in-github"&gt;https://marketplace.visualstudio.com/items?itemName=ziyasal.vscode-open-in-github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Prettier
&lt;/h3&gt;

&lt;p&gt;The defacto code formatter supporting a wide variety of languages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode"&gt;https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Document This
&lt;/h3&gt;

&lt;p&gt;Called from the command palette, Document This will automatically document the outermost function under your cursor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=joelday.docthis"&gt;https://marketplace.visualstudio.com/items?itemName=joelday.docthis&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Comments
&lt;/h3&gt;

&lt;p&gt;Adds better syntax highlighting to specific content within comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments"&gt;https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdownlint
&lt;/h3&gt;

&lt;p&gt;Format your Markdown files using a linting library with standard and sane defaults based on a combination of Markdown and CommonMark markup languages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint"&gt;https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Lens
&lt;/h3&gt;

&lt;p&gt;Used in your &lt;code&gt;package.json&lt;/code&gt; files, this extension allows you to view and even update to the current latest version of an npm package, as well as the next package supported by it’s version string.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens"&gt;https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to setup XAMPP-VM in MacOS</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Mon, 29 Jun 2020 07:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/how-to-setup-xampp-vm-in-macos-3930</link>
      <guid>https://dev.to/brandonb927/how-to-setup-xampp-vm-in-macos-3930</guid>
      <description>&lt;p&gt;If you’re someone just getting started with web development and you’re leveling up your learning to include PHP and other server-side languages, and you have an Apple Mac product of some kind running macOS 10.15, it’s very likely that you’ve ran into a wall trying to get your first &lt;code&gt;index.php&lt;/code&gt; file to run. You could just open the Terminal.app and type &lt;code&gt;php index.php&lt;/code&gt; which would run your file through the built-in php interpreter, but let’s say you want to view it in your web browser; how do you do that? Well, this is where &lt;a href="https://www.apachefriends.org/index.html"&gt;XAMPP&lt;/a&gt; comes in.&lt;/p&gt;

&lt;p&gt;XAMPP is a suite of tools delivered to you as a single package that you download and install on your computer. Bundled inside it is Apache (a web server), MariaDB (an open source MySQL-compliant database server), PHP(a server-side programming language), and Perl (a programming language that many people love). The XAMPP project is a great tool to use for developing websites as it is cross-platform (Windows and Linux packages are available for it too) and allows you to build your project before you move on to other more advanced ways of serving your content (like using other macOS tools, or even configuring a Docker-based setup).&lt;/p&gt;

&lt;p&gt;The newer version of XAMPP for macOS is called &lt;a href="https://www.apachefriends.org/download.html#download-apple"&gt;XAMPP-VM&lt;/a&gt;. Unfortunately the &lt;a href="https://www.apachefriends.org/faq_stackman.html"&gt;documentation&lt;/a&gt; for XAMPP-VM is a little lacking for beginners, so in this post I intend to fill in the gaps of knowledge. Note: It is expected that you understand the basics of how to configure an Apache server before moving on to the rest of this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The first step to getting XAMPP-VM installed can be done one of two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install XAMPP-VM by downloading it &lt;a href="https://www.apachefriends.org/download.html#download-apple"&gt;directly from the website&lt;/a&gt;, opening the DMG file, and dragging the XAMPP.app file to the Applications folder.&lt;/li&gt;
&lt;li&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Download and install it with homebrew (much easier).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install xampp-vm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Once installed, run &lt;code&gt;XAMPP.app&lt;/code&gt; from the Applications folder. You should be presented with the application containing some buttons and an empty “status” area. First thing you want to do is click the &lt;code&gt;Start&lt;/code&gt; button here; this fires up the VM and gets the server running so we can complete the rest of the configuration. Next you want to click on the &lt;code&gt;Volumes&lt;/code&gt; tab at the top, then &lt;code&gt;Mount&lt;/code&gt;, then &lt;code&gt;Explore&lt;/code&gt; once the &lt;code&gt;Mount&lt;/code&gt; button is greyed out.&lt;/p&gt;

&lt;p&gt;If you clicked &lt;code&gt;Explore&lt;/code&gt;, Finder should open to the location where the XAMPP server config is available to browse. Click on the &lt;code&gt;etc&lt;/code&gt; folder here, then go to the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apache
&lt;/h2&gt;

&lt;p&gt;There’s a few tweaks you need to make to the Apache config before moving forward:&lt;/p&gt;

&lt;h3&gt;
  
  
  Main config &lt;code&gt;httpd.conf&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Find and edit the &lt;code&gt;httpd.conf&lt;/code&gt; file in the &lt;code&gt;etc&lt;/code&gt; folder, and uncomment the line containing &lt;code&gt;#Include etc/extra/httpd-vhosts.conf&lt;/code&gt; by removing the &lt;code&gt;#&lt;/code&gt;. This will allow us to configure Apache using &lt;a href="https://httpd.apache.org/docs/2.4/vhosts/"&gt;VirtualHosts&lt;/a&gt; without having to edit the main Apache config file anymore.&lt;/p&gt;

&lt;h3&gt;
  
  
  VirtualHosts &lt;code&gt;httpd-vhosts.conf&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;From the &lt;code&gt;etc&lt;/code&gt; folder click on the &lt;code&gt;extra&lt;/code&gt; folder and then open the &lt;code&gt;httpd-vhosts.conf&lt;/code&gt; file in your editor. You should be presented with a file containing two dummy entries, feel free to delete the last entry and edit the first one to match the example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;VirtualHost *:80&amp;gt;
  DocumentRoot "/opt/lampp/htdocs/testfolder"
  ServerName testserver
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In Finder, create a new folder called &lt;code&gt;testfolder&lt;/code&gt; inside the &lt;code&gt;htdocs&lt;/code&gt; folder that sides beside the &lt;code&gt;etc&lt;/code&gt; folder we’ve been doing our edits in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosts file &lt;code&gt;/etc/hosts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Open the &lt;code&gt;/etc/hosts&lt;/code&gt; file and add a new entry like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1 testserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Make sure the value you used in the vhosts config file matches the server name in the hosts file (in this case we use &lt;code&gt;testserver&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing in a browser
&lt;/h3&gt;

&lt;p&gt;Open your browser and navigate to &lt;code&gt;http://testserver:8080&lt;/code&gt;. You should be presented with an index file listing page that should be blank. If yes, you’ve successfully configured Apache to server your PHP files! Now all you need to do is start writing your PHP files inside the &lt;code&gt;/opt/lampp/htdocs/testfolder&lt;/code&gt; folder and you should be able to view them in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developing a PHP application in XAMPP-VM
&lt;/h2&gt;

&lt;p&gt;The core piece of this post is this: &lt;strong&gt;all development work needs to happen inside of the mounted &lt;code&gt;lampp&lt;/code&gt; folder&lt;/strong&gt;. This means you can’t symlink your Dropbox folder or any other folder from your local drive into the XAMPP-VM drive, but instead have to copy-paste your work from elsewhere in, or work from directly inside the VM drive. The XAMPP blog has a &lt;a href="https://www.apachefriends.org/blog/xampp_vm_cakephp_20170711.html"&gt;great post&lt;/a&gt; covering this.&lt;/p&gt;

&lt;p&gt;The reason for this: When you clicked the &lt;code&gt;Mount&lt;/code&gt; button above, this triggered XAMPP.app to “mount a network drive” and symlinked it in Finder so you could access it easily. When you “Unmount” the drive or shut down the VM, you can no-longer access your server content because it resides within the mounted network drive.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to use a DSLR or mirrorless camera as a webcam on MacOS</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Wed, 26 Feb 2020 08:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/how-to-use-a-dslr-or-mirrorless-camera-as-a-webcam-on-macos-1g0c</link>
      <guid>https://dev.to/brandonb927/how-to-use-a-dslr-or-mirrorless-camera-as-a-webcam-on-macos-1g0c</guid>
      <description>&lt;p&gt;This post contains affiliated links and I would &lt;strong&gt;really&lt;/strong&gt; appreciate it if you purchased any of the devices/items mentioned in this post through their respective link, if it makes financial sense to do so. 😄&lt;/p&gt;

&lt;p&gt;In this post I’m going to show you how you can turn a drab-looking webcam video feed into one that will have people you know talking. The solution that I propose could be used for a multitude of reasons, like: remote conferencing, “talking head” style video tutorials, and even live-streaming on Twitch or other platforms. The possibilities are up to you as a creator of content and how you choose to use it. Minus the green screen, I basically have all the parts to run my own Twitch channel, minus the skill and the audience… 😅&lt;/p&gt;

&lt;p&gt;Over the years and having worked semi-remote at a few companies, I’ve migrated from built-in camera and mic for meetings, to using an external webcam to better display my face and surroundings, then later incorporating an external microphone for better audio. Once I reached a happy medium, I didn’t really tweak my setup. Before I started on this journey, my initial setup included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;a href="https://amazon.ca/dp/B07K95WFWM?tag=brandonb0a-20"&gt;Logitech C920 webcam&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;an &lt;a href="https://amazon.ca/dp/B007JX8O0Y?tag=brandonb0a-20"&gt;Audio-Technica AT2005USB USB microphone&lt;/a&gt; (which was purchased back in 2015 for a &lt;a href="https://brandonb.ca/projects/faviconvalley"&gt;failed podcast&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup worked for me, but the I noticed on particularly bright days that the webcam video quality would get washed out. This is mostly due to my desk being directly in front of a window due to space constraints in my home office. After living with this for some time and tweaking the Logitech camera settings a bit to try and make it slightly better, I decided that upgrading my camera was the only option and necessary to improving the video quality any further.&lt;/p&gt;

&lt;p&gt;My original setup, with just the Logitech C920, looked something like this image:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9TuQh_ep--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/1-drab-logitech-c920.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9TuQh_ep--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/1-drab-logitech-c920.png" alt="Logitech C920 view"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;and with a new camera and some &lt;a href="https://amazon.ca/dp/B077PZM382?tag=brandonb0a-20"&gt;dimmable bi-colour LED lights and diffusers&lt;/a&gt;, I turned it into this:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oUwM2B6w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/2-amazing-lumix-g85-cam-link-4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oUwM2B6w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/2-amazing-lumix-g85-cam-link-4k.png" alt="Lumix G85 with 12-35mm @ f2.8 + Elgato Cam Link 4K view"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;Instantly, a few things happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the white balance in the image was no-longer shifted to an odd temperature&lt;/li&gt;
&lt;li&gt;the quality of the image drastically improved&lt;/li&gt;
&lt;li&gt;using a camera lens with a low aperture creates a softer background, allowing the viewer to focus on me and not what’s behind me&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Current Setup
&lt;/h2&gt;

&lt;p&gt;My current setup is running macOS 10.14.6 Mojave on a MacBook Pro 15” 2018 (supplied by my employer), so your mileage may vary using another machine/operating system. As far as where I learned any of this, I owe a lot to a &lt;a href="https://www.youtube.com/watch?v=WedG8LKO6ks"&gt;video&lt;/a&gt; from one of my favourite YouTubers &lt;a href="https://www.youtube.com/user/dslrvideoshooter/videos"&gt;DSLRVideoShooter&lt;/a&gt;, &lt;a href="https://mattstauffer.com/blog/setting-up-your-webcam-lights-and-audio-for-remote-work-podcasting-videos-and-streaming/"&gt;this post by Matt Stauffer&lt;/a&gt;, and &lt;a href="http://kcd.im/uses"&gt;the setup that Kent C. Dodds uses&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It also helps that I was already interested in videography and identify as an amateur photographer, so I understand the importance of light in a scene, the quality of the glass on the camera, and the capabilities of the camera sensor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parts list
&lt;/h3&gt;

&lt;p&gt;Below is a list of parts I’ve compiled based on my Amazon order history for this project. I bought everything roughly around the same time to put this all together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DSLR or mirrorless camera&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B07K3FN5MR?tag=brandonb0a-20"&gt;Elgato Cam Link 4K&lt;/a&gt; — used to connect the camera to the MacBook/computer, acting as a USB webcam interface&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B009S750LA?tag=brandonb0a-20"&gt;Dual monitor desktop mount&lt;/a&gt; — used to hold up my monitors, at a great price&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B00B21TLQU?tag=brandonb0a-20"&gt;Single monitor desktop mount&lt;/a&gt; — used as a somewhat free-standing camera mount that can swing around the desk with relative ease rather than remaining a fixed position forever&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B07F71FLX4?tag=brandonb0a-20"&gt;Some 3/8-inch and 1/4-inch tripod screws&lt;/a&gt; — used to attach the monopod extension to the single monitor mount arm&lt;/li&gt;
&lt;li&gt;some screw/nut washers — to give some height to the tripod screw so the threading doesn’t go too far inside the monopod extension&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B07MV48BXY?tag=brandonb0a-20"&gt;Monopod extension pole&lt;/a&gt; — attaches to the single monitor mount arm with a screw from above&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B07TJHN4KP?tag=brandonb0a-20"&gt;Mini ball head&lt;/a&gt; — goes on top of the monopod extension pole&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amazon.ca/dp/B077PZM382?tag=brandonb0a-20"&gt;Dimmable bi-colour LED lights and diffusers&lt;/a&gt; — used to provide nice soft lighting&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A DSLR or Mirrorless Camera
&lt;/h3&gt;

&lt;p&gt;For this project, I used a &lt;a href="https://amazon.ca/dp/B01LYU3WZR?tag=brandonb0a-20"&gt;Lumix G85&lt;/a&gt; mirrorless camera which I already had for videography, but you can find other cameras that will also work so long as they have an HDMI output. You’ll also want to pick a fairly light (in weight) camera as large bodies with bulky lenses will potentially cause unnecessary strain on the parts holding it up, and you don’t want them falling randomly in the middle of the night onto the floor from this height.&lt;/p&gt;

&lt;p&gt;A large factor in the decision to purchase a camera is whether or not it has HDMI-out, and also whether it offers a “clean” HDMI feed. You may also need an adapter from HDMI to mini- or micro-HDMI for your camera. Another factor is whether you want to use a battery or purchase a “dummy battery” for it and have the camera powered from an electrical outlet.&lt;/p&gt;

&lt;p&gt;Once you’re able to connect an HDMI cable to your camera somehow, be it through an adapter or a dongle, move onto the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Elgato Cam Link 4K
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://amazon.ca/dp/B07K3FN5MR?tag=brandonb0a-20"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--H6S79nnr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://images-na.ssl-images-amazon.com/images/I/71UgBrYFHWL._SL1500_.jpg" alt='"Cam Link 4K diagram"'&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The one piece of tech that allows us to use the camera like a webcam is the &lt;a href="https://amazon.ca/dp/B07K3FN5MR?tag=brandonb0a-20"&gt;Elgato Cam Link 4K&lt;/a&gt; which I picked up recently. It’s a bit pricey, but the functionality is what makes this a great buy: virtually any HDMI signal you plug in to the Cam Link will show up in your settings as if it were a USB webcam 🤯 &lt;em&gt;with no extra software required&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The way it works in practice: plug the HDMI cable coming from your camera into the HDMI port on the back end of the Cam Link 4K, then plug the USB end into your computer. In my case I need a USB 3.x to USB Type-C adapter for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat&lt;/strong&gt; : I’ve seen some chatter around the internet about the Cam Link 4K not working unless it is plugged into a USB 3.x &lt;em&gt;powered&lt;/em&gt; USB hub, or directly into a USB 3.x port. My own personal experience can corroborate that the Cam Link &lt;strong&gt;does not work when plugged into a USB 2.0 port or hub&lt;/strong&gt; and requires you to have it directly plugged into a 3.x USB port.  &lt;/p&gt;

&lt;p&gt;I &lt;strong&gt;was&lt;/strong&gt; able to get it to work reliably when plugged into the USB 3.x port on the back of my Dell U2718Q monitor and have the USB upstream cable connected directly to a USB 3.x port on my MacBook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the video
&lt;/h3&gt;

&lt;p&gt;In order to test that the camera is working with the Cam Link 4K, open up Quicktime and create a “New Movie Recording” (File &amp;gt; New Movie Recording). This should open up a recording window which might show your Facetime camera by default, or whatever your default webcam is set to. If your camera is turned on, connected to the Cam Link 4K which is connected to you computer, then you should be able to select it as the “Camera” that Quicktime will use.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Jd1fqeGU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/quicktime-select-camera.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Jd1fqeGU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/quicktime-select-camera.png" alt="Quicktime camera and microphone selection"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting the camera for the best angle
&lt;/h3&gt;

&lt;p&gt;Generally speaking, getting the camera at or slightly above your eye line provides the best angle for remote meetings. This means we need to get the camera on top of, or above, the main screen you will be viewing. I have a dual-monitor setup so this meant getting the camera above my main monitor. To do this I employed the used of my &lt;a href="https://amazon.ca/dp/B00B21TLQU?tag=brandonb0a-20"&gt;single monitor desktop mount&lt;/a&gt; and removed the VESA mount from it. I then removed the mount that the VESA plat attaches to leaving a hole in the arm large enough to get a screw through and attach the &lt;a href="https://amazon.ca/dp/B07MV48BXY?tag=brandonb0a-20"&gt;monopod extension pole&lt;/a&gt;. &lt;a href="https://youtu.be/WedG8LKO6ks?t=377"&gt;Here is a video&lt;/a&gt; describing very similarly how it is done (without having to drill a larger hole as my monopod extension arm had a 1/4-20 thread and the screw for it fit through the hole without modification).&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LZ2zd8up--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/camera-monopod-mount.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LZ2zd8up--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/camera-monopod-mount.jpg" alt="Monopod mounted to monitor arm, taken on iPhone 11 Pro in wide-angle mode"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MTrzE6Ss--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/final-desk-setup.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MTrzE6Ss--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/2020/02/final-desk-setup.jpg" alt="Final desk setup with camera mounted"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;Though I already had a set of &lt;a href="https://amazon.ca/dp/B01FDA3J36?tag=brandonb0a-20"&gt;friction arm mounts&lt;/a&gt; and used one temporarily to mount the camera while waiting for parts to arrive, this &lt;a href="https://amazon.ca/dp/B07SV6NVDS?tag=brandonb0a-20"&gt;11-inch adjustable articulating friction arm&lt;/a&gt; is the preferred purchase now should you choose to use this instead of the monopod extension mount.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lighting
&lt;/h3&gt;

&lt;p&gt;Lighting is one of the most important aspects to achieving great video quality. I probably didn’t have to splurge and upgrade from the Logitech C920 because if I had a better control of my lighting, the camera sensor wouldn’t have to work so hard. A well-lit “scene” can be what makes or breaks it for your camera, and better lighting will always produce a better result, &lt;a href="https://youtu.be/Sx3wAbeHLKk?t=195"&gt;no matter the camera you use&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://amazon.ca/dp/B077PZM382?tag=brandonb0a-20"&gt;LED lights and diffusers&lt;/a&gt; that are used in this post were purchased previously and came with their own stands, AC adapters and cables, but not the diffusers (which I purchased later). If I were ordering them now, I would purchase some &lt;a href="https://amazon.ca/dp/B07L755X9G?tag=brandonb0a-20"&gt;Elgato Key Lights&lt;/a&gt;. Alternatively, you could attempt to &lt;a href="https://www.reddit.com/r/Twitch/comments/ahek89/hackgato_key_light_diy_elgato_key_light/"&gt;make your own&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting better audio
&lt;/h3&gt;

&lt;p&gt;Up until now, if you’re the kind of person who doesn’t have a need for an external computer monitor to supplement your MacBook screen, you’ve probably relied on your built-in microphone, or even used the built-in microphone on your external webcam (if you have one). Achieving better audio involves ditching those built-in devices and using an external microphone.&lt;/p&gt;

&lt;p&gt;Something like a &lt;a href="https://amazon.ca/dp/B01AG56HYQ?tag=brandonb0a-20"&gt;plug and play lavalier microphone&lt;/a&gt; which plugs into the mic port on your camera (if it has one) or the headphone port on your MacBook. You could even get a USB microphone like &lt;a href="https://amazon.ca/dp/B07ZPBFVKK?tag=brandonb0a-20"&gt;this dynamic XLR &amp;amp; USB-c microphone&lt;/a&gt;, or &lt;a href="https://amazon.ca/dp/B007JX8O0Y?tag=brandonb0a-20"&gt;this dynamic USB microphone&lt;/a&gt;, which plug directly into a USB port.&lt;/p&gt;

&lt;p&gt;Any of these choices will significantly increas the quality of your voice! These microphones will make it easier for participants to hear you and can even produce some praise from them over how good your voice sounds. Marco Arment has &lt;a href="https://marco.org/podcasting-microphones#xlrusb"&gt;a great article&lt;/a&gt; covering a wide variety of microphones that he has personally tested and provided feedback on for podcasting but all of which would work here.&lt;/p&gt;

&lt;p&gt;You’re not limited to microphones either, you could simply purchase a &lt;a href="https://amazon.ca/dp/B00WGQNJK4?tag=brandonb0a-20"&gt;USB wired headset&lt;/a&gt; and call it a day. For some, this might be more convenient than a dedicated microphone as well as you can wear it while having your hands free to type, and wherever you move your head it won’t affect the direction of your voice into the mic.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Setup Sonarr and Radarr to notify via SMS with Twilio Runtime</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Tue, 10 Apr 2018 07:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/setup-sonarr-and-radarr-to-notify-via-sms-with-twilio-runtime-la8</link>
      <guid>https://dev.to/brandonb927/setup-sonarr-and-radarr-to-notify-via-sms-with-twilio-runtime-la8</guid>
      <description>&lt;p&gt;So you read my post on setting up the &lt;a href="https://brandonb.ca/ultimate-media-server-setup"&gt;ultimate media server for movies and tv shows&lt;/a&gt; and you’re in awe over how amazing a feeling it is to automate something that is such a pain to manage manually. You might also wonder what it would be like to get notifications when episodes or movies are downloaded. I came up with a rather non-obvious solution that might pique your interest: getting download, etc. notifications via text message.&lt;/p&gt;

&lt;p&gt;One of the somewhat buried features of Sonarr and Radarr is the ability to notify you when episodes or movies have been grabbed, downloaded, upgraded, and renamed. There are a few options for these: email, twitter DMs, various push notification services, and webhooks, to name a few. Email notifications work in a pinch but you can’t customize the content, and some of the various push notifications are cost prohibitive for a project like this. What that leaves you with is using webhooks to notify you, but how does one even begin to utilize this feature?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Twilio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.twilio.com"&gt;Twilio&lt;/a&gt; is a developer-centric platform for integrating programmable voice and SMS into your products and projects. If you haven’t yet, &lt;a href="https://www.twilio.com/try-twilio"&gt;create a Twilio account&lt;/a&gt; and buy a local phone number to use for Programmable SMS. If you’ve got an account and a phone number already, great!&lt;/p&gt;

&lt;h3&gt;
  
  
  Twilio Runtime Functions
&lt;/h3&gt;

&lt;p&gt;Over the last few years Twilio has really expanded their services, one of which is providing a “serverless” environment like &lt;a href="https://aws.amazon.com/lambda/"&gt;AWS Lambda&lt;/a&gt; hosted on their platform called &lt;a href="https://www.twilio.com/console/runtime/functions/manage"&gt;Functions&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Twilio Functions is a serverless environment to empower developers like you to quickly and easily create production-grade event-driven applications that scale with your business.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.twilio.com/functions"&gt;Twilio Functions&lt;/a&gt; is a very inexpensive solution, so much so that at the time of writing your first 10K invocations are free, then $0.0001/invocation thereafter. I’d be &lt;em&gt;very surprised&lt;/em&gt; if you went over the 10K free tier within a year for this project. You do pay $0.0075 CAD (at the time of writing) per SMS sent plus $1 CAD/month for each phone number you send from, however the overall cost is negligible in the long run.&lt;/p&gt;

&lt;p&gt;Below is a summarization of how the flow works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--te28Sx6u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/Screen_Shot_2017-07-13_at_12.55.39_PM.width-500.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--te28Sx6u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/Screen_Shot_2017-07-13_at_12.55.39_PM.width-500.png" alt="How Twilio Runtime Functions work, workflow diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE&lt;/em&gt;: I don’t think that Twilio Runtime was designed to work in the way I’m proposing it be used. In the future this method may not work as mentioned here so your mileage may vary.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create a new Runtime Function
&lt;/h4&gt;

&lt;p&gt;At this point head over to the &lt;a href="https://www.twilio.com/console/runtime/functions/manage"&gt;Runtime Functions&lt;/a&gt; console page. From there, create a new Function, then select &lt;code&gt;Blank Template&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once in the Function editor, set the Function Name and set the Path to whatever endpoint you want to use (for this post I use &lt;code&gt;/notifier&lt;/code&gt;) to execute your function.&lt;/p&gt;

&lt;p&gt;Take a look at the code editor on the page as this is where your handler will be written to deal with an incoming webhook request and send a text message. Since this handler is a serverless function, you’ll be writing it in Javascript.&lt;/p&gt;

&lt;h5&gt;
  
  
  Example
&lt;/h5&gt;

&lt;p&gt;A very basic handler that returns a &lt;code&gt;200 OK&lt;/code&gt; on request and logs out the &lt;code&gt;context&lt;/code&gt; and &lt;code&gt;event&lt;/code&gt; objects looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Got the message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Providing neither error or response will result in a 200 OK&lt;/span&gt;
  &lt;span class="nx"&gt;callback&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;h4&gt;
  
  
  Configuration and Handler Setup
&lt;/h4&gt;

&lt;p&gt;You can &lt;a href="https://www.twilio.com/console/runtime/functions/configure"&gt;configure your Runtime Functions&lt;/a&gt; to include a pre-initialized Twilio client in the &lt;code&gt;context&lt;/code&gt; object, the npm package dependencies, and even environment variables available to the handler. You’ll want to take a look at the &lt;a href="https://www.twilio.com/docs/runtime/functions/invocation"&gt;Function execution docs&lt;/a&gt; to learn all of the details about how to construct your handler, as well as some examples to get you started.&lt;/p&gt;

&lt;p&gt;What I eventually came up to handle webhook events from Sonarr and Radarr in one handler is below. You can take this chunk of code and paste it verbatim into your Function editor and click Save to get started quickly, but try to reason about it before you blindly copy-paste code from the internet 🙂.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash/get&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;EVENT_DOWNLOAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Download&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EVENT_UPGRADE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMovieMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Radarr: `&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;EVENT_DOWNLOAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is ready to watch in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;EVENT_UPGRADE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has upgraded to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Message for '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' event is not defined`&lt;/span&gt;
      &lt;span class="k"&gt;break&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;message&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;getTVShowMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;epTitles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;episode&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seasonNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;episodeNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;episode&lt;/span&gt;
    &lt;span class="nx"&gt;epTitles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (S&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;seasonNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;E&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;episodeNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Sonarr: `&lt;/span&gt;
  &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;EVENT_DOWNLOAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has episodes '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;epTitles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;' ready to watch`&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="na"&gt;EVENT_UPGRADE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; have upgraded episodes '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;epTitles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;' ready to watch`&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`Message for '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' event is not defined`&lt;/span&gt;
      &lt;span class="k"&gt;break&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;message&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Got the message! Parsing now...&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;eventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eventType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Probably not a request we care about, just 200 OK&lt;/span&gt;
    &lt;span class="nx"&gt;callback&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTwilioClient&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;messageBody&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if we're parsing a Radarr or Sonarr event&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;movie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;movie&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Radarr event&lt;/span&gt;
    &lt;span class="nx"&gt;messageBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getMovieMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Sonarr event&lt;/span&gt;
    &lt;span class="nx"&gt;messageBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getTVShowMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Parsed and built the message, sending a text.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TO_NUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FROM_NUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messageBody&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We're done!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Testing The Function Endpoint
&lt;/h4&gt;

&lt;p&gt;Below are some test commands I used to simulate a webhook hitting my Function. When you’ve got your function setup and deployed, you can test it out by copying your personal Runtime HTTP url from the Function page and replacing &lt;code&gt;https://secret-runtime-slug.twil.io/notifier&lt;/code&gt; in the below examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Single TV show episode&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"episodes":[{"id":123,"episodeNumber":1,"seasonNumber":1,"title":"Test title","qualityVersion":0}],"eventType":"Test","series":{"id":1,"title":"Test Title","path":"C:\\testpath","tvdbId":1234}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | http POST https://secret-runtime-slug.twil.io/notifier

&lt;span class="c"&gt;# Multiple TV show episodes&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"episodes":[{"id":123,"episodeNumber":1,"seasonNumber":1,"title":"Test title","qualityVersion":0},{"id":456,"episodeNumber":9,"seasonNumber":4,"title":"Another test title","qualityVersion":0},{"id":789,"episodeNumber":23,"seasonNumber":10,"title":"Yet another test title","qualityVersion":0}],"eventType":"Test","series":{"id":1,"title":"Test Title","path":"C:\\testpath","tvdbId":1234}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | http POST https://secret-runtime-slug.twil.io/notifier

&lt;span class="c"&gt;# Movie&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"remoteMovie":{"tmdbId":1234,"imdbId":"5678","title":"Test title","year":1970},"release":{"quality":"Test Quality","qualityVersion":1,"releaseGroup":"Test Group","releaseTitle":"Test Title","indexer":"Test Indexer","size":9999999},"eventType":"Test","movie":{"id":1,"title":"Test Title","releaseDate":"1970-01-01","folderPath":"C:\\testpath"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | http POST https://secret-runtime-slug.twil.io/notifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: I’m using &lt;a href="https://httpie.org"&gt;&lt;code&gt;httpie&lt;/code&gt;&lt;/a&gt; where &lt;code&gt;http&lt;/code&gt; is used. The payloads used for these test calls are ripped straight from the data that both Sonarr and Radarr send when configuring a webhook notification in their respective app UI so you can use them as-is instead of using the “Test” button in their UI.&lt;/p&gt;

&lt;p&gt;Below is a screenshot of what it looks like when receiving a text in  Messages:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ocN1-K95--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/setup-sonarr-radarr-notify-sms-with-twilio/twilio-messages.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ocN1-K95--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dc1r9kxqg42ml.cloudfront.net/brandonb.ca/posts/setup-sonarr-radarr-notify-sms-with-twilio/twilio-messages.png" alt=" Messages Twilio SMS conversation"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;As you can see, I can receive these notification messages on not only my phone but also my Mac! This ensures that at least one of my devices will receive a notification somewhere but at minimum I’ll get them on my phone.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How To Setup A Windows 10 Application Network Killswitch</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Fri, 12 Jan 2018 08:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/how-to-setup-a-windows-10-application-network-killswitch-pme</link>
      <guid>https://dev.to/brandonb927/how-to-setup-a-windows-10-application-network-killswitch-pme</guid>
      <description>&lt;p&gt;A more appropriate title for this could be: How to setup Windows 10 firewall rules to kill network traffic to an application when it disconnects from your VPN connection, for whatever reason.&lt;/p&gt;

&lt;p&gt;If you’re concerned with privacy and you’re using a VPN connection on your computer to browse the internet, you’re likely wanting to protect your downloading habits from prying eyes among other things. In a previous post I &lt;a href="https://dev.to/ultimate-media-server-setup"&gt;documented the ultimate automated media setup&lt;/a&gt; and covered a bit on setting up a VPN to ensure you’re safe from your ISP and other 3rd-party snoopers. In this post I want to show you how to setup an application in Windows 10 to kill network activity &lt;em&gt;only for that application&lt;/em&gt; using just the Windows Firewall and some straight-forward inbound/outbound rules.&lt;/p&gt;

&lt;p&gt;To begin, you’ll want to ensure that your main network interface has a “Private” network profile and your VPN network interface has a “Public” network profile. If these profiles don’t match what you’re seeing in the Network and Sharing Center in Windows, don’t worry as this is something you’ll be changing. If you can’t modify these profiles, you might have to move on and try something else as I have yet to find another solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the network adapter profiles
&lt;/h2&gt;

&lt;p&gt;Find and open the “Local Security Policy” application by using the search in the start menu. In the left sidebar “Security Settings” list find “Network List Manager Policies” and click on it. In the right side of the window find your regular network interface adapter (it’ll be the one that isn’t your VPN adapter most likely, and not the one that says “All Networks”.) Double-click to open your adapter properties, then go to the “Network Location” tab. Ensure that the Location type is set to “Private” and the User Permissions is set to “User cannot change location”. Do this again for your VPN adapter but ensure that the Location type is “Public” instead.&lt;/p&gt;

&lt;p&gt;Now you can go about setting up the Windows Firewall rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Windows Firewall rules
&lt;/h2&gt;

&lt;p&gt;Find and open the “Windows Firewall” application by using the search in the start menu again. You’ll want to delete any rules you have for the application you want to have a killswitch setup for in both “Inbound” and Outbound rule sets. Usually applications setup any networking rules the first time they are run after being installed and they try to make a connection to your network.&lt;/p&gt;

&lt;p&gt;Once you’ve done that, start with setting up a new “Inbound” rule (the properties of the “Outbound” rule will be the exact same.)&lt;/p&gt;

&lt;p&gt;Select “Program”, then “Next”.&lt;/p&gt;

&lt;p&gt;On the second page, you’ll want to insert the path to your application or find it using the browse button. Once you find and select it, click “Next” again.&lt;/p&gt;

&lt;p&gt;On the third page make sure you &lt;strong&gt;check&lt;/strong&gt; the “Block the connection” option, then click Next.&lt;/p&gt;

&lt;p&gt;On the fourth page, &lt;strong&gt;un-check&lt;/strong&gt; the “Public” profile, then click “Next”.&lt;/p&gt;

&lt;p&gt;On the final page, give it a name (something akin to “Block &amp;lt;application name&amp;gt;” is fine) then click “Finish”.&lt;/p&gt;

&lt;p&gt;Complete the above for the “Outbound” rule following the exact same rule creation steps.&lt;/p&gt;

&lt;p&gt;Once both rules have been create, go back to “Inbound” rules and find the rule you created first. Open Properties on that rule by either right-clicking or selecting the rule and selecting Properties in the far right sidebar. From here select the “Scope” tab, in the “Local IP address” area select “These IP Addresses” option, then click Add. Insert your static IP here or you can insert your IP subnet range (192.168.0.0/16, etc) if you’re not using a static IP. Once completed, click OK then OK again.&lt;/p&gt;

&lt;p&gt;Repeat these firewall rule steps again but for the “Outbound” rule you created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the solution
&lt;/h2&gt;

&lt;p&gt;You’re almost done! Connect to your VPN server then open the application that you’re intending to block network access for. Make the application can perform some network activity such as downloading or accessing remote resources beforehand. You’ll notice that when the application is running and the VPN is connected that all is well and everything operates normally. You can test that your setup is correct by disconnecting your VPN in the middle of the application generating network traffic. What you should see is an immediate shutdown of any network activity from the application, or a gradual slowdown over the course of a minute coming to a complete halt eventually.&lt;/p&gt;

&lt;p&gt;If all of the above is correct, you’re done now! You can feel good knowing that if your VPN connection is interrupted or disconnected for whatever reason, applications you’ve enabled these rules for will not attempt to connect to the internet until the connection is restored.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Getting started with Homebridge on a Raspberry Pi Zero W</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Wed, 22 Nov 2017 08:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/getting-started-with-homebridge-on-a-raspberry-pi-zero-w-242h</link>
      <guid>https://dev.to/brandonb927/getting-started-with-homebridge-on-a-raspberry-pi-zero-w-242h</guid>
      <description>&lt;p&gt;This post is a continuation of my original post guiding you through &lt;a href="https://dev.to/raspberry-pi-zero-w-headless-setup-on-macos"&gt;setting up a headless Raspberry Pi Zero W on macOS&lt;/a&gt;. Immediately after I wrote that post, not much happened with my rPi and it sat unplugged for a few months while I mulled over a project to put it to use for. Over the coming months however I have been increasingly interested in home automation exclusively with &lt;a href="https://www.macrumors.com/guide/homekit-101-getting-started-beginners/"&gt;HomeKit&lt;/a&gt;. I got to wondering, “I should be able to control all the connected devices in my home using just the Home app running iOS 11 on my iPhone 7 Plus and not these other inferior apps!”.&lt;/p&gt;

&lt;p&gt;While doing research for home automation, I stumbled on &lt;a href="https://github.com/nfarina/homebridge"&gt;homebridge&lt;/a&gt; and it dawned on me that to use my rPi just for it. After getting everything installed and setup a few times (wiping the SD card between installs and retrying), I came to realize how surprisingly easy it is to setup! In this post I’m going to show you how to get your own rPi running homebridge, and even recommend some plugins to use to make it useful.&lt;/p&gt;

&lt;p&gt;To start, I began taking inventory of all the things in my house I could add to homebridge to use with HomeKit. I had initially planned on only integrating my 2016 Samsung Smart TV and Playstation 4 in some fashion, but once I got everything installed and running I purchased a few TP-Link HS100 smart wifi plugs. The wifi plugs were on sale at my local BestBuy for $20/ea which is 50% off the normal price 😱 I couldn’t say no, plus I’d had my eye on them already since research into homebridge plugins yielded them to have very good support with the system regardless of not being supported at all by HomeKit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install node and homebridge
&lt;/h3&gt;

&lt;p&gt;Start by installing node with npm &lt;a href="https://github.com/sdesalas/node-pi-zero#v890"&gt;from here&lt;/a&gt; (don’t install from &lt;code&gt;apt-get&lt;/code&gt; because the package in the PPA is incredibly out of date). An alternative to npm would be to use &lt;a href="https://yarnpkg.com/en/"&gt;yarn&lt;/a&gt;, but that route remains untested to my knowledge.&lt;/p&gt;

&lt;p&gt;Once node is installed, go ahead and install &lt;a href="https://github.com/nfarina/homebridge"&gt;homebridge&lt;/a&gt;. Installing it is as simple as installing a global npm package!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g homebridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running homebridge
&lt;/h2&gt;

&lt;p&gt;You can experiment with running homebridge from the cli once installed by just calling the executable. It’ll look for config in &lt;code&gt;/home/&amp;lt;user&amp;gt;/.homebridge&lt;/code&gt; so feel free to add a basic config file there for testing.&lt;/p&gt;

&lt;p&gt;The ideal way to use homebridge, however, is running it as a service in the background &lt;a href="https://gist.github.com/johannrichard/0ad0de1feb6adb9eb61a/"&gt;with systemd&lt;/a&gt;. This allows homebridge to auto-run on startup and doesn’t require a shell being open on your computer. One caveat I found with this setup, though easily solvable, is that the path to the homebridge executable is unknown to the required &lt;code&gt;homebridge&lt;/code&gt; system user you have to create while seting it up. Its not documented anywhere, but can be fixed by adding the node &lt;code&gt;bin&lt;/code&gt; path to the user. You can do this by creating a new file in &lt;code&gt;/etc/profile.d&lt;/code&gt; and making it executable&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo touch /etc/profile.d/node.sh
sudo chmod +x /etc/profile.d/node.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where the contents are&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export PATH=$PATH:/opt/nodejs/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does is add &lt;code&gt;/opt/nodejs/bin&lt;/code&gt; to the &lt;code&gt;PATH&lt;/code&gt; environment variable of every user profile. This means the system user &lt;code&gt;homebridge&lt;/code&gt; can now run the homebridge exectuable normally without a full path to its location. The alternative is to specify the full path of the executable when calling it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring homebridge
&lt;/h3&gt;

&lt;p&gt;At this point, reboot the pi for the system user to pick up the path to the homebridge executable and start it up. You won’t have any configuration setup for homebridge to use (unless you jumped ahead to here 😉) and it will complain about this. You can see the live stream of logs for the homebridge service with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo journalctl -u homebridge -f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the file &lt;code&gt;/var/lib/homebridge/config.json&lt;/code&gt; and fill it with the the following starter configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "bridge": {
    "name": "Homebridge",
    "username": "1A:2B:3C:4D:5E:6F",
    "port": 45525,
    "pin": "937-19-468"
  },
  "description": "SmartHome with Homebridge",
  "accessories": [...],
  "platforms": [...]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Notes
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;a href="https://www.miniwebtool.com/mac-address-generator/"&gt;this tool&lt;/a&gt; to generate a random &lt;code&gt;username&lt;/code&gt; value in the configuration&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;...&lt;/code&gt; with the plugins that you desire once you confirm homebridge can run on it’s own&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After you’ve made all your changes, ensure that the &lt;code&gt;homebridge&lt;/code&gt; user has ownership of the &lt;code&gt;/var/lib/homebridge&lt;/code&gt; folder with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo chown -R homebridge /var/lib/homebridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; This step is &lt;em&gt;very crucial&lt;/em&gt; because if the homebridge executable can’t write it’s various caches to the folder, things might blow up on you!&lt;/p&gt;

&lt;h3&gt;
  
  
  Extending homebridge
&lt;/h3&gt;

&lt;p&gt;Homebridge by itself isn’t very useful you’ll find, and where it really shines is its &lt;a href="https://www.npmjs.com/search?q=homebridge-plugin&amp;amp;page=1&amp;amp;ranking=popularity"&gt;community-backed plugin system&lt;/a&gt;. Just doing a search on npmjs.com for &lt;code&gt;homebridge-plugin&lt;/code&gt; yields a few hundred plugins to choose from. Sort by popularity and the first few pages are full of good plugins.&lt;/p&gt;

&lt;p&gt;Here’s a list of the ones I’m using/recommending:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/homebridge-cmdswitch2"&gt;https://www.npmjs.com/package/homebridge-cmdswitch2&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;👆 In combination with &lt;a href="https://github.com/dhleong/ps4-waker"&gt;https://github.com/dhleong/ps4-waker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/homebridge-samsungsmarttv"&gt;&lt;/a&gt;&lt;a href="https://www.npmjs.com/package/homebridge-samsungsmarttv"&gt;https://www.npmjs.com/package/homebridge-samsungsmarttv&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/brandonb927/homebridge-plugin-samsungsmarttv"&gt;My ongoing fork&lt;/a&gt; (waiting for iOS 11 to support the &lt;code&gt;Speaker&lt;/code&gt; service properly in the Home app, they’re part of the HomeKit spec just not implemented at the time of writing)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/homebridge-tplink-smarthome"&gt;&lt;/a&gt;&lt;a href="https://www.npmjs.com/package/homebridge-tplink-smarthome"&gt;https://www.npmjs.com/package/homebridge-tplink-smarthome&lt;/a&gt; (Support for switches, bulbs, and plugs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Using homebridge with the iOS 11 Home app
&lt;/h3&gt;

&lt;p&gt;The moment you’ve probably been waiting for: setting Homebridge and “accessories” (connected devices) up in iOS Home is very straight forward. The in-app documentation is very easy to follow, and if you’re running the homebridge cli tool from a terminal (not the service-based system user), you can just scan the QR code that is generated when you run &lt;code&gt;homebridge&lt;/code&gt; with the Home app. The alternate Home setup method is that you’ll have to enter the &lt;code&gt;pin&lt;/code&gt; you configured in the &lt;code&gt;config.json&lt;/code&gt; file intead.&lt;/p&gt;

&lt;p&gt;Below is a video of the end product and how well it works with the iOS 11 Home app. The video starts out showing how the different devices are triggered in the app (and how some devices have to be interfaced with, particularly my smart TV and PS4 there is some latency which I can go into in detail later) followed by show how to interface with devices through Home Scenes using Siri.&lt;/p&gt;


        
        &lt;p&gt;Hmm, your browser doesn't seem to support HTML5 video.&lt;/p&gt;
      

</description>
      <category>raspberrypi</category>
      <category>homebridge</category>
    </item>
    <item>
      <title>Raspberry Pi Zero W headless setup on macOS</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Thu, 11 May 2017 07:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/raspberry-pi-zero-w-headless-setup-on-macos-3kb7</link>
      <guid>https://dev.to/brandonb927/raspberry-pi-zero-w-headless-setup-on-macos-3kb7</guid>
      <description>&lt;p&gt;Recently I acquired a RaspberryPi Zero W, a few years after I regretted selling my RaspberryPi B board back in 2013. The following is an account of the trials and tribulations I endured to get it connected to the internet!&lt;/p&gt;

&lt;p&gt;This setup requires no monitor, or external keyboard, not even the OTG USB cable that I bought because I thought I needed it.&lt;/p&gt;

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

&lt;p&gt;If you’re reading this you’re probably here to learn how to get a rPi Zero W running Raspbian and connected wirelessly to the internet. To do that, there’s a few things you’ll need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A copy of the &lt;a href="https://www.raspberrypi.org/downloads/raspbian/" rel="noopener noreferrer"&gt;Raspbian Jessie Lite image&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An 8GB+ MicroSD card (and some way to plug it into your computer in order to flash an image to it)&lt;/li&gt;
&lt;li&gt;A micro USB cable (to plug the rPi into your computer temporarily)&lt;/li&gt;
&lt;li&gt;A 2.4Ghz wifi access point (the rPi Zero W doesn’t support 5Ghz which I learned the hard way 😞 )&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have all of these things, great! If not, order all the parts quickly on BuyAPi and continue as if you have them in hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing the SD card
&lt;/h2&gt;

&lt;p&gt;I’ve ran these commands on macOS, so your mileage may vary on Windows/Linux. Plug the SD Card into your computer and once it is mounted you’ll want to unmount it from the terminal to flash it.&lt;/p&gt;

&lt;p&gt;To unmount the drive and prep it for writing the image, run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo df -h # Use the output of this to determine &amp;lt;diskname&amp;gt; in the next command
sudo diskutil unmount /dev/&amp;lt;diskname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the previous command completed, you have a drive that you can copy the Raspbian image to!&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the image to the SD card
&lt;/h2&gt;

&lt;p&gt;Run the command below to start writing the image to the SD card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dd bs=1m if=/path/to/raspbian-jessie-lite.img of=/dev/rdisk2
# Where disk2 is the number of the disk from earlier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command above should only take a few minutes depending on the size of the SD card. After it completes you’ll need to do a few things.&lt;/p&gt;

&lt;p&gt;Start by opening your terminal and running &lt;code&gt;touch /Volumes/boot/ssh&lt;/code&gt;. The existence of this empty file in the root of the drive will enable SSH on first boot when we connect it with the USB cable.&lt;/p&gt;

&lt;p&gt;Next, append &lt;code&gt;dtoverlay=dwc2&lt;/code&gt; to the end of the &lt;code&gt;/Volumes/boot/config.txt&lt;/code&gt; file on a new line.&lt;/p&gt;

&lt;p&gt;Finally, open &lt;code&gt;/Volumes/boot/cmdline.txt&lt;/code&gt; and insert &lt;code&gt;modules-load=dwc2,g_ether&lt;/code&gt; after the &lt;code&gt;rootwait&lt;/code&gt; entry following the same space-delimited pattern as the rest of the file.&lt;/p&gt;

&lt;p&gt;Once you’ve completed the steps outlined above, unmount the drive again from the terminal and plug it into the rPi and connect the power USB cable to the board.&lt;/p&gt;

&lt;h2&gt;
  
  
  First boot into the OS
&lt;/h2&gt;

&lt;p&gt;Give the rPi about 60s to boot up and then connect the other USB cable from the OTG port to your computer.&lt;/p&gt;

&lt;p&gt;Once connected, open &lt;code&gt;System Preferences -&amp;gt; Network&lt;/code&gt; and you’ll notice there is a new &lt;code&gt;RNDIS/Ethernet Gadget&lt;/code&gt; network adapter added in the list. It will probably show up with a &lt;code&gt;169...&lt;/code&gt; IP address so you’ll need to head to &lt;code&gt;System Preferences -&amp;gt; Sharing&lt;/code&gt; now to enable “Internet Sharing” for the &lt;code&gt;RNDIS/Ethernet Gadget&lt;/code&gt; adapter.&lt;/p&gt;

&lt;p&gt;Make sure you check the correct boxes as illustrated below:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fraspberry-pi-zero-w-headless-macos%2Fnetwork-adapters.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fraspberry-pi-zero-w-headless-macos%2Fnetwork-adapters.png" alt="macOS Network Adapters"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fraspberry-pi-zero-w-headless-macos%2Fnetwork-configuration.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fraspberry-pi-zero-w-headless-macos%2Fnetwork-configuration.png" alt="macOS Network Sharing Configuration"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;Once you have local network access to the rPi, you can ssh in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh pi@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SSH into the RaspberryPi
&lt;/h2&gt;

&lt;p&gt;Once you’re ssh’d into the rPi, you can setup the wifi so that being connected via USB is nolonger required.&lt;/p&gt;

&lt;p&gt;In order to get the right configuration for your wifi, run the &lt;code&gt;wpa_passphrase&lt;/code&gt; utility with your SSID and it will ask for your password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wpa_passphrase "SSID HERE"
# You will be prompted to enter your password.
# The utility should generate something like:
# network={
# ssid="SSID"
# #psk="PASSWORD"
# psk=...
# }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; : If you have a custom wifi setup that isn’t WPA2-PSK/TKIP encrypted network, you will need to do some research into the &lt;a href="https://linux.die.net/man/5/wpa_supplicant.conf" rel="noopener noreferrer"&gt;config values&lt;/a&gt; for &lt;code&gt;wpa_supplicant&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;Copy the output of the command above into the &lt;code&gt;/etc/wpa_supplicant/wpa_supplicant.conf&lt;/code&gt; file and overwrite whats in the &lt;code&gt;network={...}&lt;/code&gt; section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After pasting the output, save and exit the file. &lt;code&gt;wpa_supplicant&lt;/code&gt; should pick up on the changes to the file and connect to the wifi automatically.&lt;/p&gt;

&lt;p&gt;You can confirm that your rPi has connected to your wifi by running &lt;code&gt;sudo ifconfig wlan0&lt;/code&gt; on the rPi in your SSH session. In the output, look for &lt;code&gt;inet addr:...&lt;/code&gt; which should display an ip address. If this looks correct, then your rPi is connected to your DHCP server/router.&lt;/p&gt;

&lt;p&gt;The other option is by checking in the GUI of the router itself in your browser to see if the router issued an IP address to the rPi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update the system
&lt;/h2&gt;

&lt;p&gt;Once you’re connected to the wifi, the first thing you should do is update &lt;code&gt;apt-get&lt;/code&gt; and upgrade Raspbian as there are undoubtedly some security packages between the image creation and the time you install it on your rPi.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get dist-upgrade -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Once you’d completed the above steps, you should now have a functioning RaspberryPi Zero W connected to the internet and ssh’able!&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional things to do after setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gagle/raspberrypi-motd" rel="noopener noreferrer"&gt;Setup an MOTD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;a href="https://github.com/robbyrussell/oh-my-zsh" rel="noopener noreferrer"&gt;oh-my-zsh&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install &lt;a href="https://github.com/creationix/nvm" rel="noopener noreferrer"&gt;nodejs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://blog.gbaman.info/?p=791" rel="noopener noreferrer"&gt;http://blog.gbaman.info/?p=791&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md" rel="noopener noreferrer"&gt;https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://blog.remmelt.com/2017/03/20/easy-headless-setup-for-raspberry-pi-zero-w-on-osx/" rel="noopener noreferrer"&gt;http://blog.remmelt.com/2017/03/20/easy-headless-setup-for-raspberry-pi-zero-w-on-osx/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>raspberrypi</category>
      <category>macos</category>
      <category>raspbian</category>
    </item>
    <item>
      <title>Getting started with Pebble SDK and Rocky.js</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Wed, 02 Nov 2016 07:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/getting-started-with-pebble-sdk-and-rockyjs-2849</link>
      <guid>https://dev.to/brandonb927/getting-started-with-pebble-sdk-and-rockyjs-2849</guid>
      <description>&lt;p&gt;Last updated: January 6th 2017&lt;/p&gt;

&lt;p&gt;A month after publishing this post, &lt;a href="https://www.bloomberg.com/news/articles/2016-12-07/pebble-said-to-discuss-selling-software-assets-to-fitbit" rel="noopener noreferrer"&gt;Pebble was acquired by Fitbit&lt;/a&gt; and if you haven’t seen &lt;a href="https://twitter.com/brandonb927/status/806518072970383361" rel="noopener noreferrer"&gt;my reaction&lt;/a&gt; on Twitter, I’ll have you know that I’m not very happy about it. Fitbit has vowed to ensure &lt;a href="https://developer.pebble.com/blog/2016/12/14/first-steps-forward-with-fitbit/" rel="noopener noreferrer"&gt;all active Pebble devices will stay running through 2017&lt;/a&gt;, however with all things I am skeptical.&lt;/p&gt;

&lt;p&gt;From Dec 8th 2016 on, here be dragons.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; : I built and published my first watchface using RockyJS, available on the &lt;a href="https://apps.getpebble.com/en_US/application/58070ba89392ff32cf0002ce" rel="noopener noreferrer"&gt;Pebble app store&lt;/a&gt;! I wouldn’t mind if you went there gave it a big ole’ ❤ 😁&lt;/p&gt;

&lt;p&gt;The source is also available on &lt;a href="https://github.com/brandonb927/pebble-watchface-vw-unofficial/" rel="noopener noreferrer"&gt;github&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I owned a Fitbit Charge HR, but I grew out of it. Wanting something else, the Pebble Time device seemed to fit my checklist of “wants and needs”. The hackability of the Pebble, the community, and the ecosystem are what drew me to it. I wasn’t a fan of the original device using a black and white e-ink display, but when the colour e-ink display was announced I became invested.&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- break --&amp;gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fmy-pebble-time-steel.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fmy-pebble-time-steel.jpg" alt="My Pebble Time Steel"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;After getting my Pebble, I started looking up what I can do to develop for it. While reading through the documentation, I realized I’d rather not learn &lt;code&gt;C&lt;/code&gt; language to write apps. The &lt;code&gt;C&lt;/code&gt; apps do use a tiny bit of Javascript to glue it to the phone’s app using &lt;code&gt;PebbleKit&lt;/code&gt;, but that’s a small chunk of code. Fortunately, you can use &lt;a href="https://pebble.github.io/rockyjs/" rel="noopener noreferrer"&gt;RockyJS&lt;/a&gt; to write watchfaces completely in Javascript. RockyJS is a developing framework for building embedded &lt;a href="https://developer.pebble.com/blog/2016/08/15/introducing-rockyjs-watchfaces/" rel="noopener noreferrer"&gt;Javascript watchapps&lt;/a&gt;. As of the most recent major Pebble firmware update, there is now an embedded engine that runs Javascript on the device. This allows RockyJS-built watchapps to run natively on the Pebble watch without compiling to C.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The only prerequisites you need to start developing RockyJS watchfaces are a slight knowledge of Javascript, and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API" rel="noopener noreferrer"&gt;Canvas API&lt;/a&gt;. Everything else you need to know is an added bonus and you’ll see why in the next few sections.&lt;/p&gt;

&lt;p&gt;The basic concept behind RockyJS is thus: it abstracts the concept of the &lt;code&gt;canvas&lt;/code&gt; away from you (because you’re rendering to a fixed screen size), and instead provides you with a &lt;code&gt;context&lt;/code&gt; object to work with that is populated with the API methods available. From the &lt;code&gt;context&lt;/code&gt; object you then call drawing commands like &lt;code&gt;ctx.arc&lt;/code&gt;, &lt;code&gt;ctx.rect&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;As you can see in the example below RockyJS is event-based, so it is most performant when rendering when certain events fire such as &lt;code&gt;minutechange&lt;/code&gt; or &lt;code&gt;hourchange&lt;/code&gt; (on the minute and top of the hour.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The draw event is called when the watchface is initialized
rocky.on('draw', function (event) {
  // The draw event contains the context object that you use to draw to
  var ctx = event.context
  // Do stuff with the ctx object at this point
  ...
})

// When the minutechange event fires (every minute on the minute)
// request that rocky draws to the screen
rocky.on('minutechange', function (event) {
  rocky.requestDraw()
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;There are a few routes you can take to develop watchfaces:&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudPebble
&lt;/h3&gt;

&lt;p&gt;If you’re getting started, &lt;a href="https://cloudpebble.net" rel="noopener noreferrer"&gt;CloudPebble&lt;/a&gt; is a great place to begin. It’s currently the only Pebble IDE/editor with emulator support built-in for every Pebble SDK and platform. You can even connect your &lt;em&gt;physical Pebble device&lt;/em&gt; to your CloudPebble account and push the project build to your watch in &lt;strong&gt;real time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/5XqGhjDB48YqA/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/5XqGhjDB48YqA/giphy.gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setting up CloudPebble support in the iOS Pebble app goes like so:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-1.png" alt="My Pebble Time Steel"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-2.png" alt="My Pebble Time Steel"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;br&gt;
          &lt;br&gt;
          &lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdc1r9kxqg42ml.cloudfront.net%2Fbrandonb.ca%2Fposts%2Fpebble-app-3.png" alt="My Pebble Time Steel"&gt;&lt;/a&gt;&lt;br&gt;
        &lt;/p&gt;

&lt;p&gt;Once you have the Developer mode turned on, the &lt;code&gt;CloudPebble Connection&lt;/code&gt; will remain “waiting” until you connect it to your CloudPebble account and push your first project through your phone to your Pebble device.&lt;/p&gt;

&lt;p&gt;To do this, head over to CloudPebble and &lt;a href="https://auth.getpebble.com/users/sign_up" rel="noopener noreferrer"&gt;create an account&lt;/a&gt;. Once set up, you can create a new project. Ensure the &lt;code&gt;Project Type&lt;/code&gt; is set as &lt;code&gt;Rocky.js&lt;/code&gt;. Now you can start coding in the editor!&lt;/p&gt;

&lt;p&gt;When you’ve got a project going and it builds correctly in the emulator, you can deploy the build file to your Pebble device. Go to &lt;code&gt;Compilation&lt;/code&gt; in the sidebar, then ensure that &lt;code&gt;Phone&lt;/code&gt; is selected instead of &lt;code&gt;Emulator&lt;/code&gt;. If you’ve configured all of this correctly, you should be able to click &lt;code&gt;Install and Run&lt;/code&gt;. Watch your pebble device as it loads your watchface then displays it!&lt;/p&gt;
&lt;h3&gt;
  
  
  Local Development
&lt;/h3&gt;

&lt;p&gt;Pebble has an SDK that you can download with &lt;a href="http://brew.sh/" rel="noopener noreferrer"&gt;&lt;code&gt;brew&lt;/code&gt;&lt;/a&gt; by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew update &amp;amp;&amp;amp; brew upgrade pebble-sdk &amp;amp;&amp;amp; pebble sdk install latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By allowing devs to develop locally using the SDK, you can do the same things that CloudPebble offers but on your local computer. The SDK ships with an emulator and allows you to deploy to your physical Pebble device as well. There is also a chance for you to build out your own build steps/processes and use &lt;a href="https://github.com/blendsoul/pemulator" rel="noopener noreferrer"&gt;pEmulator&lt;/a&gt; in the browser.&lt;/p&gt;

&lt;p&gt;Installing your watchface on a physical Pebble device is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pebble build
pebble install [--phone &amp;lt;IP ADDR&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bugs &amp;amp; issues
&lt;/h2&gt;

&lt;p&gt;There are a few bugs/issues using RockyJS for watchfaces as of this writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unsupported web APIs and not all &lt;code&gt;canvas&lt;/code&gt; rendering context methods are avilable for use in a RockyJS watchface, &lt;a href="https://developer.pebble.com/docs/rockyjs/" rel="noopener noreferrer"&gt;check here for API parity&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Can’t write true “apps”, only watchface apps are supported right now&lt;/li&gt;
&lt;li&gt;Using ECMAScript right now is a no-go, transpiling might work but I haven’t tested it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;There’s not a lot of resources out there for RockyJS development quite yet, so the pickings are a bit slim. However, here are some of the resources I have found incredibly useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The community is a great place to join up and learn from - &lt;a href="https://developer.pebble.com/community/online/" rel="noopener noreferrer"&gt;https://developer.pebble.com/community/online/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pebble-examples/rocky-watchface-tutorial-part1" rel="noopener noreferrer"&gt;RockyJS watchface tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Develop locally with an in-browser Pebble device emulator (not a real emulator though) called &lt;a href="https://github.com/blendsoul/pemulator" rel="noopener noreferrer"&gt;pEmulator&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rockyjs</category>
      <category>pebble</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Developing Simple JSON APIs on Surge</title>
      <dc:creator>Brandon Brown</dc:creator>
      <pubDate>Wed, 02 Mar 2016 08:00:00 +0000</pubDate>
      <link>https://dev.to/brandonb927/developing-simple-json-apis-on-surge-1ala</link>
      <guid>https://dev.to/brandonb927/developing-simple-json-apis-on-surge-1ala</guid>
      <description>&lt;p&gt;Earlier this morning I read a post authored by another Surge user &lt;a href="http://www.raymondcamden.com/2016/03/01/adding-an-api-to-a-static-site/" rel="noopener noreferrer"&gt;Raymond Camden&lt;/a&gt; on how one could leverage Jekyll to build JSON files to construct rudimentary read-only JSON APIs. It occurred to me that I had accomplished this recently on my own site with the &lt;a href="https://brandonb.io/checkins" rel="noopener noreferrer"&gt;check-in map page&lt;/a&gt;, thus inspiring me to start writing about it!&lt;/p&gt;

&lt;p&gt;Let me state up front that the title of this post is also kind of loaded, and by saying &lt;code&gt;API&lt;/code&gt; I mean just the &lt;code&gt;Read&lt;/code&gt; part of the &lt;code&gt;CRUD (Create Read Update Delete)&lt;/code&gt; model. This site is hosted on &lt;a href="https://surge.sh" rel="noopener noreferrer"&gt;Surge&lt;/a&gt; which is a static-site hosting platform. Due to the nature of Surge and static sites, there is no "backend" to utilize a sripting language, so we're left with figuring out ways of authoring read-only JSON APIs. This is where Surge, being massively distributed across a powerful CDN, shines at serving JSON files that are cached and easily consumable. With all of this in mind, theoretically, one could build a static API backed by automated deploy scripts and it would be fairly scalable to a point.&lt;/p&gt;

&lt;p&gt;Raymond talks about building JSON with &lt;code&gt;{% for … %}&lt;/code&gt; loops in Jekyll or by powering it with JSON files in the &lt;code&gt;_data&lt;/code&gt; folder. These are Jekyll-specific methods and by any account is a completely legit way to do it. When implementing my "API" I started from a different slightly different angle because I was involving &lt;em&gt;another&lt;/em&gt; 3rd-party API to power my own. The decision I inevitably landed on was to build my "API" as a Gulp task which would be part of my build/deploy process, then leverage the Foursquare API to power my own "API" pre-deploy.&lt;/p&gt;

&lt;p&gt;Initially, I had wanted to do all the work in the browser on the front-end when deciding to start this project. This meant the map data fetching would be async in the browser and there would be no backend, primarily because Surge doesn't provide one. The problem I ran into however was that the Foursquare API doesn't support front-end publishable API keys like MapBox does. In order to get data from the service you need an OAuth token, and anyone who's been on the internet for any length of time knows front-end based OAuth is non-existent. This would have posed a security loophole had I gone down that road of publishing my secret key on the Internet.&lt;/p&gt;

&lt;p&gt;Faced with this hurdle, I had to rethink the implementation of data fetching from the Foursquare API and how it was going to happen on the front-end. It later dawned on me that one could just provide a prebuilt JSON file with the all of the data needed to power the map marker locations. Bingo!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fi.imgur.com%2FMjlyUSt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/http%3A%2F%2Fi.imgur.com%2FMjlyUSt.gif" alt="Bringo!"&gt;&lt;/a&gt;&lt;br&gt;
  &lt;br&gt;&lt;br&gt;
  &lt;br&gt;&lt;/p&gt;

&lt;p&gt;The Gulp task was a pain in the ass to get going but with time I got it correct. It turns out calling HTTP endpoints with paged requests synchronously then writing to a file asynchronously is kind of a big pain. Below is the code I eventually rolled with, commented for better readability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gulp_tasks/tasks/foursquare.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;closeSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;openSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;existsSync&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;gulp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gulp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;gutil&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gulp-util&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync-request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Get the Foursquare OAuth token that we've previously acquired from the API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FOURSQUARE_OAUTH_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FOURSQUARE_OAUTH_TOKEN&lt;/span&gt;

&lt;span class="c1"&gt;// Set the API Base URL, straight forward&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.foursquare.com/v2/users/self/checkins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Give the API a unique URL (the docs demonstrate this, though I'm not sure why)&lt;/span&gt;
&lt;span class="c1"&gt;// https://developer.foursquare.com/overview/auth&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?oauth_token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FOURSQUARE_OAUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;v=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;checkinItems&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;folderPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;buildPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api`&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;folderPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/foursquare.json`&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;errorMsg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✗ Request to Foursquare failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;successMsg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✓ Hitting Foursquare API:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="c1"&gt;// Get Swarm checkins synchronously&lt;/span&gt;
  &lt;span class="kd"&gt;let&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;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINT&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;data&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;getBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;red&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorMsg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create the `api` directory if it doesn't already exist&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folderPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;folderPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Foursquare says you can only retrieve a max of 250 items per response, so we go with that&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;maxApiCheckins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;

  &lt;span class="c1"&gt;// We want to use the total checkin count to use with paginatation math&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalApiCheckins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;

  &lt;span class="c1"&gt;// Get the total (ceiling) pages we can call to the api&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalApiCheckinPages&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalApiCheckins&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxApiCheckins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Start the paging offset at 0&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;checkinOffset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="c1"&gt;// Get the checkins and page through them&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="k"&gt;while &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="nx"&gt;totalApiCheckinPages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Build the paging URL to use with the API call&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;limit=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxApiCheckins&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;offset=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;checkinOffset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;green&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;successMsg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_URL&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_URL&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;checkinData&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;checkinData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&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;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;red&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorMsg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Build a giant object of Venue ID =&amp;gt; Venue + Checkins Array&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;checkin&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;checkinData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkins&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;venue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;checkin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;venue&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;venue&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;venue&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="o"&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;venue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;venue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkins&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;venue&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkins&lt;/span&gt;&lt;span class="dl"&gt;'&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;checkin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Sort dates of checkins, descending by date&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;venueId&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;venueId&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkins&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&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;a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&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;a&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;b&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Bump the offset to use for paging the API&lt;/span&gt;
    &lt;span class="nx"&gt;checkinOffset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;maxApiCheckins&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="c1"&gt;// Write the JSON data to the file finally&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;jsonData&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkinItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UTF-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Tasks to generate the Foursquare JSON data file&lt;/span&gt;
&lt;span class="nx"&gt;gulp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foursquare:dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;buildJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./build_dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;gulp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foursquare:prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;buildJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./build_prod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&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;Output in my terminal looks something like this when the task is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[11:30:08] Starting 'foursquare:dev'…
[11:30:09] ✓ Hitting Foursquare API: https://api.foursquare.com/v2/users/self/checkins?oauth_token=OAUTHTOKEN&amp;amp;v=1456947006694&amp;amp;limit=250&amp;amp;offset=0
[11:30:11] ✓ Hitting Foursquare API: https://api.foursquare.com/v2/users/self/checkins?oauth_token=OAUTHTOKEN&amp;amp;v=1456947006694&amp;amp;limit=250&amp;amp;offset=250
[11:30:14] ✓ Hitting Foursquare API: https://api.foursquare.com/v2/users/self/checkins?oauth_token=OAUTHTOKEN&amp;amp;v=1456947006694&amp;amp;limit=250&amp;amp;offset=500
[11:30:16] ✓ Hitting Foursquare API: https://api.foursquare.com/v2/users/self/checkins?oauth_token=OAUTHTOKEN&amp;amp;v=1456947006694&amp;amp;limit=250&amp;amp;offset=750
[11:30:18] ✓ Hitting Foursquare API: https://api.foursquare.com/v2/users/self/checkins?oauth_token=OAUTHTOKEN&amp;amp;v=1456947006694&amp;amp;limit=250&amp;amp;offset=1000
[11:30:19] Finished 'foursquare:dev' after 10 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gist of the Gulp task is basically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a temporary file to write the JSON data to&lt;/li&gt;
&lt;li&gt;Get the total count of check-ins so we can page the API in the subsequent requests&lt;/li&gt;
&lt;li&gt;Page over the API, building an object that looks like
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Venue ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;venue:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Venue object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkins:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Array of check-in objects for this venue&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="err"&gt;…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Sort the venue check-ins by the date, descending in time&lt;/li&gt;
&lt;li&gt;Write the final compiled object data to the JSON file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once I was able to build the JSON file and have it output to my local build folder, I started on building the front-end map page. I won't go into details about how I implemented that section because it's in plain un-obfuscated JS inlined in the page on Github. You can view the source &lt;a href="https://github.com/brandonb927/brandonb.io/blob/master/_includes/foot.html#L7-L95" rel="noopener noreferrer"&gt;here&lt;/a&gt; and the &lt;a href="https://brandonb.io/api/foursquare.json" rel="noopener noreferrer"&gt;JSON file&lt;/a&gt; that exists on my site (forewarning, the JSON file is pretty big and your browser might crash like mine did).&lt;/p&gt;

&lt;p&gt;I'm constantly updating the look and feel of the Check-Ins page so check back periodically for updates 😁.&lt;/p&gt;

</description>
      <category>surge</category>
      <category>cdn</category>
      <category>json</category>
      <category>api</category>
    </item>
  </channel>
</rss>
