<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Learn Enough News & Blog]]></title>
  <link href="https://news.learnenough.com/atom.xml" rel="self"/>
  <link href="https://news.learnenough.com/"/>
  <updated>2023-08-21T20:56:56+00:00</updated>
  <id>https://news.learnenough.com/</id>
  <author>
    <name><![CDATA[]]></name>
    <email><![CDATA[info@learnenough.com]]></email>
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  <entry>
    <title type="html"><![CDATA[Learn Enough Python to Be Dangerous]]></title>
    <link href="https://news.learnenough.com/learn-enough-python-to-be-dangerous"/>
    <updated>2022-12-15T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/learn-enough-python-to-be-dangerous</id>
    <content type="html">
      <![CDATA[
      <p><a href="https://www.michaelhartl.com/" target="_blank" rel="noopener">Michael Hartl</a> here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>I’m pleased to announce that <a href="https://www.learnenough.com/python" target="_blank" rel="noopener"><em>Learn Enough Python to Be Dangerous</em></a> is now available!<span class="intersentencespace"></span> As usual, the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">online course</a> features both book-length text (approximately 450 pages print-equivalent) and embedded streaming videos (nearly 9 hours’ worth), as well as integrated progress tracking and over 100 exercises.<span class="intersentencespace"></span> It is also available as a <a href="https://www.informit.com/store/learn-enough-python-to-be-dangerous-software-development-9780138050955" target="_blank" rel="noopener">print edition and ebook</a> and as a <a href="https://www.informit.com/store/learn-enough-python-to-be-dangerous-livelessons-software-9780138050733" target="_blank" rel="noopener">standalone video bundle</a>.<span class="intersentencespace"></span> The only prerequisites for <em>Learn Enough Python to Be Dangerous</em> are the developer tools covered by the Learn Enough Command Line, Text Editor, and Git tutorials.</p>
<div class="section-star" id="a_meaningful_moment"><h2><a href="#a_meaningful_moment" class="heading">A meaningful moment</a></h2>
<p>The release of <em>Learn Enough Python to Be Dangerous</em> is especially meaningful to me for two reasons.<span class="intersentencespace"></span> First, although nowadays I’m probably better known for my work in the Ruby community (especially the <em>Ruby on Rails Tutorial</em>), Python is the first programming language I really loved.<span class="intersentencespace"></span> I used Python heavily both in graduate school and in my first startup (where I wrote my own half-baked web framework in Python since Django hadn’t been developed yet).<span class="intersentencespace"></span> With Python’s combination of power and intuitive design, I felt like I could use it to solve practically any problem I encountered.</p>
<p>Second, this is a poignant moment because <em>Learn Enough Python to Be Dangerous</em> is my final Learn Enough tutorial.<span class="intersentencespace"></span> As noted in the <a href="/big-news-about-learn-enough" target="_blank" rel="noopener">acquisition announcement</a>, Learn Enough was acquired earlier this year by a tech private equity group.<span class="intersentencespace"></span> Although completing <em>Learn Enough Python to Be Dangerous</em> was not a requirement of the acquisition, I started work on it last year and was eager to see the project to its completion.<span class="intersentencespace"></span> Also, did I mention that Python is the first programming language I really loved?</p>
</div>
<div class="section-star" id="a_multitude_of_topics"><h2><a href="#a_multitude_of_topics" class="heading">A multitude of topics</a></h2>
<p>Students of <em>Learn Enough JavaScript to Be Dangerous</em> and <em>Learn Enough Ruby to Be Dangerous</em> will find a lot of material they are already familiar with.<span class="intersentencespace"></span> Indeed, this is very much by design—seeing the same basic problems solved in multiple languages is a great way to learn.<span class="intersentencespace"></span> The topics thus include things like:</p>
<ul>
<li>“Hello, world!” in the Python interpreter, as a standalone program, as a shell script, and as a web app
</li>
<li>Objects like lists (similar to arrays), dictionaries (also called associative arrays or hashes), times and datetimes, and regular expressions
</li>
<li>Functions and functional programming, including list, dictionary, set, and generator comprehensions
</li>
<li>Objects and classes
</li>
<li>Testing and test-driven development (TDD)
</li>
<li>Publishing a Python package
</li>
<li>Using Python for shell scripts
</li>
<li>Writing and deploying a Python web application with Flask (Python’s equivalent of Ruby’s Sinatra)
</li></ul>
<p>You can get a good sense of what the tutorial covers by taking a look at the table of contents and the three free sample chapters in the <a href="https://www.learnenough.com/python-tutorial" target="_blank" rel="noopener">online book</a>.</p>
</div>
<div class="section-star" id="a_datascience_monster"><h2><a href="#a_datascience_monster" class="heading">A data-science monster</a></h2>
<p>In addition to the topics mentioned above, <em>Learn Enough Python to Be Dangerous</em> includes a monster chapter (approximately 100 pages) on a topic not covered at all in the JavaScript or Ruby tutorials but one that’s a big deal in the Python world—namely, Python tools for data science:</p>
<ul>
<li>Jupyter for interactive calculations
</li>
<li>NumPy for numerical computations
</li>
<li>Matplotlib for data visualization
</li>
<li>pandas for data analysis
</li>
<li>scikit-learn for machine learning
</li></ul>
<p class="noindent">Preparing the data-science chapter required a tremendous amount of effort, drawing heavily on my background in scientific computing and including generous assistance from a couple of highly qualified friends (a professional data scientist and an economics professor who teaches a university course on—wait for it—data science with Python).</p>
<p>This one chapter alone added several months to the project (both text and videos), but I’m thrilled with the result.<span class="intersentencespace"></span> <em>Learn Enough Python to Be Dangerous</em> not only gives you a solid overall foundation in Python, it also gives you a great head start on using Python for data science.</p>
</div>
<div class="section-star" id="how_to_get_it"><h2><a href="#how_to_get_it" class="heading">How to get it</a></h2>
<p>Although available as a standalone course, the best way to get <em>Learn Enough Python to Be Dangerous</em> is with the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">All Access subscription</a>, which includes all of its prerequisites as well as the full <em>Ruby on Rails Tutorial</em>.<span class="intersentencespace"></span> The print edition and ebook are also available directly from the publisher at <a href="https://www.informit.com/store/learn-enough-python-to-be-dangerous-software-development-9780138050955" target="_blank" rel="noopener">InformIT</a>.</p>
<ul>
<li><a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access</a>: The All Access subscription includes <em>all</em> of the Learn Enough tutorials, including Developer Tools, HTML &amp; CSS, JavaScript, Ruby, Ruby on Rails, and (of course) Python.<span class="intersentencespace"></span>
</li>
<li>InformIT from Pearson Education: The <a href="https://www.informit.com/store/learn-enough-python-to-be-dangerous-software-development-9780138050955" target="_blank" rel="noopener">print edition and ebook</a> and a <a href="https://www.informit.com/store/learn-enough-python-to-be-dangerous-livelessons-software-9780138050733" target="_blank" rel="noopener">standalone video bundle</a> are available for purchase directly from the publisher’s website.<span class="intersentencespace"></span>
</li></ul>
<p>Finishing <em>Learn Enough Python to Be Dangerous</em> has been deeply satisfying and gives me a real sense of closure.<span class="intersentencespace"></span> I really wanted to share my take on this wonderful language with the world, and I’m glad I had the opportunity to do so.</p>
<p>I hope you enjoy it!</p>
<p class="noindent">Best wishes, <span class="break"></span></p>
<p class="noindent"><a href="https://www.michaelhartl.com/" target="_blank" rel="noopener">Michael Hartl</a> <br>
Founder and principal author <br>
Learn Enough</p>
<p>P.S.<span class="intersentencespace"></span> If you’d like to keep in touch, you can subscribe to my <a href="https://substack.michaelhartl.com/" target="_blank" rel="noopener">personal mailing list</a> and <a href="https://twitter.com/mhartl" target="_blank" rel="noopener">follow me on Twitter</a>.
</p></div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Print versions of Learn Enough HTML, CSS and Layout and Learn Enough JavaScript]]></title>
    <link href="https://news.learnenough.com/html-css-layout-javascript-print-versions"/>
    <updated>2022-09-02T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/html-css-layout-javascript-print-versions</id>
    <content type="html">
      <![CDATA[
      <p><a href="https://www.michaelhartl.com/" target="_blank" rel="noopener">Michael Hartl</a> here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>In a <a href="/learn-enough-developer-tools-to-be-dangerous-print-edition" target="_blank" rel="noopener">previous post</a>, we told you about the print version of <a href="https://www.informit.com/store/learn-enough-developer-tools-to-be-dangerous-command-9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools to Be Dangerous</em></a> (in partnership with Pearson Education), which combines the material in the Learn Enough ebooks on <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener">Command Line</a>, <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener">Text Editor</a>, and <a href="https://www.learnenough.com/git" target="_blank" rel="noopener">Git</a>.</p>
<p>I’m now pleased to announce the availability of two more volumes, one combining the material from the Learn Enough tutorials on <a href="https://www.learnenough.com/html" target="_blank" rel="noopener">HTML</a> and <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener">CSS &amp; Layout</a> and another with the contents of our <a href="https://www.learnenough.com/javascript" target="_blank" rel="noopener">JavaScript tutorial</a>.<span class="intersentencespace"></span> That brings the number of print versions to three (the links are to InformIT, Pearson’s direct-sales website):<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup></p>
<ol>
<li><a href="https://www.informit.com/store/learn-enough-developer-tools-to-be-dangerous-command-9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools to Be Dangerous</em></a> by Michael Hartl
</li>
<li><a href="https://www.informit.com/store/learn-enough-html-css-and-layout-to-be-dangerous-an-9780137843107" target="_blank" rel="noopener"><em>Learn Enough HTML, CSS and Layout to Be Dangerous</em></a> by Lee Donahoe and Michael Hartl
</li>
<li><a href="https://www.informit.com/store/learn-enough-javascript-to-be-dangerous-write-programs-9780137843749" target="_blank" rel="noopener"><em>Learn Enough JavaScript to Be Dangerous</em></a> by Michael Hartl
</li></ol>
<p class="noindent">(Note that the contents of both the print versions and Pearson’s eBook versions are virtually identical to the materials available at the <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough website</a>, but this is a great option for anyone who prefers print editions.)</p>
<p>For a limited time, you can also get 45% off using the discount code <strong>BACKTOLEARN</strong> during checkout (<a href="#fig-backtolearn" class="hyperref">Figure <span class="ref">1</span></a>).<span class="intersentencespace"></span> (A couple of Learn Enough titles even made the front page of the Back to Learn promotion page <a href="#fig-backtolearn_promotion_page" class="hyperref">Figure <span class="ref">2</span></a>)!)<span class="intersentencespace"></span> After September 14, 2022, you can still use the code <strong>LEARNENOUGH</strong> to get 35% off (<a href="#fig-learn_enough_discount_code" class="hyperref">Figure <span class="ref">3</span></a>).</p>
<p>Enjoy!</p>
<div class="center figure" id="fig-backtolearn" data-tralics-id="uid5" data-number="1">
<div class="graphics image"><img src="images/figures/backtolearn.png" alt="images/figures/backtolearn" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">Entering the <strong>BACKTOLEARN</strong> discount code.
</span></div></div>
<div class="center figure" id="fig-backtolearn_promotion_page" data-tralics-id="uid6" data-number="2">
<div class="graphics image"><img src="images/figures/backtolearn_promotion_page.png" alt="images/figures/backtolearn_promotion_page" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">Learn Enough on the Back to Learn promotion page!
</span></div></div>
<div class="center figure" id="fig-learn_enough_discount_code" data-tralics-id="uid7" data-number="3">
<div class="graphics image"><img src="images/figures/learn_enough_discount_code.png" alt="images/figures/learn_enough_discount_code" /></div><div class="caption"><span class="header">Figure 3: </span><span class="description">Entering the <strong>LEARNENOUGH</strong> discount code.
</span></div></div>
<div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> <a href="https://www.informit.com/store/learn-enough-ruby-to-be-dangerous-write-programs-publish-9780137844135" target="_blank" rel="noopener"><em>Learn Enough Ruby to Be Dangerous</em></a> is also available at InformIT, but only as an eBook.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Learn Enough and the Rails Tutorial have been acquired]]></title>
    <link href="https://news.learnenough.com/big-news-about-learn-enough"/>
    <updated>2022-08-11T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/big-news-about-learn-enough</id>
    <content type="html">
      <![CDATA[
      <p><a href="https://www.michaelhartl.com/">Michael Hartl</a> here from <a href="https://www.learnenough.com">Learn Enough</a> and the <a href="https://www.railstutorial.org/">Rails Tutorial</a>.</p>

<p>I’m excited to share the news that Learn Enough recently accepted an acquisition offer from a private equity firm run by entrepreneur and longtime Ruby on Rails community member Jonathan Siegel. As part of this, Learn Enough is teaming up to offer an accredited degree in computer science through recently founded <a href="https://www.hammingcollege.com/">Hamming College</a>, for which I am serving as an advisor.</p>

<p>Although the other Learn Enough founders and I have handed over day-to-day operations of Learn Enough and the Rails Tutorial to Jonathan and his team, we have continued producing new tutorial materials, including the <a href="/launch-of-the-ruby-on-rails-tutorial-7th-edition">7th edition of the <em>Ruby on Rails Tutorial</em></a> and a major new <a href="/css-grid">chapter + video on CSS grid</a>. We have also signed a deal to publish our tutorials through Pearson Education, including print versions and videos through its online portal. Finally, I am currently working on <em>Learn Enough Python to Be Dangerous</em> and hope to release the book and accompanying videos later this year. <em>Update</em>: <a href="/learn-enough-python-to-be-dangerous"><em>Learn Enough Python to Be Dangerous</em> has been released!</a></p>

<h2 id="a-surprise-inquiry">A surprise inquiry</h2>

<p>Jonathan Siegel and I have known each other for over 15 years. We first met in 2006 at one of the first Ruby on Rails training courses, taught by Mike Clark and <em>Programming Ruby</em> author Dave Thomas. Jonathan went on to found a series of successful Rails-based companies, while I ended up coauthoring a Rails tutorial called <em>RailsSpace</em> and then produced seven editions of the <em>Ruby on Rails Tutorial</em>. Jonathan and I kept in touch over the years, so when I heard from him a few months ago I wasn’t all that surprised. I <em>was</em> surprised, however, at the subject of his inquiry, which was to explore a possible acquisition.</p>

<p>My first step was to discuss the possibility with the other Learn Enough founders, Lee Donahoe and Nick Merwin. With the publication of the Learn Enough tutorials on <em>CSS &amp; Layout</em>, JavaScript, and Ruby, and the publication deal with Pearson, we agreed that we’d reached a natural stopping point and were open to moving on to other things.</p>

<p>Our top priority was to make sure any potential acquirer really understood our business and was committed to doing right by our learners. In conversation with Jonathan and his team, we discovered that they were actually long-time learners at Learn Enough themselves, so they certainly understood what we are all about! Jonathan also has a passion for creating compelling educational opportunities, and we quickly determined that there was a good fit in terms of philosophy and values.</p>

<p>Having agreed that it made sense to move forward, we signed a Letter of Intent (LOI) and started the due-diligence process. It was a lot of work, but I’m pleased to report that it went about as smoothly as could reasonably be expected under the circumstances.</p>

<h2 id="hamming-college">Hamming College</h2>

<p>Part of Jonathan’s vision for Learn Enough and the Rails Tutorial was to take things to the next level by using the Learn Enough curriculum as part of the foundation for an accredited Bachelor of Science (B.Sc.) degree in Computer Science. The result is <a href="https://www.hammingcollege.com/">Hamming College</a>, named after pioneering mathematician and computer scientist <a href="https://en.wikipedia.org/wiki/Richard_Hamming">Richard Hamming</a>, inventor of <a href="https://en.wikipedia.org/wiki/Hamming_code">Hamming codes</a> and winner of the Turing Award in 1968.</p>

<p>Here’s Jonathan’s account of how Hamming College was conceived:</p>

<blockquote>
  <p>Three years ago I joined the first ABA-accredited “interactive” law degree program—offered by Syracuse University. The students in the parallel residential program were unhappy with the idea that my cohort was avoiding “the Socratic method” and the somewhat cliche pressure-cooker environment of law school. When I started the mainly-online program, I was required to do the same reading, lecture hours and engagement that the on-campus students experienced—and to me, I was experiencing the pressure-cooker.</p>

  <p>Fast forward 18 months—in the midst of COVID—and the entire campus went online. Those residential students and their instructors became fully online alongside our cohort. What I found was that my classes built for online were actually far better than the residential classes brought online. My lectures were built in bite-sized chunks, reasonably scripted and had frequent, calculated breaks to check my progress and encourage recall. My instructor class time was on Zoom, but jumped right into interactive discussion and every student had to be ready to be on call. Although the on-call feel was similar between both styles of instruction, the traditional lectures were just that—one-to-two hour long stream-of-consciousness with few interruptions. These were less enjoyable and less effective for learning—and I realized that maybe the residential students had it backwards—online learning done right can be far superior to campus learning.</p>

  <p>With this in mind, we set out to build an online computer science program that combined the rigor of a traditional university (what I experienced in my undergraduate CompSci degree and a few years as a CompSci Ph.D.) with the joy of well-presented online material (like Learn Enough).</p>
</blockquote>

<p>With the Learn Enough curriculum as a foundation, Hamming College is now offering an accredited degree in Computer Science, and is also working to offer independent college credit for Learn Enough courses.</p>

<h2 id="softcover">Softcover</h2>

<p>As some of you may know, Learn Enough is actually part of <a href="https://www.softcover.io/">Softcover</a>, a publishing platform for technical authors. There are no plans to change how Softcover operates, so authors of current Softcover books don’t have to do anything at this point.</p>

<p>If the Softcover service is ever going to be shut down, there will be ample warning and a generous sunset period. In addition, Softcover has no lock-in, so authors will always have the option to publish their works elsewhere.</p>

<p>I’ll continue to oversee the Softcover <a href="https://github.com/softcover/softcover">open-source project</a>, which has a mature feature set and doesn’t require much in the way of further development.</p>

<h2 id="what-the-future-holds">What the future holds</h2>

<p>Lee, Nick, and I are all staying around during a transitional period while I finish up the Python tutorial. I’ll also continue to serve as an advisor to Hamming College even after phasing out my day-to-day involvement in Learn Enough and the Rails Tutorial.</p>

<p>I have lots of ideas for projects going forward, including languages, philosophy, and reconnecting with my math and physics roots. Although I expect to take a little time off to recuperate from the push to finish <em>Learn Enough Python</em>, it’s likely I’ll have things to share publicly at some point. Please subscribe to my <a href="https://substack.michaelhartl.com/">personal mailing list</a> or <a href="https://twitter.com/mhartl">follow me on Twitter</a> if you’d like to keep in touch.</p>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>Thanks to Jonathan Siegel, John Cross, and all the other folks we worked with during this process. Everything at every stage was handled in good faith and with good cheer. A big thank-you to Josh Pigford for his generous advice and for an introduction to Joey Tran and Colby Gartin of Acceleron Law, who did a fantastic job on the legal side of the deal.</p>

<p>Of course, a huge thanks to my cofounders, Lee Donahoe and Nick Merwin. Finding good cofounders is extremely hard, and I got lucky. I remember thinking, shortly after I met them in 2007, “I could definitely work with these guys.” It took a few years for that to happen, but I’m glad it eventually did.</p>

<p>Finally, thanks to all our great learners, who have been a source of so much positive energy and inspiration over the years. Learn Enough is in great hands, and I’m confident there are good things ahead.</p>

       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[CSS Grid]]></title>
    <link href="https://news.learnenough.com/css-grid"/>
    <updated>2022-06-21T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/css-grid</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>I’ve got an exciting announcement about <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout to Be Dangerous</em></a> (part of a <a href="/full-draft-of-the-ruby-on-rails-tutorial-7th-edition" target="_blank" rel="noopener">bunch of announcements</a> alluded to in a previous post): We just released a special bonus chapter on a more recent and advanced layout technique known as <em>CSS grid</em>.</p>
<p>CSS grid is a system for making—you guessed it!—grids.<span class="intersentencespace"></span> The newly released grid chapter is over 100 pages long, bringing the full tutorial to over 600 pages of CSS &amp; Layout goodness.<span class="intersentencespace"></span> The companion grid videos come in at nearly two hours (bringing the total for the CSS &amp; Layout videos to almost 16 hours), and feature coauthor Lee Donahoe walking an occasionally clueless CSS newbie (me) through the many intricacies of grid.</p>
<div class="center figure" id="fig-grid-diag-complex" data-tralics-id="uid1" data-number="1">
<div class="graphics image"><img src="images/figures/grid-diag-complex.png" alt="images/figures/grid-diag-complex" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">Two types of layout covered by the chapter on grid.
</span></div></div>
<p>This release is a free upgrade for previous purchasers of the CSS &amp; Layout tutorial.<span class="intersentencespace"></span> Just go to your <a href="https://www.learnenough.com/courses/downloads" target="_blank" rel="noopener">Downloads</a> page to get the latest copy of the book or videos.<span class="intersentencespace"></span> It is also automatically available to All Access and CSS &amp; Layout course subscribers on the <a href="https://www.learnenough.com/your-courses" target="_blank" rel="noopener">Your Courses</a> page.</p>
<p>For everyone else, there are two main ways to get the new material on CSS grid:</p>
<ul>
<li><a href="https://www.learnenough.com/css-and-layout#landing-pricing" target="_blank" rel="noopener">CSS course subscription</a>: The new chapter on CSS grid will be included in the individual course subscription.<span class="intersentencespace"></span>
</li>
<li><a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">All Access subscription</a>: The new chapter on CSS grid and the material from <em>all</em> the Learn Enough tutorials (including the <em>Ruby on Rails Tutorial</em>) are included in the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access</a> subscription.<span class="intersentencespace"></span> This option includes online text, streaming videos, progress tracking, exercise answers, and access to a private Slack chat group.<span class="intersentencespace"></span>
</li></ul>
<div class="center figure" id="fig-grid2-opts-examp" data-tralics-id="uid4" data-number="2">
<div class="graphics image"><img src="images/figures/grid2-opts-examp.png" alt="images/figures/grid2-opts-examp" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">The culmination of the new chapter on grid.
</span></div></div>
<p>No matter which option you select, we hope you enjoy the new material on CSS grid!<span class="intersentencespace"></span> <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout to Be Dangerous</em></a> has always been designed to teach you how to build real-world, professional-grade websites, focusing on the most important techniques of CSS and page layout.<span class="intersentencespace"></span> With this new chapter on grid, it will continue to serve that purpose for many years to come.
</p>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Launch of the Ruby on Rails Tutorial 7th Edition]]></title>
    <link href="https://news.learnenough.com/launch-of-the-ruby-on-rails-tutorial-7th-edition"/>
    <updated>2022-05-31T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/launch-of-the-ruby-on-rails-tutorial-7th-edition</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.<span class="intersentencespace"></span> I’m pleased to announce the launch of the <a href="https://www.learnenough.com/ruby-on-rails-7th-edition" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a> 7th edition!<span class="intersentencespace"></span> This new edition covers professional-grade web development with Rails 7.</p>
<p>The <em>Ruby on Rails Tutorial</em> is available as an <a href="https://www.learnenough.com/ruby-on-rails-7th-edition#landing-pricing" target="_blank" rel="noopener">online course</a> and as a part of our <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">All Access Subscription</a>.<span class="intersentencespace"></span> Details below!</p>
<div class="subsection-star"><h3><a class="heading">Details</a></h3>
<p>As noted in the 7th edition <a href="/full-draft-of-the-ruby-on-rails-tutorial-7th-edition" target="_blank" rel="noopener">draft announcement</a>, the material in the <em>Ruby on Rails Tutorial</em> has been quite stable now for several years.<span class="intersentencespace"></span> This is great because it means what you learn won’t soon go out of date.<span class="intersentencespace"></span> Nonetheless, there are still some great updates in the latest edition:</p>
<ul>
<li>Full compatibility with Rails 7
</li>
<li>Improved integration tests with techniques for breaking long tests into more manageable pieces
</li>
<li>Coverage of the latest (and we hope last!) way to integrate JavaScript with Rails (using Importmap, so no more NPM, Webpacker, or Yarn!)<span class="intersentencespace"></span>
</li>
<li>How to use the new Active Storage <code>variant</code> method for resizing uploaded images
</li>
<li>New <code>render</code> status responses for compatibility with Turbo (see below)
</li>
<li>An introduction to Hotwire and Turbo for building fast and responsive sites rivaling the speed of “single-page” JavaScript apps
</li></ul>
<p>The videos are largely the same as in previous editions (most of the videos use Rails 4 yet still work fine!), but they have been carefully updated with text notes where necessary.<span class="intersentencespace"></span> In addition, several of the screencasts have been completely redone—most notably, the ones covering JavaScript in Rails, longer integration tests, and Hotwire/Turbo.</p>
</div>
<div class="subsection-star"><h3><a class="heading">Online course</a></h3>
<p>The <em>Ruby on Rails Tutorial</em> is available as a 14-chapter online course with the equivalent of 952 pages of text and 20 hours of streaming videos.<span class="intersentencespace"></span> The course also includes exercise answers, progress tracking, and membership in a private Slack chat group.<span class="intersentencespace"></span> There are two main ways to get it:</p>
<ul>
<li><a href="https://www.learnenough.com/ruby-on-rails-7th-edition#landing-pricing" target="_blank" rel="noopener">Ruby on Rails Tutorial course subscription</a>: $29/month for full access to the Rails Tutorial online course
</li>
<li><a href="https://www.learnenough.com/ruby-on-rails-7th-edition#landing-pricing" target="_blank" rel="noopener">Learn Enough All Access subscription</a>: $49/month for the Rails Tutorial course plus <em>all</em> the Learn Enough courses (Command Line, Text Editor, Git, HTML, CSS &amp; Layout, JavaScript, and Ruby)
</li></ul>
<p>If you already have an All Access subscription, you automatically have access to the new course—just look for <strong>Ruby on Rails (Rails 7)</strong> on the <a href="https://www.learnenough.com/your-courses" target="_blank" rel="noopener">Your Courses</a> page.<span class="intersentencespace"></span> If you have a subscription to the Rails 6 course, you can switch to Rails 7 at any time on the same <a href="https://www.learnenough.com/your-courses" target="_blank" rel="noopener">Your Courses</a> page since the two courses have the same monthly price.</p>
</div>
<div class="subsection-star"><h3><a class="heading">Individual purchase</a></h3>
<p>The <em>Rails Tutorial</em> is also available for individual purchase as an ebook or ebook/video bundle:</p>
<ul>
<li><strong>The </strong><em><strong>Rails Tutorial</strong></em><strong> ebook</strong> (952 pages): Get the full text of the <em>Ruby on Rails Tutorial</em> in PDF, EPUB, MOBI, and HTML formats.<span class="intersentencespace"></span> We’re offering a launch/upgrade discount of
<strong>20% off</strong> (expires this Friday, June 3).
</li>
<li><strong>The </strong><em><strong>Rails Tutorial</strong></em><strong> ebook/screencast bundle</strong>: Get both the ebook and downloadable video screencasts for one low price.<span class="intersentencespace"></span> We’re offering a launch/upgrade discount of
<strong>33% off</strong> to make this valuable bundle more affordable (expires this Friday, June 3).<span class="intersentencespace"></span> 
</li></ul>
<p>If you bought the 7th edition after the draft launch in April, check your Downloads page to get the video files and the latest version of the ebook.</p>
<p><strong>Edit:</strong> As of November 2022, we no longer offer individual purchases of ebooks/videos.<span class="intersentencespace"></span> Please refer to our <a href="https://www.learnenough.com/plans" target="_blank" rel="noopener">Learn Enough</a> page for available purchase options.</p>
</div>
<div class="subsection-star"><h3><a class="heading">Guaranteed!</a></h3>
<p>As always, Learn Enough offers a 60-day, 100% money-back guarantee on all purchases (including subscriptions).<span class="intersentencespace"></span> Also, in case cost is a factor, please consider applying for a <a href="https://www.learnenough.com/scholarship" target="_blank" rel="noopener">Learn Enough Scholarship</a>.</p>
<p>Enjoy!
</p></div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Learn Enough Developer Tools to Be Dangerous Print Edition]]></title>
    <link href="https://news.learnenough.com/learn-enough-developer-tools-to-be-dangerous-print-edition"/>
    <updated>2022-05-02T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/learn-enough-developer-tools-to-be-dangerous-print-edition</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>I promised in my <a href="/full-draft-of-the-ruby-on-rails-tutorial-7th-edition" target="_blank" rel="noopener">previous post on the <em>Rails Tutorial</em> 7th edition</a> that we have a lot of exciting announcements coming up.<span class="intersentencespace"></span> That was the first one—here’s the second one: I’m pleased to announce the publication of the print edition of <a href="https://click.linksynergy.com/link?id=XTk759K*tTU&amp;offerid=145238.3124581&amp;type=2&amp;murl=https%3A%2F%2Fwww.informit.com%2Ftitle%2F9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools to Be Dangerous</em></a>, available now for Pre-Order.<span class="intersentencespace"></span> (If you click on that book link, be sure to use the discount code <strong>LEARNENOUGH</strong> to get 35% off when checking out.<span class="intersentencespace"></span> See below for details.)</p>
<p><a href="https://click.linksynergy.com/link?id=XTk759K*tTU&amp;offerid=145238.3124581&amp;type=2&amp;murl=https%3A%2F%2Fwww.informit.com%2Ftitle%2F9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools to Be Dangerous</em></a>, produced in cooperation with Pearson Education, combines the material in the Learn Enough ebooks on <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener">Command Line</a>, <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener">Text Editor</a>, and <a href="https://www.learnenough.com/git" target="_blank" rel="noopener">Git</a> into one convenient volume.</p>
<p>Learn enough about…</p>
<ul>
<li>Running a terminal, entering and editing commands, and using man pages
</li>
<li>Manipulating and inspecting files: from basic copying to finding patterns
</li>
<li>Organizing files with directories
</li>
<li>Minimum Viable Vim and the Most Important Vim Command™
</li>
<li>Basic and advanced editing techniques with editors like Atom and VS Code
</li>
<li>Using the human-readable Markdown language for writing quick documentation
</li>
<li>Formatting source code and writing executable scripts
</li>
<li>Getting started with Git and GitHub
</li>
<li>Using key Git workflows: commit, push, branch, merge, and more
</li>
<li>Collaborating on Git projects and resolving code conflicts
</li></ul>
<p>Here’s how to get the print edition of <a href="https://click.linksynergy.com/link?id=XTk759K*tTU&amp;offerid=145238.3124581&amp;type=2&amp;murl=https%3A%2F%2Fwww.informit.com%2Ftitle%2F9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools to Be Dangerous</em></a>:</p>
<ol>
<li>Visit the <a href="https://click.linksynergy.com/link?id=XTk759K*tTU&amp;offerid=145238.3124581&amp;type=2&amp;murl=https%3A%2F%2Fwww.informit.com%2Ftitle%2F9780137843459" target="_blank" rel="noopener"><em>Learn Enough Developer Tools</em> page at InformIT</a> (Pearson’s direct-sales website).<span class="intersentencespace"></span>
</li>
<li>Select Pre-Order for the regular Book option (<a href="#fig-developer_tools_book" class="hyperref">Figure <span class="ref">1</span></a>).<span class="intersentencespace"></span> The Book + eBook Bundle is also a good deal if you don’t already have the ebooks for the individual <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener">Command Line</a>, <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener">Text Editor</a>, and <a href="https://www.learnenough.com/git" target="_blank" rel="noopener">Git</a> tutorials, but just be aware that the contents are virtually identical to the ebooks available at <a href="https://www.learnenough.com" target="_blank" rel="noopener">learnenough.com</a>.<span class="intersentencespace"></span>
</li>
<li>When checking out, type in the discount code <strong>LEARNENOUGH</strong> and click the <strong>Enter Code</strong> button to get 35% off (<a href="#fig-developer_tools_discount_code" class="hyperref">Figure <span class="ref">2</span></a>).<span class="intersentencespace"></span>
</li>
<li>InformIT will automatically ship you a copy of the print edition when it comes out later this month (currently scheduled for May 26).<span class="intersentencespace"></span> Enjoy!<span class="intersentencespace"></span>
</li></ol>
<div class="center figure" id="fig-developer_tools_book" data-tralics-id="uid15" data-number="1">
<div class="graphics image"><img src="images/figures/developer_tools_book.png" alt="images/figures/developer_tools_book" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">Selecting the print book (or Book + eBook bundle) at InformIT.
</span></div></div>
<div class="center figure" id="fig-developer_tools_discount_code" data-tralics-id="uid16" data-number="2">
<div class="graphics image"><img src="images/figures/developer_tools_discount_code.png" alt="images/figures/developer_tools_discount_code" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">Entering the <strong>LEARNENOUGH</strong> discount code.
</span></div></div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Full Draft of the Ruby on Rails Tutorial, 7th Edition]]></title>
    <link href="https://news.learnenough.com/full-draft-of-the-ruby-on-rails-tutorial-7th-edition"/>
    <updated>2022-04-19T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/full-draft-of-the-ruby-on-rails-tutorial-7th-edition</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a> and the <em>Ruby on Rails Tutorial</em>.</p>
<p>It’s been a while since I last reached out, but that’s not because we’ve been idle.<span class="intersentencespace"></span> Quite the opposite—we’ve been incredibly busy here at Learn Enough and have a bunch of exciting announcements to make over the next few months.</p>
<p>That includes my current announcement: the immediate availability of a <strong>full draft of the 7th edition of the </strong><em><strong>Ruby on Rails Tutorial</strong></em><strong></strong>.<span class="intersentencespace"></span> For over a decade, the <em>Ruby on Rails Tutorial</em> has been one of the leading introductions to web development, and now it has been updated for the latest version of Rails.</p>
<p>After seven editions, the <em>Rails Tutorial</em> is pretty stable in almost every way, but there are still some great updates in the latest edition:</p>
<ul>
<li>Full compatibility with Rails 7
</li>
<li>New <code>render</code> status responses for compatibility with Turbo (see below)
</li>
<li>Improved integration tests with techniques for breaking long minitest tests into smaller pieces using Ruby classes
</li>
<li>Coverage of the latest (and we hope last!) way to integrate JavaScript with Rails using Importmap (no more NPM, Webpacker, or Yarn!)<span class="intersentencespace"></span>
</li>
<li>An introduction to Hotwire and Turbo for building fast and responsive sites rivaling “single-page” JavaScript apps
</li></ul>
<p>The first four chapters of the draft are available for free online <a href="https://www.learnenough.com/ruby-on-rails-7th-edition-tutorial" target="_blank" rel="noopener">here</a>.<span class="intersentencespace"></span> Ebook and video screencasts are also available for purchase:</p>
<ul>
<li><strong>The </strong><em><strong>Rails Tutorial</strong></em><strong> ebook</strong> (952 pages, PDF/EPUB/MOBI/HTML): Get the full draft and a free copy of the final version when it’s done.<span class="intersentencespace"></span> We’re offering a launch/upgrade discount of
<strong>20% off</strong> through the final launch (planned for next month).
</li>
<li><strong>The </strong><em><strong>Rails Tutorial</strong></em><strong> ebook/screencast bundle</strong>: Get both the ebook and the video screencasts (currently in preparation).<span class="intersentencespace"></span> We’re offering a launch/upgrade discount of
<strong>33% off</strong> to make this valuable bundle more affordable.<span class="intersentencespace"></span> <span class="break"></span> <em>Note</em>: Because the screencasts aren’t finished yet, there’s just a placeholder file for now, but with your purchase you’ll automatically get access to the downloadable video files when they’re done (probably sometime next month).<span class="intersentencespace"></span> 
</li></ul>
<p><strong>Edit:</strong> As of November 2022, we no longer offer individual purchases of ebooks/videos.<span class="intersentencespace"></span> Please refer to our <a href="https://www.learnenough.com/plans" target="_blank" rel="noopener">Learn Enough</a> page for available purchase options.</p>
<p>I hope you’ll take a look at the draft and let me know what you think.<span class="intersentencespace"></span> All bug and typo reports, no matter how small, are greatly appreciated.<span class="intersentencespace"></span> You can send such reports to michael@learnenough.com.</p>
<p>As always, we offer a 60-day, 100% money-back guarantee on all purchases.</p>
<p>Enjoy!
</p>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Heroku Errors]]></title>
    <link href="https://news.learnenough.com/heroku-errors"/>
    <updated>2021-08-12T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/heroku-errors</id>
    <content type="html">
      <![CDATA[
      <p>We’re currently experiencing an outage due to errors at our hosting provider. We’re working on the problem and will be back up as soon as possible.</p>

<p><strong>Update:</strong> The sites are back up. The problem was a breaking change in how Heroku handles DNS routing. (We hadn’t deployed a new version of our app in three days when the errors started to occur.) We have updated our configuration per a suggestion by Heroku customer support. Thanks to all our customers for their patience while this issue got resolved.</p>

       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Closed Captioning/Subtitles for Learn Enough Video Courses]]></title>
    <link href="https://news.learnenough.com/closed-captioning-subtitles-learn-enough-courses-video"/>
    <updated>2021-05-12T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/closed-captioning-subtitles-learn-enough-courses-video</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.<span class="intersentencespace"></span> This is just a quick announcement that we’ve recently added closed captioning (sometimes called “subtitles”)<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup> to all Learn Enough video courses (including the Ruby on Rails Tutorial).<span class="intersentencespace"></span> The captions are available to all <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access</a> subscribers (as well as to subscribers to the individual courses).</p>
<p>To activate closed captioning in the <a href="https://www.learnenough.com/your-courses" target="_blank" rel="noopener">Learn Enough courses interface</a>, simply go to a course video (<a href="#fig-course_video" class="hyperref">Figure <span class="ref">1</span></a>) and click on the CC in the video window to select “english CC” (<a href="#fig-english_cc" class="hyperref">Figure <span class="ref">2</span></a>), with the result as seen in <a href="#fig-closed_captions" class="hyperref">Figure <span class="ref">3</span></a>.<span class="intersentencespace"></span> You can switch back by clicking “captions off” in the same place (<a href="#fig-captions_off" class="hyperref">Figure <span class="ref">4</span></a>).</p>
<div class="center figure" id="fig-course_video" data-tralics-id="uid2" data-number="1">
<div class="graphics image"><img src="images/figures/course_video.png" alt="images/figures/course_video" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">A Learn Enough course with streaming video.
</span></div></div>
<div class="center figure" id="fig-english_cc" data-tralics-id="uid3" data-number="2">
<div class="graphics image"><img src="images/figures/english_cc.png" alt="images/figures/english_cc" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">Selecting “english CC” for closed captioning.
</span></div></div>
<div class="center figure" id="fig-closed_captions" data-tralics-id="uid4" data-number="3">
<div class="graphics image"><img src="images/figures/closed_captions.png" alt="images/figures/closed_captions" /></div><div class="caption"><span class="header">Figure 3: </span><span class="description">The caption itself.
</span></div></div>
<div class="center figure" id="fig-captions_off" data-tralics-id="uid5" data-number="4">
<div class="graphics image"><img src="images/figures/captions_off.png" alt="images/figures/captions_off" /></div><div class="caption"><span class="header">Figure 4: </span><span class="description">Turning closed captioning off.
</span></div></div>
<p>The captions are automatically generated, so there may be some minor errors compared to the audio, but they should be enough to give you the gist.<span class="intersentencespace"></span> Combined with the text itself, the addition of closed captioning makes the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough courses</a> accessible to a much wider variety of people.<span class="intersentencespace"></span> Enjoy!</p>
<p>P.S. The captions aren’t currently included with downloadable videos like the Learn Enough Everything Bundle, but <a href="mailto:support@learnenough.com" target="_blank" rel="noopener">email us</a> and we’ll be happy to give you copies of the generated files.
</p><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> <a href="https://m.xkcd.com/1475/" target="_blank" rel="noopener">Technically</a>, subtitles <a href="https://en.wikipedia.org/wiki/Closed_captioning" target="_blank" rel="noopener">refer</a> to the “transcription or translation of the dialogue when sound is available but not understood”, whereas closed captions involve the “transcription or translation of the dialogue, sound effects, relevant musical cues, and other relevant audio information when sound is unavailable or not clearly audible”.<span class="intersentencespace"></span> This new Learn Enough feature covers both cases.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Learn Enough for Teams]]></title>
    <link href="https://news.learnenough.com/learn-enough-for-teams"/>
    <updated>2021-04-08T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/learn-enough-for-teams</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>Learn Enough now has teams!<span class="intersentencespace"></span> <a href="https://www.learnenough.com/for-teams" target="_blank" rel="noopener">Learn Enough for Teams</a> lets members of your organization learn key software development skills at their own pace:</p>
<ol>
<li>Get junior developers on the way to being senior developers
</li>
<li>Train senior developers on specific technologies like Ruby and Rails
</li>
<li>Teach non-developers like designers and project managers skills useful for working with developers
</li>
<li>Get non-developers on the way to being developers
</li></ol>
<div id="sec-what_s_included" data-tralics-id="cid1" class="section" data-number="1"><h2><a href="#sec-what_s_included" class="heading"><span class="number">1 </span>What’s included</a></h2>
<p><a href="https://www.learnenough.com/for-teams" target="_blank" rel="noopener">Learn Enough Team Licenses</a> include access to all of the Learn Enough tutorials: self-paced online courses with, streaming videos, progress tracking, exercises, and exercise answers.<span class="intersentencespace"></span> Subjects include the Unix command line, text editors, Git, HTML, CSS  Layout, JavaScript, Ruby, and Ruby on Rails.</p>
<div class="graphics image"><img src="images/figures/tutorials.png" alt="images/figures/tutorials" /></div>
<p>Team Licenses also include full download access to all of the Learn Enough ebooks and videos (a $452 value!).<span class="intersentencespace"></span> It’s basically the Learn Enough <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">All Access Subscription</a> with the ability to download course materials all rolled into one.</p>
<p>With Learn Enough’s OAuth integration, there’s also zero invitation management, and you’ll pay only for what you use.</p>
<p class="noindent"><strong>Learn Enough for Teams</strong></p>
<ul>
<li>Full access to all the Learn Enough courses (including the <em>Ruby on Rails Tutorial</em>)
</li>
<li>Full ebook and video download access for all team members (a $452 value!)<span class="intersentencespace"></span>
</li>
<li>Using Google OAuth means you pay only for the seats you use
</li></ul>
</div>
<div id="sec-using_learn_enough_for_teams" data-tralics-id="cid2" class="section" data-number="2"><h2><a href="#sec-using_learn_enough_for_teams" class="heading"><span class="number">2 </span>Using Learn Enough for Teams</a></h2>
<p>Getting started with Learn Enough for Teams is simple.<span class="intersentencespace"></span> Each team has a Team Owner, who can also be a subscriber but doesn’t have to be.<span class="intersentencespace"></span> The rest of this post will assume that you’re the owner in question.</p>
<div id="sec-signing_up" data-tralics-id="uid8" class="subsection" data-number="2.1"><h3><a href="#sec-signing_up" class="heading"><span class="number">2.1 </span>Signing up</a></h3>
<p>To sign up for a Learn Enough Team License, start by visiting <a href="https://www.learnenough.com/for-teams" target="_blank" rel="noopener">Learn Enough for Teams</a> and then log in to your Google Workspace account (formerly G Suite) (<a href="#fig-sign_up" class="hyperref">Figure <span class="ref">1</span></a>).</p>
<div class="center figure" id="fig-sign_up" data-tralics-id="uid9" data-number="1">
<div class="graphics image"><img src="images/figures/sign_up.png" alt="images/figures/sign_up" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">Signing up for a Team License using Google OAuth.
</span></div></div>
<blockquote class="quotation"><p class="quote"><em>Note: If your organization manages authentication using a different OAuth provider, please reach out to us at teams@learnenough.com so that we can get our team licenses set up to talk to your user management system.</em></p>
</blockquote><p>You will be prompted to sign in with a Google account.<span class="intersentencespace"></span> Be sure to use the email address corresponding to your company’s Google Workspace account.<span class="intersentencespace"></span> For example, if I were starting a Team License for <a href="https://www.softcover.io" target="_blank" rel="noopener">Softcover</a> (Learn Enough’s parent company), I would use michael@softcover.io, as shown in <a href="#fig-oauth_account" class="hyperref">Figure <span class="ref">2</span></a>.</p>
<div class="center figure" id="fig-oauth_account" data-tralics-id="uid10" data-number="2">
<div class="graphics image"><img src="images/figures/oauth_account.png" alt="images/figures/oauth_account" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">Selecting the account for Google OAuth.
</span></div></div>
<p>After logging in, you’ll be redirected to the main Learn Enough for Teams signup page, where you should fill in your organization’s name, upload an optional logo, and include the number of a company credit card (<a href="#fig-team_details" class="hyperref">Figure <span class="ref">3</span></a>).</p>
<div class="center figure" id="fig-team_details" data-tralics-id="uid11" data-number="3">
<div class="graphics image"><img src="images/figures/team_details.png" alt="images/figures/team_details" /></div><div class="caption"><span class="header">Figure 3: </span><span class="description">Filling in team details.
</span></div></div>
<p>Once you complete your checkout (<a href="#fig-complete_checkout" class="hyperref">Figure <span class="ref">4</span></a>), you’ll be forwarded to your team dashboard (<a href="#fig-team_dashboard" class="hyperref">Figure <span class="ref">5</span></a>).<span class="intersentencespace"></span> Note that, as Team Owner, you won’t be charged anything initially—you pay only for the users who are invited and activated (<a href="#sec-inviting_users" class="hyperref">Section <span class="ref">2.2</span></a>).<span class="intersentencespace"></span> Learn Enough for Teams bills monthly and comes with a 60-day money-back guarantee.</p>
<div class="center figure" id="fig-complete_checkout" data-tralics-id="uid12" data-number="4">
<div class="graphics image"><img src="images/figures/complete_checkout.png" alt="images/figures/complete_checkout" /></div><div class="caption"><span class="header">Figure 4: </span><span class="description">Completing the Team Licenses checkout.
</span></div></div>
<div class="center figure" id="fig-team_dashboard" data-tralics-id="uid13" data-number="5">
<div class="graphics image"><img src="images/figures/team_dashboard.png" alt="images/figures/team_dashboard" /></div><div class="caption"><span class="header">Figure 5: </span><span class="description">The initial team dashboard.
</span></div></div>
</div>
<div id="sec-inviting_users" data-tralics-id="uid14" class="subsection" data-number="2.2"><h3><a href="#sec-inviting_users" class="heading"><span class="number">2.2 </span>Inviting users</a></h3>
<p>As Team Owner, you can invite new users by clicking on the Invite Users tab and entering the new member’s name and company email address (<a href="#fig-invite_user" class="hyperref">Figure <span class="ref">6</span></a>).<span class="intersentencespace"></span> (Be sure to use an email address that’s part of the same Google Workspace account as in <a href="#sec-signing_up" class="hyperref">Section <span class="ref">2.1</span></a>.)</p>
<div class="center figure" id="fig-invite_user" data-tralics-id="uid15" data-number="6">
<div class="graphics image"><img src="images/figures/invite_user.png" alt="images/figures/invite_user" /></div><div class="caption"><span class="header">Figure 6: </span><span class="description">Inviting a user to the team.
</span></div></div>
<p>New users are added in a Pending state (<a href="#fig-pending_user" class="hyperref">Figure <span class="ref">7</span></a>) and are sent an invitation to sign in using Google OAuth (<a href="#fig-team_user_login" class="hyperref">Figure <span class="ref">8</span></a>).</p>
<div class="center figure" id="fig-pending_user" data-tralics-id="uid16" data-number="7">
<div class="graphics image"><img src="images/figures/pending_user.png" alt="images/figures/pending_user" /></div><div class="caption"><span class="header">Figure 7: </span><span class="description">A pending team user.
</span></div></div>
<div class="center figure" id="fig-team_user_login" data-tralics-id="uid17" data-number="8">
<div class="graphics image"><img src="images/figures/team_user_login.png" alt="images/figures/team_user_login" /></div><div class="caption"><span class="header">Figure 8: </span><span class="description">A team user logging in with OAuth.
</span></div></div>
<p>Upon signing in, the new user’s account is automatically activated and they are forwarded to their Learn Enough courses page (<a href="#fig-courses_page" class="hyperref">Figure <span class="ref">9</span></a>).<span class="intersentencespace"></span> From there, they can start any course or download any book or video they like (<a href="#fig-your_courses" class="hyperref">Figure <span class="ref">10</span></a>)—such as the first Learn Enough tutorial, <em>Learn Enough Command Line to Be Dangerous</em> (<a href="#fig-command_line_course" class="hyperref">Figure <span class="ref">11</span></a>).</p>
<div class="center figure" id="fig-courses_page" data-tralics-id="uid18" data-number="9">
<div class="graphics image"><img src="images/figures/courses_page.png" alt="images/figures/courses_page" /></div><div class="caption"><span class="header">Figure 9: </span><span class="description">The Learn Enough courses page.
</span></div></div>
<div class="center figure" id="fig-your_courses" data-tralics-id="uid19" data-number="10">
<div class="graphics image"><img src="images/figures/your_courses.png" alt="images/figures/your_courses" /></div><div class="caption"><span class="header">Figure 10: </span><span class="description">Team Licenses include both courses and downloads.
</span></div></div>
<div class="center figure" id="fig-command_line_course" data-tralics-id="uid20" data-number="11">
<div class="graphics image"><img src="images/figures/command_line_course.png" alt="images/figures/command_line_course" /></div><div class="caption"><span class="header">Figure 11: </span><span class="description">The course for <em>Learn Enough Command Line to Be Dangerous</em>.
</span></div></div>
<p>For future logins, team members can simply use the OAuth option on the <a href="https://www.learnenough.com/login" target="_blank" rel="noopener">login page</a> (<a href="#fig-oauth_login" class="hyperref">Figure <span class="ref">12</span></a>).</p>
<div class="center figure" id="fig-oauth_login" data-tralics-id="uid21" data-number="12">
<div class="graphics image"><img src="images/figures/oauth_login.png" alt="images/figures/oauth_login" /></div><div class="caption"><span class="header">Figure 12: </span><span class="description">The OAuth option on the login page.
</span></div></div>
</div>
<div id="sec-that_s_it" data-tralics-id="uid22" class="subsection" data-number="2.3"><h3><a href="#sec-that_s_it" class="heading"><span class="number">2.3 </span>That’s it!</a></h3>
<p>That’s it!<span class="intersentencespace"></span> Your team now has full access to both the Learn Enough online courses and downloads for all of the Learn Enough ebooks and videos, including the full <em>Ruby on Rails Tutorial</em>.</p>
<p><a href="https://www.learnenough.com/for-teams" target="_blank" rel="noopener">Learn Enough for Teams</a> is a relatively new feature that is under active development.<span class="intersentencespace"></span> If you have any feedback or suggestions, or would like an individual demo, please let us know at <a href="mailto:teams@learnenough.com" target="_blank" rel="noopener">teams@learnenough.com</a>.<span class="intersentencespace"></span> Thanks!</p>
<p><small><em><a href="https://www.flickr.com/photos/yodelanecdotal/3927004398" target="_blank" rel="noopener">Team image</a> used under the terms of the Creative Commons <a href="https://creativecommons.org/licenses/by/2.0/" target="_blank" rel="noopener">Attribution Generic 2.0</a> license</em></small><small>.
</small></p></div></div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[<em>Real Artists Ship</em>]]></title>
    <link href="https://news.learnenough.com/shipping-real-artists-ship"/>
    <updated>2021-03-01T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/shipping-real-artists-ship</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>As legendary Apple cofounder Steve Jobs once said: <em>Real artists ship.</em><span class="intersentencespace"></span> What he meant was that, as tempting as it is to privately polish in perpetuity, makers must <em>ship</em> their work—that is, actually finish it and get it out into the world.<span class="intersentencespace"></span> This can be scary, because shipping means exposing your work not only to fans but also to critics.<span class="intersentencespace"></span> “What if people don’t like what I’ve made?”<span class="intersentencespace"></span> <em>Real artists ship.</em></p>
<p>It’s important to understand that shipping is a separate skill from making.<span class="intersentencespace"></span> Many makers get good at making things but never learn to ship.<span class="intersentencespace"></span> To prevent this sad situation, the <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough tutorials</a> emphasize shipping along with making.</p>
<div class="section-star" id="shipping_with_a_deploy_script"><h2><a href="#shipping_with_a_deploy_script" class="heading">Shipping with a <code>deploy</code> script</a></h2>
<p><em>Shipping</em> is the real motivation for the <code>deploy</code> script mentioned in my previous post, “<a href="/power-of-scripting-deploy-script" target="_blank" rel="noopener">The Power of Scripting: A Deploy Script</a>”.<span class="intersentencespace"></span> In particular, <code>deploy</code> is designed to lower as much as possible the friction associated with shipping.<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup></p>
<div class="center"><div class="graphics image"><img src="images/figures/container_ship.jpg" alt="images/figures/container_ship" /></div>
</div><p>In this spirit, this post will add a couple of the extensions proposed in “<a href="/power-of-scripting-deploy-script" target="_blank" rel="noopener">The Power of Scripting</a>”, namely, pulling and pushing any Git changes associated with the project being deployed.<span class="intersentencespace"></span> The method I use in my own private version of the <code>deploy</code> script has always been called <code>ship</code> in honor of the philosophy discussed above.</p>
<p><a href="#code-final_deploy" class="hyperref">Listing <span class="ref">1</span></a> shows <a href="/power-of-scripting-deploy-script#code-final_deploy" target="_blank" rel="noopener">where we left off</a> (with the vertical ellipsis indicating omitted boolean method definitions).<span class="intersentencespace"></span> The structure of the main script is simple: we have a bunch of <code>if</code>-<code>else</code> statements that test for the project type and then issue the corresponding system command (using the <a href="https://www.learnenough.com/ruby" target="_blank" rel="noopener">Ruby</a> <code>system</code> method).<span class="intersentencespace"></span> Our strategy in this post is to replace <code>system</code> with <code>ship</code>, and then have <code>ship</code> push and pull the Git repo before running the given command.</p>
<div class="codelisting" id="code-final_deploy" data-tralics-id="uid2" data-number="1"><div class="heading"><span class="number">Listing 1:</span> 

<span class="description">The final deploy script from “<a href="/power-of-scripting-deploy-script#code-final_deploy" target="_blank" rel="noopener">The Power of Scripting</a>”.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"fileutils"</span>

<span class="c1"># Deploys a project.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="c1"># Returns true for a Git repository.</span>
<span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># The main script.</span>
<span class="k">if</span> <span class="n">softcover_book?</span>
<span class="hll">  <span class="nb">system</span> <span class="s2">"softcover deploy"</span>
</span><span class="k">elsif</span> <span class="n">ruby_gem?</span>
<span class="hll">  <span class="nb">system</span> <span class="s2">"rake release"</span>
</span><span class="k">elsif</span> <span class="n">ruby_web_app?</span>
<span class="hll">  <span class="nb">system</span> <span class="s2">"git push heroku"</span>
</span><span class="k">elsif</span> <span class="n">jekyll?</span> <span class="o">||</span> <span class="n">git_repo?</span>
<span class="hll">  <span class="nb">system</span> <span class="s2">"git push"</span>
</span><span class="k">end</span>
</pre></div></div></div><p>As it happens, <em>all</em> the project types supported by the <code>deploy</code> script in <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">1</span></a> are typically Git repos as well.<span class="intersentencespace"></span> For the purposes of this post, then, we’ll assume that we can run <code>git pull</code> and <code>git push</code> with no ill effect.<span class="intersentencespace"></span> Handling exceptions to this case is left as an exercise to the reader.<sup id="cha-0_footnote-ref-2" class="footnote"><a href="#cha-0_footnote-2">2</a></sup></p>
<div class="center"><div class="graphics image"><img src="images/figures/ship.jpg" alt="images/figures/ship" /></div>
</div><p>So, what should <code>ship</code> do?<span class="intersentencespace"></span> Since we don’t want to deploy a project without pulling in remote changes, we should run <code>git pull</code> first:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git pull
</pre></div></div>
<p class="noindent">If that command succeeds, then we can run <code>git push</code> as well.<span class="intersentencespace"></span> The way to do this at the <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener">command line</a> is using “and”, written with two ampersands:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git pull <span class="o">&amp;&amp;</span> git push    <span class="c1"># Runs git push only if git pull succeeds</span>
</pre></div></div>
<p>Finally, we can run the specific command needed by the project type in question:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git pull <span class="o">&amp;&amp;</span> git push <span class="o">&amp;&amp;</span> &lt;specific command&gt;
</pre></div></div>
<p class="noindent">For example, for a Rails app, we would run this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git pull <span class="o">&amp;&amp;</span> git push <span class="o">&amp;&amp;</span> git push heroku
</pre></div></div>
<p>The preceding discussion suggests defining a <code>ship</code> command as follows:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">ship</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
  <span class="nb">system</span> <span class="s2">"git pull &amp;&amp; git push &amp;&amp; </span><span class="si">#{</span><span class="n">command</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</pre></div></div>
<p class="noindent">This accomplishes exactly the task required: first we pull in any remote changes, then we push up any local changes, and finally we run the project-specific deployment command.</p>
<p>Replacing <code>system</code> with <code>ship</code> in <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">1</span></a> then gives us the “shipping” version of our <code>deploy</code> script, as shown in <a href="#code-deploy_with_ship" class="hyperref">Listing <span class="ref">2</span></a>.</p>
<div class="codelisting" id="code-deploy_with_ship" data-tralics-id="uid4" data-number="2"><div class="heading"><span class="number">Listing 2:</span> 

<span class="description">Deploying with <code>ship</code>.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"fileutils"</span>

<span class="c1"># Deploys a project.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="c1"># Returns true for a Git repository.</span>
<span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>

<span class="hll"><span class="c1"># Ships a product.</span>
</span><span class="hll"><span class="k">def</span> <span class="nf">ship</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
</span><span class="hll">  <span class="nb">system</span> <span class="s2">"git pull &amp;&amp; git push &amp;&amp; </span><span class="si">#{</span><span class="n">command</span><span class="si">}</span><span class="s2">"</span>
</span><span class="hll"><span class="k">end</span>
</span>
<span class="c1"># The main script.</span>
<span class="k">if</span> <span class="n">softcover_book?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"softcover deploy"</span>
</span><span class="k">elsif</span> <span class="n">ruby_gem?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"rake release"</span>
</span><span class="k">elsif</span> <span class="n">ruby_web_app?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"git push heroku"</span>
</span><span class="k">elsif</span> <span class="n">jekyll?</span> <span class="o">||</span> <span class="n">git_repo?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"git push"</span>
</span><span class="k">end</span>
</pre></div></div></div><p class="noindent">At this point, running</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>deploy
</pre></div></div>
<p class="noindent">will automatically sync up all relevant Git repos and then deploy the project to production.</p>
<p>By the way, you may have noticed that <a href="#code-deploy_with_ship" class="hyperref">Listing <span class="ref">2</span></a> contains a minor redundancy: <a href="https://jekyllrb.com" target="_blank" rel="noopener">Jekyll</a> sites and Git repos <em>already</em> have a <code>git push</code> as their deployment command.<span class="intersentencespace"></span> In that case, <code>ship</code> will run <code>git push</code> twice:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git pull <span class="o">&amp;&amp;</span> git push <span class="o">&amp;&amp;</span> git push
</pre></div></div>
<p class="noindent">Luckily, this does no harm—running <code>git push</code> on an already-pushed repo just outputs “Everything up-to-date”:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git push <span class="o">&amp;&amp;</span> git push
<span class="go">Enumerating objects: 18, done.</span>
<span class="go">Counting objects: 100% (18/18), done.</span>
<span class="go">Delta compression using up to 4 threads</span>
<span class="go">Compressing objects: 100% (11/11), done.</span>
<span class="go">Writing objects: 100% (11/11), 263.30 KiB | 26.33 MiB/s, done.</span>
<span class="go">Total 11 (delta 7), reused 0 (delta 0), pack-reused 0</span>
<span class="go">remote: Resolving deltas: 100% (7/7), completed with 7 local objects.</span>
<span class="go">To https://github.com/softcover/learnenough-news.git</span>
<span class="go">   3e7a7ff..53705ec  master -&gt; master</span>
<span class="hll"><span class="go">Everything up-to-date</span>
</span></pre></div></div>
<p class="noindent">Here the first <code>git push</code> pushes up some changes (to GitHub, in this case), while the second <code>git push</code> gives the highlighted output shown.</p>
</div>
<div class="section-star" id="time_to_ship"><h2><a href="#time_to_ship" class="heading">Time to ship</a></h2>
<p>That’s it!<span class="intersentencespace"></span> Time to make Steve Jobs proud and ship.<sup id="cha-0_footnote-ref-3" class="footnote"><a href="#cha-0_footnote-3">3</a></sup></p>
<div class="center"><div class="graphics image"><img src="images/figures/steve_jobs_macbook.jpg" alt="images/figures/steve_jobs_macbook" /></div>
</div><p>To learn more about how to ship a wide variety of products, including Git repos, static Jekyll sites, dynamic JavaScript sites, and Sinatra and Rails web apps, check out the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access subscription</a>.<span class="intersentencespace"></span> 7-day free trial, 60-day money-back guarantee.
</p></div><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> <a href="https://commons.wikimedia.org/wiki/File:MAERSK_MC_KINNEY_M%C3%96LLER_%26_MARSEILLE_MAERSK_(48694054418).jpg" target="_blank" rel="noopener">Shipping container picture</a> used under the terms of the Creative Commons <a href="https://creativecommons.org/licenses/by-sa/2.0/deed.en" target="_blank" rel="noopener">Attribution-ShareAlike 2.0 Generic</a> license.</div>
    <div id="cha-0_footnote-2" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-2">2.</a> <em>Hint:</em> Just use <code>if git_repo?</code> inside of <code>ship</code> to skip the Git pull &amp; push commands for projects that aren’t being versioned with Git.</div>
    <div id="cha-0_footnote-3" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-3">3.</a> <a href="https://commons.wikimedia.org/wiki/File:Steve_Jobs.jpg" target="_blank" rel="noopener">Image</a> cropped and used under the terms of the Creative Commons <a href="https://creativecommons.org/licenses/by/3.0/deed.en" target="_blank" rel="noopener">Attribution 3.0 Unported</a> license.<span class="intersentencespace"></span> Other images used in this post are in the public domain.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The Power of Scripting: A Deploy Script]]></title>
    <link href="https://news.learnenough.com/power-of-scripting-deploy-script"/>
    <updated>2021-02-22T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/power-of-scripting-deploy-script</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.<span class="intersentencespace"></span> This post is about the power of <em>scripting</em>, which refers to the creation of programs (known as <em>scripts</em>) that are generally shorter and simpler than full-size applications while still being highly useful.<span class="intersentencespace"></span> The most common kind of script is a <em>shell script</em>, which is typically run at the <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener">command line</a>.</p>
<p>The specific example we’ll be looking at is an only slightly simplified version of a script that I use all the time, often multiple times per day.<span class="intersentencespace"></span> It’s called <code>deploy</code>, and its purpose is to deploy a wide variety of different project types using a method appropriate to each type.<span class="intersentencespace"></span> For example, <code>deploy</code> can be used to deploy books, static websites, Ruby gems, and Rails apps, all using the same command.</p>
<p>The language we’ll be using is <em>Ruby</em>, an elegant object-oriented programming language that’s excellent for writing shell scripts (among many other things).<span class="intersentencespace"></span> Because Ruby is generally easy to read, there’s a good chance you’ll be able to follow this post even if you don’t have much (or any) experience with the language.<span class="intersentencespace"></span> (If you’re interested in learning Ruby, check out <a href="https://www.learnenough.com/ruby" target="_blank" rel="noopener"><em>Learn Enough Ruby to Be Dangerous</em></a> and the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>.)<span class="intersentencespace"></span> This is a useful aspect of <a href="https://www.learnenough.com/command-line-tutorial/basics#aside-technical_sophistication" target="_blank" rel="noopener">technical sophistication</a>: being able to read and generally understand a computer language you don’t necessarily know.</p>
<p><em>Note</em>: In a post of this sort, it’s impossible to anticipate the exact background of every possible reader.<span class="intersentencespace"></span> Many clarifying links are provided, but some googling may be required; for a systematic introduction to <em>all</em> the relevant topics in a carefully designed order, see the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access subscription</a> (7-day free trial, 60-day money-back guarantee).</p>
<div id="sec-different_projects" data-tralics-id="cid1" class="section" data-number="1"><h2><a href="#sec-different_projects" class="heading"><span class="number">1 </span>A bunch of different projects</a></h2>
<p>Like many developers and other tech-savvy people, I maintain several different kinds of projects:</p>
<ul>
<li>Static websites (<a href="/" target="_blank" rel="noopener">news.learnenough.com</a> (i.e., this site), <a href="https://www.michaelhartl.com/" target="_blank" rel="noopener">michaelhartl.com</a>)
</li>
<li>Dynamic web apps (<a href="https://www.learnenough.com/" target="_blank" rel="noopener">learnenough.com</a>, <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">railstutorial.org</a>, <a href="https://tauday.com/" target="_blank" rel="noopener">tauday.com</a>)
</li>
<li>Ruby gems (<a href="https://github.com/softcover/softcover" target="_blank" rel="noopener">softcover</a>, <a href="https://github.com/softcover/polytexnic" target="_blank" rel="noopener">polytexnic</a>, <a href="https://github.com/mhartl/jekyll-latex" target="_blank" rel="noopener">jekyll-latex</a>, <a href="https://github.com/mhartl/git-utils" target="_blank" rel="noopener">git-utils</a>)
</li>
<li>Books, essays, and articles (<a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a>, <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a>, et al.; the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>; <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a>)
</li>
<li>Various directories versioned as Git repositories (<code>txt</code> directory, personal <code>bin</code> directory, etc.)<span class="intersentencespace"></span>
</li></ul>
<p>A frequent task common to all these projects is <em>deploying</em> them, defined loosely as moving them from a local development machine to some remote server.<span class="intersentencespace"></span> Because I typically deploy things in this generalized sense multiple times a day, it can be cognitively costly to have to remember which deployment method to use.<span class="intersentencespace"></span> In order to lower the resulting <a href="https://en.wikipedia.org/wiki/Cognitive_load" target="_blank" rel="noopener">cognitive load</a>, some years ago I began developing a custom <code>deploy</code> script, which I’ve been refining ever since.</p>
<p>The basic idea is that, no matter which kind of project directory I’m working in, I can just type</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>deploy
</pre></div></div>
<p class="noindent">and the deploy script will do the right thing for the corresponding project type.<span class="intersentencespace"></span> If it’s a book, <code>deploy</code> publishes the book; if it’s a Rails app, <code>deploy</code> updates the app on the live server; and so on.<span class="intersentencespace"></span> Let’s see how it works!</p>
</div>
<div id="sec-a_git_deploy_script" data-tralics-id="cid2" class="section" data-number="2"><h2><a href="#sec-a_git_deploy_script" class="heading"><span class="number">2 </span>A Git deploy script</a></h2>
<p>We’ll get started with the simplest version of <code>deploy</code>, a script to deploy a Git repository.<span class="intersentencespace"></span> For the purposes of a plain Git repo (as opposed to one that’s also a Rails app, a Ruby gem, etc.), we’ll define “deploy” as “push to a remote server like GitHub, GitLab, or Bitbucket.”<span class="intersentencespace"></span> As discussed in <a href="https://www.learnenough.com/git" target="_blank" rel="noopener"><em>Learn Enough Git to Be Dangerous</em></a>, this involves running the command <code>git push</code>, which pushes the changes in the repository to <code>origin</code>, i.e., the default place to push changes.</p>
<p>To make an executable script, we’ll work in the local <code>bin</code> directory, which should be included on the <code>PATH</code> (as described in <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a>) so that it can be executed from anywhere:<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup></p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span><span class="nb">cd</span> ~/bin             <span class="c1"># Changes directory to bin relative to home dir ~</span>
<span class="gp">$ </span>touch deploy         <span class="c1"># Creates a new file called deploy</span>
<span class="gp">$ </span>chmod +x deploy      <span class="c1"># Changes the mode to make the script executable</span>
</pre></div></div>
<p class="noindent">The final line here arranges for us to be able to run the deploy script as a regular command-line program, like this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>deploy    <span class="c1"># Doesn't do anything yet</span>
</pre></div></div>
<p>For the script itself, we’ll use a “shebang” line to identify it as a Ruby script (<a href="https://www.learnenough.com/ruby" target="_blank" rel="noopener"><em>Learn Enough Ruby</em></a>):</p>
<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
</pre></div></div>
<p class="noindent">Now we need to detect whether or not the current directory contains a Git repository.<span class="intersentencespace"></span> Our repository-detection method is to look for the presence of a special hidden directory called <code>.git</code> (read “dot git”),<sup id="cha-0_footnote-ref-2" class="footnote"><a href="#cha-0_footnote-2">2</a></sup> which is where Git stores repository-specific information.<span class="intersentencespace"></span> We can test for the presence of a <code>.git</code> directory using Ruby’s <code>File.directory?</code> method:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>    <span class="c1"># Returns true if .git is a directory</span>
</pre></div></div>
<p class="noindent">The question mark <code>?</code><span class="intersentencespace"></span> at the end of <code>directory?</code> is Ruby’s way of indicating a <em>boolean method</em>, that is, a method that returns <code>true</code> or <code>false</code>.</p>
<p>If there is a <code>.git</code> directory, we’ll deploy the repository by “shelling out” to the underlying system using the <code>system</code> command in order to run <code>git push</code>.<span class="intersentencespace"></span> Opening <code>~/bin/deploy</code> in a text editor and putting everything together gives the proto-script shown in <a href="#code-git_deploy" class="hyperref">Listing <span class="ref">1</span></a>.</p>
<div class="codelisting" id="code-git_deploy" data-tralics-id="uid8" data-number="1"><div class="heading"><span class="number">Listing 1:</span> 

<span class="description">Deploying a Git repository.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>

<span class="c1"># Deploys a Git repository.</span>
<span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div></div>
<div id="sec-refactoring_git_deploy" data-tralics-id="uid9" class="subsection" data-number="2.1"><h3><a href="#sec-refactoring_git_deploy" class="heading"><span class="number">2.1 </span>Refactoring Git deploy</a></h3>
<p>We can make a small but important refinement to the script in <a href="#code-git_deploy" class="hyperref">Listing <span class="ref">1</span></a> by introducing an <em>abstraction layer</em> between the repository detection—in this case, detecting the presence of a <code>.git</code> directory—and the rest of our program.<span class="intersentencespace"></span> We can do this by defining a new method called <code>git_repo?</code>:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>
</pre></div></div>
<p class="noindent">Here we’ve included a question mark <code>?</code><span class="intersentencespace"></span> at the end of <code>git_repo?</code><span class="intersentencespace"></span> to follow the Ruby convention discussed in <a href="#sec-a_git_deploy_script" class="hyperref">Section <span class="ref">2</span></a>, in this case indicating that the method in question returns <code>true</code> if it is a Git repo and <code>false</code> if it isn’t.</p>
<p><a href="https://en.wikipedia.org/wiki/Code_refactoring" target="_blank" rel="noopener">Refactoring</a> the code in <a href="#code-git_deploy" class="hyperref">Listing <span class="ref">1</span></a> slightly then gives the equivalent script shown in <a href="#code-git_deploy_refactored" class="hyperref">Listing <span class="ref">2</span></a>.</p>
<div class="codelisting" id="code-git_deploy_refactored" data-tralics-id="uid10" data-number="2"><div class="heading"><span class="number">Listing 2:</span> 

<span class="description">A refactored Git deploy script.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>

<span class="c1"># Returns true for a Git repository.</span>
<span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Deploys a Git repository.</span>
<span class="k">if</span> <span class="n">git_repo?</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div></div><p class="noindent">Note that the refactored script in <a href="#code-git_deploy_refactored" class="hyperref">Listing <span class="ref">2</span></a> is actually longer than the script in <a href="#code-git_deploy" class="hyperref">Listing <span class="ref">1</span></a>.<span class="intersentencespace"></span> This is often the case when adding abstraction layers, but what we lose in brevity we gain in clarity.<span class="intersentencespace"></span> The meaning of</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">git_repo?</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div>
<p class="noindent">is immediately understandable in a way that</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div>
<p class="noindent">is not.<span class="intersentencespace"></span> Moreover, the benefit of adding abstraction layers compounds quickly, as we’ll see in <a href="#sec-completing_the_script" class="hyperref">Section <span class="ref">3.2</span></a>.</p>
</div></div>
<div id="sec-extending_the_script" data-tralics-id="cid3" class="section" data-number="3"><h2><a href="#sec-extending_the_script" class="heading"><span class="number">3 </span>Extending the script</a></h2>
<p>With the foundational work laid in <a href="#sec-a_git_deploy_script" class="hyperref">Section <span class="ref">2</span></a>, we’re now in a position to extend <code>deploy</code> by adding support for other kinds of deployable projects.<span class="intersentencespace"></span> Here’s a list of the most common projects I deploy, together with the methods of deployment:</p>
<ul>
<li>Static website using <a href="https://jekyllrb.com" target="_blank" rel="noopener">Jekyll</a>: <code>git push</code> to <code>origin</code> (typically <a href="https://pages.github.com" target="_blank" rel="noopener">GitHub Pages</a>)
</li>
<li>Rails or Sinatra app: <code>git push heroku</code>
</li>
<li>Ruby gem: run standard <code>rake release</code>
</li>
<li><a href="https://www.softcover.io" target="_blank" rel="noopener">Softcover</a> book or article: <code>softcover deploy</code>
</li>
<li>Git repo: <code>git push</code>
</li></ul>
<p>For each project type, we simply need to define an analogue of <code>git_repo?</code> (<a href="#code-git_deploy_refactored" class="hyperref">Listing <span class="ref">2</span></a>) for that project.<span class="intersentencespace"></span> We can then auto-detect the project type of the current directory and use <code>system</code> to take the corresponding action.</p>
<p>We can actually accomplish everything we need with just three different Ruby techniques:</p>
<ul>
<li><code>File.directory?("name")</code>: As discussed in <a href="#sec-a_git_deploy_script" class="hyperref">Section <span class="ref">2</span></a>, this returns <code>true</code> if <code>name</code> is a directory and <code>false</code> otherwise.<span class="intersentencespace"></span>
</li>
<li><code>File.exist?("name")</code>: Returns <code>true</code> if <code>name</code> exists and <code>false</code> otherwise.<sup id="cha-0_footnote-ref-3" class="footnote"><a href="#cha-0_footnote-3">3</a></sup>
</li>
<li><code>Dir["*.gemspec"].any?</code>: Combines <code>Dir</code> from the Ruby Standard Library <code>FileUtils</code> module and <code>any?</code> to return <code>true</code> if there are any files in the current directory matching the pattern <code>"*.gemspec"</code>, i.e., ending in the string <code>".gemspec"</code>.<span class="intersentencespace"></span>
</li></ul>
<div id="sec-adding_jekyll_sites_and_softcover_books" data-tralics-id="uid20" class="subsection" data-number="3.1"><h3><a href="#sec-adding_jekyll_sites_and_softcover_books" class="heading"><span class="number">3.1 </span>Adding Jekyll sites and Softcover books</a></h3>
<p>One of the easiest ways to determine a project type is looking for a directory characteristic of that project.<span class="intersentencespace"></span> We saw this with the <code>.git</code> directory in <a href="#sec-a_git_deploy_script" class="hyperref">Section <span class="ref">2</span></a>, and it works for other project types as well.</p>
<p>For example, every site built using the Jekyll static-site generator (covered in <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout to Be Dangerous</em></a> and used to build this blog) has a directory called <code>_site</code> where the static site is stored.<span class="intersentencespace"></span> This means we can add a <code>jekyll_site?</code> method in analogy with <code>git_repo?</code><span class="intersentencespace"></span> from <a href="#code-git_deploy_refactored" class="hyperref">Listing <span class="ref">2</span></a>:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">jekyll_site?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"_site"</span><span class="p">)</span>
<span class="k">end</span>
</pre></div></div>
<p>Similarly, <a href="https://manual.softcover.io/book" target="_blank" rel="noopener">Softcover books</a> have a <code>latex_styles</code> directory, so we can define <code>softcover_book?</code> like this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">softcover_book?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"latex_styles"</span><span class="p">)</span>
<span class="k">end</span>
</pre></div></div>
<p>This suggests an augmented <code>deploy</code> script as shown in <a href="#code-augmented_deploy" class="hyperref">Listing <span class="ref">3</span></a>.<span class="intersentencespace"></span> Note that services like <a href="https://pages.github.com" target="_blank" rel="noopener">GitHub Pages</a> automatically build Jekyll sites, so in this case a simple <code>git push</code> is all that’s needed to deploy.<span class="intersentencespace"></span> As a result, we can combine Jekyll sites and plain Git repos into one step using the <code>||</code> (“or”) operator.</p>
<div class="codelisting" id="code-augmented_deploy" data-tralics-id="uid21" data-number="3"><div class="heading"><span class="number">Listing 3:</span> 

<span class="description">An augmented deploy script.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>

<span class="c1"># Deploys a project.</span>

<span class="c1"># Returns true for a Softcover book.</span>
<span class="k">def</span> <span class="nf">softcover_book?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"latex_styles"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Jekyll site.</span>
<span class="k">def</span> <span class="nf">jekyll?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"_site"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Git repository.</span>
<span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># The main script.</span>
<span class="k">if</span> <span class="n">softcover_book?</span>
  <span class="nb">system</span> <span class="s2">"softcover deploy"</span>
<span class="k">elsif</span> <span class="n">jekyll?</span> <span class="o">||</span> <span class="n">git_repo?</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div></div></div>
<div id="sec-completing_the_script" data-tralics-id="uid22" class="subsection" data-number="3.2"><h3><a href="#sec-completing_the_script" class="heading"><span class="number">3.2 </span>Completing the script</a></h3>
<p>The final projects to support in our <code>deploy</code> script are Ruby gems and Ruby web apps (<a href="https://rubyonrails.org" target="_blank" rel="noopener">Rails</a><sup id="cha-0_footnote-ref-4" class="footnote"><a href="#cha-0_footnote-4">4</a></sup> or <a href="http://sinatrarb.com" target="_blank" rel="noopener">Sinatra</a><sup id="cha-0_footnote-ref-5" class="footnote"><a href="#cha-0_footnote-5">5</a></sup>).<span class="intersentencespace"></span> For the first case, we’ll use the presence of the <code>gemspec</code> file needed by all Ruby gems, which we can test for as follows:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="nb">require</span> <span class="s2">"fileutils"</span>
<span class="no">Dir</span><span class="o">[</span><span class="s2">"*.gemspec"</span><span class="o">].</span><span class="n">any?</span>    <span class="c1"># Returns true if any files end in ".gemspec"</span>
</pre></div></div>
<p class="noindent">The first line here is needed because, as noted briefly in <a href="#sec-extending_the_script" class="hyperref">Section <span class="ref">3</span></a>, <code>Dir</code> is part of the <code>FileUtils</code> module in the Ruby Standard Library.<span class="intersentencespace"></span> This allows us to define a <code>ruby_gem?</code> boolean like this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="c1"># Returns true for a Ruby gem.</span>
<span class="k">def</span> <span class="nf">ruby_gem?</span>
  <span class="no">Dir</span><span class="o">[</span><span class="s2">"*.gemspec"</span><span class="o">].</span><span class="n">any?</span>
<span class="k">end</span>
</pre></div></div>
<p>Finally, the trickiest test is for a Ruby web app.<span class="intersentencespace"></span> The complication is that most of the things characteristic of Ruby web apps are present in Jekyll projects and Ruby gems as well.<span class="intersentencespace"></span> For example, we could test for the presence of a file called <code>Gemfile</code>, but both Jekyll and Ruby gems have that, too.<span class="intersentencespace"></span> We could test for a <code>test</code> directory, but Ruby gems often have the same thing.<span class="intersentencespace"></span> But what <em>does</em> work is checking for <em>both</em> a <code>Gemfile</code> <em>and</em> (<code>&amp;&amp;</code>) a <code>test</code> directory but <em>not</em> (<code>!</code>) a Ruby gem:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="c1"># Returns true for a Ruby-based web app.</span>
<span class="c1"># Matches Rails and Sinatra apps.</span>
<span class="k">def</span> <span class="nf">ruby_web_app?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">exist?</span><span class="p">(</span><span class="s2">"Gemfile"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">ruby_gem?</span>
<span class="k">end</span>
</pre></div></div>
<p>Putting everything together gives the final <code>deploy</code> script shown in <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">4</span></a>.<span class="intersentencespace"></span> Making your way through this script is an excellent exercise in reading code (especially if you don’t already know Ruby!).<span class="intersentencespace"></span> Note that the use of abstraction layers (<a href="#sec-refactoring_git_deploy" class="hyperref">Section <span class="ref">2.1</span></a>) means the main part of the script (at the bottom of the file) is readable even if you don’t understand all the implementation details.</p>
<div class="codelisting" id="code-final_deploy" data-tralics-id="uid25" data-number="4"><div class="heading"><span class="number">Listing 4:</span> 

<span class="description">The final deploy script.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"fileutils"</span>

<span class="c1"># Deploys a project.</span>

<span class="c1"># Returns true for a Softcover book.</span>
<span class="k">def</span> <span class="nf">softcover_book?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"latex_styles"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Ruby gem.</span>
<span class="k">def</span> <span class="nf">ruby_gem?</span>
  <span class="no">Dir</span><span class="o">[</span><span class="s2">"*.gemspec"</span><span class="o">].</span><span class="n">any?</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Ruby-based web app.</span>
<span class="c1"># Matches Rails and Sinatra apps.</span>
<span class="k">def</span> <span class="nf">ruby_web_app?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">exist?</span><span class="p">(</span><span class="s2">"Gemfile"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">ruby_gem?</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Jekyll site.</span>
<span class="k">def</span> <span class="nf">jekyll?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">"_site"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># Returns true for a Git repository.</span>
<span class="k">def</span> <span class="nf">git_repo?</span>
  <span class="no">File</span><span class="o">.</span><span class="n">directory?</span><span class="p">(</span><span class="s2">".git"</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1"># The main script.</span>
<span class="k">if</span> <span class="n">softcover_book?</span>
  <span class="nb">system</span> <span class="s2">"softcover deploy"</span>
<span class="k">elsif</span> <span class="n">ruby_gem?</span>
  <span class="nb">system</span> <span class="s2">"rake release"</span>
<span class="k">elsif</span> <span class="n">ruby_web_app?</span>
  <span class="nb">system</span> <span class="s2">"git push heroku"</span>
<span class="k">elsif</span> <span class="n">jekyll?</span> <span class="o">||</span> <span class="n">git_repo?</span>
  <span class="nb">system</span> <span class="s2">"git push"</span>
<span class="k">end</span>
</pre></div></div></div><p>With the code in <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">4</span></a>, a simple</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>deploy
</pre></div></div>
<p class="noindent">is all that’s required to deploy any of the project types supported by the script.<sup id="cha-0_footnote-ref-6" class="footnote"><a href="#cha-0_footnote-6">6</a></sup></p>
<p>The <code>deploy</code> script in <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">4</span></a> is already quite useful.<span class="intersentencespace"></span> For my personal setup, I include a few refinements and extensions, including the following:</p>
<ol>
<li>Include Rails or Sinatra apps that use <a href="https://rspec.info" target="_blank" rel="noopener">RSpec</a> for testing (which involves looking for a <code>spec</code> directory in addition to <code>test</code>).<span class="intersentencespace"></span>
</li>
<li>Handle Jekyll sites that are built locally as well as those that are built on GitHub Pages (<a href="#aside-forgetting" class="hyperref">Box <span class="ref">1</span></a>).<span class="intersentencespace"></span>
</li>
<li>For all projects, automatically pull Git repos before pushing.<span class="intersentencespace"></span>
</li>
<li>Automatically push Git repos for non-Git-only projects (e.g., do a <code>git push</code> before deploying a Rails app with <code>git push heroku</code>).<span class="intersentencespace"></span>
</li></ol>
<p>To implement 3 &amp; 4, my standard <code>deploy</code> script includes a <code>ship</code> method as shown in <a href="#code-ship" class="hyperref">Listing <span class="ref">5</span></a>, which replaces the <code>system</code> command from <a href="#code-final_deploy" class="hyperref">Listing <span class="ref">4</span></a>.</p>
<div class="codelisting" id="code-ship" data-tralics-id="uid31" data-number="5"><div class="heading"><span class="number">Listing 5:</span> 

<span class="description">Defining a <code>ship</code> method to sync Git repos automatically.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">~/bin/deploy</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="ch">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"fileutils"</span>

<span class="c1"># Deploys a project.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="hll"><span class="c1"># Ships a product.</span>
</span><span class="hll"><span class="k">def</span> <span class="nf">ship</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
</span>  <span class="n">pull</span> <span class="o">=</span> <span class="s2">"git pull"</span>
  <span class="n">push</span> <span class="o">=</span> <span class="s2">"git push"</span>
  <span class="n">full_command</span> <span class="o">=</span> <span class="n">command</span> <span class="o">==</span> <span class="n">push</span> <span class="p">?</span> <span class="s2">"</span><span class="si">#{</span><span class="n">pull</span><span class="si">}</span><span class="s2"> &amp;&amp; </span><span class="si">#{</span><span class="n">command</span><span class="si">}</span><span class="s2">"</span> <span class="p">:</span>
                                   <span class="s2">"</span><span class="si">#{</span><span class="n">pull</span><span class="si">}</span><span class="s2"> &amp;&amp; </span><span class="si">#{</span><span class="n">push</span><span class="si">}</span><span class="s2"> &amp;&amp; </span><span class="si">#{</span><span class="n">command</span><span class="si">}</span><span class="s2">"</span>
  <span class="nb">puts</span>   <span class="n">full_command</span>
  <span class="nb">system</span> <span class="n">full_command</span>
<span class="k">end</span>

<span class="c1"># The main script.</span>
<span class="k">if</span> <span class="n">softcover_book?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"softcover deploy"</span>
</span><span class="k">elsif</span> <span class="n">ruby_gem?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"rake release"</span>
</span><span class="k">elsif</span> <span class="n">ruby_web_app?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"git push heroku"</span>
</span><span class="k">elsif</span> <span class="n">jekyll?</span> <span class="o">||</span> <span class="n">git_repo?</span>
<span class="hll">  <span class="n">ship</span> <span class="s2">"git push"</span>
</span><span class="k">end</span>
</pre></div></div></div><p>With the ideas in this post (and maybe the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access subscription</a>), you’ll be in a good position to add these enhancements to your own version, as well as to write and extend utility scripts of your own.<span class="intersentencespace"></span> Good luck!</p>
<div class="aside" id="aside-forgetting" data-tralics-id="uid32" data-number="1"><div class="heading"><span class="number">Box 1.</span> 

<span class="description">The power of forgetting.</span></div>
<p>The present site actually belongs to the final refinement discussed in the text: it’s a Jekyll site built locally and then uploaded in its entirety as static HTML.<span class="intersentencespace"></span> The corresponding step in <code class="tt">deploy</code> involves detecting the presence of a <code class="tt">docs</code> directory, which is where GitHub Pages looks for a statically generated site.<span class="intersentencespace"></span> If <code class="tt">File.directory?("docs")</code> is true, the <code class="tt">deploy</code> builds the site locally and then initiates the upload to GitHub Pages.</p>
<p>The amazing thing is that <em>I had forgotten that I did this.</em><span class="intersentencespace"></span> I didn’t remember if <a href="https://news.learnenough.com/" target="_blank" rel="noopener">news.learnenough.com</a> was built locally or on GitHub Pages.<span class="intersentencespace"></span> Being able to forget such details is the entire purpose of the <code class="tt">deploy</code> script; rather than keeping track of the exact steps to deploy a particular project—in this case, a static site built using Jekyll—I can just type <code class="tt">deploy</code> and be confident that the script will take care of the details for me.</p>

</div></div></div><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> If you don’t have such a directory, see <a href="https://www.learnenough.com/text-editor-tutorial/advanced_text_editing/writing_an_executable_script" target="_blank" rel="noopener">Section 3.3</a> of <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a> for the details on how to set one up (including how to add it to the <code>PATH</code>).</div>
    <div id="cha-0_footnote-2" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-2">2.</a> The leading dot in <code>.git</code> is what makes it hidden.<span class="intersentencespace"></span> This is the general convention for Unix systems, including Linux and macOS.</div>
    <div id="cha-0_footnote-3" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-3">3.</a> We could actually use <code>File.exist?(".git")</code> for the <code>.git</code> directory instead of <code>File.directory?(".git")</code>, but this would yield a false positive in the rare <a href="https://en.wikipedia.org/wiki/Edge_case" target="_blank" rel="noopener">edge case</a> of a project with a <em>file</em> (but not a directory) called <code>.git</code>.<span class="intersentencespace"></span> In general, it’s a good idea to use the most specific criterion available in order to avoid such edge cases.</div>
    <div id="cha-0_footnote-4" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-4">4.</a> See the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>.</div>
    <div id="cha-0_footnote-5" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-5">5.</a> To learn the basics of Sinatra, see <a href="https://www.learnenough.com/ruby-tutorial/live_web_app" target="_blank" rel="noopener">Chapter 10</a> of <a href="https://www.learnenough.com/ruby" target="_blank" rel="noopener"><em>Learn Enough Ruby to Be Dangerous</em></a>.</div>
    <div id="cha-0_footnote-6" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-6">6.</a> I’m so lazy that I actually made an <em>alias</em> for <code>deploy</code> using <code>alias d='deploy'</code> in my <code>.zshrc</code> file.<span class="intersentencespace"></span> (See <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a> and “<a href="https://news.learnenough.com/macos-bash-zshell" target="_blank" rel="noopener">Using Z Shell on Macs with the Learn Enough Tutorials</a>” for more.)<span class="intersentencespace"></span> That way I only ever have to type <code>d</code> to deploy a project.<span class="intersentencespace"></span> Brevity <a href="https://en.wiktionary.org/wiki/FTW#English" target="_blank" rel="noopener">FTW</a>!</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[SEO in One Post]]></title>
    <link href="https://news.learnenough.com/seo-in-one-post"/>
    <updated>2021-02-10T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/seo-in-one-post</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.<span class="intersentencespace"></span> Is it really possible to learn everything you need to know about so-called <em>search-engine optimization</em> in a single blog post?<span class="intersentencespace"></span> Read on to find out…</p>
<p>Ever since mighty Google came on the scene, every skilled website creator has had to know something about ranking well in search-engine results.<span class="intersentencespace"></span> Generally known as <em>search-engine optimization</em>, or <em>SEO</em> for short, this subject is generally considered to be fairly big and complex, with entire books and even companies dedicated to it.<span class="intersentencespace"></span> The core of SEO is actually quite small, though—(almost) simple enough to fit in a <a href="https://twitter.com/mhartl/status/841415751151976449" target="_blank" rel="noopener">single tweet</a> (<a href="#fig-seo_tweet" class="hyperref">Figure <span class="ref">1</span></a>).</p>
<div class="center figure" id="fig-seo_tweet" data-tralics-id="uid1" data-number="1">
<div class="graphics image"><img src="images/figures/seo_tweet.png" alt="images/figures/seo_tweet" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">SEO in <a href="https://twitter.com/mhartl/status/841415751151976449" target="_blank" rel="noopener">one tweet</a>.
</span></div></div>
<p>Because of Twitter’s (at the time) 140-character limit, I couldn’t <em>literally</em> fit all of SEO into a single tweet, but it’s only a slight exaggeration, and you can fit nearly all of it into one post.<span class="intersentencespace"></span> In line with the <a href="https://en.wikipedia.org/wiki/Pareto_principle" target="_blank" rel="noopener">Pareto Principle</a> (also known as the 80/20 Rule), you can get most of the benefits of SEO with a tiny fraction of the effort.</p>
<div class="section-star" id="two_easysteps"><h2><a href="#two_easysteps" class="heading">Two (easy?) steps</a></h2>
<p>One of the reasons SEO books can run into the hundreds of pages is that they need to cover a <em>lot</em> of subjects, including HTML, web publishing, and website hosting.<span class="intersentencespace"></span> When you follow the <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough tutorials</a>, though, all the necessary subjects are automatically included.<span class="intersentencespace"></span> As a result, one post with two steps is all you really need.</p>
<p>Let’s recap the two steps from <a href="#fig-seo_tweet" class="hyperref">Figure <span class="ref">1</span></a>:</p>
<ol>
<li>Get the title, meta, and heading tags right.<span class="intersentencespace"></span>
</li>
<li>Make something people want to link to.<span class="intersentencespace"></span>
</li></ol>
<p class="noindent">Step 2 is up to you: undeniably a challenge, and beyond the scope of this post.</p>
<p>Luckily, with the right knowledge, Step 1 is relatively easy.<span class="intersentencespace"></span> And nearly all of it is included in <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout to Be Dangerous</em></a> and its prerequisites, especially <a href="https://www.learnenough.com/html" target="_blank" rel="noopener"><em>Learn Enough HTML to Be Dangerous</em></a>.<span class="intersentencespace"></span> In fact, this post is adapted from an aside box called “<a href="https://www.learnenough.com/css-and-layout-tutorial/details#aside-seo" target="_blank" rel="noopener">SEO in one box</a>” from <a href="https://www.learnenough.com/css-and-layout-tutorial/details" target="_blank" rel="noopener">Chapter 10</a> of <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout to Be Dangerous</em></a>.</p>
<div class="graphics image"><img src="images/figures/learn_enough_results.png" alt="images/figures/learn_enough_results" /></div>
<p>All of the HTML tags needed for a website with good SEO are discussed in <a href="https://www.learnenough.com/html" target="_blank" rel="noopener"><em>Learn Enough HTML</em></a> and <a href="https://www.learnenough.com/css-and-layout" target="_blank" rel="noopener"><em>Learn Enough CSS &amp; Layout</em></a>.<span class="intersentencespace"></span> The most important is probably the <code>title</code> tag (covered in <em>LE CSS</em> <a href="https://www.learnenough.com/css-and-layout-tutorial/details#sec-custom_title" target="_blank" rel="noopener">Section 10.3.1</a>), which should contain a descriptive title that naturally contains the words you want to target.<span class="intersentencespace"></span> The next important tags are <code>meta</code> tags (<em>LE CSS</em> <a href="https://www.learnenough.com/css-and-layout-tutorial/details#sec-custom_descriptions" target="_blank" rel="noopener">Section 10.3.2</a>), which contain “meta” information that isn’t displayed to users but is readable by search engines.<span class="intersentencespace"></span> Finally, the page’s heading or “h” tags are important, especially the two top-level headings <code>h1</code> and <code>h2</code> (<em>LE HTML</em> <a href="https://www.learnenough.com/html-tutorial/filling_in_the_index_page#sec-headings" target="_blank" rel="noopener">Section 2.1</a>).</p>
<div class="subsection-star"><h3><a class="heading">Targeting the Tau</a></h3>
<p>As a concrete example, let’s consider a math essay I published in 2010 that has become somewhat famous (some would say infamous) among math/science/computer nerds.<span class="intersentencespace"></span> The name of the essay is <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a>,<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup> and its subject is pi (the ratio of a circle’s circumference to its diameter) vs. tau (the ratio of a circle’s circumference to its radius).<span class="intersentencespace"></span> It also has an associated holiday, <a href="https://tauday.com/" target="_blank" rel="noopener">Tau Day</a>, observed each year on June 28 (6/28 in the American calendar system).</p>
<p>In order to rank high on search terms like “pi is wrong”, “tau day”, “the tau manifesto”, and “michael hartl”, a good <code>title</code> might look something like this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>
  Tau Day | No, really, pi is wrong:
  The Tau Manifesto by Michael Hartl
<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
</pre></div></div>
<p class="noindent">Next, we could add a <code>meta</code> tag with a more detailed description:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">"description"</span>
      <span class="na">content</span><span class="o">=</span><span class="s">"The Tau Manifesto is dedicated to one of the most important</span>
<span class="s">               numbers in mathematics…"</span><span class="p">&gt;</span>
</pre></div></div>
<p class="noindent">Finally, we could add could add <code>h1</code> and <code>h2</code> tags with text similar to the <code>title</code>:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>
  No, really, pi is wrong
  The Tau Manifesto
<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>by Michael Hartl<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
</pre></div></div>
<p class="noindent">(In the real site, there is a <code>span</code> tag to allow for styling the parts of the title differently, and the author attribution is an <code>h3</code> for reasons I don’t recall.<span class="intersentencespace"></span> The principles, however, are the same.)</p>
<p>Putting everything together would give something like the HTML skeleton in <a href="#code-tau_skeleton" class="hyperref">Listing <span class="ref">1</span></a>.</p>
<div class="codelisting" id="code-tau_skeleton" data-tralics-id="uid5" data-number="1"><div class="heading"><span class="number">Listing 1:</span> 

<span class="description">An HTML skeleton for <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a>.</span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>
      Tau Day | No, really, pi is wrong: The Tau Manifesto by Michael Hartl
    <span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">"description"</span>
          <span class="na">content</span><span class="o">=</span><span class="s">"The Tau Manifesto is dedicated to one of the most important</span>
<span class="s">                   numbers in mathematics…"</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>
      No, really, pi is wrong
      The Tau Manifesto
    <span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>by Michael Hartl<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
    .
    .
    .
  <span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre></div></div></div><div class="graphics image"><img src="images/figures/tau_day.png" alt="images/figures/tau_day" /></div>
<p>As noted before, making something people want to link to is up to you.<span class="intersentencespace"></span> Fortunately, <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a> struck a chord (even <a href="https://youtu.be/3174T-3-59Q" target="_blank" rel="noopener">literally</a>), leading to a lot of inbound links.<span class="intersentencespace"></span> Together with its good HTML structure, this lead to ranking on the first page of Google results not only for things like “<a href="https://www.google.com/search?q=tau+day" target="_blank" rel="noopener">tau day</a>” and “<a href="https://www.google.com/search?q=pi+is+wrong" target="_blank" rel="noopener">pi is wrong</a>”, but even for the <a href="https://www.google.com/search?q=tau" target="_blank" rel="noopener">Greek letter “tau” itself</a>.<sup id="cha-0_footnote-ref-2" class="footnote"><a href="#cha-0_footnote-2">2</a></sup></p>
</div>
<div class="subsection-star"><h3><a class="heading">Domain keywords</a></h3>
<p>If you have the luxury of tailoring your domain name to the keywords you’re targeting, an exact domain match can also help a lot.<span class="intersentencespace"></span> This step isn’t always possible, but when it is it can work exceptionally well, as it did with, e.g., the <a href="https://www.google.com/search?q=rails+tutorial" target="_blank" rel="noopener">Rails Tutorial</a>.<span class="intersentencespace"></span> And even if you don’t have an exact domain match, you can still get a lot of value out of “pretty URLs” (<em>LE CSS</em> <a href="https://www.learnenough.com/css-and-layout-tutorial/templates_and_frontmatter#sec-pages-folders" target="_blank" rel="noopener">Section 6.4</a>) that include the keywords you’re targeting, such as <a href="https://www.google.com/search?q=css+and+layout+tutorial" target="_blank" rel="noopener">learnenough.com/css-and-layout</a>.<span class="intersentencespace"></span> Everything you need to get started with custom domains is covered in the free Learn Enough tutorial <a href="https://www.learnenough.com/custom-domains" target="_blank" rel="noopener"><em>Learn Enough Custom Domains to Be Dangerous</em></a>.</p>
</div></div>
<div class="section-star" id="off_to_the_races"><h2><a href="#off_to_the_races" class="heading">Off to the races</a></h2>
<p>Everything in this post, although tailored a bit to search engines, is completely honest: there’s nothing in the HTML that’s not related to our site’s mission, and keywords aren’t repeated to excess.<span class="intersentencespace"></span> This philosophy applies even more to the main content of the page—if you’re actually writing about the subject whose keywords you’re targeting, the right word usage will emerge organically, so don’t worry about the details of your page copy.<span class="intersentencespace"></span> Indeed, search engines typically punish attempts to artificially manipulate the results, so any such efforts are likely to backfire.</p>
<p>Finally, when it comes to getting inbound links, quality content is king.<span class="intersentencespace"></span> Attempts to game search engines (with reciprocal link exchanges, <a href="https://en.wikipedia.org/wiki/Link_farm" target="_blank" rel="noopener">link farms</a>, etc.) are punished even more severely than attempts to manipulate the page copy, so don’t even try it.</p>
<div class="graphics image"><img src="images/figures/racecar.jpg" alt="images/figures/racecar" /></div>
<p>SEO <a href="https://www.youtube.com/watch?v=THNPmhBl-8I" target="_blank" rel="noopener">isn’t exactly rocket science</a>.<span class="intersentencespace"></span> Using only the techniques in this post, we’ve made sites with highly ranked search results for the English words “<a href="https://www.google.com/search?q=softcover" target="_blank" rel="noopener">softcover</a>” and “<a href="https://www.google.com/search?q=coveralls" target="_blank" rel="noopener">coveralls</a>”, the phrase “<a href="https://www.google.com/search?q=learn+enough" target="_blank" rel="noopener">learn enough</a>”, the competitive search term “<a href="https://www.google.com/search?q=rails+tutorial" target="_blank" rel="noopener">rails tutorial</a>”, the phrase “<a href="https://www.google.com/search?q=pi+is+wrong" target="_blank" rel="noopener">pi is wrong</a>”, and (as already noted) even a <a href="https://www.google.com/search?q=tau" target="_blank" rel="noopener">letter in the Greek alphabet</a>.<span class="intersentencespace"></span> With some work and a little luck (and maybe the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access Subscription</a>), you too could be off to the SEO races.<sup id="cha-0_footnote-ref-3" class="footnote"><a href="#cha-0_footnote-3">3</a></sup><span class="intersentencespace"></span> Good luck!</p>
<p><em>The <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access Subscription</a> includes access to all the Learn Enough tutorial courses.<span class="intersentencespace"></span> 7-day free trial, 100% money-back guarantee.</em>
</p></div><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> Essay titles typically get typeset using quotes, as in “The Tau Manifesto”, but that never felt substantial enough, so I generally set <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a> in italics.</div>
    <div id="cha-0_footnote-2" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-2">2.</a> <a href="https://tauday.com/tau-manifesto" target="_blank" rel="noopener"><em>The Tau Manifesto</em></a> used to rank even higher, but a film called <em>TAU</em> came out and knocked it down a few notches.<span class="intersentencespace"></span> But it’s on the first page of results for <em>a letter in the Greek alphabet</em>—that’s still pretty impressive!</div>
    <div id="cha-0_footnote-3" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-3">3.</a> Racecar image downloaded from <a href="https://commons.wikimedia.org/wiki/File:Lewis_Hamilton_2017_Malaysia_FP2.jpg" target="_blank" rel="noopener">Wikimedia</a> on 2018-03-27.<span class="intersentencespace"></span> Copyright © 2017 by Morio and used unaltered under the terms of the <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en" target="_blank" rel="noopener">Creative Commons Attribution-ShareAlike 4.0 Unported</a> license.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Eager Loading and the N+1 Query Problem]]></title>
    <link href="https://news.learnenough.com/eager-loading"/>
    <updated>2021-02-04T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/eager-loading</id>
    <content type="html">
      <![CDATA[
      <blockquote class="quotation"><p class="quote">This is a special guest post from <a href="https://twitter.com/kegilpin" target="_blank" rel="noopener">Kevin Gilpin</a> of <a href="https://appland.org" target="_blank" rel="noopener">AppLand</a>.<span class="intersentencespace"></span> Kevin reached out about
an improvement to the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a> regarding an important type of database query optimization known as
<a href="https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations" target="_blank" rel="noopener"><em>eager loading</em></a>, which is the solution to a corresponding problem known as an “<span class="inline_math">\( N+1 \)</span> query”.<span class="intersentencespace"></span> I suggested he write up a post on the subject for the Learn Enough blog, and he happily agreed.<span class="intersentencespace"></span> I even updated the <a href="https://www.learnenough.com/ruby-on-rails-6th-edition-tutorial/following_users#sec-scopes_subselects_and_a_lambda" target="_blank" rel="noopener">relevant section</a> of the <em>Rails Tutorial</em> accordingly.<span class="intersentencespace"></span> Thanks to Kevin for the excellent contribution!</p>
<p class="quote"><em>UPDATE</em>: There’s now a <a href="https://dev.to/appland/find-the-n-1-query-problem-in-rails-apps-in-under-5-mins-2bgg" target="_blank" rel="noopener">video version</a> narrated by <a href="https://twitter.com/dw_ux" target="_blank" rel="noopener">Dan from AppLand</a>!</p>
<p class="quote">Michael Hartl<span class="break"></span>
Author, the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a></p>
</blockquote> <hr>
<p>Working with any popular programming framework today typically means using an object-relational mapping (ORM) library like Rails’ <a href="https://guides.rubyonrails.org/active_record_basics.html" target="_blank" rel="noopener">Active Record</a>
to interact with the database.<span class="intersentencespace"></span> ORM systems are a definite plus for developer productivity and general happiness.<span class="intersentencespace"></span> They also allow a programmer to get by with a little bit less knowledge (sometimes a whole lot less) about what’s
happening in between their code and the database.<span class="intersentencespace"></span> But that convenience comes with some pitfalls, including the common <em><span class="inline_math">\( N+1 \)</span> query problem</em>.<span class="intersentencespace"></span> What does that mean?<span class="intersentencespace"></span> Read on to find out!</p>
<div class="section-star" id="working_with_an_orm"><h2><a href="#working_with_an_orm" class="heading">Working with an ORM</a></h2>
<p>When we use an ORM library to load what our code calls an <em>object</em> (and the database calls a <em>row</em>),
providing the unique identifier (primary key) is sufficient to generate and execute the <a href="https://en.wikipedia.org/wiki/SQL" target="_blank" rel="noopener">Structured Query Language</a> (SQL) needed
to load our object data into memory.<span class="intersentencespace"></span> Similarly, we can update attributes on the object and then simply
<code>save</code> it.<span class="intersentencespace"></span> Once again, SQL is generated automatically by the ORM to save the changes to the database.</p>
<p>This is straightforward when working with a single row of data.<span class="intersentencespace"></span> But there are lots of places in our application
where we want to show data in lists, tables, graphs, etc.<span class="intersentencespace"></span> If we were writing that SQL by hand, we’d write a <code>SELECT</code> clause
to specify the columns we want and a <code>WHERE</code> clause that matches all the data we want to see.<span class="intersentencespace"></span> In this way, we’d grab the
entire table’s worth of data in one query.<span class="intersentencespace"></span> This batch retrieval is lot more efficient than querying the rows one at a time
using <span class="inline_math">\( N \)</span> queries for <span class="inline_math">\( N \)</span> rows.</p>
</div>
<div class="section-star" id="batch_retrieval_and_associations"><h2><a href="#batch_retrieval_and_associations" class="heading">Batch retrieval and associations</a></h2>
<p>ORM systems have a couple of features to support batch retrieval.<span class="intersentencespace"></span> The first is <em>accessor methods</em> that allow the programmer
to specify the conditions of a <code>WHERE</code> clause and return multiple objects in an array, rather than just a single object.<span class="intersentencespace"></span> This feature makes it easy to find all rows that, for example, match a user-specified search term.<span class="intersentencespace"></span> The second (more complex) feature is <em>associations</em>.<span class="intersentencespace"></span> An association is a relationship (or, in database terms, a <em>relation</em>) between two types of
data.</p>
<p>A classic example of an association, which is used in the <a href="https://github.com/mhartl/sample_app_6th_ed" target="_blank" rel="noopener">Rails Tutorial sample app</a>, is <code>users -&gt; microposts -&gt; attachments</code>.<span class="intersentencespace"></span> The Rails
sample app requires all users to log in.<span class="intersentencespace"></span> Each user is represented in the database as a row in the <code>users</code> table.<span class="intersentencespace"></span> Once logged in, users can submit <code>microposts</code> (think of a micropost as a “tweet”).<span class="intersentencespace"></span> Each micropost can have an optional image attachment.<span class="intersentencespace"></span> In database terms, the relationship between users and microposts is one-to-many, and between microposts
and attachments is one-to-one.<span class="intersentencespace"></span> If an attachment could be shared by multiple microposts, then the relation would be
many-to-one.</p>
<p>Let’s take a look at how the ORM interacts with this schema in a common scenario: showing the microposts “feed” for a user consisting of the microposts of the user plus those of the other users being followed.<span class="intersentencespace"></span> Consider a controller that retrieves a page of microposts for this user:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">paginate</span><span class="p">(</span><span class="ss">page</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]</span><span class="p">)</span>
</pre></div></div>
<p>The ORM is smart enough to fetch one page of microposts in one batch, making the this operation simple and efficient… right?<span class="intersentencespace"></span> Not necessarily.</p>
</div>
<div class="section-star" id="pitfall_–_the_n1_query_problem"><h2><a href="#pitfall_–_the_n1_query_problem" class="heading">Pitfall – The N+1 query problem</a></h2>
<p>When we look under the covers, there’s a problem.</p>
<p>Suppose the HTML page template that uses <code>@microposts</code> looks something like <a href="#code-microposts_template" class="hyperref">Listing <span class="ref">1</span></a>.</p>
<div class="codelisting" id="code-microposts_template" data-tralics-id="uid1" data-number="1"><div class="heading"><span class="number">Listing 1:</span> 

<span class="description">The <a href="https://haml.info" target="_blank" rel="noopener">Haml</a> code to show microposts.</span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="p">-</span> <span class="vi">@microposts</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">post</span><span class="o">|</span>
  <span class="nt">%li</span>
    <span class="p">=</span> <span class="n">post</span><span class="o">.</span><span class="n">body</span>
    <span class="nt">%br</span>
    created by
    <span class="p">=</span> <span class="n">post</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">name</span>
    <span class="nt">%ul</span>
      <span class="p">-</span> <span class="n">post</span><span class="o">.</span><span class="n">attachments</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">attachment</span><span class="o">|</span>
        <span class="nt">%li</span>
        <span class="p">=</span> <span class="n">attachment</span><span class="o">.</span><span class="n">url</span>
</pre></div></div></div><p class="noindent">(The code in <a href="#code-microposts_template" class="hyperref">Listing <span class="ref">1</span></a> uses the <a href="https://haml.info" target="_blank" rel="noopener">Haml</a> syntax for representing HTML.<span class="intersentencespace"></span> Refer to the <a href="https://haml.info/docs.html" target="_blank" rel="noopener">Haml documentation</a> or use your <a href="https://www.railstutorial.org/book/beginning#aside-technical_sophistication" target="_blank" rel="noopener">technical sophistication</a> to figure out the correspondence between Haml syntax and HTML tags.)</p>
<p>Using one of the techniques described in <a href="#aside-sql_queries" class="hyperref">Box <span class="ref">1</span></a>, we can observe
the queries which are issued by this block of code.<span class="intersentencespace"></span> The first query that we see fetches the number of microposts for the user:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">SELECT</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="k">FROM</span>
  <span class="ss">"microposts"</span>
<span class="k">WHERE</span>
  <span class="p">(</span><span class="n">user_id</span> <span class="o">=</span> <span class="o">?</span><span class="p">)</span>
</pre></div></div>
<p>Next, there’s a query to fetch a page of microposts:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="k">SELECT</span>
  <span class="ss">"microposts"</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span>
  <span class="ss">"microposts"</span>
<span class="k">WHERE</span>
  <span class="p">(</span><span class="n">user_id</span> <span class="o">=</span> <span class="mi">762146111</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span>
  <span class="ss">"microposts"</span><span class="p">.</span><span class="ss">"created_at"</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="o">?</span> <span class="k">OFFSET</span> <span class="o">?</span>
</pre></div></div>
<p>The <code>LIMIT</code> and <code>OFFSET</code> are the query clauses that select the page offset and the page size.<span class="intersentencespace"></span> Note that an <code>ORDER BY</code> is always
required for pagination; otherwise, the <code>OFFSET</code> might not have the desired effect across multiple queries.</p>
<p>So far, so good.</p>
<p>But now there’s a problem.<span class="intersentencespace"></span> We see the two queries shown in <a href="#code-attachments_query" class="hyperref">Listing <span class="ref">2</span></a> and <a href="#code-users_query" class="hyperref">Listing <span class="ref">3</span></a> repeated many times—once for each micropost, in fact.</p>
<div class="codelisting" id="code-attachments_query" data-tralics-id="uid2" data-number="2"><div class="heading"><span class="number">Listing 2:</span> 

<span class="description">The SQL for finding micropost attachments.</span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="k">SELECT</span>
  <span class="ss">"active_storage_attachments"</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span>
  <span class="ss">"active_storage_attachments"</span>
<span class="k">WHERE</span>
  <span class="ss">"active_storage_attachments"</span><span class="p">.</span><span class="ss">"record_id"</span> <span class="o">=</span> <span class="o">?</span>
  <span class="k">AND</span> <span class="ss">"active_storage_attachments"</span><span class="p">.</span><span class="ss">"record_type"</span> <span class="o">=</span> <span class="o">?</span>
  <span class="k">AND</span> <span class="ss">"active_storage_attachments"</span><span class="p">.</span><span class="ss">"name"</span> <span class="o">=</span> <span class="o">?</span>
<span class="k">LIMIT</span>
  <span class="o">?</span>
</pre></div></div></div><div class="codelisting" id="code-users_query" data-tralics-id="uid3" data-number="3"><div class="heading"><span class="number">Listing 3:</span> 

<span class="description">The SQL query for finding users associated with a micropost.</span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="k">SELECT</span>
  <span class="ss">"users"</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span>
  <span class="ss">"users"</span>
<span class="k">WHERE</span>
  <span class="ss">"users"</span><span class="p">.</span><span class="ss">"id"</span> <span class="o">=</span> <span class="o">?</span>
<span class="k">LIMIT</span>
  <span class="o">?</span>
</pre></div></div></div><p>What’s going on?<span class="intersentencespace"></span> What we are seeing here is known as the <em><span class="inline_math">\( N+1 \)</span> query problem</em>.<span class="intersentencespace"></span> The number of queries to fetch the data is not <span class="inline_math">\( 1 \)</span>, as we would
expect; rather, it’s <span class="inline_math">\( 1 \)</span> to fetch the page of microposts, plus an additional <span class="inline_math">\( N \)</span> queries for the users, for a total of <span class="inline_math">\( N+1 \)</span>.<span class="intersentencespace"></span> (In this case, there are an additional <span class="inline_math">\( N \)</span> queries for the user attachments, so the total is actually <span class="inline_math">\( 2N+1 \)</span>, which is even worse.)</p>
<p>When SQL is hand-coded, a programmer has control over which queries are issued.<span class="intersentencespace"></span> The downside is that there can be a lot of
repetitive and mundane work (as well as security risk) required to perform simple tasks of saving and loading data.<span class="intersentencespace"></span> ORMs
relieve programmers of that work and risk, making
development faster and more secure.<span class="intersentencespace"></span> But because queries are issued automatically, we cannot tell by looking at
one section of the code—in this case, the HTML template—which queries will be executed behind the scenes.<span class="intersentencespace"></span> Changes to one part of the code could change the SQL behavior of
virtually any other part of the code.<span class="intersentencespace"></span> That’s pretty tough to handle.<span class="intersentencespace"></span> So, what to do?</p>
<div class="aside" id="aside-sql_queries" data-tralics-id="uid4" data-number="1"><div class="heading"><span class="number">Box 1.</span> 

<span class="description">Active Record SQL</span></div>
<p>Figuring out what SQL your code is issuing can be tricky.</p>
<p>One useful tool is the <a href="https://github.com/MiniProfiler/rack-mini-profiler" target="_blank" rel="noopener">mini_profiler</a> Ruby gem.<span class="intersentencespace"></span> When you integrate and activate <code class="tt">mini_profiler</code>, it
displays a breakdown of the time spent in your backend code.<span class="intersentencespace"></span> You can click into
SQL for detailed timing of each query.</p>
<p><code class="tt">mini_profiler</code> shows you all the queries, but it doesn’t show you which part of the code
is responsible for each query.<span class="intersentencespace"></span> For visual debugging of web services, code, and SQL
you can use the open-source
<a href="https://appland.org" target="_blank" rel="noopener">AppLand framework</a>,
which consists of an open data format, data-recording clients, visualization libraries, and
IDE integrations such as the
<a href="https://marketplace.visualstudio.com/items?itemName=appland.appmap" target="_blank" rel="noopener">AppMap extension for VSCode</a>.<span class="intersentencespace"></span> The AppLand framework records the behavior of your code,
either from test cases or from user interaction.<span class="intersentencespace"></span> The AppLand IDE extension displays the recorded data as interactive
diagrams, graphs, tables, and visualizations.<span class="intersentencespace"></span> You can see exactly which function is
generating each query, and you can navigate all the way down to the Ruby code level if you want to.</p>
<p>Here’s an example:</p>
<div class="center"><div class="graphics image"><img src="images/figures/appmap_sql.png" alt="images/figures/appmap_sql" /></div>
</div>
</div></div>
<div class="section-star" id="the_fix"><h2><a href="#the_fix" class="heading">The fix</a></h2>
<p>Fortunately, ORMs such as ActiveRecord do provide a way to fix this <span class="inline_math">\( N+1 \)</span> query problem.<span class="intersentencespace"></span> The solution is called
<em>eager loading</em>.<span class="intersentencespace"></span> “Eager loading” is named to contrast with “lazy loading”, in which data is retrieved just-in-time
as it’s needed.<span class="intersentencespace"></span> Lazy loading doesn’t refer to lazy programmers; it refers to lazy code.<span class="intersentencespace"></span> With lazy loading, the
code takes the “lazy” approach of waiting until data is needed to load it.<span class="intersentencespace"></span> Unfortunately, in the example above,
each micropost is lazy-loading its users and its attachments.<span class="intersentencespace"></span> The microposts aren’t “smart” enough to coordinate with each
other and “lazy load” all the attachments at once—it’s every micropost for itself.<span class="intersentencespace"></span> The result is a slew of queries as each micropost loads its attachments and associated user.</p>
<p>Eager loading is a programmable option in which data associations are loaded “eagerly”, as in up-front,
right now.<span class="intersentencespace"></span> The advantage of eager loading is that the ORM will be smart enough to use the eagerly loaded data
when it’s needed.</p>
<p>More concretely, let’s look at an actual Active Record query from the Rails Tutorial sample app.<span class="intersentencespace"></span> <a href="#code-feed" class="hyperref">Listing <span class="ref">4</span></a> shows the micropost feed from the <a href="https://github.com/mhartl/sample_app_6th_ed/blob/master/app/models/user.rb#L88" target="_blank" rel="noopener">User model</a>.</p>
<div class="codelisting" id="code-feed" data-tralics-id="uid5" data-number="4"><div class="heading"><span class="number">Listing 4:</span> 

<span class="description">The micropost status feed.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">app/models/user.rb</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="c1"># Returns a user's status feed.</span>
  <span class="k">def</span> <span class="nf">feed</span>
    <span class="n">following_ids</span> <span class="o">=</span> <span class="s2">"SELECT followed_id FROM relationships</span>
<span class="s2">                     WHERE  follower_id = :user_id"</span>
    <span class="no">Micropost</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">"user_id IN (</span><span class="si">#{</span><span class="n">following_ids</span><span class="si">}</span><span class="s2">)</span>
<span class="s2">                     OR user_id = :user_id"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
  <span class="k">end</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>
<span class="k">end</span>
</pre></div></div></div><p>What we want to do is turn this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="no">Micropost</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">"user_id IN (</span><span class="si">#{</span><span class="n">following_ids</span><span class="si">}</span><span class="s2">)</span>
<span class="s2">                 OR user_id = :user_id"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
</pre></div></div>
<p class="noindent">into this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="no">Micropost</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">"user_id IN (</span><span class="si">#{</span><span class="n">following_ids</span><span class="si">}</span><span class="s2">)</span>
<span class="s2">                 OR user_id = :user_id"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
<span class="hll">         <span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">image_attachment</span><span class="p">:</span> <span class="ss">:blob</span><span class="p">)</span>
</span></pre></div></div>
<p>The <code>includes</code> clause is the key.<span class="intersentencespace"></span> As the <a href="https://apidock.com/rails/ActiveRecord/QueryMethods/includes" target="_blank" rel="noopener">Active Record documentation</a> says, <code>includes</code> enables the programmer to:</p>
<blockquote class="quotation"><p class="quote">specify relationships to be included in the result set… This will often result in a performance improvement over a simple join.<span class="intersentencespace"></span></p>
</blockquote><p class="noindent">In this case, we’re using Active Record to create an underlying SQL query that <code>includes</code> both the associated user (via the symbol <code>:user</code>) and any image attachments (via <code>image_attachment: :blob</code>).</p>
<p>If we don’t need that extra data, then eager loading is a wasted effort, which is why Active Record doesn’t do it automatically.<span class="intersentencespace"></span> But if we
do need it, the efficiency savings can be substantial.</p>
</div>
<div class="section-star" id="finding_n1_query_bugs"><h2><a href="#finding_n1_query_bugs" class="heading">Finding N+1 query bugs</a></h2>
<p>Figuring out when we have an <span class="inline_math">\( N+1 \)</span> query problem is tricky, because it’s not the type of problem that’s revealed by any
mainstream testing strategy.<span class="intersentencespace"></span> Unit tests, functional tests, and integration tests will all happily pass whether
the data is being lazy-loaded or eager-loaded.<span class="intersentencespace"></span> Performance tests may reveal an issue, but a single <span class="inline_math">\( N+1 \)</span> query problem,
while inefficient, does not generally result in a really big performance penalty.<span class="intersentencespace"></span> Rather, <span class="inline_math">\( N+1 \)</span> queries are sort of like
an extra weight or drag on our app, slowly adding more and more inefficiency until, as a whole, our app
is noticeably slowed down.</p>
<p>An effective way to find <span class="inline_math">\( N+1 \)</span> query problems is to look ar the number of SQL queries that are performed in a
realistic situation, such as an integration test, and compare this number with a “reasonable” expectation.<span class="intersentencespace"></span> Two tools, when combined together, will yield the query count for any test case in a straightforward way.<span class="intersentencespace"></span> These two tools are <code class="tt">appmap-ruby</code> and <code class="tt">jq</code>.</p>
<div class="subsection-star"><h3><a class="heading">appmap-ruby</a></h3>
<p><a href="https://github.com/applandinc/appmap-ruby" target="_blank" rel="noopener">appmap</a> is a Ruby gem that records the execution of code
and writes it as a JSON file.</p>
</div>
<div class="subsection-star"><h3><a class="heading">jq</a></h3>
<p><a href="https://stedolan.github.io/jq/" target="_blank" rel="noopener">jq</a> is a lightweight and flexible command-line JSON processor.<span class="intersentencespace"></span> See the <a href="https://stedolan.github.io/jq/" target="_blank" rel="noopener">jq website</a> for installation instructions on your system.<span class="intersentencespace"></span> (Mac users with Homebrew can install jq using <code>brew install jq</code>.)</p>
</div>
<div class="subsection-star"><h3><a class="heading">appmap-ruby + jq</a></h3>
<p>We can use the following procedure to count the number of SQL queries in a test case:</p>
<ol>
<li>Run the test case with <code class="tt">appmap-ruby</code> enabled, which will create a JSON file containing the recorded code events.<span class="intersentencespace"></span>
</li>
<li>Process the recorded code events through <code class="tt">jq</code> to count the number of SQL queries.<span class="intersentencespace"></span>
</li></ol>
<p>The steps below show how to update a generic Rails app with these tools.<span class="intersentencespace"></span> If you’d like to follow along with a concrete example, you can clone the <a href="https://github.com/mhartl/sample_app_6th_ed" target="_blank" rel="noopener">Rails Tutorial sample app</a>, follow the setup steps described in the <a href="https://github.com/mhartl/sample_app_6th_ed#ruby-on-rails-tutorial-sample-application" target="_blank" rel="noopener">README</a>, and then check out the <code>eager-loading</code> branch:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>git clone https://github.com/mhartl/sample_app_6th_ed.git
<span class="gp">$ </span><span class="nb">cd</span> sample_app_6th_ed
<span class="gp">$ </span>git checkout eager-loading
</pre></div></div>
<p>The <code>eager-loading</code> branch of the reference app adds the <code class="tt">appmap</code> gem to its <code>Gemfile</code>, as shown in <a href="#code-appmap_gemfile" class="hyperref">Listing <span class="ref">5</span></a>.</p>
<div class="codelisting" id="code-appmap_gemfile" data-tralics-id="uid8" data-number="5"><div class="heading"><span class="number">Listing 5:</span> 

<span class="description">Adding the <code class="tt">appmap</code> gem to the sample app.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">Gemfile</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="p">,</span>
<span class="p">,</span>
<span class="p">,</span>
<span class="n">group</span> <span class="ss">:development</span><span class="p">,</span> <span class="ss">:test</span> <span class="k">do</span>
  <span class="n">gem</span> <span class="s1">'sqlite3'</span><span class="p">,</span> <span class="s1">'1.4.2'</span>
  <span class="n">gem</span> <span class="s1">'byebug'</span><span class="p">,</span>  <span class="s1">'11.1.3'</span><span class="p">,</span> <span class="ss">platforms</span><span class="p">:</span> <span class="o">[</span><span class="ss">:mri</span><span class="p">,</span> <span class="ss">:mingw</span><span class="p">,</span> <span class="ss">:x64_mingw</span><span class="o">]</span>
<span class="hll">  <span class="n">gem</span> <span class="s1">'appmap'</span><span class="p">,</span>  <span class="s1">'0.40.0'</span>
</span><span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
</pre></div></div></div><p>To install the new gem, run <code>bundle install</code> as usual:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>bundle install
</pre></div></div>
<p>A second change, already implemented in the reference app, is the addition of a YAML configuration file to be used by <code class="tt">appmap-ruby</code>, which appears as in <a href="#code-appmap_yml" class="hyperref">Listing <span class="ref">6</span></a>.</p>
<div class="codelisting" id="code-appmap_yml" data-tralics-id="uid9" data-number="6"><div class="heading"><span class="number">Listing 6:</span> 

<span class="description">Adding an AppMap configuration file.<span class="intersentencespace"></span> <span class="break"></span> <code class="filepath">appmap.yml</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="nt">name</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">SampleApp</span>
<span class="nt">packages</span><span class="p">:</span>
<span class="p p-Indicator">-</span> <span class="nt">path</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">app/controllers</span>
<span class="p p-Indicator">-</span> <span class="nt">path</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">app/models</span>
<span class="p p-Indicator">-</span> <span class="nt">gem</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">activerecord</span>
</pre></div></div></div><p>Taking the steps shown above one at a time, the first step is to record a test case.<span class="intersentencespace"></span> Let’s suppose we have an integration
test like <a href="https://github.com/mhartl/sample_app_6th_ed/blob/master/test/integration/microposts_interface_test.rb" target="_blank" rel="noopener"><code>microposts_interface_test.rb</code></a>.<span class="intersentencespace"></span> Run the test with <code>APPMAP=true</code>:</p>
<div class="code"><div class="highlight"><pre><span></span>  $ <span class="nv">APPMAP</span><span class="o">=</span><span class="nb">true</span> bundle <span class="nb">exec</span> ruby -Ilib -Itest <span class="se">\</span>
    test/integration/microposts_interface_test.rb
</pre></div></div>
<p>Because the sample app’s <code>test_helper.rb</code> file has been configured as described in the <a href="https://github.com/applandinc/appmap-ruby#minitest" target="_blank" rel="noopener">appmap minitest documentation</a>, this test will automatically create a file called</p>
<div class="code"><div class="highlight"><pre><span></span>tmp/appmap/minitest/Microposts_interface_micropost_interface.appmap.json
</pre></div></div>
<p>To get some insight into the application’s behavior, we can process this file through the <code class="tt">jq</code> query shown in <a href="#code-jq_test" class="hyperref">Listing <span class="ref">7</span></a>, which:</p>
<ol>
<li>Enumerates all the code events that occurred during the test case.<span class="intersentencespace"></span> Code events include HTTP server requests, function calls, and SQL queries.<span class="intersentencespace"></span>
</li>
<li>Selects just those code events that are SQL queries.<span class="intersentencespace"></span>
</li>
<li>Counts the number of queries.<span class="intersentencespace"></span>
</li></ol>
<div class="codelisting" id="code-jq_test" data-tralics-id="uid13" data-number="7"><div class="heading"><span class="number">Listing 7:</span> 

<span class="description">Processing test output using <code>jq</code>.</span>
</div>

<div class="code"><div class="highlight"><pre><span></span>  $ jq <span class="s1">'[.events[] | select(has("sql_query"))] | length'</span> <span class="se">\</span>
    tmp/appmap/minitest/Microposts_interface_micropost_interface.appmap.json
<span class="hll">  <span class="m">237</span>
</span></pre></div></div></div><p><strong>237</strong> queries is a lot for one test case!<sup id="cha-0_footnote-ref-1" class="footnote intersentence"><a href="#cha-0_footnote-1">1</a></sup><span class="intersentencespace"></span> I’ve recorded all of the 66 test cases in the Rails Sample App
6th Edition, and <a href="https://docs.google.com/spreadsheets/d/1tmIOI3RUwKhYarDViU9F37eTG4MynhN8oQSoChyVopI/edit?usp=sharing" target="_blank" rel="noopener">plotted the number of SQL queries</a> in each test case as a histogram (<a href="#fig-query_histogram" class="hyperref">Figure <span class="ref">1</span></a>).</p>
<div class="center figure" id="fig-query_histogram" data-tralics-id="uid15" data-number="1">
<div class="graphics image"><img src="images/figures/query_histogram.png" alt="images/figures/query_histogram" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">A histogram for unoptimized queries.
</span></div></div>
<p>That 237 in <a href="#fig-query_histogram" class="hyperref">Figure <span class="ref">1</span></a> really sticks out.<span class="intersentencespace"></span> Next, I have applied the eager loading fix and re-run the test cases.<span class="intersentencespace"></span> The number of queries drops to 72, as shown in <a href="#fig-optimized_query_histogram" class="hyperref">Figure <span class="ref">2</span></a>.</p>
<div class="center figure" id="fig-optimized_query_histogram" data-tralics-id="uid16" data-number="2">
<div class="graphics image"><img src="images/figures/optimized_query_histogram.png" alt="images/figures/optimized_query_histogram" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">A histogram for optimized queries.
</span></div></div>
<p>It’s still quite a few; maybe there is more room for optimization?<span class="intersentencespace"></span> I will leave that as an exercise for the reader!</p>
<p>(By the way, you may have noticed in <a href="#fig-optimized_query_histogram" class="hyperref">Figure <span class="ref">2</span></a> that one of the query counts, around 120, is actually <em>worse</em> after being “optimized”.<span class="intersentencespace"></span> This is because there is a test case in the sample app, <a href="https://github.com/mhartl/sample_app_6th_ed/blob/8bf1938e196997511cee7a44a90f1f7e2c661e3d/test/models/user_test.rb#L94" target="_blank" rel="noopener"><code>"feed should have the right posts"</code></a>, that loops through the posts of each of four users and checks that the user’s
feed includes the post.<span class="intersentencespace"></span> The number of queries in this test is actually
<em>increased</em> by adding eager loading because fetching a feed now takes three
queries instead of one.<span class="intersentencespace"></span> But this is really an artificial scenario: the app
itself never fetches <span class="inline_math">\( N\times M \)</span> feeds, and when it does fetch a feed it renders
it in the view, which traverses the associations and hence benefits from eager loading.)</p>
</div></div>
<div class="section-star" id="verifying_the_fix"><h2><a href="#verifying_the_fix" class="heading">Verifying the fix</a></h2>
<p>Once you have tuned the ORM and queries, you can add an assertion to your test case that
will verify the query count.<span class="intersentencespace"></span> In
<a href="https://github.com/mhartl/sample_app_6th_ed/blob/master/test/integration/microposts_interface_test.rb#L26" target="_blank" rel="noopener">one unoptimized test case in the Rails Sample App 6th Edition</a>,
a page issues over 100 SQL queries:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="nb">require</span> <span class="s1">'test_helper'</span>

<span class="k">class</span> <span class="nc">MicropostsInterfaceTest</span> <span class="o">&lt;</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="nb">test</span> <span class="s2">"micropost interface"</span> <span class="k">do</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="c1"># Valid submission</span>
    <span class="n">content</span> <span class="o">=</span> <span class="s2">"This micropost really ties the room together"</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">fixture_file_upload</span><span class="p">(</span><span class="s1">'kitten.jpg'</span><span class="p">,</span> <span class="s1">'image/jpeg'</span><span class="p">)</span>
    <span class="n">assert_difference</span> <span class="s1">'Micropost.count'</span><span class="p">,</span> <span class="mi">1</span> <span class="k">do</span>
      <span class="n">post</span> <span class="n">microposts_path</span><span class="p">,</span> <span class="ss">params</span><span class="p">:</span> <span class="p">{</span> <span class="ss">micropost</span><span class="p">:</span> <span class="p">{</span> <span class="ss">content</span><span class="p">:</span> <span class="n">content</span><span class="p">,</span>
                                                   <span class="ss">image</span><span class="p">:</span>   <span class="n">image</span> <span class="p">}</span> <span class="p">}</span>
    <span class="k">end</span>
    <span class="n">assert</span> <span class="n">assigns</span><span class="p">(</span><span class="ss">:micropost</span><span class="p">)</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">attached?</span>
    <span class="n">follow_redirect!</span>
    <span class="n">assert_match</span> <span class="n">content</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">body</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="o">.</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></div></div>
<p class="noindent">With eager-loading optimization applied,
the page issues under 100 queries.<span class="intersentencespace"></span> What we’d like is a test that will fail if the query count goes over this limit, thereby catching any <a href="https://en.wikipedia.org/wiki/Software_regression" target="_blank" rel="noopener">regressions</a>.</p>
<p>To make the test, we can use the <a href="https://www.rubydoc.info/gems/minitest/4.2.0/MiniTest%2FAssertions:assert_operator" target="_blank" rel="noopener"><code>assert_operator</code></a> minitest assertion, which lets us assert that one number is less than another using the symbol <code>:&lt;</code> to represent the “less than” operator:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="n">assert_operator</span> <span class="mi">50</span><span class="p">,</span> <span class="ss">:&lt;</span><span class="p">,</span> <span class="mi">100</span>
</pre></div></div>
<p class="noindent">This would assert the relationship <code>50 &lt; 100</code>, which would pass because the statement is true.</p>
<p>The actual query happens upon redirecting after successful submission, which renders the feed on the user’s home page, so we can wrap the redirect in a block that uses appmap to make a list of events associated with the newly rendered page:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="n">events</span> <span class="o">=</span> <span class="no">AppMap</span><span class="o">.</span><span class="n">record</span> <span class="k">do</span>
    <span class="n">follow_redirect!</span>
  <span class="k">end</span><span class="o">[</span><span class="s1">'events'</span><span class="o">].</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span> <span class="no">OpenStruct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">event</span><span class="p">)</span> <span class="p">}</span>
</pre></div></div>
<p class="noindent">(This code is a little advanced, so don’t worry too much about the details.<span class="intersentencespace"></span> Try putting <code>p events</code> somewhere after the code above if you’d like to see what the <code>events</code> variable looks like in this case.)</p>
<p>Using the <code>select</code> method to select SQL query events then looks like this:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="n">sql</span> <span class="o">=</span> <span class="n">events</span><span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:sql_query</span><span class="p">)</span>
</pre></div></div>
<p class="noindent">Knowing how to do this involves reading a bit of the <a href="https://github.com/applandinc/appmap-ruby" target="_blank" rel="noopener">appmap documentation</a> or using the <code>p</code> trick above to figure out what the internals of the <code>event</code> array look like.<sup id="cha-0_footnote-ref-2" class="footnote"><a href="#cha-0_footnote-2">2</a></sup></p>
<p>We can then test that the number of queries is less than 100 using <code>assert_operator</code>:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="n">assert_operator</span> <span class="n">sql</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="ss">:&lt;</span><span class="p">,</span> <span class="mi">100</span>
</pre></div></div>
<p class="noindent">Putting everything together gives the test shown in <a href="#code-eager_loading_test" class="hyperref">Listing <span class="ref">8</span></a>.</p>
<div class="codelisting" id="code-eager_loading_test" data-tralics-id="uid18" data-number="8"><div class="heading"><span class="number">Listing 8:</span> 

<span class="description">A regression test for eager loading.<span class="intersentencespace"></span> <span style="color: red"><span class="smallcaps"></span><span class="smallcaps"><strong>red</strong></span><span class="smallcaps"></span></span><span class="break"></span> <code class="filepath">test/integration/microposts_interface_test.rb</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="nb">require</span> <span class="s1">'test_helper'</span>

<span class="k">class</span> <span class="nc">MicropostsInterfaceTest</span> <span class="o">&lt;</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>

  <span class="k">def</span> <span class="nf">setup</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="nb">test</span> <span class="s2">"micropost interface"</span> <span class="k">do</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="n">assert</span> <span class="n">assigns</span><span class="p">(</span><span class="ss">:micropost</span><span class="p">)</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">attached?</span>
<span class="hll">    <span class="n">events</span> <span class="o">=</span> <span class="no">AppMap</span><span class="o">.</span><span class="n">record</span> <span class="k">do</span>
</span><span class="hll">        <span class="n">follow_redirect!</span>
</span><span class="hll">      <span class="k">end</span><span class="o">[</span><span class="s1">'events'</span><span class="o">].</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span> <span class="no">OpenStruct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">event</span><span class="p">)</span> <span class="p">}</span>
</span><span class="hll">    <span class="n">sql</span> <span class="o">=</span> <span class="n">events</span><span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:sql_query</span><span class="p">)</span>
</span><span class="hll">    <span class="c1"># Without the eager-loading optimization, &gt; 100 queries are issued here.</span>
</span><span class="hll">    <span class="n">assert_operator</span> <span class="n">sql</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="ss">:&lt;</span><span class="p">,</span> <span class="mi">100</span>
</span>    <span class="n">assert_match</span> <span class="n">content</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">body</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="o">.</span>
  <span class="k">end</span>
<span class="k">end</span>
</pre></div></div></div><p>Running the tests with the default status feed (without eager loading) should then give us a <span style="color: red"><span class="smallcaps"></span><span class="smallcaps"><strong>red</strong></span><span class="smallcaps"></span></span> test suite:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>rails <span class="nb">test</span>
<span class="go">66 tests, 344 assertions, 1 failures, 0 errors, 0 skips</span>
</pre></div></div>
<p>To get the test to <span style="color: ForestGreen"><span class="smallcaps"></span><span class="smallcaps"><strong>green</strong></span><span class="smallcaps"></span></span>, we can update the <code>feed</code> method in the User model with eager loading, as shown in <a href="#code-feed_with_eager_loading" class="hyperref">Listing <span class="ref">9</span></a>.</p>
<div class="codelisting" id="code-feed_with_eager_loading" data-tralics-id="uid19" data-number="9"><div class="heading"><span class="number">Listing 9:</span> 

<span class="description">Adding eager loading to the status feed.<span class="intersentencespace"></span> <span style="color: ForestGreen"><span class="smallcaps"></span><span class="smallcaps"><strong>green</strong></span><span class="smallcaps"></span></span><span class="break"></span> <code class="filepath">app/models/user.rb</code></span>
</div>

<div class="code"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="c1"># Returns a user's status feed.</span>
  <span class="k">def</span> <span class="nf">feed</span>
    <span class="n">following_ids</span> <span class="o">=</span> <span class="s2">"SELECT followed_id FROM relationships</span>
<span class="s2">                     WHERE  follower_id = :user_id"</span>
    <span class="no">Micropost</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">"user_id IN (</span><span class="si">#{</span><span class="n">following_ids</span><span class="si">}</span><span class="s2">)</span>
<span class="s2">                     OR user_id = :user_id"</span><span class="p">,</span> <span class="ss">user_id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
<span class="hll">             <span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">image_attachment</span><span class="p">:</span> <span class="ss">:blob</span><span class="p">)</span>
</span>  <span class="k">end</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>
<span class="k">end</span>
</pre></div></div></div><p>With the eager loading added as in <a href="#code-eager_loading_test" class="hyperref">Listing <span class="ref">8</span></a>, the test suite should be <span style="color: ForestGreen"><span class="smallcaps"></span><span class="smallcaps"><strong>green</strong></span><span class="smallcaps"></span></span>:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>rails <span class="nb">test</span>
<span class="go">66 tests, 349 assertions, 0 failures, 0 errors, 0 skips</span>
</pre></div></div>
<p class="noindent">Great!<span class="intersentencespace"></span> Now our test suite will tell us immediately if eager loading ever stops working for some reason or is accidentally removed.<span class="intersentencespace"></span> (It’s worth noting that the exact number of SQL queries can be system-dependent, which can lead to spurious test failures.<span class="intersentencespace"></span> For production systems, it’s a good idea to avoid this issue by using a standard test environment at a continuous-integration service like <a href="https://travis-ci.org" target="_blank" rel="noopener">Travis CI</a> or <a href="https://circleci.com" target="_blank" rel="noopener">Circle CI</a>.)</p>
</div>
<div class="section-star" id="in_conclusion"><h2><a href="#in_conclusion" class="heading">In conclusion</a></h2>
<p>ORMs are powerful.<span class="intersentencespace"></span> With great power comes great responsibility, and in this case, it’s a responsibility
to keep an eye on what that ORM is doing under the covers.<span class="intersentencespace"></span> It’s very easy to accidentally introduce an <span class="inline_math">\( N+1 \)</span> query problem, because all a developer needs to do is
traverse a new object relationship in a view, and a separate SQL query <em>for each object</em> can be triggered.</p>
<p>In this blog post, you’ve learned what the <span class="inline_math">\( N+1 \)</span> query problem is, how to fix it with eager loading,
and how to add a test case to verify that the fix is working.<span class="intersentencespace"></span> That should be everything you need to
tackle this thorny problem.<span class="intersentencespace"></span> Good luck!
</p></div><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> This number could be slightly system-dependent; when running <a href="#code-jq_test" class="hyperref">Listing <span class="ref">7</span></a>, I got 208 instead of 237.<span class="break"></span> —MH</div>
    <div id="cha-0_footnote-2" class="footnote"><p><a class="footnote-link" href="#cha-0_footnote-ref-2">2.</a> The <code>p &lt;var&gt;</code> method is just an alias for <code>puts &lt;var&gt;.inspect</code>, so <code>p</code> is a convenient way to view the contents of things like arrays or hashes by printing literal versions to the screen:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="go">$ irb</span>
<span class="gp">&gt;&gt; </span><span class="n">a</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="o">]</span>
<span class="gp">&gt;&gt; </span><span class="nb">puts</span> <span class="n">a</span>
<span class="go">1</span>
<span class="go">2</span>
<span class="go">3</span>
<span class="gp">&gt;&gt; </span><span class="nb">p</span> <span class="n">a</span>
<span class="go">[1, 2, 3]</span>
</pre></div></div>
</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Sending Email with Rails]]></title>
    <link href="https://news.learnenough.com/sending-email-with-rails"/>
    <updated>2021-01-28T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/sending-email-with-rails</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a> and <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a>.</p>
<p>One of the topics covered by the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a> is sending email with Rails.<span class="intersentencespace"></span> It can be difficult to find up-to-date resources on this subject, which is part of why I added a couple of chapters on it starting in the 4th edition of the tutorial.</p>
<p>For reasons described below, I’ve now updated the main chapter on sending email to make it 100% free:</p>
<ul>
<li><a href="https://www.railstutorial.org/book/account_activation" target="_blank" rel="noopener">Chapter 11: Account activation</a>
</li>
<li>See especially <a href="https://www.railstutorial.org/book/account_activation#sec-activation_email_in_production" target="_blank" rel="noopener">Section 11.4: Email in production</a>.<span class="intersentencespace"></span>
</li></ul>
<div class="graphics image">
  <img src="images/figures/account_activation.png" alt="images/figures/account_activation" />
</div>
<p>The current service covered by the tutorial for sending emails in production is <em>SendGrid</em>.<span class="intersentencespace"></span> SendGrid has a generous free tier and generally works well for the purposes of the <em>Rails Tutorial</em>.<span class="intersentencespace"></span> There are two caveats, though:</p>
<ol>
<li>SendGrid recently changed how they handle authentication, rendering the treatment of the subject in the <a href="https://amzn.to/3pA5LF1" target="_blank" rel="noopener">print edition</a><sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup> obsolete.<span class="intersentencespace"></span>
</li>
<li>I’ve had numerous reports of SendGrid accounts being immediately frozen, with long reply times from customer service.<span class="intersentencespace"></span> I’ve intervened repeatedly with SendGrid customer support on behalf of <em>Rails Tutorial</em> readers, and have also filed a ticket to find a more permanent solution.<span class="intersentencespace"></span> My hope is that we can find a way to preapprove Rails Tutorial–affiliated accounts, but the situation as it stands is unsustainable, and I might have to swap out SendGrid at some point.<span class="intersentencespace"></span> <em>UPDATE</em>: Based on a tip from a helpful reader and some googling around, I discovered SendGrid’s <a href="https://sendgrid.com/docs/ui/sending-email/sender-verification/" target="_blank" rel="noopener">Single Sender Verification page</a>, which explains how to add a verified Single Sender that I hope will prevent the problem described.<span class="intersentencespace"></span> I have updated the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a> accordingly.<span class="intersentencespace"></span>
</li></ol>
<p><a href="https://www.railstutorial.org/book/account_activation" target="_blank" rel="noopener">Releasing Chapter 11 for free</a> solves the first problem since the online version uses the latest SendGrid authentication technique (specifically, an <em>API key</em> in place of the previous username/password authentication).</p>
<div class="graphics image">
  <img src="images/figures/sendgrid_create_api_key.png" alt="images/figures/sendgrid_create_api_key" />
</div>
<p>You can consider <a href="https://www.railstutorial.org/book/account_activation" target="_blank" rel="noopener">Chapter 11</a> to be a general reference on sending email in production since even those not following the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a> can clone the <a href="https://github.com/mhartl/sample_app_6th_ed" target="_blank" rel="noopener">sample app</a> and check out the corresponding branch:</p>
<div class="code">
    <div class="highlight">
      <pre><span></span><span class="gp">$ </span>git clone https://github.com/mhartl/sample_app_6th_ed
<span class="gp">$ </span>git checkout account-activation
</pre>
    </div>
  </div>

<p class="noindent">(See the <a href="https://github.com/mhartl/sample_app_6th_ed#ruby-on-rails-tutorial-sample-application" target="_blank" rel="noopener">sample app README</a> for more information.)</p>
<p>In addition, if I end up having to swap out SendGrid for another email service, I can now simply update the freely available chapter with the new code.<span class="intersentencespace"></span> (I already have a working prototype using MailGun as an alternative.)<span class="intersentencespace"></span> Should that become necessary, I’ll plan to update this post with a note to that effect.</p>
<p>Please enjoy <a href="https://www.railstutorial.org/book/account_activation" target="_blank" rel="noopener">Chapter 11: Account activation</a> and especially <a href="https://www.railstutorial.org/book/account_activation#sec-activation_email_in_production" target="_blank" rel="noopener">Section 11.4: Email in production</a> as a free reference on sending email with Rails!
</p><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> This is an affiliate link.<span class="intersentencespace"></span> As an Amazon Associate, I earn from qualifying purchases.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[It’s Normal Not to Remember Everything!]]></title>
    <link href="https://news.learnenough.com/normal-not-to-remember-everything"/>
    <updated>2021-01-21T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/normal-not-to-remember-everything</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a> and the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener">Rails Tutorial</a>.<span class="intersentencespace"></span> <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough</a> is the best place to learn to code using the real tools used every day by software developers.</p>
<p>A Learn Enough reader recently reached out with this comment after finishing our first course, <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a>:</p>
<blockquote class="quotation"><p class="quote">I feel as though I didn’t retain absolutely everything.<span class="intersentencespace"></span> Is that normal?<span class="intersentencespace"></span></p>
</blockquote><p>This is an excellent question.<span class="intersentencespace"></span> The answer is: <em>Yes, absolutely!</em><span class="intersentencespace"></span> It’s completely normal not to retain everything the first time through.</p>
<p>But you need to remember things in order to use them, right?<span class="intersentencespace"></span> Yes, absolutely that as well.<span class="intersentencespace"></span> Luckily, Learn Enough has built-in methods for helping you do that.</p>
<div class="section-star" id="layers"><h2><a href="#layers" class="heading">Layers</a></h2>
<p>The secret to helping you retain the material in the Learn Enough tutorials is that they are made like a layer cake, with one layer built on top of the next (<a href="#fig-layer_cake" class="hyperref">Figure <span class="ref">1</span></a>).<sup id="cha-0_footnote-ref-1" class="footnote"><a href="#cha-0_footnote-1">1</a></sup></p>
<div class="center figure" id="fig-layer_cake" data-tralics-id="uid2" data-number="1">
<div class="graphics image"><img src="images/figures/layer_cake.jpg" alt="images/figures/layer_cake" /></div><div class="caption"><span class="header">Figure 1: </span><span class="description">The Learn Enough tutorials are like a layer cake.
</span></div></div>
<p>This means later Learn Enough tutorials reinforce the material in previous ones.<span class="intersentencespace"></span> For example, the second Learn Enough tutorial, <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a>, uses the <code>which</code> command, which tells you where a particular command-line program is located on your system:</p>
<div class="code"><div class="highlight"><pre><span></span><span class="gp">$ </span>which ls
<span class="go">/bin/ls</span>
</pre></div></div>
<p>Similarly, the third Learn Enough tutorial, <a href="https://www.learnenough.com/git" target="_blank" rel="noopener"><em>Learn Enough Git to Be Dangerous</em></a>, references the <code>mv</code> and <code>rm</code> commands, which are used at the command line to <strong>m</strong>o<strong>v</strong>e and <strong>r</strong>e<strong>m</strong>ove things, respectively.<span class="intersentencespace"></span> In both cases, the commands in question are covered in <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a>, so encountering them a second time later on reminds you what they do.</p>
<div id="sec-cross_references" data-tralics-id="uid3" class="subsection" data-number="1"><h3><a href="#sec-cross_references" class="heading"><span class="number">1 </span>Cross-references FTW</a></h3>
<p>But what if you already forgot about those commands the next time you encounter them?<span class="intersentencespace"></span> Couldn’t that be confusing?<span class="intersentencespace"></span> Indeed it could, which is why the Learn Enough tutorials also include lots of <em>cross-reference links</em> between tutorials.<span class="intersentencespace"></span> For example, the occurrences of <code>which</code>, <code>mv</code>, and <code>rm</code> mentioned above appear as in <a href="#fig-which_ref" class="hyperref">Figure <span class="ref">2</span></a> and <a href="#fig-mv_rm_ref" class="hyperref">Figure <span class="ref">3</span></a>.</p>
<div class="center figure" id="fig-which_ref" data-tralics-id="uid4" data-number="2">
<div class="graphics image"><img src="images/figures/which_ref.png" alt="images/figures/which_ref" /></div><div class="caption"><span class="header">Figure 2: </span><span class="description">A cross-reference link for the <code>which</code> command in <a href="https://www.learnenough.com/text-editor" target="_blank" rel="noopener"><em>Learn Enough Text Editor to Be Dangerous</em></a>.
</span></div></div>
<div class="center figure" id="fig-mv_rm_ref" data-tralics-id="uid5" data-number="3">
<div class="graphics image"><img src="images/figures/mv_rm_ref.png" alt="images/figures/mv_rm_ref" /></div><div class="caption"><span class="header">Figure 3: </span><span class="description">A cross-reference link for the <code>mv</code> and <code>rm</code> commands in <a href="https://www.learnenough.com/git" target="_blank" rel="noopener"><em>Learn Enough Git to Be Dangerous</em></a>.
</span></div></div>
<p>Those links go directly to the relevant sections of the corresponding tutorial (in this case, <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a>).<span class="intersentencespace"></span> This means that, if you’re following <a href="https://www.learnenough.com/git" target="_blank" rel="noopener"><em>Learn Enough Git to Be Dangerous</em></a> and need to read up on <code>mv</code> and <code>rm</code>, you’re only a click away (<a href="#fig-renaming_copying_deleting" class="hyperref">Figure <span class="ref">4</span></a>).<span class="intersentencespace"></span> This design lets you quickly refresh your memory with the relevant material in the previous tutorial and then hit the “Back” button to continue where you left off.</p>
<div class="center figure" id="fig-renaming_copying_deleting" data-tralics-id="uid6" data-number="4">
<div class="graphics image"><img src="images/figures/renaming_copying_deleting.png" alt="images/figures/renaming_copying_deleting" /></div><div class="caption"><span class="header">Figure 4: </span><span class="description">The <a href="https://www.learnenough.com/command-line-tutorial/manipulating_files#sec-renaming_copying_deleting" target="_blank" rel="noopener">section</a> in <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a> that covers <code>mv</code> and <code>rm</code>.
</span></div></div>
<p>By the way, the same cross-reference technique is also used <em>within</em> the Learn Enough tutorials.<span class="intersentencespace"></span> This can be especially helpful in longer tutorials like the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>.<span class="intersentencespace"></span> For example, an <a href="https://www.railstutorial.org/book/static_pages#aside-undoing_things" target="_blank" rel="noopener">aside box</a> in <a href="https://www.railstutorial.org/book/static_pages" target="_blank" rel="noopener">Chapter 3</a> of the <em>Rails Tutorial</em> discusses a particular aspect of the <code>rails generate</code> command, which is covered in a couple of sections in <a href="https://www.railstutorial.org/book/toy_app" target="_blank" rel="noopener">Chapter 3</a>.<span class="intersentencespace"></span> Accordingly, there are cross-reference links to those exact sections, as shown in <a href="#fig-rails_generate_ref" class="hyperref">Figure <span class="ref">5</span></a>.</p>
<div class="center figure" id="fig-rails_generate_ref" data-tralics-id="uid7" data-number="5">
<div class="graphics image"><img src="images/figures/rails_generate_ref.png" alt="images/figures/rails_generate_ref" /></div><div class="caption"><span class="header">Figure 5: </span><span class="description">Internal cross-references in the <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>.
</span></div></div>
</div></div>
<div class="section-star" id="how_to_get_started_with_learn_enough"><h2><a href="#how_to_get_started_with_learn_enough" class="heading">How to get started with Learn Enough</a></h2>
<p>One common question about Learn Enough is: Where should I start?</p>
<p>One possibility is to start with the first tutorial, <a href="https://www.learnenough.com/command-line" target="_blank" rel="noopener"><em>Learn Enough Command Line to Be Dangerous</em></a>.<span class="intersentencespace"></span> Even if you already know something about the Unix command line, you can still skim the material, skip ahead if you already know it, and then do the exercises as a refresher.</p>
<p>The discussion above suggests another possibility, though: just start with whichever tutorial looks most interesting or best seems to match your experience level.<span class="intersentencespace"></span> Whenever you run into something you don’t know, follow the cross-reference to learn about that particular subject, and maybe even decide to follow the earlier tutorial in its entirety.<span class="intersentencespace"></span> You can go then back and finish the one you first started.<span class="intersentencespace"></span> The Learn Enough course progress tracking (<a href="#fig-progress_tracking" class="hyperref">Figure <span class="ref">6</span></a>) makes it especially easy to pick up where you left off.</p>
<div class="center figure" id="fig-progress_tracking" data-tralics-id="uid8" data-number="6">
<div class="graphics image"><img src="images/figures/progress_tracking.png" alt="images/figures/progress_tracking" /></div><div class="caption"><span class="header">Figure 6: </span><span class="description">The progress tracking interface for the <a href="https://www.learnenough.com/" target="_blank" rel="noopener">Learn Enough courses</a>.
</span></div></div>
<p>No matter which way you go, you can proceed secure in the knowledge that the Learn Enough tutorials always have your back when it comes to retaining the most important material.<span class="intersentencespace"></span> Get started today with a <a href="https://www.learnenough.com/" target="_blank" rel="noopener">7-day free trial</a> to the <a href="https://www.learnenough.com/all-access" target="_blank" rel="noopener">Learn Enough All Access subscription</a>, which includes all 9 Learn Enough courses (including the full <a href="https://www.railstutorial.org/" target="_blank" rel="noopener"><em>Ruby on Rails Tutorial</em></a>)!<span class="intersentencespace"></span> <em>Note</em>: All Learn Enough products and courses come with a 60-day money-back guarantee.
</p></div><div id="cha-0_footnotes">
  <div class="footnotes">
    <div id="cha-0_footnote-1" class="footnote"><a class="footnote-link" href="#cha-0_footnote-ref-1">1.</a> The <a href="https://pixabay.com/photos/food-cake-portion-dessert-4869871/" target="_blank" rel="noopener">layer-cake image</a> shown in <a href="#fig-layer_cake" class="hyperref">Figure <span class="ref">1</span></a> doesn’t require attribution, but I like to give credit anyway.</div>
  </div>
</div>
       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Free Learn Enough Action Cable Videos on YouTube]]></title>
    <link href="https://news.learnenough.com/free-learn-enough-action-cable-videos-on-youtube"/>
    <updated>2020-12-04T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/free-learn-enough-action-cable-videos-on-youtube</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/">Rails Tutorial</a> and <a href="https://www.learnenough.com/">Learn Enough</a>. I’ve just published some free YouTube videos from <a href="https://www.learnenough.com/action-cable"><em>Learn Enough Action Cable to Be Dangerous</em></a>:
</p>
<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d2uMPyzO4bVoeR15rsJc32S">Chapter 1: WebSockets and Action Cable</a></li>
</ul>
<p>The videos are useful by themselves and also give you a good idea of what’s included in the <a href="https://www.learnenough.com/all-access">Learn Enough All Access subscription</a>.
</p>
<p><em>Learn Enough Action Cable</em> is the last tutorial in the main Learn Enough tutorial sequence, so you can now find links to <em>all</em> the individual tutorial posts on the <a href="https://www.learnenough.com/courses">Learn Enough Courses page</a> using the “Read full post” links.
</p>
<p>The All Access subscription, available now with a <a href="https://www.learnenough.com/all-access">7-day free trial</a>, includes online books and streaming videos for all the main Learn Enough tutorials. Enjoy!
</p>

       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Free Ruby on Rails Tutorial Videos on YouTube]]></title>
    <link href="https://news.learnenough.com/free-ruby-on-rails-tutorial-videos-on-youtube"/>
    <updated>2020-11-18T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/free-ruby-on-rails-tutorial-videos-on-youtube</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/">Rails Tutorial</a> and <a href="https://www.learnenough.com/">Learn Enough</a>. I’ve just published some free YouTube videos from the <a href="https://www.railstutorial.org/"><em>Ruby on Rails Tutorial</em></a>:
</p>
<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d1H4dlRSGSC8Sv83BMYPuAD">Chapter 1: From zero to deploy</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d1M9Hl_KrLQkKqvYpqJc_ZM">Chapter 2: A toy app</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d3_eBXgvadgJwybu0SoHZUn">Chapter 3: Mostly static pages</a></li>
</ul>
<p>The videos are useful by themselves and also give you a good idea of what’s included in the <a href="https://www.learnenough.com/all-access">Learn Enough All Access subscription</a>.
</p>
<p>The All Access subscription, available now with a <a href="https://www.learnenough.com/all-access">7-day free trial</a>, includes online books and streaming videos for <em>all</em> the Learn Enough tutorial courses.
</p>
<p>Enjoy!</p>

       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Free Learn Enough Ruby Videos on YouTube]]></title>
    <link href="https://news.learnenough.com/free-learn-enough-ruby-videos-on-youtube"/>
    <updated>2020-11-10T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/free-learn-enough-ruby-videos-on-youtube</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/">Rails Tutorial</a> and <a href="https://www.learnenough.com/">Learn Enough</a>. I’ve just published some free YouTube videos from <a href="https://www.learnenough.com/ruby"><em>Learn Enough Ruby to Be Dangerous</em></a>:
</p>
<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d3a2Iyn02ej9wrStd8ApMxd">Chapter 1: Hello, world!</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d1g2oeiB1nCD9AZ7DNV6Ae8">Chapter 2: Strings</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d1Nq4nM9idoCZvqe6MwqKGV">Chapter 3: Arrays</a></li>
</ul>
<p>The videos are useful by themselves and also give you a good idea of what’s included in the <a href="https://www.learnenough.com/all-access">Learn Enough All Access subscription</a>.
</p>
<p>The All Access subscription, available now with a <a href="https://www.learnenough.com/all-access">7-day free trial</a>, includes online books and streaming videos for all the main \href{https://www.learnenough.com/courses}{Learn Enough tutorials}.
</p>
<p>Enjoy!
</p>


       
       ]]>
    </content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Free Learn Enough JavaScript Videos on YouTube]]></title>
    <link href="https://news.learnenough.com/free-learn-enough-javascript-videos-on-youtube"/>
    <updated>2020-10-29T00:00:00+00:00</updated>
    <id>https://news.learnenough.com/free-learn-enough-javascript-videos-on-youtube</id>
    <content type="html">
      <![CDATA[
      <p>Michael Hartl here from the <a href="https://www.railstutorial.org/">Rails Tutorial</a> and <a href="https://www.learnenough.com/">Learn Enough</a>. I’ve just published some free YouTube videos from <a href="https://www.learnenough.com/javascript"><em>Learn Enough JavaScript to Be Dangerous</em></a>:
</p>
<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d3WK-Yi3Trs_aDk_8hP6t3i">Chapter 1: Hello, world!</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d1ZmXvmPJkiLqZ2DMdHYr30">Chapter 2: Strings</a></li>
  <li><a href="https://www.youtube.com/playlist?list=PLdo16AWp70d2hg46_nORQhjIyvHQtW72j">Chapter 3: Arrays</a></li>
</ul>
<p>The videos are useful by themselves and also give you a good idea of what’s included in the <a href="https://www.learnenough.com/all-access">Learn Enough All Access subscription</a>.
</p>
<p>The <a href="https://www.learnenough.com/all-access">All Access subscription</a>, available now with a 7-day free trial, includes online books and streaming videos for all the main \href{https://www.learnenough.com/courses}{Learn Enough tutorials}.<br />
</p>
<p>Enjoy!
</p>

       
       ]]>
    </content>
  </entry>
  
</feed>
