<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Web-Development | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/web-development/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:52 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/web-development/index.xml" rel="self" type="application/rss+xml"/><item><title>Frontend Without a Framework | Behind the Scenes of The M.Akita Chronicles</title><link>https://akitaonrails.github.io/en/2026/02/19/frontend-without-a-framework-behind-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/02/19/frontend-without-a-framework-behind-the-m-akita-chronicles/</guid><pubDate>Thu, 19 Feb 2026 01:08:23 +0000</pubDate><description>&lt;p&gt;This post is part of a series; follow along with the tag &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. This is part 4.&lt;/p&gt;
&lt;p&gt;And be sure to subscribe to my new newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s an obsession in the market that frontend = React. Or Vue. Or Svelte. Or the framework of the week. What if I told you it&amp;rsquo;s possible to have a modern, responsive frontend with dark mode, email-compatible, multi-platform — without a single line of JavaScript framework?&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is part of a series; follow along with the tag <a href="/en/tags/the-m-akita-chronicles/">/themakitachronicles</a>. This is part 4.</p>
<p>And be sure to subscribe to my new newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>There&rsquo;s an obsession in the market that frontend = React. Or Vue. Or Svelte. Or the framework of the week. What if I told you it&rsquo;s possible to have a modern, responsive frontend with dark mode, email-compatible, multi-platform — without a single line of JavaScript framework?</p>
<p>I&rsquo;m not talking about static sites from the 2000s. I&rsquo;m talking about Tailwind CSS v4 on Rails, Hugo with the Hextra theme for the blog, and email templates that work from Gmail to Outlook 2016. All in the same project, with one developer.</p>
<h2>Tailwind CSS v4: The End of Handcrafted CSS<span class="hx:absolute hx:-mt-20" id="tailwind-css-v4-the-end-of-handcrafted-css"></span>
    <a href="#tailwind-css-v4-the-end-of-handcrafted-css" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I have a confession: CSS has always been my kryptonite. Not because it&rsquo;s hard — but because it&rsquo;s <strong>unpredictable</strong>. You change a <code>margin-top</code> and break the layout of a page you never touched. Cascade, specificity, inheritance — it&rsquo;s a minefield.</p>
<p>Tailwind solves this in the most pragmatic way possible: utility classes right in the HTML.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;flex items-center gap-4 p-6 bg-white dark:bg-gray-800
</span></span></span><span class="line"><span class="cl"><span class="s">            rounded-xl shadow-sm border border-gray-200 dark:border-gray-700&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;w-12 h-12 rounded-full&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/images/avatar.jpg&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">h3</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;font-semibold text-gray-900 dark:text-gray-100&#34;</span><span class="p">&gt;</span>Título<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;text-sm text-gray-500 dark:text-gray-400&#34;</span><span class="p">&gt;</span>Descrição<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>&ldquo;But this is just inline style with extra steps!&rdquo; — No, it isn&rsquo;t. It&rsquo;s a design system with constraints. You don&rsquo;t pick &ldquo;margin: 17px&rdquo; — you pick <code>m-4</code> (16px) or <code>m-5</code> (20px). That constraint forces visual consistency without needing a design system documented across 40 pages.</p>
<p>And Tailwind v4, which came to Rails via <code>tailwindcss-rails</code>, has a fundamental change: <strong>CSS-first configuration</strong>. No more <code>tailwind.config.js</code>. Everything is CSS:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="s2">&#34;tailwindcss&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">theme</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">--color-brand</span><span class="o">:</span> <span class="p">#</span><span class="nn">8b5cf6</span><span class="o">;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">--font-sans</span><span class="o">:</span> <span class="s2">&#34;Inter&#34;</span><span class="o">,</span> <span class="nt">system-ui</span><span class="o">,</span> <span class="nt">sans-serif</span><span class="o">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>One less config file. One less build step. Simpler.</p>
<h2>Dark Mode: Respect Your User<span class="hx:absolute hx:-mt-20" id="dark-mode-respect-your-user"></span>
    <a href="#dark-mode-respect-your-user" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-05-42.jpg" alt="change theme"  loading="lazy" /></p>
<p>Something that deeply annoys me is a site that ignores the operating system&rsquo;s theme preference. The user set dark mode on the OS — respect that.</p>
<p>With Tailwind, dark mode is trivial:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100&#34;</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>dark:</code> prefix applies the style when the operating system is in dark mode (via <code>prefers-color-scheme: dark</code>). No JavaScript needed. No toggle needed. No cookie needed. It works.</p>
<p>But &ldquo;it works&rdquo; is the bare minimum. The real challenge is maintaining <strong>consistency</strong> across dozens of components. Every color you use needs a <code>dark:</code> variant. Every background, every text, every border.</p>
<p>The trick: create conventions and follow them. Main background? <code>bg-white dark:bg-gray-900</code>. Primary text? <code>text-gray-900 dark:text-gray-100</code>. Secondary text? <code>text-gray-500 dark:text-gray-400</code>. Borders? <code>border-gray-200 dark:border-gray-700</code>. With 4-5 pairs defined, 90% of components are covered.</p>
<h2>Hugo + Hextra: Blog Without a Database<span class="hx:absolute hx:-mt-20" id="hugo--hextra-blog-without-a-database"></span>
    <a href="#hugo--hextra-blog-without-a-database" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The project&rsquo;s blog runs Hugo with the Hextra theme. Hugo is a static site generator written in Go — absurdly fast. A full rebuild in &lt;500ms even with dozens of pages.</p>
<p>What makes Hextra special is that it&rsquo;s a <strong>documentation</strong> theme adapted for blogs. Navigation sidebar, integrated search, table of contents — everything ready. And customizable via partial layouts:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>layouts/
├── _partials/
│   ├── custom/
│   │   └── head-end.html          # Injects analytics, fonts, custom CSS
│   └── section-box.html           # Shared partial (DRY, see below)
├── blog/
│   └── home.html                  # Custom homepage
├── newsletters/
│   └── single.html                # Individual newsletter template
├── podcast-transcripts/
│   └── list.html                  # Episode listing
└── shortcodes/                    # 17 shortcodes — components without JS
    ├── podcast-turn.html          # Speech bubble in the transcript
    ├── akita-comment.html         # Akita&#39;s comment
    ├── marvin-comment.html        # Marvin&#39;s comment
    ├── history-timeline.html      # Historical events timeline
    ├── market-ticker.html         # Financial tickers table
    ├── market-map.html            # FinViz S&amp;P 500 map
    ├── subscribe-cta.html         # Subscription call-to-action
    ├── score.html                 # Relevance badge (high/medium/low)
    ├── holidays.html              # Holidays and commemorative dates
    ├── ref-stories.html           # Reference links
    ├── book-downloads.html        # Book download links
    ├── hacker-news.html           # ┐
    ├── anime-ranking.html         # │ Section boxes — delegate to
    ├── youtube-digest.html        # │ section-box.html (see below)
    ├── world-events.html          # │
    ├── market-news.html           # │
    └── qa.html                    # ┘</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each content type has its own template. Newsletters have a navigation sidebar and visual sections. Podcast transcripts have an embedded player and dialogue formatting. The same Markdown content is rendered differently depending on the <code>type</code> in the frontmatter.</p>
<h2>Shortcodes: Components Without JavaScript<span class="hx:absolute hx:-mt-20" id="shortcodes-components-without-javascript"></span>
    <a href="#shortcodes-components-without-javascript" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Hugo shortcodes are the equivalent of React components, but without a build step:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- layouts/shortcodes/podcast-turn.html --&gt;</span>
</span></span><span class="line"><span class="cl">{{ $speaker := .Get &#34;speaker&#34; }}
</span></span><span class="line"><span class="cl">{{ $isAkita := eq $speaker &#34;akita&#34; }}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;podcast-turn&#34;</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;display: flex; {{ if $isAkita }}flex-direction: row{{ else }}flex-direction: row-reverse{{ end }};&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/images/{{ $speaker }}.jpg&#34;</span>
</span></span><span class="line"><span class="cl">       <span class="na">style</span><span class="o">=</span><span class="s">&#34;width: 40px; height: 40px; border-radius: 50%;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;bubble {{ if $isAkita }}akita-bubble{{ else }}marvin-bubble{{ end }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    {{ .Inner | markdownify }}
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Markdown, the author writes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{{&lt; podcast-turn speaker=&#34;akita&#34; &gt;}}
Então, vamos falar sobre o que aconteceu essa semana no mundo tech...
{{&lt; /podcast-turn &gt;}}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-06-23.jpg" alt="baloon"  loading="lazy" /></p>
<p>And Hugo renders a styled chat bubble, with avatar, correct alignment, and processed markdown. Zero JavaScript. Zero framework. HTML + CSS + template logic.</p>
<h3>DRY With a Shared Partial<span class="hx:absolute hx:-mt-20" id="dry-with-a-shared-partial"></span>
    <a href="#dry-with-a-shared-partial" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With 17 shortcodes, duplication becomes a problem fast. Seven of them — the &ldquo;section boxes&rdquo; that wrap newsletter sections (Hacker News, Anime, YouTube, etc.) — have the same visual structure: icon, title, colored border, Markdown content. Only the color and label change.</p>
<p>The solution is a shared partial that takes parameters:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- layouts/partials/section-box.html --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;section-box {{ .class }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;section-box-header&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>{{ .icon }}<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> {{ .label }}
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;section-box-content&#34;</span><span class="p">&gt;</span>{{ .content | markdownify }}<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And each shortcode becomes a one-liner:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- layouts/shortcodes/hacker-news.html --&gt;</span>
</span></span><span class="line"><span class="cl">{{ partial &#34;section-box&#34; (dict &#34;class&#34; &#34;hacker-news&#34; &#34;icon&#34; &#34;<span class="ni">&amp;#128274;</span>&#34; &#34;label&#34; &#34;The Hacker News&#34; &#34;content&#34; .Inner) }}</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-07-02.jpg" alt="hacker news"  loading="lazy" /></p>
<p>It&rsquo;s the Hugo equivalent of component inheritance: a polymorphic partial parameterized by <code>dict</code>. The CSS in <code>custom.css</code> defines colors per class (<code>.hacker-news</code>, <code>.anime-ranking</code>, etc.) with light/dark variants. Result: 7 shortcodes that used to have duplicated HTML are now 7 lines + 1 partial + CSS.</p>
<p>But there&rsquo;s a crucial catch: <strong>shortcodes are exclusive to Hugo</strong>. The same content that goes to the blog also goes out by email. And email doesn&rsquo;t understand shortcodes.</p>
<h2>The Challenge: Same Content, Two Renders<span class="hx:absolute hx:-mt-20" id="the-challenge-same-content-two-renders"></span>
    <a href="#the-challenge-same-content-two-renders" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the central frontend problem in this kind of project: the same Markdown file needs to work in two completely different contexts.</p>
<p><strong>On the blog (Hugo):</strong></p>
<ul>
<li>Shortcodes work (<code>\{\{&lt; subscribe-cta &gt;\}\}</code>)</li>
<li>CSS with classes (Tailwind, Hextra)</li>
<li>Dark mode via <code>prefers-color-scheme</code></li>
<li>Responsive images</li>
<li>Flexible layout</li>
</ul>
<p><strong>In email (Rails → Amazon SES):</strong></p>
<ul>
<li>Shortcodes are dead text — they have to be removed</li>
<li>CSS must be <strong>inline</strong> (many email clients ignore <code>&lt;style&gt;</code>)</li>
<li>Dark mode? Forget it for 80% of clients</li>
<li>Images with absolute URLs and fixed size</li>
<li>Layout with <code>&lt;table&gt;</code> (yes, in 2026)</li>
</ul>
<p>The solution: two independent rendering pipelines. The Markdown content is the source of truth. Hugo transforms it for the web. Rails transforms it for email. Each one with its own rules.</p>
<p>On the Rails side, the <code>MarkdownToHtml</code> service needs to:</p>
<ol>
<li>Process standard Markdown</li>
<li>Convert <code>\{\{&lt; shortcode &gt;\}\}</code> into equivalent HTML or strip them — via a <code>SHORTCODE_PATTERNS</code> hash that maps each shortcode to a regex + inline template with CSS</li>
<li>Apply the theme (light/dark based on the subscriber&rsquo;s preference)</li>
<li>Inject inline CSS for email client compatibility</li>
<li>Rewrite image URLs to absolute</li>
</ol>
<p>It&rsquo;s more work than it sounds. Each shortcode needs an email-safe equivalent. Comment avatars need absolute URLs. Sections with colored backgrounds need <code>&lt;td style=&quot;background-color:...&quot;&gt;</code> instead of CSS classes.</p>
<h2>Email HTML: The Worst Frontend in the World<span class="hx:absolute hx:-mt-20" id="email-html-the-worst-frontend-in-the-world"></span>
    <a href="#email-html-the-worst-frontend-in-the-world" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you think cross-browser compatibility is hard, try cross-email-client compatibility. Outlook 2016 renders HTML using the <strong>Microsoft Word engine</strong>. Yes, Word. That&rsquo;s not a joke.</p>
<p>The survival rules:</p>
<ol>
<li><strong>Layout with <code>&lt;table&gt;</code></strong>. <code>display: flex</code>? <code>grid</code>? They don&rsquo;t exist in Outlook.</li>
<li><strong>Inline CSS on every element</strong>. <code>&lt;style&gt;</code> tags are ignored by several clients.</li>
<li><strong>No <code>margin</code> on images</strong>. Use <code>padding</code> on the parent <code>&lt;td&gt;</code>.</li>
<li><strong>Web fonts don&rsquo;t work</strong>. Use a font stack: <code>font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;</code></li>
<li><strong>Themes are a subscriber preference</strong>, not automatic detection — many email clients don&rsquo;t reliably support <code>prefers-color-scheme</code>.</li>
</ol>
<p>The project&rsquo;s email template is an ERB that takes theme variables:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&lt;td style=&#34;padding: 8px 28px; color: &lt;%= @theme[:text] %&gt;;
           background: &lt;%= @theme[:card_bg] %&gt;;&#34;&gt;
  &lt;%= raw @content %&gt;
&lt;/td&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The theme defines all the colors:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">THEMES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">light</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;#1a1a1a&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">card_bg</span><span class="p">:</span> <span class="s2">&#34;#ffffff&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">link_color</span><span class="p">:</span> <span class="s2">&#34;#7c3aed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="ss">dark</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="ss">text</span><span class="p">:</span> <span class="s2">&#34;#e6edf3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">card_bg</span><span class="p">:</span> <span class="s2">&#34;#161b22&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">link_color</span><span class="p">:</span> <span class="s2">&#34;#a78bfa&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each subscriber picks their theme at signup time. The email is rendered with the correct colors for that person. No media queries, no automatic detection — <strong>because email clients don&rsquo;t support it reliably</strong>. It&rsquo;s simpler to give the user the choice.</p>
<h2>Color Palette: HSL and the Science Behind the Choices<span class="hx:absolute hx:-mt-20" id="color-palette-hsl-and-the-science-behind-the-choices"></span>
    <a href="#color-palette-hsl-and-the-science-behind-the-choices" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>&ldquo;But how did you pick these colors?&rdquo; — Not by eyeballing it. There&rsquo;s a science behind it, and it&rsquo;s called <strong>HSL</strong> (Hue, Saturation, Lightness).</p>
<p>Most developers think about color as hex (<code>#e8eef8</code>) or RGB (<code>rgb(232, 238, 248)</code>). Those are machine formats — useful for rendering, useless for <strong>designing</strong>. When you look at <code>#3b50a2</code> and <code>#c93030</code> side by side, there&rsquo;s no way to intuitively know if they&rsquo;ll work together. They&rsquo;re just numbers.</p>
<p>HSL solves this. Instead of mixing light channels (Red, Green, Blue), you describe color in human terms:</p>
<ul>
<li><strong>Hue</strong>: the position on the color wheel, from 0° to 360°. Red = 0°, green = 120°, blue = 240°.</li>
<li><strong>Saturation</strong>: how &ldquo;vivid&rdquo; the color is. 0% = gray, 100% = pure color.</li>
<li><strong>Lightness</strong>: how light or dark. 0% = black, 100% = white.</li>
</ul>
<p>With HSL, you control each dimension independently. And that&rsquo;s where building the palette comes in.</p>
<h3>Step 1: Distribute Hues Around the Color Wheel<span class="hx:absolute hx:-mt-20" id="step-1-distribute-hues-around-the-color-wheel"></span>
    <a href="#step-1-distribute-hues-around-the-color-wheel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-09-47.jpg" alt="color wheel"  loading="lazy" /></p>
<p>The newsletter has 8 section types, each with its own color. For all of them to &ldquo;belong&rdquo; visually to the same project, the hues are distributed evenly around the 360° wheel:</p>
<table>
  <thead>
      <tr>
          <th>Section</th>
          <th>Hue</th>
          <th>Degrees</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>YouTube</td>
          <td>Red</td>
          <td>0°</td>
      </tr>
      <tr>
          <td>History</td>
          <td>Sepia</td>
          <td>35°</td>
      </tr>
      <tr>
          <td>World</td>
          <td>Amber</td>
          <td>45°</td>
      </tr>
      <tr>
          <td>Book/Downloads</td>
          <td>Green</td>
          <td>140°</td>
      </tr>
      <tr>
          <td>Holidays</td>
          <td>Teal</td>
          <td>180°</td>
      </tr>
      <tr>
          <td>Anime/Q&amp;A</td>
          <td>Blue</td>
          <td>210°</td>
      </tr>
      <tr>
          <td>Market News</td>
          <td>Royal</td>
          <td>225°</td>
      </tr>
      <tr>
          <td>Hacker News</td>
          <td>Purple</td>
          <td>270°</td>
      </tr>
  </tbody>
</table>
<p>Eight hues spaced around the wheel. That&rsquo;s not random — it&rsquo;s the same logic as a <strong>polychromatic palette</strong> in color theory. When hues are equidistant, the human eye perceives harmony even with completely different colors. It&rsquo;s the same principle artists have been using with a 12-color wheel since the 18th century.</p>
<h3>Step 2: Lock Saturation and Lightness Per Layer<span class="hx:absolute hx:-mt-20" id="step-2-lock-saturation-and-lightness-per-layer"></span>
    <a href="#step-2-lock-saturation-and-lightness-per-layer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/HSV_color_solid_cylinder_saturation_gray.jpg" alt="saturation luminosity"  loading="lazy" /></p>
<p>Here&rsquo;s the trick that makes it all work together. Each visual &ldquo;layer&rdquo; (background, text, border) has <strong>fixed</strong> saturation and lightness — only the hue changes:</p>
<p><strong>Light theme:</strong></p>
<ul>
<li><strong>Backgrounds</strong>: ~92-95% lightness, moderate saturation. With lightness that high, even different saturations result in pastel colors with similar visual weight — no section &ldquo;screams&rdquo; louder than another.</li>
<li><strong>Text</strong>: ~15-20% lightness. Dark enough to guarantee <strong>&gt;7:1 contrast ratio</strong> against the light background (above the WCAG AAA minimum of 7:1).</li>
<li><strong>Borders</strong>: ~45-55% lightness. The chromatic accent — visible but not dominant.</li>
</ul>
<p><strong>Dark theme:</strong></p>
<ul>
<li><strong>Backgrounds</strong>: ~10-12% lightness. Uniform depth — every section has the same &ldquo;darkness&rdquo;.</li>
<li><strong>Text</strong>: ~80-85% lightness. Light enough for <strong>&gt;6:1 contrast ratio</strong> on the dark background.</li>
<li><strong>Borders</strong>: same lightness range as in the light theme — they work as accents in both contexts.</li>
</ul>
<p>In practice, this means generating the dark variant of any color is mechanical: keep the hue, invert the lightness. A background that was 94% becomes 11%. Text that was 18% becomes 83%. That&rsquo;s not design — it&rsquo;s arithmetic.</p>
<h3>Step 3: Check Contrast (WCAG)<span class="hx:absolute hx:-mt-20" id="step-3-check-contrast-wcag"></span>
    <a href="#step-3-check-contrast-wcag" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://accessibilityassistant.com/blog/accessibility-insights/wcag-2-colour-contrast-accessibility-guidelines/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/Why-Colour-Contrast-Accessibility-Matters-More-Than-Ever.jpg" alt="Why Colour Contrast Accessibility Matters More Than Ever"  loading="lazy" /></a></p>
<p>Pretty colors nobody can read are worthless. The WCAG standard (Web Content Accessibility Guidelines) defines minimum ratios:</p>
<ul>
<li><strong>AA</strong>: 4.5:1 for normal text, 3:1 for large text</li>
<li><strong>AAA</strong>: 7:1 for normal text, 4.5:1 for large text</li>
</ul>
<p>The light backgrounds at ~95% lightness against text at ~20% lightness give a ratio &gt;7:1 — WCAG AAA. The dark backgrounds (~11%) against light text (~82%) give &gt;6:1 — comfortably WCAG AA, near AAA.</p>
<p>This isn&rsquo;t altruism. Low-contrast email is harder to read, generates less engagement, and in extreme cases is flagged by email clients&rsquo; accessibility filters.</p>
<h3>The Result<span class="hx:absolute hx:-mt-20" id="the-result"></span>
    <a href="#the-result" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-07-50.jpg" alt="example colors"  loading="lazy" /></p>
<p>When you open the newsletter and see the Hacker News section (purple), followed by YouTube (red), followed by Anime (blue) — they look like &ldquo;the same project&rdquo; despite being completely different colors. That&rsquo;s HSL at work: different hues, same saturation, same lightness. The eye perceives coherence without knowing how to explain why.</p>
<p>And the best part: adding a new section is trivial. Pick a hue that isn&rsquo;t taken on the wheel, apply the same saturation/lightness rules per layer, and the new section automatically &ldquo;belongs&rdquo; to the existing design.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># The comment in the code sums up the rules:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Hues evenly spaced: blue 210, green 140, sepia 35, amber 45, royal 225, purple 270, red 0, teal 180</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Light backgrounds: ~95% lightness, ~25% saturation</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Light text: ~20% lightness (&gt;7:1 contrast ratio on light bg)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Dark backgrounds: ~10-12% lightness</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Dark text: ~80-85% lightness (&gt;6:1 contrast ratio on dark bg)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># - Borders: ~45-55% lightness</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Six rules. Eight hues. Two themes. 100% deterministic. Zero subjective &ldquo;I think this blue looks pretty&rdquo; decisions — it&rsquo;s a design system derived from geometric properties of the HSL color space.</p>
<h2>Well-Formed HTML: The Spam Filter Nobody Tells You About<span class="hx:absolute hx:-mt-20" id="well-formed-html-the-spam-filter-nobody-tells-you-about"></span>
    <a href="#well-formed-html-the-spam-filter-nobody-tells-you-about" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Everything I described above — tables, inline CSS, font stacks — solves the <strong>rendering</strong> problem. But there&rsquo;s a second reason to follow these rules that most developers ignore: <strong>email providers analyze the quality of your HTML to decide if it&rsquo;s spam</strong>.</p>
<p>Gmail, Outlook, Yahoo — all of them run your email&rsquo;s HTML through heuristics before showing it to the user. Malformed HTML is one of the classic spam signals. It makes sense: spammers don&rsquo;t test templates. They spit out broken HTML generated by cheap mass-mailing tools. If your email looks like spammer output, it gets treated as spam.</p>
<p>The signals that trip filters:</p>
<ol>
<li><strong>Unclosed or improperly nested tags</strong>. A <code>&lt;div&gt;</code> that opens and never closes, or a <code>&lt;/td&gt;</code> before the <code>&lt;/tr&gt;</code>, is an immediate flag. Providers expect HTML that parses without errors — not HTML &ldquo;that the browser fixes up&rdquo;.</li>
<li><strong>Unbalanced image-to-text ratio</strong>. An email that&rsquo;s basically a giant image with little text? Classic spam. Providers want to see real text — not text inside images. The rule of thumb is to keep at least 60% text, 40% images.</li>
<li><strong>Missing <code>alt</code> attribute on images</strong>. Beyond accessibility, an empty or missing <code>alt</code> is a sign of carelessness — and carelessness correlates with spam. Every <code>&lt;img&gt;</code> needs a descriptive <code>alt</code>.</li>
<li><strong>CSS that hides content</strong>. <code>display: none</code>, <code>visibility: hidden</code>, <code>font-size: 0</code>, text the same color as the background — those are classic keyword stuffing techniques. Even if you use them legitimately for responsive email, the filter doesn&rsquo;t know the difference.</li>
<li><strong>Excessively heavy HTML</strong>. An email with 500KB of HTML raises suspicion. Keep it light — the actual content rarely needs more than 100KB of HTML.</li>
<li><strong>Links with text that doesn&rsquo;t match the href</strong>. <code>&lt;a href=&quot;https://site-real.com&quot;&gt;https://site-falso.com&lt;/a&gt;</code> is textbook phishing. Even subtle discrepancies (domain in the text different from the domain in the link) are penalized.</li>
</ol>
<p>The point that ties it all together: <strong>the same practices that make email render correctly in Outlook are the ones that keep your emails out of spam</strong>. Tables with clean structure? Well-formed HTML. Inline CSS on every element instead of hacks with <code>&lt;style&gt;</code>? No hidden CSS. Images with explicit dimensions and <code>alt</code> text? Legitimate content.</p>
<p>It&rsquo;s no coincidence. Email providers calibrated their filters by watching what spammers do — and spammers do exactly the opposite of well-built HTML email. When you follow best practices for compatibility, <strong>for free</strong> you&rsquo;re also signaling legitimacy to spam filters.</p>
<p>The practical lesson: treat your email&rsquo;s HTML as if it were an XML document. Validate, test, and make sure every tag opens and closes in the right order. Use tools like <a href="https://www.emailonacid.com/"target="_blank" rel="noopener">Email on Acid</a> or <a href="https://www.litmus.com/"target="_blank" rel="noopener">Litmus</a> to test in multiple clients — they also report spam scoring issues. And resist the temptation to copy ready-made templates from the internet without validating them — many are a disaster of malformed HTML that slips past Gmail but gives Outlook a heart attack and gets Yahoo&rsquo;s spam filter to flag you as suspicious.</p>
<h2>RSS Feeds: The Forgotten Frontend<span class="hx:absolute hx:-mt-20" id="rss-feeds-the-forgotten-frontend"></span>
    <a href="#rss-feeds-the-forgotten-frontend" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_01-12-48.jpg" alt="RSS"  loading="lazy" /></p>
<p>A detail web developers ignore: RSS is a frontend. Podcasts on Spotify and Apple Podcasts are consumed via RSS. The feed needs:</p>
<ul>
<li>Valid XML (podcast parsers are strict)</li>
<li>Complete metadata (title, description, duration, file size)</li>
<li>Image URLs that <strong>actually work</strong> (Spotify validates them)</li>
<li>Cover art with minimum dimensions (1400×1400 for Spotify)</li>
<li>Unique GUIDs per episode</li>
</ul>
<p>Hugo generates RSS automatically, but the default template doesn&rsquo;t have the fields podcast platforms require. A custom template with iTunes tags is necessary:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;itunes:duration&gt;</span>{{ .Params.duration_seconds }}<span class="nt">&lt;/itunes:duration&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;itunes:image</span> <span class="na">href=</span><span class="s">&#34;{{ .Params.cover_image | absURL }}&#34;</span><span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;enclosure</span> <span class="na">url=</span><span class="s">&#34;{{ .Params.audio_url }}&#34;</span>
</span></span><span class="line"><span class="cl">           <span class="na">length=</span><span class="s">&#34;{{ .Params.file_size }}&#34;</span>
</span></span><span class="line"><span class="cl">           <span class="na">type=</span><span class="s">&#34;audio/mpeg&#34;</span><span class="nt">/&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>The Philosophy: Content First, Framework Never<span class="hx:absolute hx:-mt-20" id="the-philosophy-content-first-framework-never"></span>
    <a href="#the-philosophy-content-first-framework-never" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The pattern that emerges from this whole setup is: <strong>content is sovereign</strong>. Markdown with YAML frontmatter is the universal format. Each frontend (blog, email, RSS, podcast player) consumes the same content and renders it its own way.</p>
<p>There&rsquo;s no &ldquo;React component that fetches data from an API&rdquo;. There&rsquo;s a Markdown file read by whoever needs it. This is radically simple — and radically powerful. Swap the blog engine from Hugo to something else? Change the templates, the content stays the same. Swap the email provider from SES to Sendgrid? Change the mailer, the templates stay the same.</p>
<p>It&rsquo;s the Unix philosophy applied to frontend: each tool does one thing well, connected by a universal format (Markdown + YAML).</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You don&rsquo;t need React to have a modern frontend. Tailwind v4 gives you a design system with constraints, native dark mode, and zero JavaScript. Hugo gives you an absurdly fast blog with flexible templates. ERB with inline CSS gives you emails that work in any client.</p>
<p>The real investment is understanding the constraints of each platform and building rendering pipelines for each one. The payoff comes from that, not from learning the trendy framework that&rsquo;ll die in 18 months.</p>
<p>At the end of the day, the best frontend abstraction is none at all. HTML, CSS, and Markdown. Simple, debuggable, and going to work 10 years from now.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>frontend</category><category>web-development</category></item><item><title>Web Scraping in 2026 | Behind the Scenes of The M.Akita Chronicles</title><link>https://akitaonrails.github.io/en/2026/02/18/web-scraping-in-2026-behind-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/02/18/web-scraping-in-2026-behind-the-m-akita-chronicles/</guid><pubDate>Wed, 18 Feb 2026 14:57:22 +0000</pubDate><description>&lt;p&gt;This post is part of a series; follow along via the tag &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. This is part 2.&lt;/p&gt;
&lt;p&gt;And don&amp;rsquo;t forget to subscribe to my new newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Ten years ago, fetching data from a website was trivial. Four lines of Ruby: &lt;code&gt;Net::HTTP.get&lt;/code&gt;, Nokogiri parse, CSS selector, done. The whole world was open to any script with a decent User-Agent.&lt;/p&gt;
&lt;p&gt;Today? Half the sites I need to read to put together my newsletter return &amp;ldquo;Are you a robot?&amp;rdquo; for any conventional HTTP request. The other half renders the content entirely in JavaScript — there&amp;rsquo;s no HTML at all in the server response. And the most paranoid ones combine both.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is part of a series; follow along via the tag <a href="/en/tags/the-m-akita-chronicles/">/themakitachronicles</a>. This is part 2.</p>
<p>And don&rsquo;t forget to subscribe to my new newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Ten years ago, fetching data from a website was trivial. Four lines of Ruby: <code>Net::HTTP.get</code>, Nokogiri parse, CSS selector, done. The whole world was open to any script with a decent User-Agent.</p>
<p>Today? Half the sites I need to read to put together my newsletter return &ldquo;Are you a robot?&rdquo; for any conventional HTTP request. The other half renders the content entirely in JavaScript — there&rsquo;s no HTML at all in the server response. And the most paranoid ones combine both.</p>
<p>My goal was to create a daily routine so that M.arvin would deliver the top news to me in the morning on my Discord. That way I wouldn&rsquo;t have to go hunting for the news of the day. Sounded simple&hellip; at first.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-18_11-59-42.jpg" alt="daily routine"  loading="lazy" /></p>
<p>This post is about how I went from &ldquo;one HTTP call&rdquo; to &ldquo;packing an entire Chromium into Docker&rdquo; — and why this is the reality of any project that needs to read the web programmatically in 2026.</p>
<h2>The Golden Age: Simple HTTP<span class="hx:absolute hx:-mt-20" id="the-golden-age-simple-http"></span>
    <a href="#the-golden-age-simple-http" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s start with what still works. Some services offer structured data with zero resistance:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># RSS feeds: the most resilient format on the web</span>
</span></span><span class="line"><span class="cl"><span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s2">&#34;https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com/world&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">xml</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">Document</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">xml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">get_elements</span><span class="p">(</span><span class="s2">&#34;//item&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span>   <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;link&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Public APIs: clean JSON, documented, stable</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;https://api.jikan.moe/v4/top/anime?filter=airing&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">animes</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">parsed_response</span><span class="o">[</span><span class="s2">&#34;data&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GitHub trending: server-rendered HTML with no protection</span>
</span></span><span class="line"><span class="cl"><span class="n">html</span> <span class="o">=</span> <span class="no">HTTParty</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;https://github.com/trending&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">body</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">repos</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;article.Box-row&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Hacker News, GitHub Trending, anime APIs, RSS feeds, Project Gutenberg — they all work with a <code>GET</code> and a parser. No JavaScript, no implicit authentication, no fingerprinting. Just like the web was supposed to be.</p>
<p>The problem is that these are the exception. Sites with financial data, exclusive news, or premium content — exactly the data that matters most — are increasingly armored.</p>
<h2>The First Obstacle: SPAs and JavaScript Rendering<span class="hx:absolute hx:-mt-20" id="the-first-obstacle-spas-and-javascript-rendering"></span>
    <a href="#the-first-obstacle-spas-and-javascript-rendering" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When the server returns something like:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;root&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/app-3fa8c2.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Your <code>Net::HTTP.get</code> returns exactly that: an empty div and a link to a 2 MB bundle. The real content — articles, prices, lists — is assembled in the browser by React, Vue, Angular, or the framework of the week.</p>
<p>For a script, this page is useless. To extract the content, you need to execute the JavaScript. That means running a real browser.</p>
<p>In the Ruby world, the answer is the <code>ferrum</code> gem — a CDP (Chrome DevTools Protocol) client that controls Chromium programmatically:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">browser</span> <span class="o">=</span> <span class="no">Ferrum</span><span class="o">::</span><span class="no">Browser</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">headless</span><span class="p">:</span> <span class="s2">&#34;new&#34;</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="s2">&#34;https://www.morningstar.com/markets&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">network</span><span class="o">.</span><span class="n">wait_for_idle</span>  <span class="c1"># wait for JS to finish loading</span>
</span></span><span class="line"><span class="cl"><span class="n">html</span> <span class="o">=</span> <span class="n">browser</span><span class="o">.</span><span class="n">body</span>  <span class="c1"># now the DOM is populated</span>
</span></span><span class="line"><span class="cl"><span class="n">browser</span><span class="o">.</span><span class="n">quit</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That <code>browser.body</code> returns the HTML <strong>after</strong> the JavaScript has executed. The same data you see in the browser, now accessible to your parser.</p>
<p>But here&rsquo;s where the headache begins: you need a Chromium binary available on the system. In Docker, that means adding ~300MB to the image. On the server, that&rsquo;s one more dependency to manage. And the latency of opening a browser, navigating, waiting for JS to render, and extracting the HTML — we&rsquo;re talking 3 to 10 seconds per page, instead of the 200ms of an HTTP GET.</p>
<p>The project uses this as the main strategy in <code>ArticleFetcher</code>: first it tries with a browser (to catch SPAs), then falls back to simple HTTP (to grab OpenGraph metadata when the browser fails). Most of the links that readers send in Discord are from sites that render with JavaScript, so browser-first pays off.</p>
<h2>The Second Obstacle: Bot Detection<span class="hx:absolute hx:-mt-20" id="the-second-obstacle-bot-detection"></span>
    <a href="#the-second-obstacle-bot-detection" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Opening a page with headless Chromium solves the JavaScript problem. But it solves 50% of the sites — not 100%. Because there&rsquo;s an entire industry dedicated to distinguishing real browsers from automated browsers.</p>
<h3>What Headless Chrome Reveals<span class="hx:absolute hx:-mt-20" id="what-headless-chrome-reveals"></span>
    <a href="#what-headless-chrome-reveals" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Chromium in headless mode is almost identical to regular Chrome. Almost. The differences are subtle, but enough for detection:</p>
<p><strong>1. <code>navigator.webdriver</code></strong> — In any Chrome controlled by automation, this property returns <code>true</code>. In normal browsers, <code>undefined</code>. It&rsquo;s the most obvious flag.</p>
<p><strong>2. <code>navigator.plugins</code></strong> — Normal Chrome has 3-5 plugins (PDF viewer, etc.). Headless returns an empty array. Bot detected.</p>
<p><strong>3. <code>navigator.userAgentData</code></strong> — The Client Hints API. Real Chrome returns brands, platform, mobile flag. Headless may not have that.</p>
<p><strong>4. WebGL fingerprint</strong> — The WebGL renderer in a headless Chrome returns &ldquo;SwiftShader&rdquo; (software renderer). Real Chrome returns &ldquo;NVIDIA GeForce&rdquo; or &ldquo;Intel Iris&rdquo; or whatever. Bot detected.</p>
<p><strong>5. Timing and behavior</strong> — Real browsers take time to load. They scroll. They move the mouse. They have focus events. A script that does <code>go_to</code> → <code>body</code> in 2 seconds doesn&rsquo;t look like a human.</p>
<p>The solution is what I call StealthBrowser — a wrapper that injects JavaScript patches before any script on the page runs:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">StealthBrowser</span>
</span></span><span class="line"><span class="cl">  <span class="no">STEALTH_JS</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">JS</span>
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Remove</span> <span class="n">the</span> <span class="n">automation</span> <span class="n">flag</span>
</span></span><span class="line"><span class="cl">    <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;webdriver&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kp">false</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Simulate</span> <span class="n">real</span> <span class="n">plugins</span>
</span></span><span class="line"><span class="cl">    <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;plugins&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Chrome PDF Plugin&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;internal-pdf-viewer&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Chrome PDF Viewer&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;mhjfbmdgcfjbbpaeojofohoefgiehjai&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="nb">name</span><span class="p">:</span> <span class="s1">&#39;Native Client&#39;</span><span class="p">,</span> <span class="ss">filename</span><span class="p">:</span> <span class="s1">&#39;internal-nacl-plugin&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Simulate</span> <span class="n">a</span> <span class="n">real</span> <span class="no">WebGL</span> <span class="n">renderer</span>
</span></span><span class="line"><span class="cl">    <span class="n">const</span> <span class="n">getParameter</span> <span class="o">=</span> <span class="no">WebGLRenderingContext</span><span class="o">.</span><span class="n">prototype</span><span class="o">.</span><span class="n">getParameter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="no">WebGLRenderingContext</span><span class="o">.</span><span class="n">prototype</span><span class="o">.</span><span class="n">getParameter</span> <span class="o">=</span> <span class="n">function</span><span class="p">(</span><span class="nb">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nb">p</span> <span class="o">===</span> <span class="mi">37445</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;Google Inc. (NVIDIA)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nb">p</span> <span class="o">===</span> <span class="mi">37446</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;ANGLE (NVIDIA, GeForce GTX 1080 ...)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="n">getParameter</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">this</span><span class="p">,</span> <span class="nb">p</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="sr">//</span> <span class="no">Client</span> <span class="no">Hints</span> <span class="no">API</span> <span class="p">(</span><span class="n">absent</span> <span class="k">in</span> <span class="n">headless</span> <span class="o">=</span> <span class="n">bot</span> <span class="n">signal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">navigator</span><span class="o">.</span><span class="n">userAgentData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="no">Object</span><span class="o">.</span><span class="n">defineProperty</span><span class="p">(</span><span class="n">navigator</span><span class="p">,</span> <span class="s1">&#39;userAgentData&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="ss">get</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">          <span class="ss">brands</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="ss">brand</span><span class="p">:</span> <span class="s1">&#39;Chromium&#39;</span><span class="p">,</span> <span class="ss">version</span><span class="p">:</span> <span class="s1">&#39;131&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span> <span class="ss">brand</span><span class="p">:</span> <span class="s1">&#39;Google Chrome&#39;</span><span class="p">,</span> <span class="ss">version</span><span class="p">:</span> <span class="s1">&#39;131&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="ss">mobile</span><span class="p">:</span> <span class="kp">false</span><span class="p">,</span> <span class="ss">platform</span><span class="p">:</span> <span class="s1">&#39;Linux&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="no">JS</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="nb">name</span><span class="p">:,</span> <span class="ss">wait_for</span><span class="p">:</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span> <span class="o">=</span> <span class="no">Ferrum</span><span class="o">::</span><span class="no">Browser</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">headless</span><span class="p">:</span> <span class="s2">&#34;new&#34;</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">browser_options</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;disable-blink-features&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;AutomationControlled&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;user-agent&#34;</span> <span class="o">=&gt;</span> <span class="no">REAL_USER_AGENT</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Inject BEFORE any script on the page runs</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">.</span><span class="n">page</span><span class="o">.</span><span class="n">command</span><span class="p">(</span><span class="s2">&#34;Page.addScriptToEvaluateOnNewDocument&#34;</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="no">STEALTH_JS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">yield</span> <span class="n">browser</span>
</span></span><span class="line"><span class="cl">  <span class="k">ensure</span>
</span></span><span class="line"><span class="cl">    <span class="n">browser</span><span class="o">&amp;.</span><span class="n">quit</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The key trick is <code>Page.addScriptToEvaluateOnNewDocument</code> — a CDP command that registers JavaScript to be executed <strong>before</strong> any script on the page. That means when the bot detector runs, it sees the fake values we injected, not the real ones. And <code>disable-blink-features=AutomationControlled</code> removes the <code>navigator.webdriver</code> flag at the browser level, even before JavaScript.</p>
<p>This works for most sites. Morningstar, Hindenburg Research, generic news sites — they pass with the stealth. Yahoo Finance needs more — TLS fingerprinting that blocks even Chromium without crumb authentication. But the stealth browser handles the bulk.</p>
<h2>The Third Obstacle: DataDome and Anti-Bot CDPs<span class="hx:absolute hx:-mt-20" id="the-third-obstacle-datadome-and-anti-bot-cdps"></span>
    <a href="#the-third-obstacle-datadome-and-anti-bot-cdps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And now we get to the pros. <strong>DataDome</strong>, Cloudflare Turnstile, PerimeterX, Akamai Bot Manager — services specialized in detecting automation. And they play at a completely different level.</p>
<p>Reuters, for example, uses DataDome. When I tried to hit <code>reuters.com/world/</code> with StealthBrowser, I got back a 1,500-byte page with a JavaScript captcha. All the <code>navigator.webdriver</code>, WebGL, plugin patches — useless.</p>
<p>Why? Because DataDome doesn&rsquo;t just check JavaScript properties. It detects the <strong>automation protocol itself</strong>.</p>
<h3>The Fundamental Problem with CDP<span class="hx:absolute hx:-mt-20" id="the-fundamental-problem-with-cdp"></span>
    <a href="#the-fundamental-problem-with-cdp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>When <strong>Ferrum</strong> (or Puppeteer, or Playwright) controls a Chrome, it does so via the Chrome DevTools Protocol — CDP. And the first thing Ferrum does when connecting is send <code>Runtime.enable</code> — a command that enables runtime evaluation in the browser.</p>
<p>The problem is that <code>Runtime.enable</code> changes the observable behavior of the browser. Specifically, it activates internal interceptors that can be detected via <code>console.debug</code>. DataDome uses this: it does a <code>console.debug</code> with a getter that watches whether the runtime is intercepting. If it is — and with CDP it always is — bot detected.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// What DataDome does (simplified):
</span></span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">detected</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nb">Object</span><span class="p">.</span><span class="nx">defineProperty</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="s1">&#39;id&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">get</span><span class="o">:</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">detected</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="k">return</span> <span class="s1">&#39;test&#39;</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// If Runtime.enable is active, the getter is triggered
</span></span></span><span class="line"><span class="cl"><span class="c1">// In a normal browser, it isn&#39;t
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is not something you can patch with JavaScript. <code>Runtime.enable</code> is sent by Ferrum <strong>before</strong> any script runs — it&rsquo;s part of the automation protocol. To fix this, you&rsquo;d have to modify Ferrum to not send that command, which would break most of the gem&rsquo;s functionality.</p>
<h3>The Alternatives (and Why None Are Ideal)<span class="hx:absolute hx:-mt-20" id="the-alternatives-and-why-none-are-ideal"></span>
    <a href="#the-alternatives-and-why-none-are-ideal" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I researched extensively. The options:</p>
<ol>
<li>
<p><strong>nodriver (Python)</strong>: Uses Chrome via CDP without <code>Runtime.enable</code>. No equivalent in Ruby. And even so, it&rsquo;s in a cat-and-mouse race with DataDome.</p>
</li>
<li>
<p><strong>curl-impersonate</strong>: Does TLS fingerprinting identical to Chrome without opening a browser. Good for sites with TLS detection, useless for SPAs.</p>
</li>
<li>
<p><strong>Scraping APIs</strong> (ScraperAPI, ZenRows): Outsource the problem. They work, but they cost money and add latency and dependency.</p>
</li>
<li>
<p><strong>Fork Ferrum</strong>: Modify the gem to optionally not send <code>Runtime.enable</code>. Technically viable, but endless maintenance.</p>
</li>
<li>
<p><strong>Google News RSS</strong>: Google has already read Reuters. The result is in a public RSS feed. No JavaScript, no DataDome, no browser.</p>
</li>
</ol>
<p>Option 5 won. For Reuters, I traded all the headless browser complexity for:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ReutersClient</span>
</span></span><span class="line"><span class="cl">  <span class="no">RSS_URL</span> <span class="o">=</span> <span class="s2">&#34;https://news.google.com/rss/search?q=when:24h+allinurl:reuters.com/world&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch_headlines</span><span class="p">(</span><span class="ss">limit</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">xml</span> <span class="o">=</span> <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="no">URI</span><span class="p">(</span><span class="no">RSS_URL</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="no">REXML</span><span class="o">::</span><span class="no">Document</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">xml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">doc</span><span class="o">.</span><span class="n">get_elements</span><span class="p">(</span><span class="s2">&#34;//item&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="n">limit</span><span class="p">)</span><span class="o">.</span><span class="n">filter_map</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">title</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">      <span class="n">url</span>   <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="n">get_text</span><span class="p">(</span><span class="s2">&#34;link&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">      <span class="k">next</span> <span class="k">if</span> <span class="n">title</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">url</span><span class="o">.</span><span class="n">blank?</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\s*-\s*Reuters\s*\z/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">),</span> <span class="ss">url</span><span class="p">:</span> <span class="n">url</span><span class="p">,</span> <span class="ss">category</span><span class="p">:</span> <span class="kp">nil</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;ReutersClient failed: </span><span class="si">#{</span><span class="n">e</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Simple. Reliable. Zero browser dependencies. The Google News link redirects to the original article when the reader clicks — works perfectly for a newsletter.</p>
<p>Not every scraping problem needs a scraping solution. Sometimes the data is already available in an accessible format — you just have to find it.</p>
<h2>The Fourth Obstacle: Sites That Change Structure (Daily)<span class="hx:absolute hx:-mt-20" id="the-fourth-obstacle-sites-that-change-structure-daily"></span>
    <a href="#the-fourth-obstacle-sites-that-change-structure-daily" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Even when scraping works, maintenance never stops. Morningstar is a textbook case — because it made me get it wrong <strong>twice</strong> before reaching a resilient solution.</p>
<h3>Attempt 1: Select by DOM Hierarchy<span class="hx:absolute hx:-mt-20" id="attempt-1-select-by-dom-hierarchy"></span>
    <a href="#attempt-1-select-by-dom-hierarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Originally, the article links were inside <code>&lt;h2&gt;</code> and <code>&lt;h3&gt;</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/markets/article-slug&#34;</span><span class="p">&gt;</span>Article Title<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>One day later: zero results. Morningstar changed the structure to flat links with specific CSS classes — no heading elements:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mdc-basic-feed-item__mdc&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/markets/article-slug&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">header</span><span class="p">&gt;&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>Article Title<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;&lt;/</span><span class="nt">header</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mdc-metadata__mdc&#34;</span><span class="p">&gt;</span>Author · Date<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Attempt 2: Select by CSS Class<span class="hx:absolute hx:-mt-20" id="attempt-2-select-by-css-class"></span>
    <a href="#attempt-2-select-by-css-class" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The obvious fix: swap DOM hierarchy for CSS selectors based on the component class:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ARTICLE_CSS</span> <span class="o">=</span> <span class="s2">&#34;a.mdc-basic-feed-item__mdc, a.mdc-card--link__mdc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="no">ARTICLE_CSS</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">link</span><span class="o">|</span> <span class="o">...</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It worked. For exactly one day. Then: zero results again. Morningstar — like many modern sites with CSS-in-JS frameworks — generates class names dynamically. What was <code>mdc-basic-feed-item__mdc</code> became something else. And it may become something else tomorrow.</p>
<p>This is increasingly common. Sites using CSS Modules, styled-components, or Tailwind with JIT generate random hashes in class names as part of the build. Every time the site deploys, the classes change. Your hardcoded selector is a time bomb.</p>
<h3>Attempt 3: Don&rsquo;t Rely on Any Class Name<span class="hx:absolute hx:-mt-20" id="attempt-3-dont-rely-on-any-class-name"></span>
    <a href="#attempt-3-dont-rely-on-any-class-name" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The final solution is to flip the logic: instead of telling the parser <strong>which</strong> classes to look for, let the parser <strong>discover</strong> which links are articles based on structural properties that don&rsquo;t change:</p>
<ol>
<li><strong>Article links have long URLs</strong>: <code>/news/why-stock-market-is-up-today-2026-02-17</code> vs. navigation links like <code>/markets</code> or <code>/tools</code></li>
<li><strong>Article links are grouped</strong>: they appear in lists, feeds, grids — multiple links with the same CSS class</li>
<li><strong>Article links have descriptive titles</strong>: long text, not short labels like &ldquo;Markets&rdquo; or &ldquo;Tools&rdquo;</li>
</ol>
<p>The algorithm:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">parse_html</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="n">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 1. Collect all internal &lt;a&gt; with href, class, and enough text</span>
</span></span><span class="line"><span class="cl">  <span class="n">candidates</span> <span class="o">=</span> <span class="n">doc</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;a[href]&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">filter_map</span> <span class="k">do</span> <span class="o">|</span><span class="n">link</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">href</span> <span class="o">=</span> <span class="n">link</span><span class="o">[</span><span class="s2">&#34;href&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">    <span class="n">css_class</span> <span class="o">=</span> <span class="n">link</span><span class="o">[</span><span class="s2">&#34;class&#34;</span><span class="o">].</span><span class="n">to_s</span><span class="o">.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">href</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">css_class</span><span class="o">.</span><span class="n">blank?</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">unless</span> <span class="n">internal_href?</span><span class="p">(</span><span class="n">href</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">extract_path</span><span class="p">(</span><span class="n">href</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">segments</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reject</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:empty?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">segments</span><span class="o">.</span><span class="n">length</span> <span class="o">&lt;</span> <span class="mi">2</span>  <span class="c1"># nav links have 1 segment: /markets</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="n">extract_title</span><span class="p">(</span><span class="n">link</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">next</span> <span class="k">if</span> <span class="n">title</span><span class="o">.</span><span class="n">blank?</span> <span class="o">||</span> <span class="n">title</span><span class="o">.</span><span class="n">length</span> <span class="o">&lt;</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">{</span> <span class="ss">title</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="n">absolutize</span><span class="p">(</span><span class="n">href</span><span class="p">),</span> <span class="ss">css_class</span><span class="p">:</span> <span class="n">css_class</span><span class="p">,</span> <span class="ss">slug_length</span><span class="p">:</span> <span class="n">path</span><span class="o">.</span><span class="n">length</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. Group by CSS class</span>
</span></span><span class="line"><span class="cl">  <span class="n">groups</span> <span class="o">=</span> <span class="n">candidates</span><span class="o">.</span><span class="n">group_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">[</span><span class="ss">:css_class</span><span class="o">]</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Filter: minimum 3 links in the group (article clusters, not stray links)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 4. Pick the group with the highest average slug length</span>
</span></span><span class="line"><span class="cl">  <span class="n">best</span> <span class="o">=</span> <span class="n">groups</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">_cls</span><span class="p">,</span> <span class="n">items</span><span class="o">|</span> <span class="n">items</span><span class="o">.</span><span class="n">length</span> <span class="o">&gt;=</span> <span class="mi">3</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">max_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">_cls</span><span class="p">,</span> <span class="n">items</span><span class="o">|</span> <span class="n">items</span><span class="o">.</span><span class="n">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span> <span class="n">i</span><span class="o">[</span><span class="ss">:slug_length</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">to_f</span> <span class="o">/</span> <span class="n">items</span><span class="o">.</span><span class="n">length</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="o">[]</span> <span class="k">unless</span> <span class="n">best</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">best</span><span class="o">[</span><span class="mi">1</span><span class="o">].</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="ss">:url</span><span class="p">)</span> <span class="p">}</span><span class="o">.</span><span class="n">uniq</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="o">|</span> <span class="n">a</span><span class="o">[</span><span class="ss">:url</span><span class="o">]</span> <span class="p">}</span><span class="o">.</span><span class="n">first</span><span class="p">(</span><span class="n">limit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What saves this approach: <strong>no class name is hardcoded</strong>. Morningstar can rename every class tomorrow and the parser will keep working — because it discovers the article pattern through structure, not through names.</p>
<p>What&rsquo;s stable on a site:</p>
<ul>
<li><strong>Article URLs are always longer</strong> than navigation URLs (that&rsquo;s architectural, not cosmetic)</li>
<li><strong>Articles are always grouped</strong> under the same component (that&rsquo;s UX, not implementation)</li>
<li><strong>Article titles are descriptive</strong> (that&rsquo;s editorial, not technical)</li>
</ul>
<p>What&rsquo;s <strong>not</strong> stable:</p>
<ul>
<li>CSS class names (change on every build)</li>
<li>DOM hierarchy (changes on every redesign)</li>
<li><code>data-*</code> attributes (change as the frontend team decides)</li>
</ul>
<p>Bottom line: don&rsquo;t try to identify elements by name — identify them by behavior. Article links behave differently from navigation links in measurable ways (URL length, clustering, title), and those properties are inherent to what the element IS, not to how it was implemented.</p>
<h2>The Reliability Hierarchy<span class="hx:absolute hx:-mt-20" id="the-reliability-hierarchy"></span>
    <a href="#the-reliability-hierarchy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After hours of banging my head, a hierarchy of data sources became obvious:</p>
<table>
  <thead>
      <tr>
          <th>Source</th>
          <th>Reliability</th>
          <th>Latency</th>
          <th>Maintenance</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Official API (JSON)</td>
          <td>High</td>
          <td>~200ms</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>RSS/Atom feed</td>
          <td>High</td>
          <td>~300ms</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Server-rendered HTML</td>
          <td>Medium</td>
          <td>~500ms</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>SPA with headless browser</td>
          <td>Low-Medium</td>
          <td>~5s</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Site with active anti-bot</td>
          <td>Low</td>
          <td>~8s+</td>
          <td>Very high</td>
      </tr>
  </tbody>
</table>
<p>The rule: <strong>always prefer the simplest source that works</strong>. If the site has RSS, use RSS. If it has an API, use the API. Only go to the headless browser when there&rsquo;s no alternative.</p>
<p>In the project, the distribution ended up like this:</p>
<p><strong>Simple HTTP</strong> (Net::HTTP or HTTParty):</p>
<ul>
<li>Google News RSS (Reuters world headlines)</li>
<li>Hacker News RSS (top stories)</li>
<li>HackerNoon RSS (top stories)</li>
<li>dev.to API (trending articles)</li>
<li>Jikan API (anime rankings)</li>
<li>YouTube Data API (videos)</li>
<li>Yahoo Finance news RSS</li>
</ul>
<p><strong>Headless browser</strong> (StealthBrowser + Ferrum):</p>
<ul>
<li>Morningstar (SPA + dynamic components)</li>
<li>Yahoo Finance charts (TLS fingerprinting + crumb auth)</li>
<li>FinViz market map (canvas rendering)</li>
<li>Generic reader articles (any URL could be an SPA)</li>
<li>Hindenburg Research (JavaScript-rendered)</li>
</ul>
<p>In other words: <strong>most data sources work without a browser</strong>. The browser is needed for a handful of problem sites — but those sites are the ones that cause the most trouble.</p>
<h2>The Real Cost<span class="hx:absolute hx:-mt-20" id="the-real-cost"></span>
    <a href="#the-real-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s be practical about what &ldquo;packing a Chrome&rdquo; means:</p>
<p><strong>Docker image</strong>: +300MB. Chromium and its dependencies (fonts, graphics libs, sandbox) are huge. In an optimized Dockerfile with multi-stage build:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Install Chromium and minimal dependencies</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y chromium fonts-liberation libnss3 libatk-bridge2.0-0 <span class="se">\
</span></span></span><span class="line"><span class="cl">    libdrm2 libxkbcommon0 libgbm1</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Memory</strong>: Each Chromium instance consumes 200-400MB of RAM. If you have 3 parallel workers, that&rsquo;s 1.2GB just for browsers. On a 2GB VPS, that&rsquo;s 60% of the memory.</p>
<p><strong>Latency</strong>: A page that takes 200ms via HTTP takes 3-8 seconds via headless browser (process boot, navigation, JS execution, network idle). For 10 data sources, that&rsquo;s 30-80 seconds instead of 2.</p>
<p><strong>Stability</strong>: Chromium can crash, hang, or leak memory. A 30-second timeout with cleanup in <code>ensure</code> is mandatory:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="nb">name</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span> <span class="o">=</span> <span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span><span class="o">.</span><span class="n">go_to</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">yield</span> <span class="n">browser</span>
</span></span><span class="line"><span class="cl"><span class="k">ensure</span>
</span></span><span class="line"><span class="cl">  <span class="n">browser</span><span class="o">&amp;.</span><span class="n">quit</span>  <span class="c1"># ALWAYS quit, even on exception</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you don&rsquo;t <code>quit</code>, the Chromium process stays hanging around burning CPU and memory. I&rsquo;ve had a server with 15 zombie Chrome processes eating 3GB of RAM because an <code>ensure</code> was missing.</p>
<h2>The Future (Or: It&rsquo;s Going to Get Worse)<span class="hx:absolute hx:-mt-20" id="the-future-or-its-going-to-get-worse"></span>
    <a href="#the-future-or-its-going-to-get-worse" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The trend is clear: sites are getting harder to access programmatically. Every quarter, the DataDomes and Cloudflares of the world add new detection techniques. Headless Chrome gets more similar to real Chrome — but the detectors keep up.</p>
<p>The web that was born open and machine-readable is becoming a walled garden. Even RSS, which is trivial to implement and useful to everyone, is being abandoned by many sites — because they don&rsquo;t want their data read outside the context (and the ads) of the original site.</p>
<p>For anyone building systems that aggregate information, the playbook is:</p>
<ol>
<li><strong>Use APIs and feeds whenever they exist</strong>. More stable, faster, more respectful</li>
<li><strong>Keep a stealth browser for SPAs</strong> that have no alternative</li>
<li><strong>Never hardcode CSS selectors</strong>. Sites with CSS-in-JS change class names on every deploy. Identify elements by structural properties (URL length, clustering, title size) that survive redesigns</li>
<li><strong>Detect blocks and fail gracefully</strong> — <code>og:description</code> as fallback, title extracted from the URL as a last resort</li>
<li><strong>Never depend on a single access method</strong>. The site that works today may block tomorrow</li>
<li><strong>Accept that some sites are inaccessible</strong>. If DataDome won&rsquo;t allow it, the data will have to come from another source</li>
</ol>
<p>The web in 2026 is a battlefield between those who want to read and those who want to control the reading. But in the long run, the creativity of those who want to access data wins — but it costs more effort every year. And, contrary to what I thought at the beginning: it&rsquo;s never simple.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>web-development</category><category>automation</category></item><item><title>Why it is just lazy to bad-mouth Ruby on Rails</title><link>https://akitaonrails.github.io/en/2017/08/03/why-is-it-just-lazy-to-bad-mouth-ruby-on-rails/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/08/03/why-is-it-just-lazy-to-bad-mouth-ruby-on-rails/</guid><pubDate>Thu, 03 Aug 2017 18:01:00 -0300</pubDate><description>&lt;p&gt;It&amp;rsquo;s inevitable these days: we will see an article proclaiming the demise of Ruby on Rails every once in a while. It&amp;rsquo;s the easiest click bait, like &lt;a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener"&gt;this one from TNW&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, you may say &lt;em&gt;&amp;ldquo;another Ruby fanboy.&amp;rdquo;&lt;/em&gt; That&amp;rsquo;s fair, but a terrible argument, as it&amp;rsquo;s a poor and common &lt;a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener"&gt;argumentum ad hominem&lt;/a&gt;. And on the subject of fallacies, the click-bait article above is wrong exactly because it falls for a blatant &lt;a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener"&gt;Post hoc ergo propter hoc&lt;/a&gt; fallacy plus some more &lt;a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener"&gt;confirmation bias&lt;/a&gt; which we are all guilty of falling for all the time.&lt;/p&gt;</description><content:encoded><![CDATA[<p>It&rsquo;s inevitable these days: we will see an article proclaiming the demise of Ruby on Rails every once in a while. It&rsquo;s the easiest click bait, like <a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener">this one from TNW</a>.</p>
<p>Now, you may say <em>&ldquo;another Ruby fanboy.&rdquo;</em> That&rsquo;s fair, but a terrible argument, as it&rsquo;s a poor and common <a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener">argumentum ad hominem</a>. And on the subject of fallacies, the click-bait article above is wrong exactly because it falls for a blatant <a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener">Post hoc ergo propter hoc</a> fallacy plus some more <a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener">confirmation bias</a> which we are all guilty of falling for all the time.</p>
<p>I&rsquo;m not saying that the author wrote fallacies on purpose. Unfortunately, it&rsquo;s just too easy to fall for fallacies. Especially when everybody has an intrinsic desire to confirm one&rsquo;s biases. Even trying to be careful, I end up doing that as well.</p>
<p>On the specific subject of Rails, start here:</p>
<ul>
<li><a href="http://www.akitaonrails.com/en/2016/05/23/rails-venceu-o-elefante-na-sala"target="_blank" rel="noopener">Rails has won: The Elephant in the Room</a></li>
<li><a href="http://www.akitaonrails.com/en/2016/08/19/a-comunidade-ruby-e-a-reputacao"target="_blank" rel="noopener">The Ruby Community and Reputation</a></li>
<li><a href="http://www.akitaonrails.com/en/2017/05/14/guilda-de-programadores-religiao-e-esportes"target="_blank" rel="noopener">Programmers Guild - Religion and Sports</a></li>
</ul>
<p>As a general rule, anyone announcing the future demise of anything is most probably wrong. It&rsquo;s fun to play guessing games, but articles in high profile websites like TNW should be more careful as many beginners in the field end up trusting their opinions.</p>
<p>Which is why I usually tell people to stop trusting opinions. Everybody has an opinion, which is why it&rsquo;s so cheap. Then people artificially grade &ldquo;celebrities&rdquo; or &ldquo;gurus&rdquo; as having opinions of higher value. I don&rsquo;t buy it, neither should you.</p>
<p><a href="https://en.wikipedia.org/wiki/Black_swan_theory"target="_blank" rel="noopener">Black Swans</a> do happen. But it&rsquo;s an exercise in futility to try to predict them. You may be lucky, once or twice. But it&rsquo;s useless to attempt. Which is why I am a high proponent of <a href="https://en.wikipedia.org/wiki/Antifragility"target="_blank" rel="noopener">Antifragility</a>, so I will give you the general rules to understand what to do.</p>
<p>Am I saying that Ruby on Rails will live on forever? Don&rsquo;t be absurd, of course not. Nothing lasts forever. More than 2 decades actively working in the field taught me that betting everything on one horse is a mistake. It&rsquo;s valid in the financial markets, it&rsquo;s valid in the tech market.</p>
<p>Instead of stock or bonds, your asset is your knowledge and your practice. Diversification of your portfolio is usually a better bet.</p>
<p>You invested all your 401k in Bitcoins. Now you will try everything to try to confirm your bias by subscribing to every news piece and opinion that confirms your bias, and you will denounce every article that may have an argument against your bet. Am I saying to not invest in Bitcoins? Hell no. Just don&rsquo;t be stupid and invest everything on anything.</p>
<p>You invested all your time learning <a href="https://en.wikipedia.org/wiki/Google_Web_Toolkit"target="_blank" rel="noopener">Google Web Toolkit</a> in 2006. Good luck for you finding an article to confirm that bias. This is one case, an after-the-fact, that we can&rsquo;t do much but consensually agree on its death.</p>
<blockquote>
  <p>The job of a proper professional programmer is not mastery of a single tool or even a single technology stack. Your job is to be as good as possible in learning and practicing in the technology field.</p>

</blockquote>
<p>Having said all that, and even though the article&rsquo;s structure is inaccurate, The feeling is not wrong. Ruby on Rails no longer represents what it was 10 years ago. Now, before you start freaking out, let&rsquo;s explore it down memory lane.</p>
<h3>2003 - A New Renaissance<span class="hx:absolute hx:-mt-20" id="2003---a-new-renaissance"></span>
    <a href="#2003---a-new-renaissance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As an after-the-fact exercise, one has to realize that a series of events helped unfold the Rails phenomenon between 2003 and 2011. It was not by design. It was not planned. It could&rsquo;ve been something else.</p>
<ul>
<li>We were just recovering from the 2001 Internet dot-com Bubble Crash.</li>
<li>Social networks were starting to emerge, we had Orkut, MySpace, Friendster. Still no Facebook on the radar.</li>
<li>Blogs were starting to pick up steam and competing with traditional news outlets.</li>
<li>Big vendors focused solely on the Enterprise market at that point and the rest were deemed amateurish.</li>
<li>In the mainstream either you were doing .NET or Java on the Enterprise or doing hotsites in Flash and PHP for ad hoc agencies.</li>
<li>Even open source tools like Eclipse were primarily used through enterprisey umbrellas like WebSphere or WebLogic.</li>
<li>The Agile Manifesto had just been published in 2001 and the mainstream market didn&rsquo;t pay much attention. PMI and RUP were the order of the day.</li>
</ul>
<p>Then we reached 2004.</p>
<ul>
<li>Google successfully launched Gmail to much fanfare. Dynamic HTML and Ajax demoed how we could achieve Desktop-level applications in the Web. This is also where Javascript earns a new role beyond stupid web mini games or ads.</li>
<li>Facebook skyrocketed and we were shown glimpses of what granular social networks look like.</li>
<li>Apple consolidated its comeback as a relevant tech giant with the successful resurrection of OS X, proving that Unix on the consumer desktop was viable.</li>
<li>Microsoft entered the 21st century after a super heavy antitrust case that almost obliterated the company. Bill Gates resigned, Steve Ballmer took the reins and they had their darkest decade</li>
<li>Years of vendor lock-in finally started to wear off developers, no one actually wanted to use IBM, BEA, Oracle anymore and the promise of the open source revolution started in the 90&rsquo;s finally started to solidify</li>
<li>The dot-com crash left a depression in the tech market but it also left behind an infrastructure that was finally being put to good use</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/654/s-l225.jpg" alt="How Bill Blew it"  loading="lazy" /></p>
<p>Ruby on Rails shows up as a very small curiosity, in an obscure tech conference in Brazil, created by a small but promising agency called 37signals. Signaling something most developers had never seen before: a flexible language - unlike Java or C# or PHP - that could be used to implement Gmail-like web applications with a flavor of the Apple vision and steering away to everything that Microsoft and IBM represented while incorporating Agile-like techniques such as Test-Driven Development in a practical way and out-of-the-box.</p>
<p><a href="https://www.youtube.com/watch?v=Gzj723LkRJY"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/655/75879992_fa890a9a07.jpg" alt="15 minute blog"  loading="lazy" /></a></p>
<p>What else could you ask for in 2005?</p>
<p>Then <strong>2006</strong> happens: Amazon AWS releases S3 and EC2 and the term &ldquo;Elastic Cloud&rdquo; becomes the new hype.</p>
<p>Up to that point, &ldquo;web development&rdquo; either meant expensive and bureaucratic IT departments in big corporations or inexpensive but weak shared hosting in services such as Dreamhost, 1&amp;1, Media Temple.</p>
<p>Elastic Cloud holds the key to surpass the enterprise and make it possible for independent developers to build the elusive &ldquo;web scale&rdquo;.</p>
<p>Ruby on Rails is exactly where it should be: starting to be adopted by a new crop of Silicon Valley tech start-ups trying to make a dent in the universe in this new Ajax-based Cloud-era. Remember that Rails was first released with RJS, a way to write Javascript through Ruby using Prototype and Scriptaculous. jQuery was not even mainstream yet.</p>
<p>Twitter shows up. Groupon shows up. Engine Yard shows up. And much more follow.</p>
<p>It&rsquo;s all happening blistering fast, bear in mind that I am only touching the surface here.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/4XpnKHJAok8"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p><strong>2007</strong> arrives and at least 3 big things happen:</p>
<ul>
<li>Another Rails-based start-up is born: Heroku. If AWS EC2 coined the term &ldquo;Infrastructure as a Service&rdquo; (IaaS) Heroku defined what a &ldquo;Platform as Service&rdquo; (PaaS) means. It was able to put EC2 to good use and make its power approachable.</li>
<li>At the same time, developers have grown bitter of version control systems such as CVS and again, big vendor options such as Rational ClearCase, Microsoft SourceSafe, and others were just terrible. Subversion had emerged but its many shortcomings would become its demise. Linus Torvalds decides to fix the problem and release Git. Mainstream developers finally learn basic computer science terms such as DAGs (&ldquo;Directed Acyclic Graphs&rdquo;).</li>
<li>The iPhone is released and a new mobile revolution takes place. Dominant players like Nokia and Blackberry were shaken to their core. Developers want a piece of it. Mobile Safari becomes high profile overnight. And its WebKit core rises to stardom, eclipsing old veterans like Firefox/Gecko and Opera.</li>
</ul>
<p><strong>2008</strong>: the birth of GitHub and the dawn of the DevOps.</p>
<p>Git was great but too low level. Several other contenders tried to become the new standard such as Mercurial and Bazaar. But the birth of GitHub would change all that. Again, another Rails-based start-up making the news.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/653/Theres-An-App-for-That-Tanglewood-Moms.jpg" alt="There’s an App for that"  loading="lazy" /></p>
<p>But the honeymoon is showing signs of reaching an end: prominent Ruby developer, Zed Shaw, <a href="https://techcrunch.com/2008/01/01/zed-shaw-puts-the-smack-down-on-the-rails-community/"target="_blank" rel="noopener">loudly leaves the community</a> by January.</p>
<p>During this year we also had the cold war between Engine Yard&rsquo;s Merb framework and 37signals&rsquo; Rails, resulting in the <a href="http://yehudakatz.com/2008/12/23/rails-and-merb-merge/"target="_blank" rel="noopener">controversial Rails-Merb merge</a>, placing another crack in the community.</p>
<p>But we still had a lot of work to do, so we moved on.</p>
<p><strong>2009</strong>: &ldquo;there&rsquo;s an app for that!&rdquo;</p>
<p>Apple finally releases a proper native SDK. Developers start building a new generation of productivity apps and games. WebKit starts growing in market share. Apple solidifies the concept of aesthetic in software development and Ruby on Rails is at its high at that point.</p>
<p>We begin the NoSQL era. When data storage and fetch start to really become a bottleneck, we quickly learn that traditional enterprisey databases are not &ldquo;web scale&rdquo; enough. MongoDB leads the pack. Web development starts an era of craze over NoSQL.</p>
<p>In parallel, Twitter, once the poster-child of the Rails generation, creates a crack in the Ruby community when it declares that it&rsquo;s started to use yet another obscure language: <a href="http://www.artima.com/scalazine/articles/twitter_on_scala.html"target="_blank" rel="noopener">Scala</a>!</p>
<p>This starts the endless obsession era about &ldquo;concurrency&rdquo;, yet another elusive concept that many talk big and talk a lot but only a few actually grasp its meaning. It&rsquo;s the start of the window of opportunity for Node.js, Scala, Erlang, Clojure, Go.</p>
<p>The honeymoon is over.</p>
<p><strong>2010</strong>:</p>
<ul>
<li>The demise of Sun and Oracle as the new stewardship for Java. If things were not playing out cool for Java, that was a heavy nail in the coffin.</li>
<li>Apple declares open war against Flash. Another big push for Javascript and front-end heavy web apps.</li>
</ul>
<p><a href="https://www.apple.com/hotnews/thoughts-on-flash/"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/656/apple-no-flash-250x376.jpg" alt="Thoughts on Flash"  loading="lazy" /></a></p>
<p>In my personal opinion, this is the year when Rails reached its peak in terms of influence. It was the go-to stack for developers in Macs wanting to create their brand new Lean Start-up.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/icMA9rYNmgU"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h3>2011: A New Era<span class="hx:absolute hx:-mt-20" id="2011-a-new-era"></span>
    <a href="#2011-a-new-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In around 6 years the software development market did titanic shifts:</p>
<ul>
<li>Distributed development derived from years of Open Source workflow becomes the new standard. Git and GitHub were key players. Big vendors have to play catch-up with their stinky enterprise alternatives such as TFS.</li>
<li>Agile is taken seriously in a new crop of tech startups evangelizing proper software development. Tests shift from unproductive obnoxious things into invaluable core technique. Tools like Hudson/Jenkins combined with Git workflow pave the way for what continuous integration and later continuous deployment would become.</li>
<li>AWS and the Heroku example pave the way to make old IT obsolete and create a new generation of infrastructure as software, and &ldquo;DevOps&rdquo;. There is now the urge to deploy super web scale with zero friction. Time to market becomes a key factor: release early, release often.</li>
<li>Apple releases the AppStore and helps drive the Software as Service market, with apps requiring live internet to function. The &ldquo;Cloud&rdquo; reaches the masses.</li>
<li>By making everybody on Earth carry an always-on powerful device, a smartphone, we were never so close to the Orwellian future of 1984. Everything a person does is a data point. Big Data becomes a thing. Data Sciences becomes viable as a mainstream profession. I.A. related disciplines become a necessity to reason about so much data.</li>
</ul>
<p>Android is getting closer to the iPhone. Microsoft is resurrecting from its ashes. In October, Steve Jobs dies, marking the end of the era of Apple&rsquo;s heavy influence.</p>
<p>But around 2011 another thing happened: we win.</p>
<p>Independent developers surpassed the status quo of enterprise developers. There is a shift of power. The conventions for the software development field don&rsquo;t come from the old big tool vendors anymore. We are not listening to IDE vendors like Microsoft, BEA, Oracle. We are now listening to the winners of this generation of startups: Apple, Google, Facebook.</p>
<p>The concept of <em>&ldquo;one language to rule them all&rdquo;</em> was shattered in the web camp by Ruby on Rails and in the native application space by Apple. 2 very unusual and strange languages, Ruby and Objective-C, reached mainstream status. They proved that developers can learn and master exotic languages turning them into profitable products.</p>
<p>With those foundations set, a new <strong>gold rush</strong> emerges: new languages galore, free from the notion that to become successful one had to stick to Java, C#, or C++ like languages, people finally started looking elsewhere to differentiate themselves, and thus a new generation of exotic languages arrive: Scala, Haskell, Ocaml, F#, Erlang, Clojure, Rust, Elixir, etc. The sky is the limit.</p>
<p>By killing Flash and accelerating HTML 5, ES6, and with all the controversies around concurrency, Javascript gained new life and fast adoption unlike nothing else before.</p>
<h3>Epilogue<span class="hx:absolute hx:-mt-20" id="epilogue"></span>
    <a href="#epilogue" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Is Ruby on Rails dead? No.</p>
<p>Is Ruby on Rails still the leader of the pack? No.</p>
<p>What is the legacy of the Ruby on Rails Generation? It played an important role in defining what we now consider the new status quo for web development: Git, CI, CD, Cloud, Metrics. It brought the Apple aesthetics thinking to the table in a big way.</p>
<p>That new crop of tech startups had a serious side-effect though: it put new engineers in the spotlight, elevating them to rock star levels and turning them into Steve Jobs wannabes. They just started taking everything we accomplished for granted. Recipe for disaster.</p>
<p>And while the big vendors&rsquo; influence became thin compared to the 90&rsquo;s, they are not dead. After 2011 we ushered in a new era of chaos, an era of dissatisfied developers. Where every couple of years a new derivation emerges and we have that unsatisfying feeling that we need to rewrite everything all the time.</p>
<p>The Ruby on Rails pool got diluted into smaller niche languages such as Javascript itself, Go, Elixir, Scala.</p>
<p>The Java/Microsoft mainstream is gaining new traction. Aesthetics also got diluted. Careful engineering is getting diluted. Social networks ubiquity and the easy path of quick cloud deployment is making people less and less patient. Crafting requires patience. People crave Likes, a new generation of vanity metrics arose.</p>
<p>We now have a larger number of independent non-enterprise developers with little to no patience, with an exaggerated sense of confidence, and <a href="https://en.wikipedia.org/wiki/Not_invented_here"target="_blank" rel="noopener">not-invented-here</a> syndrome, taking everything for granted and caring less and less about proper value.</p>
<p>In conclusion, the death of Ruby on Rails is indeed greatly exaggerated, it will keep going for many years still - especially if the Ruby Core Team can actually deliver on the <a href="https://blog.heroku.com/ruby-3-by-3"target="_blank" rel="noopener">3x3 goal</a>. People like to share doomsday articles while optimistic and more realistic reports such as this <a href="https://reinteractive.com/posts/320-why-ruby-on-rails-is-still-the-best-choice"target="_blank" rel="noopener">Why Ruby on Rails is still the best choice?</a> are largely ignored. Such is the way of social networks vanity.</p>
<p>But it will not be what it once was because the times have changed. We can now live by the &ldquo;On Rails&rdquo; workflow without Ruby on Rails. And it can&rsquo;t go on breaking the status quo, because it became the status quo.</p>
<p>I wonder where that will lead us. I do have a few clues, but as I said, I am not in the game of publishing click-bait headlines.</p>
]]></content:encoded><category>rails</category><category>web-development</category><category>software-engineering</category></item><item><title>Updating my Old Posts on Uploads</title><link>https://akitaonrails.github.io/en/2016/07/28/updating-my-old-posts-on-uploads/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/07/28/updating-my-old-posts-on-uploads/</guid><pubDate>Thu, 28 Jul 2016 11:56:00 -0300</pubDate><description>&lt;p&gt;Fortunately things usually change for the best, and practices that &amp;ldquo;worked ok&amp;rdquo; in the past might be obsolete by now.&lt;/p&gt;
&lt;p&gt;That happens to old blog posts, including my own as I&amp;rsquo;ve been posting for several years.&lt;/p&gt;
&lt;p&gt;In this post I will fix what I posted (in pt-BR):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.akitaonrails.com/2014/12/18/small-bites-direct-upload-para-s3-a-solucao-definitiva"target="_blank" rel="noopener"&gt;Direct Upload para S3: a Solução Definitiva!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.akitaonrails.com/2014/03/26/heroku-tips-s3-direct-upload-carrierwave-sidekiq"target="_blank" rel="noopener"&gt;S3 Direct Upload + Carrierwave + Sidekiq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.akitaonrails.com/2010/06/23/akita-responde-upload-de-arquivos"target="_blank" rel="noopener"&gt;Upload de Arquivos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.akitaonrails.com/2012/04/20/heroku-tips-enciclopedia-do-heroku"target="_blank" rel="noopener"&gt;Enciclopédia do Heroku&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They are still good posts to understand the mechanics of what I will propose here, so you should read them if you&amp;rsquo;re interested in how things work.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Fortunately things usually change for the best, and practices that &ldquo;worked ok&rdquo; in the past might be obsolete by now.</p>
<p>That happens to old blog posts, including my own as I&rsquo;ve been posting for several years.</p>
<p>In this post I will fix what I posted (in pt-BR):</p>
<ul>
<li><a href="http://www.akitaonrails.com/2014/12/18/small-bites-direct-upload-para-s3-a-solucao-definitiva"target="_blank" rel="noopener">Direct Upload para S3: a Solução Definitiva!</a></li>
<li><a href="http://www.akitaonrails.com/2014/03/26/heroku-tips-s3-direct-upload-carrierwave-sidekiq"target="_blank" rel="noopener">S3 Direct Upload + Carrierwave + Sidekiq</a></li>
<li><a href="http://www.akitaonrails.com/2010/06/23/akita-responde-upload-de-arquivos"target="_blank" rel="noopener">Upload de Arquivos</a></li>
<li><a href="http://www.akitaonrails.com/2012/04/20/heroku-tips-enciclopedia-do-heroku"target="_blank" rel="noopener">Enciclopédia do Heroku</a></li>
</ul>
<p>They are still good posts to understand the mechanics of what I will propose here, so you should read them if you&rsquo;re interested in how things work.</p>
<p><strong>TL;DR</strong> is: use Cloudinary (with Attachinary) and install a CDN (Cloudfront) right now as it&rsquo;s super trivial to do.</p>
<blockquote>
  <p>Uploading files is not a task to be taken lightly. It&rsquo;s one of those tasks that you must not consider that it &ldquo;works&rdquo; just because it works in your development machine, for a few trivial uploads. Production level behavior changes drastically and it can bring your entire project down depending on how heavily it depends on user-generated content.</p>

</blockquote>
<p>There are a lot of moving parts to understand. But in the most basic solutions you just create an HTML multipart form with a file input field and submit it to some controller endpoint directly. This controller receives the blob binary and you save it to some local directory in the server.</p>
<p>This is the <strong>worst possible solution</strong> and unfortunately this is the one you will find the most across the web.</p>
<p>There are many problems to deal with this simple implementation:</p>
<ul>
<li>We are in the era of 12+ megapixel cameras. Those are multi-megabyte sized files. Each upload will take a long time, especially if the user is trying to upload using unreliable or slow wireless networks.</li>
</ul>
<p>Once the user&rsquo;s browser connects to the web application it will hold that connection and block the entire process through the duration of the request-response cycle. Making this simple to understand, if you only have 1 single process in the server, it will be unable to respond to any other request until the upload finishes.</p>
<p>Fortunately most deployments will use NGINX in front of the web application - as they should - so this effect is minimized as NGINX will receive the entire blob before passing it to the web application process underneath, making it not so problematic in most cases.</p>
<p>But if you&rsquo;re on Heroku, the router layer has a <strong>30 seconds timeout</strong> limit. If the upload takes more than that (which is common with users in unreliable networks) this layer will cut the connection down before it finishes. Users will retry and this can cause your request queues to grow very fast.</p>
<p>Avoid uploading anything to Heroku. I consider this a <strong>feature</strong> as it forces applications deployed on Heroku to use <strong>good practices</strong> as I will explain below.</p>
<ul>
<li>You should also NOT save files to the local filesystem if you want to scale horizontally, as one server will not be able to see what&rsquo;s in the other server unless you&rsquo;re in AWS EC2 or other IaaS, with shared mounted volumes, for example. In Heroku, you can&rsquo;t rely on anything in the filesystem as the machines are volatile and whenever a dyno restarts it loses whatever was not there during the deployment.</li>
</ul>
<p>So you must upload what you received to an external storage, such as AWS S3, and this is also slow if you receive lots of uploaded files. You can rely on NGINX or Heroku&rsquo;s router layer to handle the heavy receiving of the files but you will make the request-response cycle super slow if you synchronously process and upload the files to S3.</p>
<p>The next option you think of is to add Sidekiq or some other Async Job mechanism to do this other heavy lifting. Now you have to create front-end placeholders to show to the users while the background job is not finished sending the files to S3.</p>
<p>The last option, if you dig deeper, is browser-based <strong>direct uploads to S3</strong> and just posting the URLs to your web application. This is the ideal solution, but it&rsquo;s not easy to implement in your stack.</p>
<ul>
<li>Once you nail everything above, you still make the mistake of putting the S3 URLs directly in the IMG tags in your HTML, which is not recommended by Amazon as S3 is recommended just for &ldquo;storage&rdquo; purposes. This is the easy part to fix, but most projects forget about that.</li>
</ul>
<h3>Cloudinary or Carrierwave Direct<span class="hx:absolute hx:-mt-20" id="cloudinary-or-carrierwave-direct"></span>
    <a href="#cloudinary-or-carrierwave-direct" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In my previous posts I explained each of the above issues in more detail and I offer some (complicated) solutions, such as using the Refile project. At that time the Carrierwave plugins for Direct Upload were not ready.</p>
<p>Nowadays the solution is actually super easy and this is what I recommend: go straight to <a href="http://cloudinary.com/documentation/rails_integration"target="_blank" rel="noopener">Cloudinary</a>. This is a Software as a Service solution for proper direct uploads and dynamic image processing. You should definitely explore the <a href="https://github.com/assembler/attachinary"target="_blank" rel="noopener">Attachinary</a> gem to make the process easier.</p>
<p>If you have legacy Carrierwave, Paperclip, Shrine, Dragonfly, Refile or others, one jerry-rigged - but reasonable - alternative is to just add Attachinary to the mix.</p>
<p>Let&rsquo;s say you have an old <code>user.avatar</code> uploader. You can just create a new <code>user.new_avatar</code> with Attachinary/Cloudinary. In the HTML form upload you start using just the Attachinary helpers. And in the HTML where you show the image itself, you can make a helper to check for <code>@user.new_avatar?</code> and show using <code>cl_image_tag(@user.avatar.path</code> instead, otherwise you show the old URLs.</p>
<p>You don&rsquo;t need to totally migrate everything to Cloudinary, just keep the old assets where they are and start putting the new assets in the Cloudinary configuration. It has a Free tier that can hold up to 75 thousand images and allows traffic of up to 5GB. And with just USD 44 a month you have 10 times that. So it&rsquo;s really cheap if your application is serious about file uploading.</p>
<p>If you don&rsquo;t want to commit to an external service yet, and you&rsquo;re using Carrierwave, another option you must consider is testing out <a href="https://github.com/dwilkie/carrierwave_direct"target="_blank" rel="noopener">CarrierwaveDirect</a> to test direct uploading to S3 from the browser. This will at least avoid the complexities of setting up async uploads from the web application to S3.</p>
<p>Update: <a href="https://github.com/janko-m"target="_blank" rel="noopener">Janko Marohnić</a> posted a very good comment that I think is important to quote directly:</p>
<blockquote>
  <p>Great writeup, I wholeheartedly agree that we probably don&rsquo;t &ldquo;just want something simple&rdquo;, especially concerning synchronous uploads and CDNs.</p>

</blockquote>
<blockquote>
  <p>Shrine actually has a <a href="https://github.com/janko-m/shrine-cloudinary"target="_blank" rel="noopener">Cloudinary integration</a>, and it&rsquo;s pretty advanced too; it supports direct uploads, setting upload options, storing Cloudinary metadata (hello <a href="http://cloudinary.com/blog/introducing_intelligent_responsive_image_breakpoints_solutions"target="_blank" rel="noopener">responsive breakpoints</a>) etc :). Also, the Cloudinary gem ships with a CarrierWave integration. I think it&rsquo;s beneficial to use Shrine or CarrierWave with Cloudinary, because then you get to keep your file attachments library, and just change the underlying storage service.</p>

</blockquote>
<blockquote>
  <p>As for CarrierwaveDirect, I don&rsquo;t think it&rsquo;s actually very useful, because backgrounding you need to do all by yourself anyway, so basically all it gives you is the ability to generate a direct-upload form to S3. But generating request parameters and URL to S3 is something that&rsquo;s already built in into the official <a href="https://github.com/aws/aws-sdk-ruby"target="_blank" rel="noopener">aws-sdk</a> gem, CarrierwaveDirect just seems to be reimplementing that logic. And the advantage of generating this through aws-sdk is that it&rsquo;s not HTML-specific, so you could setup an endpoint which returns this information as JSON, and now you have your multiple file uploads directly to S3 :)</p>

</blockquote>
<p>As a disclaimer I didn&rsquo;t test CarrierwaveDirect myself and I remember being quite broken 2 years ago. And I&rsquo;ve heard good things about Shrine along the way but never did proper testing. I&rsquo;d recommend giving it a try if you want to migrate away from Carrierwave.</p>
<h3>Don&rsquo;t serve assets from S3, use a CDN, any CDN<span class="hx:absolute hx:-mt-20" id="dont-serve-assets-from-s3-use-a-cdn-any-cdn"></span>
    <a href="#dont-serve-assets-from-s3-use-a-cdn-any-cdn" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you&rsquo;re sticking to S3 uploading, at the very least avoid serving those files from S3. In my &ldquo;Heroku Encyclopedia&rdquo; post I recommend using &ldquo;S3 Assets Sync&rdquo;. DO NOT DO IT: use a CDN instead, it&rsquo;s faster, easier and the correct solution.</p>
<p>The first thing you want to do is sign up to AWS&rsquo;s Cloudfront CDN service. Always use a CDN for all your assets, not only the ones <a href="http://www.algonauti.com/posts/speed-up-ugc-with-cloudfront"target="_blank" rel="noopener">internal to your web application</a> but also every user uploaded content. It&rsquo;s <a href="https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn"target="_blank" rel="noopener">super simple</a> to create a new CDN endpoint pointing to your S3 bucket.</p>
<p>Carrierwave will default to the S3 bucket host, but once you have the Cloudfront endpoint you can <a href="https://dzone.com/articles/carrierwave-heroku-cloudfront"target="_blank" rel="noopener">easily change</a> all the uploaded files URLs like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ./config/initializers/carrierwave.rb</span>
</span></span><span class="line"><span class="cl"><span class="no">CarrierWave</span><span class="o">.</span><span class="n">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">fog_credentials</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:provider</span> <span class="o">=&gt;</span> <span class="s2">&#34;AWS&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:aws_access_key_id</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;AWS_ACCESS_KEY_ID&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:aws_secret_access_key</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;AWS_SECRET_ACCESS_KEY&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">fog_directory</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;S3_BUCKET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># use only one of the following 2 settings</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># config.fog_host = &#34;http://#{ENV[&#39;S3_BUCKET&#39;]}.s3.amazonaws.com&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">fog_host</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;S3_CDN&#39;</span><span class="o">]</span> <span class="c1"># for cloudfront</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You must obviously configure the environment variable <code>S3_CDN</code> to point to the Cloudfront endpoint specific to the particular S3 bucket you&rsquo;re using.</p>
<p>The important part is: serve ALL ASSETS from a CDN, no matter what. Never directly from your web application, avoid directly serving from S3. It&rsquo;s almost trivial to implement and if you don&rsquo;t like AWS for any reason, you can choose Fastly, Cloudflare and many others.</p>
<p>They all work the same way. There is NO excuse to not use a CDN no matter the size of your application.</p>
]]></content:encoded><category>cloud</category><category>web-development</category></item><item><title>Phoenix "15 Minute Blog" comparison to Ruby on Rails</title><link>https://akitaonrails.github.io/en/2015/11/20/phoenix-15-minute-blog-comparison-to-ruby-on-rails/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/11/20/phoenix-15-minute-blog-comparison-to-ruby-on-rails/</guid><pubDate>Fri, 20 Nov 2015 17:52:00 -0200</pubDate><description>&lt;p&gt;&lt;em&gt;Update 11/23/15:&lt;/em&gt; Chris McCord, Phoenix creator, just posted an article explaining why &lt;a href="https://dockyard.com/blog/2015/11/18/phoenix-is-not-rails"target="_blank" rel="noopener"&gt;&amp;ldquo;Phoenix is not Rails&amp;rdquo;&lt;/a&gt; It goes in detail in many things I described in this article and I highly recommend you read it too.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been following the Elixir blogosphere, chances are that you stumbled upon &lt;a href="https://medium.com/@diamondgfx"target="_blank" rel="noopener"&gt;Brandon Richey&amp;rsquo;s&lt;/a&gt; take on the classic &amp;ldquo;15 minute blog&amp;rdquo;. If you didn&amp;rsquo;t you must read at least &lt;a href="https://medium.com/@diamondgfx/introduction-fe138ac6079d#.ffl48saew"target="_blank" rel="noopener"&gt;Part 1&lt;/a&gt; and &lt;a href="https://t.co/DjGDek1ZOk"target="_blank" rel="noopener"&gt;Part 2&lt;/a&gt;. It&amp;rsquo;s a very detailed tutorial that will make it easier to get your feet wet with Phoenix goodness.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>Update 11/23/15:</em> Chris McCord, Phoenix creator, just posted an article explaining why <a href="https://dockyard.com/blog/2015/11/18/phoenix-is-not-rails"target="_blank" rel="noopener">&ldquo;Phoenix is not Rails&rdquo;</a> It goes in detail in many things I described in this article and I highly recommend you read it too.</p>
<p>If you&rsquo;ve been following the Elixir blogosphere, chances are that you stumbled upon <a href="https://medium.com/@diamondgfx"target="_blank" rel="noopener">Brandon Richey&rsquo;s</a> take on the classic &ldquo;15 minute blog&rdquo;. If you didn&rsquo;t you must read at least <a href="https://medium.com/@diamondgfx/introduction-fe138ac6079d#.ffl48saew"target="_blank" rel="noopener">Part 1</a> and <a href="https://t.co/DjGDek1ZOk"target="_blank" rel="noopener">Part 2</a>. It&rsquo;s a very detailed tutorial that will make it easier to get your feet wet with Phoenix goodness.</p>
<p>This post is intended for Rails programmers wanting to know how the Phoenix Framework compares. It&rsquo;s not a completely fair comparison as this is just the good old nested resources &ldquo;hello world&rdquo; exercise. This will only scratch the surface but should serve as a good introduction.</p>
<p>For better or for worse, many still consider Ruby on Rails to be the best DSL for a web application. Rails was successful in creating a very recognizable vocabulary to describe each component of a web project. And I will argue that one of Phoenix&rsquo;s strengths is to successfully borrow the same metaphor. This definitely makes the learning curve much smoother.</p>
<p>If you just want to clone the exercises and jump right into the code itself, I have the original <a href="https://github.com/akitaonrails/pxblog_phoenix_exercise"target="_blank" rel="noopener">Pxblog in Phoenix</a> and the comparison <a href="https://github.com/akitaonrails/pxblog_rails_exercise"target="_blank" rel="noopener">Pxblog in Rails</a>.</p>
<h3>Getting Started: command line and folder structure<span class="hx:absolute hx:-mt-20" id="getting-started-command-line-and-folder-structure"></span>
    <a href="#getting-started-command-line-and-folder-structure" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Without further ado, let&rsquo;s start comparing the basic console commands:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rails new pxblog
rails g scaffold Post title:string body:string
rails g scaffold User username:string email:string password_digest:string
rake db:create
rake db:migrate
rails g migration AddUserIdToPosts
rails server</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix phoenix.new pxblog
mix phoenix.gen.html Post posts title:string body:string
mix phoenix.gen.html User users username:string email:string password_digest:string
mix ecto.create
mix ecto.migrate
mix ecto.gen.migration add_user_id_to_posts
mix phoenix.server
# iex -S mix phoenix.server to start within IEx and be able to IEx.pry</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Right off the bat we feel at home. In the Rails world we have both the <tt>rails</tt> command competing with traditional <tt>rake</tt> task managers. At the Phoenix side, they fortunately concentrated everything under the Elixir built-in <tt>mix</tt> command. There seem to be discussions about moving the Rails command tasks to Rake where they belong, but it&rsquo;s not coming soon.</p>
<p>As it is all under Mix territory, you can list Phoenix-related tasks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ mix help | grep -i phoenix

mix phoenix.digest      # Digests and compress static files
mix phoenix.gen.channel # Generates a Phoenix channel
mix phoenix.gen.html    # Generates controller, model and views for an HTML based resource
mix phoenix.gen.json    # Generates a controller and model for a JSON based resource
mix phoenix.gen.model   # Generates an Ecto model
mix phoenix.gen.secret  # Generates a secret
mix phoenix.new         # Create a new Phoenix v1.0.3 application
mix phoenix.routes      # Prints all routes
mix phoenix.server      # Starts applications and their servers</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The directory structure that <tt>phoenix.new</tt> generates is slightly different from Rails though:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/_build              # binary stuff mix compiles (ignore)
/config
- config.exs         # think Rails&#39; /config/application.rb
- dev.exs            # think Rails&#39; /config/environments/development.rb
- prod.exs           # think Rails&#39; /config/environments/production.rb
- prod.secret.exs    # think Rails&#39; /config/secrets.yml
- test.exs           # think Rails&#39; /config/environments/test.rb
/deps                # where mix deps.get put dependencies
/lib
  /pxblog
    - endpoint.ex    # a bit like config.ru and application.rb
    - repo.ex        # setup for Ecto
  - pxblog.ex        # where you can set up the OTP apps supervision tree 
/node_modules        # Phoenix integrates with Node.js
/priv
  /repo
    /migrations      # think Rails&#39; /db/migrate
    - seeds.exs      # think Rails&#39; /db/seeds.rb
  /static            # think Rails&#39; /public
    /css
    /images
    /js
/test                # think Rails&#39; /test
  /channels
  /controllers
  /models
  /support
  /views
  - test_helper.exs  # think Rails&#39; /test/test_helper.rb
/web                 # think Rails&#39; /app
  /channels          # think Rails 5&#39;s ActionCable channels
  /controllers
  /models
  /static            # think Rails&#39; /app/assets
    /assets
      /images
      /css
      /js
      /vendor
  /templates         # think Rails&#39; /app/views
    /layout
  /views             # think Rails&#39; /app/helpers but with Presenters
    - layout_view.ex
  - router.ex        # think Rails&#39; /config/routes.rb
  - web.ex           # macros to configure each MVC component
- .gitignore
- README.md
- brunch-config.js  # front-end dev reloading is controlled with Brunch
- mix.exs           # think Ruby&#39;s Gemfile (with extras)
- package.json      # Node.js dependencies</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Rails, the starting point is the <tt>config.ru</tt> Rackup application (as Rails has been a Rack app since 3.0). It then loads the <tt>config/environment.rb</tt>, then <tt>config/application.rb</tt>, then <tt>config/boot.rb</tt> which loads the gems declared in the <tt>Gemfile</tt>, together with <tt>config/initializers/*.rb</tt> and each file in <tt>config/environments</tt>, where we set up the Rails::Application and plug in configuration and the pipeline of Rack Middlewares.</p>
<p>In Phoenix, the starting point - as in any Elixir app &ndash; is the <tt>mix.exs</tt> file. In this case it points to the &ldquo;Pxblog&rdquo; OTP/Phoenix app defined in <tt>lib/pxblog/pxblog.ex</tt>. In turn it starts up and supervises the &ldquo;Pxblog.Endpoint&rdquo; and &ldquo;Pxblog.Repo&rdquo; apps, which are defined in <tt>lib/pxblog/endpoint.ex</tt> and <tt>lib/pxblog/repo.ex</tt>, respectively.</p>
<p>If you build other OTP apps, this is where you can add to the OTP Supervisor Tree. I could say that it&rsquo;s something akin to a Rails Engine, although technically it is not the same thing, but this metaphor will do.</p>
<p>One main aspect of Rails is how it divides development, test, and production configuration in different files. Phoenix has the same thing at <tt>dev.exs</tt>, <tt>test.exs</tt>, and <tt>prod.exs</tt>. This is actually a <a href="http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html#environments"target="_blank" rel="noopener">Mix feature</a>. Mix is a more accomplished version of Rake, and it makes sense, as José Valim also tried to push Thor to replace Rake, although it never picked up steam in the Ruby community (Rake being so ingrained everywhere). Web frameworks that don&rsquo;t enforce separation of environments by default, in this day and age, are useless. The cool thing is that every Elixir app generated through Mix gets this same useful feature.</p>
<p>Instead of having a <tt>database.yml</tt>, the database configuration is spread through the environment configuration files and the production settings are in a separate <tt>prod.secret.exs</tt> file, which is obviously ignored in <tt>.gitignore</tt>, like Rails&rsquo; <tt>secrets.yml</tt> file.</p>
<h3>MVC structure<span class="hx:absolute hx:-mt-20" id="mvc-structure"></span>
    <a href="#mvc-structure" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>You will see that each element of the MVC app in Phoenix starts like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/page_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">PageController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="c1"># web/models/post.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Post</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:model</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="c1"># web/views/post_view.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">PostView</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:view</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="c1"># web/router.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Router</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:router</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This <tt>Pxblog.Web</tt> module is defined in the <tt>web/web.ex</tt> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/web.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">model</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">use</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Model</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">import</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Changeset</span>
</span></span><span class="line"><span class="cl">      <span class="n">import</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Query</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">from</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">from</span><span class="p">:</span> <span class="mi">2</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">controller</span> <span class="k">do</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">view</span> <span class="k">do</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">router</span> <span class="k">do</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">channel</span> <span class="k">do</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="vi">@doc</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  When used, dispatch to the appropriate controller/view/etc.
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">__using__</span><span class="p">(</span><span class="n">which</span><span class="p">)</span> <span class="k">when</span> <span class="n">is_atom</span><span class="p">(</span><span class="n">which</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply</span><span class="p">(</span><span class="n">__MODULE__</span><span class="p">,</span> <span class="n">which</span><span class="p">,</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you haven&rsquo;t yet, this is a good time to learn about <a href="http://elixir-lang.org/getting-started/meta/macros.html"target="_blank" rel="noopener">Elixir Macros</a>. Think of the code in the <tt>quote</tt> block as being &ldquo;injected&rdquo; in each module that calls <tt>use Pxblog.Web</tt>. When you <tt>use</tt> a module it calls the <tt><strong>using</strong></tt> macro. Think of it like a Ruby Module Mixin calling the <tt>included</tt> callback and executing a <tt>class_eval</tt>. As there is no concept of Classes and subclasses, we include mixins in each module to acquire the desired behaviours.</p>
<h3>MVC Router<span class="hx:absolute hx:-mt-20" id="mvc-router"></span>
    <a href="#mvc-router" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Unlike Rails&rsquo; <tt>config/routes.rb</tt>, which defines routes, the <tt>web/router.ex</tt> defines not only the routes themselves but also transformation pipelines and routing strategies:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/router.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Router</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:router</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">pipeline</span> <span class="ss">:browser</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:accepts</span><span class="p">,</span> <span class="o">[</span><span class="s2">&#34;html&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:fetch_session</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:fetch_flash</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:protect_from_forgery</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:put_secure_browser_headers</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">pipeline</span> <span class="ss">:api</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">plug</span> <span class="ss">:accepts</span><span class="p">,</span> <span class="o">[</span><span class="s2">&#34;json&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">scope</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="no">Pxblog</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">pipe_through</span> <span class="ss">:browser</span> <span class="c1"># Use the default browser stack</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">get</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="no">PageController</span><span class="p">,</span> <span class="ss">:index</span>
</span></span><span class="line"><span class="cl">    <span class="n">resources</span> <span class="s2">&#34;/users&#34;</span><span class="p">,</span> <span class="no">UserController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">resources</span> <span class="s2">&#34;/posts&#34;</span><span class="p">,</span> <span class="no">PostController</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">resources</span> <span class="s2">&#34;/sessions&#34;</span><span class="p">,</span> <span class="no">SessionController</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:delete</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Other scopes may use custom stacks.</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># scope &#34;/api&#34;, Pxblog do</span>
</span></span><span class="line"><span class="cl">  <span class="c1">#   pipe_through :api</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You have to read it like this:</p>
<p>The <tt>pipeline</tt> block plugs &ldquo;filters&rdquo;. They are similar to Rack middlewares that we define in <tt>config/application.rb</tt> in Rails. This is very clever because one key difference between vanilla Ruby on Rails and the <a href="https://github.com/rails-api/rails-api"target="_blank" rel="noopener">Rails-API project</a> is the removal of unneeded Rack middlewares that API endpoints don&rsquo;t need.</p>
<p>In Phoenix we can define one pipeline for web browsers and another for API clients, and you can see that the difference is the removal of Plugs.</p>
<p>Then we define the scopes based on the root paths. There is one scope &ldquo;/&rdquo; which connects to the <tt>:browser</tt> pipeline and an optional (commented out) scope &ldquo;/api&rdquo; which pipes through the <tt>:api</tt> pipeline.</p>
<p>Inside the scope block, it&rsquo;s very similar to the RESTful DSL used to define routes, which again is a good adaptation from Rails Routes. <a href="http://www.phoenixframework.org/docs/routing"target="_blank" rel="noopener">Read the documentation</a> to know the details.</p>
<p>Also similar to Rails Routes, it generates proper URL helpers that become available in Controllers, Views and Templates. Let&rsquo;s start by seeing some Rails URL helpers in code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/posts/edit.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link_to &#39;Show&#39;, [@user, post] %&gt;
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link_to &#39;Edit&#39;, edit_user_post_path(@user, post) %&gt;
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link_to &#39;Destroy&#39;, [@user, post], method: :delete, data: { confirm: &#39;Are you sure?&#39; } %&gt;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And in Phoenix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- web/templates/post/edit.html.eex --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link &#34;Show&#34;, to: user_post_path(@conn, :show, @user, post) %&gt;
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link &#34;Edit&#34;, to: user_post_path(@conn, :edit, @user, post) %&gt;
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link &#34;Delete&#34;, to: user_post_path(@conn, :delete, @user, post), method: :delete, data: [confirm: &#34;Are you sure?&#34;] %&gt;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The main URL helper in Rails is able to take an Array such as <tt>[@user, post]</tt> and execute it the same way as if we wrote <tt>user_post_path(@user, post)</tt>. One difference from Rails is that instead of creating one helper for each HTTP verb you have just one helper per resource that accepts an extra parameter to indicate the verb. So we have <tt>post_path(@conn, :edit, post)</tt> instead of the Rails way of <tt>edit_post_path(post)</tt>.</p>
<p>As with Rails middlewares, a pipeline receives the request connection and pipes it through, transforming its metadata so it is cleaned up and usable within our controllers.</p>
<h3>MVC Controller<span class="hx:absolute hx:-mt-20" id="mvc-controller"></span>
    <a href="#mvc-controller" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In Phoenix we start a controller like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/post_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">PostController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Post</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:scrub_params</span><span class="p">,</span> <span class="s2">&#34;post&#34;</span> <span class="k">when</span> <span class="n">action</span> <span class="k">in</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:assign_user</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="ss">:authorize_user</span> <span class="k">when</span> <span class="n">action</span> <span class="k">in</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:delete</span><span class="o">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s similar to this Rails controller setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/controllers/posts_controller.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PostsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="n">before_action</span> <span class="ss">:assign_user</span>
</span></span><span class="line"><span class="cl">  <span class="n">before_action</span> <span class="ss">:authorize_user</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:update</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As you may have concluded, a <tt>plug</tt> call works a bit like a <tt>before_action</tt> pipeline. The <tt>scrub_params</tt> I believe is similar to Rails&rsquo; <tt>ActionDispatch::ParamParser</tt>, but I&rsquo;m not sure; I know it clears out empty string values to nils so you don&rsquo;t update your models unnecessarily.</p>
<p>But unlike Rails, where a call to <tt>redirect_to</tt> halts the pipeline, we need to explicitly halt the pipeline like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">assign_user</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="sx">%{&#34;user_id&#34; =&gt; user_id}</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">params</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span><span class="p">,</span> <span class="n">user_id</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">assign</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:user</span><span class="p">,</span> <span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">put_flash</span><span class="p">(</span><span class="ss">:error</span><span class="p">,</span> <span class="s2">&#34;Invalid user!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">redirect</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span> <span class="n">page_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:index</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">halt</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Phoenix, everything revolves around a request connection transformation pipeline that you can start configuring in the Router Plugs, Controller Plugs and Controller actions. All of them receive the connection from the previous step and return a transformed connection to the next step until it becomes a proper HTTP response. Unlike Rails, this path is much more explicit and you know that you will get this connection and you should pipe transformations through it until you render the final HTML or send back some error header.</p>
<p>To see how explicit, let&rsquo;s start with a normal Rails controller action:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">destroy</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@user</span><span class="o">.</span><span class="n">destroy</span>
</span></span><span class="line"><span class="cl">  <span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="nb">format</span><span class="o">.</span><span class="n">html</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">users_url</span><span class="p">,</span> <span class="ss">notice</span><span class="p">:</span> <span class="s1">&#39;User was successfully destroyed.&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nb">format</span><span class="o">.</span><span class="n">json</span> <span class="p">{</span> <span class="n">head</span> <span class="ss">:no_content</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Once upon a time, setting the flash notice message and redirecting were 2 different methods; newer versions merged them together for convenience. Rails also has the concept of Responders, which Phoenix doesn&rsquo;t have yet (#OpportunityToContribute!).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">delete</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;id&#34; =&gt; id}</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get!</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">Repo</span><span class="o">.</span><span class="n">delete!</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">put_flash</span><span class="p">(</span><span class="ss">:info</span><span class="p">,</span> <span class="s2">&#34;User deleted successfully.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">redirect</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span> <span class="n">user_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:index</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can see that, Responders aside, the Phoenix version is <strong>remarkably</strong> similar. And it goes like this for all RESTful actions of the controller. But more similar to the deceased Merb, each Phoenix Controller action has a proper function signature declaring the parameters it expects to receive, instead of having a global <tt>params</tt> hash that needs to go through <a href="https://github.com/rails/strong_parameters"target="_blank" rel="noopener">Strong Parameters</a>.</p>
<p>For different variations of the same parameters, you can declare multiple functions with the same name but different arguments to pattern match, like in the <tt>SessionController</tt> example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/session_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">SessionController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;user&#34; =&gt; %{&#34;username&#34; =&gt; username, &#34;password&#34; =&gt; password}}</span><span class="p">)</span> <span class="k">when</span> <span class="ow">not</span> <span class="n">is_nil</span><span class="p">(</span><span class="n">username</span><span class="p">)</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">is_nil</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get_by</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="ss">username</span><span class="p">:</span> <span class="n">username</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sign_in</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">failed_login</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here we have the same <tt>create/2</tt> function with pattern matching and guards. The second version receives anything, in case the first version fails to pattern match against the incoming parameters. Phoenix expects roughly the same structure of parameters as Rails, so it&rsquo;s very intuitive to follow.</p>
<h3>MVC Models<span class="hx:absolute hx:-mt-20" id="mvc-models"></span>
    <a href="#mvc-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instead of the good old ActiveRecord (ActiveModel), in Phoenix we have to deal with <a href="http://www.phoenixframework.org/docs/ecto-models"target="_blank" rel="noopener">Ecto Models</a>. It already supports PostgreSQL, MySQL, SQLite3, MongoDB, so you&rsquo;re good to go for 99% of cases.</p>
<p>Ecto separates Model Logic from Model Persistence Management. Instead of using the Active Record design pattern, it favors the Data Mapper pattern. This is an old discussion among Railers. Many people dislike that persistence logic is kept together with business logic, and the heavy metaprogramming magic that makes ActiveRecord both very easy to get started with and very difficult to properly master.</p>
<p>You should read José Valim&rsquo;s post about <a href="http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/"target="_blank" rel="noopener">Ecto Associations</a> to get started. But for now, let&rsquo;s compare simple Rails and Ecto models:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/models/user.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">  <span class="n">has_secure_password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">has_many</span> <span class="ss">:posts</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">validates</span> <span class="ss">:username</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">  <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Rails we have the <a href="http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html"target="_blank" rel="noopener"><tt>ActiveSupport#has_secure_password</tt></a> which uses BCrypt underneath to generate a proper password digest. If you&rsquo;re building authentication from scratch you <strong>must</strong> use this construct.</p>
<p>Phoenix does not have the same feature yet (#OpportunityToContribute!) so its version is a bit more verbose to account for the BCrypt digest logic using the <a href="https://github.com/elixircnx/comeonin"target="_blank" rel="noopener">Comeonin</a> password hashing library. Let&rsquo;s go on in small steps:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/user.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:model</span>
</span></span><span class="line"><span class="cl">  <span class="n">import</span> <span class="no">Comeonin</span><span class="o">.</span><span class="n">Bcrypt</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">hashpwsalt</span><span class="p">:</span> <span class="mi">1</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">schema</span> <span class="s2">&#34;users&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:username</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:password_digest</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">timestamps</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Virtual Fields</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">virtual</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">virtual</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">has_many</span> <span class="ss">:posts</span><span class="p">,</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Post</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The first part of Ecto Models declares the database fields, virtual fields and associations. Rails ActiveRecord prefers the approach of asking the database to send the table metadata and uses metaprogramming to create all the fields later at runtime. Many people dislike this approach and this is the alternative: explicit declaration.</p>
<p>We are mapping the <tt>User</tt> module with the <tt>&lsquo;users&rsquo;</tt> database table in the <tt>schema &ldquo;users&rdquo; do</tt> statement instead of resorting to pluralization conventions.</p>
<p>The last line has our well-known <tt>has_many</tt> association.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@required_fields</span> <span class="o">~</span><span class="n">w</span><span class="p">(</span><span class="n">username</span> <span class="n">email</span> <span class="n">password</span> <span class="n">password_confirmation</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@optional_fields</span> <span class="o">~</span><span class="n">w</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here we have declared required fields. This is just a variable with a list of fields, not the validations themselves. This will be used in the next step to accomplish something similar to <tt>validates :username, presence: true</tt>.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">hash_password</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Instead of doing something like <tt>Post.create(params)</tt> we first create a changeset and then pass it to the Ecto main Repository. The Repository is then responsible for the persistence part. The Ecto Model is responsible for validating and cleaning up the changeset that the Repository receives.</p>
<p>The <tt>changeset/2</tt> returns an <a href="http://elixir-lang.org/getting-started/structs.html"target="_blank" rel="noopener">Elixir Struct</a> for us to work with before passing it to the Repository application for persistence.</p>
<p>In this function we can declare a pipeline of validations, constraints and other attribute transformations. For example, we plug in a <tt>hash_password/2</tt> function that will get the value in <tt>password</tt> and use Comeonin.<tt>hashpwsalt/1</tt> to transform the password string into a bcrypt digest and store it in the password_digest attribute:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">hash_password</span><span class="p">(</span><span class="n">changeset</span><span class="p">)</span> <span class="k">do</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">password</span> <span class="o">=</span> <span class="n">get_change</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span> <span class="ss">:password</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">changeset</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:password_digest</span><span class="p">,</span> <span class="n">hashpwsalt</span><span class="p">(</span><span class="n">password</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">changeset</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And we return the transformed changeset so the pipeline can pick it up and pass it to other plugs, such as validations. If we wanted to add more validations we could do it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:password</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:username</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">50</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">validate_confirmation</span><span class="p">(</span><span class="ss">:password</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:username</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">hash_password</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>There you go. And in the controller, the <tt>update/2</tt> function, for example, will use the changeset like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;id&#34; =&gt; id, &#34;user&#34; =&gt; user_params}</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get!</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">changeset</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">user_params</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="no">Repo</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">changeset</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In the second line we use the Repository to query the &lsquo;users&rsquo; schema as declared in the <tt>User</tt> model.</p>
<p>Then, we transform the <tt>user</tt> struct with the <tt>user_params</tt> map that we received from the Router pipeline, as defined in the first line.</p>
<p>The transformation returns a changeset, which will contain error messages. Then we pass the changeset to the Repository again so it updates the record in the table.</p>
<h3>MVC View and Templates<span class="hx:absolute hx:-mt-20" id="mvc-view-and-templates"></span>
    <a href="#mvc-view-and-templates" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the case of the <tt>edit/2</tt> function we call the <tt>render/3</tt> function like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/views/user_view.ex</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">edit</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;id&#34; =&gt; id}</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">user</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">get!</span><span class="p">(</span><span class="no">User</span><span class="p">,</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">changeset</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="s2">&#34;edit.html&#34;</span><span class="p">,</span> <span class="ss">user</span><span class="p">:</span> <span class="n">user</span><span class="p">,</span> <span class="ss">changeset</span><span class="p">:</span> <span class="n">changeset</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This first calls the <tt>web/views/user.ex</tt> which imports stuff like helpers, transforms the <tt>user</tt> and <tt>changeset</tt> variables into <a href="http://elixir-lang.org/getting-started/module-attributes.html"target="_blank" rel="noopener">module attributes</a> (the ones starting with &ldquo;@&rdquo; if you&rsquo;ve been wondering what those are). And the View knows to find the <tt>edit.html</tt> template at <tt>web/templates/user/edit.html.eex</tt> because it says so in <tt>web/web.ex</tt>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/web.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">view</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">use</span> <span class="no">Phoenix</span><span class="o">.</span><span class="n">View</span><span class="p">,</span> <span class="ss">root</span><span class="p">:</span> <span class="s2">&#34;web/templates&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1"># Import convenience functions from controllers</span>
</span></span><span class="line"><span class="cl">      <span class="n">import</span> <span class="no">Phoenix</span><span class="o">.</span><span class="n">Controller</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">get_csrf_token</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="ss">get_flash</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="ss">view_module</span><span class="p">:</span> <span class="mi">1</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="c1"># Use all HTML functionality (forms, tags, etc)</span>
</span></span><span class="line"><span class="cl">      <span class="n">use</span> <span class="no">Phoenix</span><span class="o">.</span><span class="n">HTML</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">import</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Router</span><span class="o">.</span><span class="n">Helpers</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I did not copy and paste all the other macros in <tt>web/web.ex</tt> but check them out to see what models, controllers, router, channel import in each module you create.</p>
<p>In Rails we have the default ERB for &ldquo;Embedded Ruby&rdquo; and in Phoenix we have &ldquo;EEX&rdquo; for &ldquo;Embedded Elixir&rdquo;. It&rsquo;s essentially the same thing: an HTML template that accepts snippets of Elixir code enclosed between <tt>&lt;%= &hellip; %&gt;</tt>. So, the <tt>edit.html.eex</tt> template looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/users/edit.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>Edit user<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= render &#34;form.html&#34;, changeset: @changeset,
</span></span><span class="line"><span class="cl">                        action: user_path(@conn, :update, @user) %&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link &#39;Back&#39;, to: user_path(@conn, :show, @user) %&gt; |
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link &#34;Back&#34;, to: user_path(@conn, :index) %&gt;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Which is very similar to the equivalent <tt>edit.html.erb</tt> in Rails:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- web/templates/user/edit.html.eex --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Editing User<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= render &#39;form&#39; %&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link_to &#39;Show&#39;, @user %&gt; |
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= link_to &#39;Back&#39;, users_path %&gt;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The Phoenix version is slightly more verbose in order to not hide as much as Rails does. One can argue whether more or less magic makes it more productive, but the Phoenix version being more explicit leaves a trail of breadcrumbs that is easier to follow, especially if you&rsquo;re just getting started. Here we have no concept of &ldquo;partials&rdquo;; every template can render any other template, we just need to pass through the necessary variables for the template to function. But instead of passing a model instance we are passing a changeset.</p>
<p>The &ldquo;form.html&rdquo; template is also very similar, let&rsquo;s check out the Phoenix version first:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- web/templates/user/edit.html.eex --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= form_for @changeset, @action, fn f -&gt; %&gt;
</span></span><span class="line"><span class="cl">  <span class="err">&lt;</span>%= if @changeset.action do %&gt;
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alert alert-danger&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Oops, something went wrong! Please check the errors below:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="err">&lt;</span>%= for {attr, message} <span class="p">&lt;</span><span class="nt">-</span> <span class="na">f</span><span class="err">.</span><span class="na">errors</span> <span class="na">do</span> <span class="err">%</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span><span class="err">&lt;</span>%= humanize(attr) %&gt; <span class="err">&lt;</span>%= message %&gt;<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;form-group&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="err">&lt;</span>%= label f, :username, &#34;Username&#34;, class: &#34;control-label&#34; %&gt;
</span></span><span class="line"><span class="cl">    <span class="err">&lt;</span>%= text_input f, :username, class: &#34;form-control&#34; %&gt;
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  ...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now the Rails ERB version:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/users/_form.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl"><span class="err">&lt;</span>%= form_for(@user) do |f| %&gt;
</span></span><span class="line"><span class="cl">  <span class="err">&lt;</span>% if @user.errors.any? %&gt;
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;error_explanation&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span><span class="err">&lt;</span>%= pluralize(@user.errors.count, &#34;error&#34;) %&gt; prohibited this user from being saved:<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="err">&lt;</span>% @user.errors.full_messages.each do |message| %&gt;
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span><span class="err">&lt;</span>%= message %&gt;<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="err">&lt;</span>% end %&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;field&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="err">&lt;</span>%= f.label :username %&gt;<span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="err">&lt;</span>%= f.text_field :username %&gt;
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  ...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remarkably similar. There is some language-specific stuff like having <tt>label(f, :username)</tt> instead of <tt>f.label :username</tt>. Because in Elixir the parentheses are also optional and because Phoenix implements helpers that are very similar to the Rails version, like &ldquo;form_for&rdquo;, we feel very comfortable very fast.</p>
<p>Rails has a default layout at <tt>app/views/layouts/application.html.erb</tt> and Phoenix has a default layout at <tt>web/templates/layout/app.html.eex</tt>. The rest is pretty much the same.</p>
<p>The <tt>mix phoenix.gen.html</tt> creates a template structure that is similar to <tt>rails generate scaffold</tt> command.</p>
<p>What Phoenix calls &ldquo;views&rdquo; is more similar to what Rails calls &ldquo;helpers&rdquo;. We can use them similarly, for example, to access the current user session, we do like this in Phoenix <tt>web/views/layout_view.ex</tt>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/views/layout_view.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">LayoutView</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:view</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">current_user</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Plug</span><span class="o">.</span><span class="n">Conn</span><span class="o">.</span><span class="n">get_session</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:current_user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Which is almost the same as <tt>app/helpers/application.rb</tt> in Rails:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/helpers/application.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">ApplicationHelper</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">current_user</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span><span class="o">[</span><span class="ss">:current_user</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finally, Phoenix&rsquo;s default scaffolding already brings in Bootstrap so it looks much nicer than the 10-year-old &ldquo;scaffold.css&rdquo; that Rails generates by default. There are many gems that override that though.</p>
<h3>Tests<span class="hx:absolute hx:-mt-20" id="tests"></span>
    <a href="#tests" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The last thing is the testing system. Rails uses Minitest, Elixir uses ExUnit. Again, the helpers are so similar that you can translate almost directly from Phoenix to Rails and vice versa.</p>
<p>As a caveat, Rails tests evolved a lot in the past decade. Even without adding extra features such as Factory Girl, the standard Fixtures still don&rsquo;t have an equivalent in Phoenix (#OpportunityToContribute!).</p>
<p>Let&rsquo;s start seeing a small model test in Phoenix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># test/models/post_test.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">PostTest</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">ModelCase</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Post</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="vi">@valid_attrs</span> <span class="sx">%{body: &#34;some content&#34;, title: &#34;some content&#34;}</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@invalid_attrs</span> <span class="sx">%{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;changeset with valid attributes&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">changeset</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">Post</span><span class="p">{},</span> <span class="vi">@valid_attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">changeset</span><span class="o">.</span><span class="n">valid?</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;changeset with invalid attributes&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">changeset</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">Post</span><span class="p">{},</span> <span class="vi">@invalid_attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">refute</span> <span class="n">changeset</span><span class="o">.</span><span class="n">valid?</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And the same unit tests in Rails:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># test/models/post_test.rb</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;test_helper&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PostTest</span> <span class="o">&lt;</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
</span></span><span class="line"><span class="cl">  <span class="n">setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@valid_attr</span> <span class="o">=</span> <span class="p">{</span><span class="ss">body</span><span class="p">:</span> <span class="s2">&#34;some content&#34;</span><span class="p">,</span> <span class="ss">title</span><span class="p">:</span> <span class="s2">&#34;some content&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@invalid_attr</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;with valid attributes&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@valid_attr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">post</span><span class="o">.</span><span class="n">valid?</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;with invalid attributes&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span> <span class="o">=</span> <span class="no">Post</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@invalid_attr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">refute</span> <span class="n">post</span><span class="o">.</span><span class="n">valid?</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span> 
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remarkably similar. For the test data, in Phoenix we are using Elixir&rsquo;s module attributes, in Rails we put it in the setup step to create instance variables. They are not the same thing, but the result is similar. Again, in Rails we test the model, in Phoenix we test the changeset.</p>
<p>Now let&rsquo;s see some bits of a controller test. As I said before, because Phoenix does not have a Fixtures or Factory feature in place (although the nice guys at Thoughtbot just released a Factory Girl-like library for Phoenix called <a href="https://robots.thoughtbot.com/announcing-ex-machina"target="_blank" rel="noopener">ExMachina</a>) we have to do a bit more setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># test/controllers/session_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">SessionControllerTest</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">ConnCase</span>
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">User</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">User</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">User</span><span class="p">{},</span> <span class="sx">%{username: &#34;test&#34;, password: &#34;test&#34;, password_confirmation: &#34;test&#34;, email: &#34;test@test.com&#34;}</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Repo</span><span class="o">.</span><span class="n">insert</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">conn</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="ss">conn</span><span class="p">:</span> <span class="n">conn</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;shows the login form&#34;</span><span class="p">,</span> <span class="sx">%{conn: conn}</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">get</span> <span class="n">conn</span><span class="p">,</span> <span class="n">session_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:new</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">html_response</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span> <span class="o">=~</span> <span class="s2">&#34;Login&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;creates a new user session for a valid user&#34;</span><span class="p">,</span> <span class="sx">%{conn: conn}</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">post</span> <span class="n">conn</span><span class="p">,</span> <span class="n">session_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:create</span><span class="p">),</span> <span class="ss">user</span><span class="p">:</span> <span class="sx">%{username: &#34;test&#34;, password: &#34;test&#34;}</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">get_session</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:current_user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">get_flash</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:info</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;Sign in successful!&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">redirected_to</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="o">==</span> <span class="n">page_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="ss">:index</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The Rails controller test is getting the data from a Fixture, that&rsquo;s why the setup is shorter:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># test/controllers/session_controller.rb</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;test_helper&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SessionsControllerTest</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
</span></span><span class="line"><span class="cl">  <span class="n">setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:user_one</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;shows the login form&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">get</span> <span class="ss">:new</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert_response</span> <span class="ss">:success</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">test</span> <span class="s2">&#34;creates a new user session for a valid user&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">post</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">user</span><span class="p">:</span> <span class="p">{</span><span class="ss">username</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">username</span><span class="p">,</span> <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;password&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">session</span><span class="o">[</span><span class="ss">:current_user</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert</span> <span class="n">flash</span><span class="o">[</span><span class="ss">:notice</span><span class="o">]</span> <span class="o">==</span> <span class="s2">&#34;Sign in successful!&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">assert_redirected_to</span> <span class="n">user_posts_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But as I said before, Phoenix implements similar helpers, so it&rsquo;s very straightforward to port from Rails to Phoenix here.</p>
<p>You execute the test runner in Rails with <tt>rake test</tt> and in Phoenix with <tt>mix test</tt>. Not a lot to see here. Tests load very fast, I believe they run in parallel, and it&rsquo;s very fast, which is a good departure from when you have a really large Rails app where a test suite can take minutes to finish.</p>
<h3>Conclusions<span class="hx:absolute hx:-mt-20" id="conclusions"></span>
    <a href="#conclusions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you came this far, you may be interested in following Brandon&rsquo;s tutorial and then building your own Rails copy or you can just clone from my Github repositories. <a href="https://github.com/akitaonrails/pxblog_phoenix_exercise"target="_blank" rel="noopener">This is the Phoenix exercise</a> and <a href="https://github.com/akitaonrails/pxblog_rails_exercise"target="_blank" rel="noopener">this is the Rails exercise</a>.</p>
<p>What you can conclude thus far is that Phoenix is already a very full-featured web framework. And this is not even touching on what I think is its crown achievement: its remarkable WebSockets support, recently tested and benchmarked with real machines from Digital Ocean and achieving a peak performance of <a href="http://www.akitaonrails.com/2015/10/29/phoenix-experiment-holding-2-million-websocket-clients"target="_blank" rel="noopener"><strong>2 million concurrent connections</strong></a>.</p>
<p>I think this is the real use case for Phoenix: microservices to deliver on Erlang OTP&rsquo;s promise of hyper-reliability and hyper-concurrency. For that goal it&rsquo;s already production-ready and you can use it right now. There is still much to be learned regarding proper tuning, proper monitoring, proper production environment troubleshooting, and so on, but it&rsquo;s getting there.</p>
<p>As a complete Ruby on Rails replacement, it is not on par yet. But it&rsquo;s unfair to say that, because a new framework can&rsquo;t compete with 10 years of an entire ecosystem creating all sorts of features, tools and techniques for it. Rails is a powerhouse and it will continue to be so.</p>
<p>In this contrived example we already got one small feature we still don&rsquo;t have in Phoenix: the equivalent of <tt>ActiveSupport#has_secure_password</tt>. But this is not all, we still don&rsquo;t have the equivalents for Devise, Simple Forms, Active Admin or Rails Admin, Refile, Koala, Spree, etc. But we can have those, as the ecosystem starts to fill in the blanks (#OpportunityToContribute!) in this brand new Phoenix ecosystem. And I urge people to do so, as Thoughtbot already delivered their ExMachina so those who like Factory Girl can jump right in.</p>
<p>As it stands right now, it feels like the time when Rails 1.2 had just been released, many libraries were just too young, we still didn&rsquo;t have mature tools. But the upper hand of Phoenix and Elixir is the decades-old and battle-tested Erlang core. This is something no one else has, and the best way to capitalize on it is to go beyond what tools like Node.js have accomplished: a truly <strong>enjoyable</strong> programming environment, with a language that was truly well designed and targets programmer happiness, with a really mature core to boot.</p>
<p>Programmers shouldn&rsquo;t have to fight over small stuff. Task management? Done, use Mix. Package Management? Done, use Hex. Promises? Don&rsquo;t need them. Require semantics? Don&rsquo;t need them. You get the gist.</p>
<p>If you&rsquo;re a Rails programmer and you want to find a companion platform to augment your existing Rails solutions with highly reliable and highly concurrent microservices, Phoenix is an option ready for production right now. Jump right into it!</p>
]]></content:encoded><category>elixir-and-erlang</category><category>rails</category><category>web-development</category></item><item><title>Chatting with Scott Hanselman</title><link>https://akitaonrails.github.io/en/2008/02/18/chatting-with-scott-hanselman/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2008/02/18/chatting-with-scott-hanselman/</guid><pubDate>Mon, 18 Feb 2008 09:45:00 -0300</pubDate><description>&lt;p&gt;This week I interviewed another person from the Microsoft camp, &lt;a href="http://www.hanselman.com/"target="_blank" rel="noopener"&gt;Scott Hanselman&lt;/a&gt;. I know him from his podcast &lt;a href="http://www.hanselminutes.com/"target="_blank" rel="noopener"&gt;Hanselminutes&lt;/a&gt;. In one episode he &lt;a href="https://akitaonrails.github.io/2007/07/08/hanselminutes-entrevista-com-david-hansson-e-martin-fowler/"&gt;interviewed&lt;/a&gt; both Martin Fowler and David Hansson at last year&amp;rsquo;s RailsConf, a truly remarkable conversation.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/1370206727_84d760de06.jpg" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;He also posted a great &lt;a href="http://www.hanselman.com/blog/ASPNET35ExtensionsPlusMVCHowToScreencast.aspx"target="_blank" rel="noopener"&gt;screencast&lt;/a&gt; about Microsoft&amp;rsquo;s new alternative MVC framework and I thought it would be great to have him at my blog to talk about technology and web frameworks. As I said before I think that we should not become alienated about what&amp;rsquo;s going on in other fronts and Scott is a very forward thinking and open minded person as well.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This week I interviewed another person from the Microsoft camp, <a href="http://www.hanselman.com/"target="_blank" rel="noopener">Scott Hanselman</a>. I know him from his podcast <a href="http://www.hanselminutes.com/"target="_blank" rel="noopener">Hanselminutes</a>. In one episode he <a href="/2007/07/08/hanselminutes-entrevista-com-david-hansson-e-martin-fowler/">interviewed</a> both Martin Fowler and David Hansson at last year&rsquo;s RailsConf, a truly remarkable conversation.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/1370206727_84d760de06.jpg" alt=""  loading="lazy" /></p>
<p>He also posted a great <a href="http://www.hanselman.com/blog/ASPNET35ExtensionsPlusMVCHowToScreencast.aspx"target="_blank" rel="noopener">screencast</a> about Microsoft&rsquo;s new alternative MVC framework and I thought it would be great to have him at my blog to talk about technology and web frameworks. As I said before I think that we should not become alienated about what&rsquo;s going on in other fronts and Scott is a very forward thinking and open minded person as well.</p>
<p><strong>AkitaOnRails:</strong> First of all, let me congratulate you mr. <a href="http://www.hanselman.com/blog/BlueBadge.aspx"target="_blank" rel="noopener">progman.exe</a> :-) I understand that this was something you&rsquo;re very excited about. Can you tell us a bit more about your transition to Microsoft and your new duties?</p>
<p><strong>Scott Hanselman:</strong> I&rsquo;m working for Simon Muzio who runs <a href="http://www.asp.net"target="_blank" rel="noopener">www.asp.net</a>, <a href="http://www.iis.net"target="_blank" rel="noopener">www.iis.net</a> and large content-packed sites like that. I like to think my job is writing a book that will never end, where that book is blog posts, articles, screencasts, videos, talks, etc. I&rsquo;m a teacher at heart, so while I took the title &ldquo;Community Liaison&rdquo; I like &ldquo;Teacher.&rdquo;</p>
<p><strong>AkitaOnRails:</strong> Tell us about your history. How did you start in the computer world? Did you always develop more toward Windows or did you have a background in the Unix world as well? And before your recent departure to Microsoft you worked for <a href="http://www.hanselman.com/blog/CheckFreeClosesDealToAcquireCorillianNowTheRealWorkBegins.aspx"target="_blank" rel="noopener">Corillian</a>. What was your role there?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/computerolympics.jpg" alt=""  loading="lazy" /></p>
<p><strong>Scott Hanselman:</strong> <a href="http://www.hanselman.com/blog/TeachingChildrenAndKidsToProgramTheOldSchoolWay.aspx"target="_blank" rel="noopener">This blog</a> posts talks about my early years in school.</p>
<blockquote>
  <p>See that book on the right there? That&rsquo;s how I learned to program. This book was THE book in 1984. I typed in the whole book on a Commodore 64.</p>

</blockquote>
<p>And a VERY <a href="http://www.hanselman.com/blog/MrsMayfieldHillMyFifthGradeTeacher.aspx"target="_blank" rel="noopener">influential teacher</a> that set me on the right track.</p>
<blockquote>
  <p>Marianne Mayfield is my fifth-grade teacher and the reason I&rsquo;m in computers today. She was at my wedding, at graduations, at family events. Twenty years later and we are still close. When I was young, she recognized that I was &ldquo;at risk for trouble&rdquo; and sat down with my family in order to find a way to get me back on track. She saw that I was transfixed by the Apple II in our classroom and set it up so we could sneak the computer out of the school as long as it was back by Sunday night and no one noticed.</p>

</blockquote>
<p>(click links above for more details)</p>
<p><strong>Scott Hanselman:</strong> I was the Chief Architect at Corillian, working with other engineers and architects throughout the company, planning, designing, coding the next generation of Corillian&rsquo;s Online Banking Platform.</p>
<p><strong>AkitaOnRails:</strong> I applaud your <a href="http://www.hanselman.com/blog/TeamHanselmanAndDiabetesWalk2007.aspx"target="_blank" rel="noopener">Team Hanselman</a> campaign to help fight Diabetes. I understand that you have Diabetes as well and that, fortunately, you managed to have it under control nowadays. Can you tell us this story and the goals of your campaign?</p>
<p><strong>Scott Hanselman:</strong> My personal story is <a href="http://hanselman.com/fightdiabetes"target="_blank" rel="noopener">here</a> :</p>
<blockquote>
  <p>I currently take three shots a day of Symlin while also wearing an Insulin Pump 24-hours a day, even while I sleep. The pump saves me from an additional six shots a day, which I took for 8 years before the pump. I test my blood sugar by pricking my finger between 8 and 10 times a day – that&rsquo;s about 46,500 finger pricks so far, and miles to go before I sleep.</p>

</blockquote>
<p>And there&rsquo;s a good explanation of diabetes <a href="http://www.hanselman.com/blog/ScottsDiabetesExplanationTheAirplaneAnalogy.aspx"target="_blank" rel="noopener">here</a> :</p>
<blockquote>
  <p>Here&rsquo;s where the analogy gets interesting. Remember in the analogy we are flying from L.A. to New York, except we only get to check our altitude seven times. And, we only get to change altitude (take insulin) less than ten times. But, when I check my blood sugar, I&rsquo;m actually seeing the past. I&rsquo;m seeing a reading of what my blood sugar was 15 minutes ago. And, when I take insulin, it doesn&rsquo;t start lowering my blood sugar for at least 30 minutes.</p>

</blockquote>
<p>The campaign ended up raising over USD$30,000, and that money will be applied to research, education and programs surrounding diabetes.</p>
<p><strong>AkitaOnRails:</strong> <em>&ldquo;What in the world were you doing at a RailsConf?&rdquo;</em> That&rsquo;s what I think people from outside of the Microsoft blogosphere would ask you :-) So, are you just curious about Rails, or do you actually develop Rails-based apps? I am very interested to learn your opinions about Rails. How did you find it? What do you think is best about Rails? And more importantly, what do you think Rails could have improved?</p>
<p><strong>Scott Hanselman:</strong> I&rsquo;m always trying different programming languages and frameworks. I&rsquo;ve been doing Rails on and off for about 18 months, but it hasn&rsquo;t clicked for me yet. I&rsquo;m sure it will soon. The MVC pattern is very old…I was doing it in Java over 10 years ago, and certainly the pattern goes back 30 years. I think that Rails is a good framework considering its young age. I think it&rsquo;s Ruby that is the real magic though and I find the Ruby aesthetic to be very in line with my own.</p>
<p><strong>AkitaOnRails:</strong> Then, following my biased preferences, the next article I like is your <a href="http://www.hanselman.com/blog/ASPNET35ExtensionsPlusMVCHowToScreencast.aspx"target="_blank" rel="noopener">screencast</a> over the CTP ASP.NET MVC framework. I see that this new framework resembles Rails a lot. Of course, it may just look like that because both implement MVC2. But on the other hand, it uses very similar Rails idioms everywhere. It lacks a little bit of &lsquo;convention-over-configuration magic&rsquo; but from what I saw, it looks much better than doing conventional ASP.NET. What do you think about it? Can you summarize it a little bit for those that don&rsquo;t know this product yet? And what do you think will still improve in it?</p>
<p><strong>Scott Hanselman:</strong> If one points at Rails and says &ldquo;that&rsquo;s MVC&rdquo; then everything else looks like Rails, sure. Any good MVC framework needs to not only follow the pattern as appropriate but also abstract away HTTP (assuming it&rsquo;s a web framework) in a clean, but not complete, way. The ASP.NET MVC implementation still has some baggage that it will always have to carry because ASP.NET WebForms came first and wasn&rsquo;t designed from scratch with testability in mind. That said, it&rsquo;s all about alternatives. Use the framework that makes you happy and you feel most productive in. Some fraction, maybe 20%, of ASP.NET devs will prefer MVC. Now they have a MS support choice with good integration with C# 3.0</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/505403549_2ad6d13ce7.jpg" alt=""  loading="lazy" /><br>
at RailsConf'07</p>
<p><strong>AkitaOnRails:</strong> And what would be tech world without discussions and flame wars? I understand that most .NET developers don&rsquo;t even pay attention to Rails or other frameworks. I even heard that there are some developers questioning things like <em>&ldquo;Why yet another web framework if ASP.NET is already there?&rdquo;</em> Meaning, why reinvent the wheel and divide attentions? I particularly think that this new MVC is the way to go, but it is undeniable that the old ASP.NET has much more acceptance and support. How to reconcile both point of views?</p>
<p><strong>Scott Hanselman:</strong> That&rsquo;s just FUD (fear, uncertainty, and doubt). Think what they like, this Framework is just about alternatives. WebForms is great, and if you&rsquo;re doing Line of Business (LOB) apps with complex grids and such, it&rsquo;s not going anywhere. However, if you&rsquo;re an agile shop with a penchant for TDD and have been frustrated by ViewState, then check out ASP.NET MVC. There&rsquo;s nothing to reconcile. Some people take a car, some a motorcycle, and others walk. As long as you get from point A to point B and feel good about the journey.</p>
<p><strong>AkitaOnRails:</strong> Speaking of flame wars, there is probably one subject that gives me the creeps: the paranoid conspiracy theories regarding Miguel de Icaza and <a href="http://www.hanselman.com/blog/MonoAndItsManyFacetsScreencast.aspx"target="_blank" rel="noopener">Mono</a>. A lot of people actually believe that Miguel is &lsquo;selling&rsquo; part of the open source world to Microsoft. Have you ever used it? What do you hear throughout the community and what&rsquo;s your opinion about this matter?</p>
<p><strong>Scott Hanselman:</strong> Yes, I&rsquo;ve used Mono and I think it&rsquo;s a great way to get your .NET application working on Linux or Mac. I think Miguel is brilliant and a fine dancer. I hear in the community that they are happy Mono exists and that it&rsquo;s available as an option.</p>
<p><strong>AkitaOnRails:</strong> I will probably be cursed for this one: but I am not &lsquo;strongly&rsquo; against Microsoft as some fundamentalists are. It is public and notorious that Microsoft is a big corporation and as such it did its share of ill conceived deeds. A lot of them were already settled in court. So, it&rsquo;s more complex than people usually think. I prefer to assume that inside Microsoft there is a lot of very smart people.</p>
<p><strong>Scott Hanselman:</strong> I stay out of legal stuff. Microsoft is a huge company with a lot of smart people, but it&rsquo;s really many medium sized companies working together (usually). I can&rsquo;t speak to &ldquo;ill conceived deeds.&rdquo; I can only say that I do my best every day to not be evil, as do my coworkers. There&rsquo;s no evil master plan in my group.</p>
<p><strong>AkitaOnRails:</strong> I particularly like the roadmap for Windows Server 2008 (with it&rsquo;s minimalist approach, finally having only what you need installed). Last year you were experimenting with <a href="http://www.hanselman.com/blog/ScreencastHowToIIS7AndPHPWithFastCGI.aspx"target="_blank" rel="noopener">IIS 7</a> and its new FastCGI support. I think it was primarily intended for PHP support but it could theoretically handle Ruby on Rails as well. Have you tried Rails there? I think Microsoft is clearly going after the low-end with Windows Server 2008 + IIS 7, against the inexpensive Linux+Apache boxes. I think they won&rsquo;t be able to reach them in terms of prices but what do you think of it as an internet service contender?</p>
<p><strong>Scott Hanselman:</strong> I have little power in that arena, but every time someone will listen I&rsquo;m pushing for a nice inexpensive Web Server SKU. I&rsquo;m a huge fan of IIS7 and I&rsquo;d like as many people as possible to check it out. I haven&rsquo;t tried IIS7+Rails but I&rsquo;ve heard rumors of folks getting it working.</p>
<p><strong>AkitaOnRails:</strong> There are a lot of new features in .NET 3.5. LINQ is the one that interests me the most. Can you give us a brief description of why it&rsquo;s so interesting? And what do you think are the best improvements of the 3.5 version? Are you already using Orcas? What&rsquo;s your opinion? Oh, and are you aware of the <a href="http://www.sapphiresteel.com/"target="_blank" rel="noopener">Sapphire in Steel</a> project?</p>
<p><strong>Scott Hanselman:</strong> LINQ is interesting because it&rsquo;s its own Domain Specific Language for querying objects of all kinds, and it&rsquo;s living right inside VB and C#. It&rsquo;s a MUCH smarter compiler and it&rsquo;s a joy to work with. I&rsquo;ve installed Sapphire In Steel and I know a few people who are digging it. I&rsquo;m looking forward to Rails on .NET.</p>
<p><strong>AkitaOnRails:</strong> One of the advantages of the Microsoft .NET strategy was being prepared since its first day to be language-agnostic. Now we have the DLR that was derived from the works in IronPython and not IronRuby. But there is yet another cool language that not many people are aware of: F#, the OCaml inspired language. Have you tried it?</p>
<p><strong>Scott Hanselman:</strong> Yes, and I&rsquo;ve done two podcasts on it, one with <a href="http://www.hanselman.com/blog/HanselminutesPodcast76FWithRobertPickering.aspx"target="_blank" rel="noopener">Robert Pickering</a>, author of a book on F# and one with <a href="http://www.hanselman.com/blog/HanselminutesPodcast96StartingSmallWithFWithDustinCampbell.aspx"target="_blank" rel="noopener">Dustin Campbell</a>, an enthusiast. I&rsquo;m stoked it&rsquo;ll be a first-class language inside VS.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/1930668335_0719ad4d3f.jpg" alt=""  loading="lazy" /><br>
with John Lam</p>
<p><strong>AkitaOnRails:</strong> I am not against technical comparisons, we have to do such things to decide our steps in projects. But I am against pointless challenges. Rails is far from perfect and I am the first say it. That said, it is great for an specific niche of web development. I usually recommend that good programmers should expand their horizons and learn as much new stuff as possible. You can&rsquo;t know everything perfectly but it&rsquo;s always good to be aware of the possibilities. You <a href="http://www.hanselman.com/blog/TheProgrammingLanguageExplosion.aspx"target="_blank" rel="noopener">wrote something</a> around these lines. What do you think about it?</p>
<p><strong>Scott Hanselman:</strong> Rails is good for what it does in its niche. You don&rsquo;t create Windows Desktop apps with Rails and that&rsquo;s fine. That&rsquo;s why we don&rsquo;t compare Rails to WPF, we compare it to Web Frameworks. Comparison is good and there&rsquo;s always good to be found on either side. I avoid flame wars as well. It&rsquo;s pointless. If the shoe pinches, don&rsquo;t wear it. But, try as many shoes as you can!</p>
<p><strong>AkitaOnRails:</strong> Last year you wrote an <a href="http://www.hanselman.com/blog/TheSkynetComputeCloudIThinkThereIsAWorldMarketForMaybeFiveComputers.aspx"target="_blank" rel="noopener">interesting post</a> about the future. Google is this big giant, Amazon released surprisingly good products in the last few months like S3, EC2, SimpleDb. I am not well aware of the current Microsoft Live&rsquo;s strategy. Can you say something about where is it heading to?</p>
<p><strong>Scott Hanselman:</strong> I&rsquo;m not in that group so I have no insight outside of what&rsquo;s already been said. However, I do still hold to my predictions in that post!</p>
<blockquote>
  <p>Computing will be moved into the Cloud. It&rsquo;s already happening, we&rsquo;re 20% there. The idea has been around since the beginning, and it will, in my opinion, continue to come up until it actually happens and we build Skynet. One of these compute clouds will no doubt end up in orbit.</p>

</blockquote>
<p><strong>AkitaOnRails:</strong> And of course, I kind of &lsquo;have&rsquo; to ask you this: do you use Windows Vista? How has it been for you given that there&rsquo;s a huge underground campaign against it? Do you think it is as bad as everybody else says? I can&rsquo;t comment because I never tried it outside of my virtual machine (I am a Mac guy), but they did sell 100 million licenses.</p>
<p><strong>Scott Hanselman:</strong> Sure. I run Vista 64-bit on 3 machines, and Vista 32-bit on 2 machines. It was rough for the first 3 months while the driver support sucked, but now it&rsquo;s fine. I&rsquo;ve got 2 vista machines that haven&rsquo;t been rebooted in months, just put to sleep and woken up. Once you get good video drivers, you&rsquo;re usually cool.</p>
<p>Folks give Vista a hard time but they forget the wide range of hardware it has to run on. OS X only has to run on Macs, and a narrow range at that. Vista has to run on millions of combinations of hardware. Third Party Drivers are by FAR the thing that crashes Vista the most and it gets blamed. That&rsquo;s why I run 64-bit and use only signed drivers. I&rsquo;ve been thrilled with 64-bit. Works great.</p>
<p><strong>AkitaOnRails:</strong> Ok, this is it :-) Thanks a lot!</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/2/18/90081476_85f9a226dd.jpg" alt=""  loading="lazy" /></p>
]]></content:encoded><category>interviews</category><category>microsoft</category><category>web-development</category></item><item><title>Chatting with Adrian Holovaty</title><link>https://akitaonrails.github.io/en/2008/01/01/chatting-with-adrian-holovaty/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2008/01/01/chatting-with-adrian-holovaty/</guid><pubDate>Tue, 01 Jan 2008 02:01:00 -0200</pubDate><description>&lt;p&gt;&lt;strong&gt;Traducción en &lt;a href="http://www.marcelor.com/2008/01/conversando-con-adrian-holovaty-creador-del-proyecto-django-traduccion.html"target="_blank" rel="noopener"&gt;Español&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As I promised after the &lt;a href="http://www.akitaonrails.com/en/2007/12/15/conversando-com-avi-bryant-parte-1"target="_blank" rel="noopener"&gt;Avi Bryant&lt;/a&gt; interview, here’s a great conversation with &lt;a href="http://www.holovaty.com/"target="_blank" rel="noopener"&gt;Adrian Holovaty&lt;/a&gt;, well known creator of the &lt;a href="http://www.djangoproject.com/"target="_blank" rel="noopener"&gt;Django&lt;/a&gt; web framework written in Python.&lt;/p&gt;
&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/387373570_870fa6257c.jpg" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;For me this is an important piece because I always say that technology doesn’t have to be about divorce. Technology is about integration. I am a full-time Ruby on Rails developer and evangelist, but above all, I try to be a ‘good’ programmer. And good programmers acknowledge good technology and their creators’ achievements. And Adrian’s Django is such a remarkable achievement that deserves its attention and success.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Traducción en <a href="http://www.marcelor.com/2008/01/conversando-con-adrian-holovaty-creador-del-proyecto-django-traduccion.html"target="_blank" rel="noopener">Español</a></strong></p>
<p>As I promised after the <a href="http://www.akitaonrails.com/en/2007/12/15/conversando-com-avi-bryant-parte-1"target="_blank" rel="noopener">Avi Bryant</a> interview, here’s a great conversation with <a href="http://www.holovaty.com/"target="_blank" rel="noopener">Adrian Holovaty</a>, well known creator of the <a href="http://www.djangoproject.com/"target="_blank" rel="noopener">Django</a> web framework written in Python.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/387373570_870fa6257c.jpg" alt=""  loading="lazy" /></p>
<p>For me this is an important piece because I always say that technology doesn’t have to be about divorce. Technology is about integration. I am a full-time Ruby on Rails developer and evangelist, but above all, I try to be a ‘good’ programmer. And good programmers acknowledge good technology and their creators’ achievements. And Adrian’s Django is such a remarkable achievement that deserves its attention and success.</p>
<p>So, as my very first post of the year (published at 0:01hs!), I would like to celebrate the great minds of our ‘development’ community, wishing that the good developers use their time creating great technology instead of wasting it in useless flame wars.</p>
<p><strong>AkitaOnRails:</strong> As I always do, let’s start by talking a little bit about yourself. How long have you been a programmer?</p>
<p><strong>Adrian Holovaty:</strong> Let’s see — I’ve been tinkering with computers since I was a little boy playing with the Commodore 64 in the mid ’80s.</p>
<p>One early program I wrote was for the TI-85 calculator in my high-school math class. By the time I graduated, a whole bunch of students were using it, because it had all the formulas and stuff.</p>
<p>I didn’t take any of this stuff seriously until college, when I minored in computer science. But I didn’t end up finishing the minor, which is something I’ve always been bummed about.</p>
<p>My college major was journalism, as I originally wanted to be a newspaper reporter, but after I took a job with the Web site of the campus newspaper (<a href="http://www.themaneater.com/"target="_blank" rel="noopener">The Maneater</a> – best name for a newspaper ever!), I realized I would have a lot more fun working online than with a dead-tree product.</p>
<p>So I ended up finding a way to combine journalism/news and computer programming, and I’ve had a number of jobs doing exactly that!</p>
<p><strong>AkitaOnRails:</strong> And what led you to the computer world?</p>
<p><strong>Adrian:</strong> I guess it was in my upbringing, as my dad has been a computer programmer since the ’70s, back in the punchcard days. I remember seeing all sorts of those punchcards in our house when I was growing up.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/247456700_94dbcdbf86.jpg" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> And, of course, how did you meet and fall in love with Python?</p>
<p><strong>Adrian:</strong> My Web development path was (I think) pretty typical, at least based on the experiences of my friends and colleagues. I originally learned how to write Perl CGI scripts in a college Web development course. Then I taught myself PHP because somebody recommended it to me.</p>
<p>After doing Web development with PHP for a few years, I got kind of tired of it. At that time, I was working with Simon Willison at a newspaper Web site in Lawrence, Kansas. Simon and I were both getting tired of PHP and decided to “dive into Python” (pun intended) by reading <a href="http://en.wikipedia.org/wiki/Mark_Pilgrim"target="_blank" rel="noopener">Mark Pilgrim</a> ‘s excellent book of the same name, which was released around that time. This must’ve been late 2002.</p>
<p>We immediately fell in love with Python. And when I say “immediately,” I really do mean it. It was like a revelation, some sort of divine moment. It was the programming equivalent of love at first sight.</p>
<p>So Simon and I decided that, from then on, we were going to code everything in Python. That’s one of the perks of working for a small Web development shop — we could make those kinds of snap decisions! ï¿¼</p>
<p><strong>AkitaOnRails:</strong> I was first introduced to Python around 2000 as well. But it was through web development using <a href="http://www.zope.org/"target="_blank" rel="noopener">Zope</a>. I think it got a lot of attention at the time. Did you ever try it?</p>
<p><strong>Adrian:</strong> No, I never tried Zope. Still, to this day, I’ve never tried it. And I’ve been told that’s a good thing, because evidently it turned some people off of Python back then, for whatever reason. (Their newest version is supposed to be a ton better, but I haven’t tinkered with it, either.)</p>
<p><strong>AkitaOnRails:</strong> You’re right, and it is kind of amusing because a lot of pythonists from Brazil still use <a href="http://plone.org/"target="_blank" rel="noopener">Plone</a> (I think it evolved from Zope but I haven’t tried it either). So, We all know that Django was born out of necessity when you were developing websites for World Online. Please describe how you began there and what the everyday work is like. Maybe what led you to Django during your work day?</p>
<p><strong>Adrian:</strong> Sure! Well, generally when people think of newspapers, they think of crotchety, old-fashioned editors and reporters scribbling in paper notebooks with pencils. This Kansas newspaper that I worked at was the complete opposite of that. It’s beyond the scope of this interview, but for a number of reasons that newspaper Web site attracted (and still attracts, to this day) a really great development team — one that I would stack up against any team 10 times its size.</p>
<p>We were doing “Web 2.0” stuff back in 2002 and 2003, and we were building Web apps in days, not weeks or months. This was mostly due to the journalism environment – newspaper people like deadlines. ï¿¼</p>
<p>So we were in this culture of <em>“Web development on journalism deadlines,”</em> and we needed some tools that let us create Web applications quickly. We looked around at some existing Python libraries at the time (2003), and we ended up deciding to write our own stuff.</p>
<p>We didn’t set about to make a framework – it was a very classic, clichéd path, actually! What happened was, we built a site with Python. Then we built another one, and we realized the sites had a fair amount of code in common, so we did the right thing and extracted the common bits into a library.</p>
<p>We kept doing this – extracting and extracting, based on each new Web application that we created – and eventually we had a framework.</p>
<p><strong>AkitaOnRails:</strong> As a side-note, actually this is kind of interesting! I would love to hear what do you think World Online has that attracts good developers like yourself! Just a summary would be ok. That’s the kind of insight brazilian companies have to learn better. Letting you do Python is probably one of them ï¿¼</p>
<p><strong>Adrian:</strong> Yeah, it kind of comes down to <strong>empowering the employees</strong>. My boss, who led the Web team, delegated all the technology decisions to me and Simon. That kind of culture really encourages quality work, because it makes everybody on the team more invested.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/1454328380_6b6053d1c9.jpg" alt=""  loading="lazy" /></p>
<p>ï¿¼<br>
<strong>AkitaOnRails:</strong> You’re right. Traditional companies would rather have a more hierarchical (and bureaucratic) approach, with a dumb manager holding the whip. But I digress. So, I particularly dislike language x VS language y or framework a vs framework b kind of comparisons. Instead I’d like to hear what do you think is great about Python – the language and the platform – and Django. What are the features in Django that you hold dear to yourself, I mean, those bits of functionality that you actually think for yourself as ‘the’ greatest ideas?</p>
<p><strong>Adrian:</strong> Well, I love abstractions – like, I suspect, any programmer. At its core, Django is just a set of abstractions of common Web development tasks. It gives me joy to create high-level abstractions, to encapsulate a large problem in such a way that is made simple.</p>
<p>What I really like about Django is the depth and breadth of abstractions that it provides. And in most cases (I hope!), they’re clean, easy to use and understandable.<br>
Let me give you an example of an abstraction: creating an RSS feed.</p>
<p>When you create an RSS feed, you don’t want to have to deal with remembering angle brackets, and the exact formatting of the feed — you just care about the items in the feed. So Django provides a very simple library that lets you give it a bunch of items and creates a feed from them.</p>
<p>So that’s an obvious example, but what really excites me are the higher-level abstractions, like the concept of an “admin site.” Django comes with a completely dynamic application that makes a beautiful, production-ready CRUD site for your database. There’s no code to write – it’s only a small bit of optional configuration.<br>
There’s also something called <a href="http://www.djangoproject.com/documentation/databrowse/"target="_blank" rel="noopener"><strong>Databrowse</strong></a>, which is an abstraction of the concept <em>“Show me my data, as intelligent hypertext.”</em></p>
<p>So whenever I’m explaining Django to someone, I always end up saying, <em>“It’s just a bunch of abstractions of common Web development tasks”</em> – from low-level HTTP wrapping to higher – and higher-level concepts. The higher you get, the more productive you can be. I apologize if this is too conceptual!</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/1496211063_262b298650.jpg" alt=""  loading="lazy" /></p>
<p>As for Python — what can I say? It’s gorgeous. It’s like poetry. It’s so clean, so logical, so regular, so obvious. And the import system is to-die-for.</p>
<p>A lot of people say Python code is easy to read/understand because of the consistent whitespace and simplicity of the language. I agree with that, but I think it’s also due to the elegant import system. Why? Because if you want to know how <strong>any</strong> Python module works, just look at the code, and look at the modules it imports. As the <a href="http://www.python.org/dev/peps/pep-0020/"target="_blank" rel="noopener">Zen of Python</a> says, <em>“Namespaces are one honking great idea – let’s do more of those!”</em></p>
<p><strong>AkitaOnRails:</strong> Tech communities are great because they are so energetic and passionate. On one hand you have those that contribute and make the technology grow faster. On the other hand you have the pundits and trolls that like flame wars. Do you burn yourself sometimes, or do you tend to avoid this kind of discussion? Specifically, some time ago you and DHH were together at the <a href="http://www.djangoproject.com/snakesandrubies/"target="_blank" rel="noopener">Snakes and Rubies</a> presentation. I think it was great because it didn’t end in yet another flame war. What’s your opinion on this kind of open (not flamed) discussion?</p>
<p><strong>Adrian:</strong> The answer to this one is obvious – clearly constructive discussion is more productive.</p>
<p>At times, I’ve had my passion for Python/Django get the best of me, but I’ve gotten a lot better over the years. I’ve realized something: At the end of the day, what really matters is the sites people create with these tools, not the tools themselves. If you&rsquo;re going to judge someone, judge the sites that person makes, instead of the tools that person uses.</p>
<p>These days, if I am involved in any sort of discussion like that, it’s usually to try to calm people down. ï¿¼</p>
<p><strong>AkitaOnRails:</strong> Well said. Going back to features, you mentioned ‘Databrowse’ before, I would like to know more about it. I think Django didn’t have it by the time I first tried it. Is it a new feature (or am I the one lagging behind? ï¿¼ Can you elaborate more on this construct?</p>
<p><strong>Adrian:</strong> Databrowse is still quite under the radar! Here’s the use case.</p>
<p>Say you have a lot of data in your database, and all you want to do is <strong>look</strong> at it.<br>
Here are your current options: You can drop into the psql or mysql prompts and run a bunch of SELECT queries, but that gets tiresome. You could run something like PHPMyAdmin, but that’s more of an administration tool than a tool for <strong>browsing</strong>. You could use some sort of external application that lets you browse your database tables, MS Access-style, but that’s in the realm of the desktop app.</p>
<p>Databrowse automatically creates a Web site that displays your data, so you can click around to your heart’s content.</p>
<p>The other thing it does is point out interesting, non-obvious queries. For example, if you have a table that has a DATE column in it, it will automatically create a calendar view of that table.</p>
<p>The point isn’t for people to use this to make public-facing sites – the point is for people to use this to explore their own data, with no effort required.</p>
<p>Another use case comes from the journalism world. I used to work with a guy named Derek Willis at the Washington Post newspaper. His job at the time was to acquire huge datasets and place them on the newspaper’s intranet, so that reporters could search and browse the data in their research.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/179099354_6199af510a.jpg" alt=""  loading="lazy" /></p>
<p>Derek didn’t want to have to hand-roll a Web app each time he got a new dataset, so he used Databrowse to make intranet sites that displayed his databases — with little-to-no effort.</p>
<p>This comes back to what I was saying earlier about high-level abstractions. Databrowse is a particular type of abstraction, and it’s really cool that we include something like that for people to use, if they need it.</p>
<p><strong>AkitaOnRails:</strong> Sounds great, I am looking forward to using it. In a degree it feels like <a href="http://dabbledb.com"target="_blank" rel="noopener">Dabble Db</a> – albeit inside your own app. I think you already met <a href="http://www.akitaonrails.com/en/2007/12/15/conversando-com-avi-bryant-parte-1"target="_blank" rel="noopener">Avi Bryant</a> ? I just interviewed him and it was a very insightful conversation. Have you ever tried Seaside? (out of curiosity I have a photo of you and Avi looking at his macbook sitting down in the grass, what was that?)</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/19/627957460_48e4181526.jpg" alt=""  loading="lazy" /></p>
<p><strong>Adrian:</strong> Yes, it was inspired by DabbleDB – it’s essentially a very toned-down DabbleDB-ish thing for your own data. But I shouldn’t even say that, because DabbleDB is a whole universe more sophisticated than Databrowse. DabbleDB is awesome. ï¿¼</p>
<p>Yes, I’ve met Avi a number of times and always enjoy his company. We even played some music together – he is a fantastic harmonica player!</p>
<p>The photo you’re referring to was taken at Foo Camp in 2007.<br>
I haven’t tried Seaside, but it’s on my to-do list. If I were ever developing an application like DabbleDB that needed to maintain a <strong>ton</strong> of state, I would turn to Seaside first.</p>
<p><strong>AkitaOnRails:</strong> I see, I feel that good musicians tend to be good programmers as well. So, my first congratulations for you is about your <a href="http://www.amazon.com/Definitive-Guide-Django-Development-Right/dp/1590597257"target="_blank" rel="noopener">new book on Django</a> that was just released. I think it is a very big milestone for the Django community. More than that you were able to make it <a href="http://www.djangobook.com"target="_blank" rel="noopener">available online</a> under a Creative Commons license.</p>
<p>How did you get involved in this book? Was it difficult to convince the publisher to have the book readily available online? I ask this because at least in Brazil this is one idea that is VERY hard to push over to publishers. Tell us more about your writing experience, challenges and quirks along the way.</p>
<p><strong>Adrian:</strong> Thanks very much. There’s not much to the story here – co-conspirator Jacob and I got contacted by the Apress folks, who were interested in publishing a Django book, and we did it. Well, I should amend that: It took us <strong>a long time</strong> , but we did it. ï¿¼<br>
It wasn’t difficult to convince Apress to let us make the book available online. They had previous experience doing so with Mark Pilgrim’s <a href="http://www.diveintopython.org/"target="_blank" rel="noopener">Dive Into Python</a> book, and they’re generally cool people.</p>
<p>In retrospect, publishing the book online was a fantastic decision. Not only is the final book available online, but we made chapters available online as we wrote them, with a really nice per-paragraph commenting feature that let readers submit typos, corrections and suggestions on a very granular basis. We got a <strong>ton</strong> of fantastic feedback for which we’re immensely grateful. Having experienced this, I wouldn’t want to publish a technology book any other way.</p>
<p><strong>AkitaOnRails:</strong> Now, my second congratulations is because of that <a href="http://www.j-lab.org/batten05winners.shtml"target="_blank" rel="noopener">Batten Award</a> for outstanding achievement in online journalism. <a href="http://www.chicagocrime.org/"target="_blank" rel="noopener">ChicagoCrime.org</a> looks great and is a very good example of a <a href="http://en.wikipedia.org/wiki/Mashup_%28web_application_hybrid%29"target="_blank" rel="noopener">mashup</a> done right. Unfortunately our local government and public institutions have very little to almost no data available online for us to use. Where did the idea come from? How was its development?</p>
<p><strong>Adrian:</strong> Thanks – and don’t let that site scare you away from visiting the beautiful city of Chicago. ï¿¼</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/adrian_oscon_2006.png" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> haha, I live in São Paulo. Can’t be worse ;-)</p>
<p><strong>Adrian:</strong> The idea for chicagocrime.org came when I was bumming around the official <a href="http://www.cityofchicago.org/police"target="_blank" rel="noopener">Chicago Police Department</a> Web site and found that they publish crime data – although in an interface that’s more suited for searching than for browsing. I thought, <em>“Wow, this is some great data!”</em> and was writing a screen scraper within about 10 minutes.</p>
<p>Around that same time, I was tinkering with the Google Maps site, which had just launched, to see if I could embed Google map tiles into my own Web pages. I figured the crime data would make for a great mapping application, so I put the two together, and one of the original Google Maps mashups was born. While I was developing the site, <a href="http://www.housingmaps.com"target="_blank" rel="noopener">HousingMaps.com</a> (<a href="http://www.craigslist.org/"target="_blank" rel="noopener">Craigslist</a> + Google Maps) came out, so it beat me to the punch of being the <strong>first</strong> real Google Maps mashup – but chicagocrime.org came soon afterward.</p>
<p>The thing I’m most proud of is the fact that chicagocrime.org was one of the sites that influenced Google to open its mapping API. Back when HousingMaps, chicagocrime.org and the original crop of mashups came out, we were all reverse-engineering Google’s JavaScript!</p>
<p><strong>AkitaOnRails:</strong> I know that you probably have 2 great passions in your career: one is Python-based development, or course, and the other is journalism (dunno the correct order here ï¿¼) You’ve made yourself quite a reputation on the online media. You’re working at the Washington Post right now, is that correct? I read an article at the <a href="http://www.ojr.org/ojr/stories/060605niles/"target="_blank" rel="noopener">OJR</a> where you explained about technology being used to empower the journalist. Do you intend to be a reporter someday or you’re more into the back-end of journalism? I think you have a strong opinion about the future of journalism in the Internet Era, don’t you?</p>
<p><strong>Adrian:</strong> Well, I would say my <strong>main</strong> passion is music, but it’s hard to make a living doing that. (Django is named after the famous jazz idol <a href="http://en.wikipedia.org/wiki/Django_Reinhardt"target="_blank" rel="noopener">Django Reinhardt</a>)<br>
ï¿¼<br>
I’m no longer working at the Washington Post. In mid 2007, I was awarded a two-year grant from the <a href="http://www.holovaty.com/blog/archive/2007/05/23/1145"target="_blank" rel="noopener">Knight Foundation</a> to create a local-news site. So I founded <a href="http://everyblock.com"target="_blank" rel="noopener">EveryBlock</a> in July. We’re a team of four people and are working hard to make a really cool application.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/Picture_2.png" alt=""  loading="lazy" /></p>
<p>But, yes, going back to your question, I certainly have some strong opinions about how journalism is practiced on the Internet. This is probably out of the scope of this interview, but you can read my essay/rant awkwardly titled <a href="http://www.holovaty.com/blog/archive/2006/09/06/0307/"target="_blank" rel="noopener">A fundamental way newspaper sites need to change</a> for a taste.</p>
<p><strong>AkitaOnRails:</strong> Now, that’s news to me ï¿¼ Didn’t know you started your own company (sorry, I still have to catch up with the Python community). Can you tell us about your new endeavor?</p>
<p><strong>Adrian:</strong> Hey, I can’t blame ya – we’re staying under the radar. There’s not much to say about EveryBlock at this point other than it’s chicagocrime.org on steroids. It’s like chicagocrime, but for more cities than just Chicago and more information than just crime. ï¿¼</p>
<p><strong>AkitaOnRails:</strong> You should try it for Brazil :-)</p>
<p><strong>Adrian:</strong> Thanks to Django’s internationalization framework, that is entirely possible. ï¿¼</p>
<p><strong>AkitaOnRails:</strong> Yeah, and by the way, (digressing a little) I would recommend the brazilian movie <a href="http://en.wikipedia.org/wiki/Tropa_de_Elite"target="_blank" rel="noopener">Elite Squad</a> now that I know you’re interested in crime related data ;-)</p>
<p><strong>Adrian:</strong> Noted! I’ve added “Elite Squad” to my MOVIES_TO_WATCH.TXT :-)</p>
<p><a href="http://leahculver.com/"target="_blank" rel="noopener"><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/1529992544_f8e9fc67b2.jpg" alt=""  loading="lazy" /></a></p>
<p><strong>AkitaOnRails:</strong> Going on. Many great websites are already deployed over Django. Your own works at World Online, Washington Post being some of them. Another big name is <a href="http://en.wikipedia.org/wiki/Kevin_Rose"target="_blank" rel="noopener">Kevin Rose’s</a> <a href="http://pownce.com/leahculver/"target="_blank" rel="noopener">Pownce</a> (the creator of <a href="http://digg.com"target="_blank" rel="noopener">Digg</a>). You, Kevin and other influential people usually meet during conferences, don’t you? What do you think influenced him to try Django? Another great thing about Pownce has to be <a href="http://leahculver.com/"target="_blank" rel="noopener">Leah Culver</a>. A programmer like her is simply unheard of here in Brazil, which is a pity. You should make her the official Django cheerleader!</p>
<p><strong>Adrian:</strong> I’ve never met Kevin Rose, but from what I’ve read, I believe Leah chose to use Django because either it had been recommended to her, or she liked Python, or something. I don’t know the backstory. I haven’t met Leah, either, but she was cool enough to travel to Lawrence, Kansas, for our last <a href="http://leahculver.com/2007/12/01/django-sprint-part-2/"target="_blank" rel="noopener">Django sprint</a> a few weeks ago (which I couldn’t attend in person, unfortunately).</p>
<p><strong>AkitaOnRails:</strong> It is not a rule written in stone, but in Rails community we ‘tend’ to praise Mac related tech. Of course, it is not a requirement for any language and Linux is as good. But what can I say? I am an Apple fanboy ï¿¼ I would like to know what’s your development environment. What tools do you use to develop your Django-based websites?</p>
<p><strong>Adrian:</strong> I recently switched to a Mac after several years of using Linux on the desktop, but don’t read anything into that — I’m not a fanboy by any means, of either Linux or Mac. I miss Linux, to be honest, and in a number of ways I’ve been unimpressed by OS X’s Unix features. (Basic stuff such as readline support in the default Python is broken, etc.)</p>
<p>The nice things about the Mac make it worth the switch, though. Even after having used the Mac as my primary machine for 6 months, I’m still impressed that I can just close the laptop and it’ll go to sleep automatically. That never worked with Linux!</p>
<p><strong>AkitaOnRails:</strong> I ask this because many people looking at Rails and Django are Java and C# programmers that are used to have a full blown IDE. I usually say that any common text editor does the job, and if you really want power vim and emacs are a perfect fit. What do you say?</p>
<p><strong>Adrian:</strong> I’ve never used an IDE to program, so I’m the wrong person to ask.<br>
Actually, I misspoke – I used an IDE-ish thing in college to program assembly. It showed the contents of the accumulators and that stuff.</p>
<p><strong>AkitaOnRails:</strong> Oh, I almost forgot to ask: did you and <a href="http://www.python.org/~guido/"target="_blank" rel="noopener">Guido van Rossum</a> ever met? I would like to know your opinion about the <a href="http://www.python.org/dev/peps/pep-3000/"target="_blank" rel="noopener">Python 3000</a> development. Is there anything that’s in the new version that you really anticipate?<br>
ï¿¼</p>
<p><strong>Adrian:</strong> Yes, I’ve met Guido a number of times and was even lucky enough to have dinner with him once! It’s almost silly and unnecessary to say this, but he is incredibly smart.</p>
<p>I really like the new concepts and features in Python 3000, and I had the opportunity to participate in a <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=212259"target="_blank" rel="noopener">sprint at Google</a> Chicago a couple of months ago. My favorite feature is the migration to Unicode strings by default, because it forces the encoding issue front and center.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/Picture_3.png" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> Finally, what was all the fuss about Django being versioned 1.0 or jumping straight to 2.0? I didn’t follow the discussions, just read some headlines a while back. And what can people expect for the next release of Django? I understand that you don’t go big bang releases that changes the game, but what do you see getting released in the next few months in terms of new features?</p>
<p><strong>Adrian:</strong> Ha – this is an amusing story. I’ve gotten worn out by people constantly asking “When is 1.0 coming out? When is 1.0 coming out?” My response always is, <em>“Why do you need an arbitrary number assigned to the product? Many people are using the current version, so don’t hold back. Version numbers are pretty meaningless.”</em></p>
<p>So I suggested on the <a href="http://groups.google.com/group/django-developers/browse_thread/thread/b4c237ad76f9eeca/86e2d86b9fa8280f?lnk=gst&amp;q=version&#43;2.0#86e2d86b9fa8280f"target="_blank" rel="noopener">django-developers</a> mailing list that we assign the version number “2.0,” as if to reinforce the fact that version numbers are indeed arbitrary. But, having given it some more thought, I’ve changed my mind on that. The amount of confusion it would cause would not be justified by the amount of rebellious pleasure I would get out of it.</p>
<p>As far as upcoming features, there are a number of ongoing branches of the Django codebase that will all eventually be folded into the main trunk. One is a branch called <a href="http://code.djangoproject.com/wiki/NewformsAdminBranch"target="_blank" rel="noopener">newforms-admin,</a> which dramatically improves the amount of customization developers can make to the Django admin site. Another is <a href="http://code.djangoproject.com/wiki/QuerysetRefactorBranch"target="_blank" rel="noopener">queryset-refactor,</a> which is a refactoring of our database layer, again, to make it more extensible. Other than that, there are a few small features that we need to wrap up, and it’ll be time for 1.0.</p>
<p><strong>AkitaOnRails:</strong> Haha, I think you’re right, I don’t like this ‘cult of the dot-oh’ as well, it’s pretty meaningless for an open source project that’s constantly evolving. But some people do have some concerns specially in a more enterprisey-like environment. Anyway, and for those that want to see Django in action, what are the websites that you recommend people visit? Some of the Washington Post, maybe? Of course, some of the great features – as the admin site – are only for admins (duh) ï¿¼ But people like to see ‘living’ things instead of a bunch of tutorials.</p>
<p><strong>Adrian:</strong> As an aside, I see that Rails David just <a href="http://www.loudthinking.com/posts/20-dont-overestimate-the-power-of-versions"target="_blank" rel="noopener">wrote a rant</a> about the same topic.</p>
<p>Good Django-powered sites? Let’s see – there’s <a href="http://www.tabblo.com/"target="_blank" rel="noopener">Tabblo</a>, a photo-sharing and editing site that has some really nice interactive features. There’s <a href="http://curse.com"target="_blank" rel="noopener">Curse.com</a>, the massive online-gaming site. There’s <a href="http://lawrence.com"target="_blank" rel="noopener">lawrence.com</a>, a site I helped build, which is one of the best local-entertainment sites in the world (if I may say!). And there’s a site (I forget its name) that will print a paper book from a series of Wikipedia articles that you specify – that blew my mind, the first time I saw it.</p>
<p><strong>AkitaOnRails:</strong> So, I think we are done ï¿¼:-) That was a very pleasant conversation. I wish you have a great New Year.</p>
<p><strong>Adrian:</strong> Thanks for the great questions and conversation, and happy new year!</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/12/31/Picture_1.png" alt=""  loading="lazy" /></p>
]]></content:encoded><category>interviews</category><category>web-development</category></item><item><title>Rails Manifesto</title><link>https://akitaonrails.github.io/en/2006/04/11/rails-manifesto/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2006/04/11/rails-manifesto/</guid><pubDate>Tue, 11 Apr 2006 12:09:00 -0300</pubDate><description>&lt;p&gt;A developer once described Ruby as two parts Perl, one part Python, and one part Smalltalk.&lt;/p&gt;
&lt;p&gt;When &lt;a href="http://www.artima.com/intv/ruby.html"target="_blank" rel="noopener"&gt;Yukihiro Matsumoto&lt;/a&gt; created Ruby as a replacement for Perl and Python, inspired by Smalltalk, in 1993, he could hardly have imagined how far his language would reach outside Japan.&lt;/p&gt;
&lt;p&gt;But in mid-2004, when &lt;a href="http://www.loudthinking.com/about.html"target="_blank" rel="noopener"&gt;David Heinemeier Hansson&lt;/a&gt; released his &amp;ldquo;Rails&amp;rdquo; framework — born from the products he built at his company 37signals — I believe he already knew exactly where he was headed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>A developer once described Ruby as two parts Perl, one part Python, and one part Smalltalk.</p>
<p>When <a href="http://www.artima.com/intv/ruby.html"target="_blank" rel="noopener">Yukihiro Matsumoto</a> created Ruby as a replacement for Perl and Python, inspired by Smalltalk, in 1993, he could hardly have imagined how far his language would reach outside Japan.</p>
<p>But in mid-2004, when <a href="http://www.loudthinking.com/about.html"target="_blank" rel="noopener">David Heinemeier Hansson</a> released his &ldquo;Rails&rdquo; framework — born from the products he built at his company 37signals — I believe he already knew exactly where he was headed.</p>
<p>Ruby on Rails is a product of the current <a href="http://www.blogger.com/post-create.g?blogID=25846821"target="_blank" rel="noopener">Web 2.0</a> movement. I&rsquo;m sure most people still haven&rsquo;t grasped it yet, but we are at the inflection point of a new generation.</p>
<p>It&rsquo;s a fact: Sun got it wrong with J2EE. A flawed implementation of a flawed set of requirements. Those who followed that story will remember the nightmare of EJB 1 and 2, the slow and poor Web Services support, more delays in the promise of EJB 3, and so on.</p>
<p>We are wasting enormous amounts of time building simple things. Web applications shouldn&rsquo;t be complex. The Web was created to be simple. But J2EE was built to solve 100% of &ldquo;Enterprise&rdquo; problems. And we all know that when you try to fix everything, you end up fixing nothing properly.</p>
<p>With Rails, we finally become compatible with the dynamic spirit of Web 2.0. Web Services, Ajax — none of that is hard. And the traditional stuff: persistence layers, version control, MVC, templates, caching, security — all become what they always should have been: trivial.</p>
<p>When someone wants to change the requirements, we cringe: it takes enormous effort to implement modifications. After all, we invented an entire family of paraphernalia — like UML — precisely to ossify and obstruct any kind of change. Let&rsquo;s be honest: UML was created so consultants could charge more for their work.</p>
<p>With Rails, changes are welcome. Rails is the first platform truly compatible with the Agile thinking advocated in the <a href="http://agilemanifesto.org/principles.html"target="_blank" rel="noopener">Agile Manifesto</a>. J2EE made the term famous, but that promise never delivered for it.</p>
<p>To kick off my articles, I think it&rsquo;s important to clarify two concepts that Rails made famous:</p>
<ul>
<li>
<p><em>DRY – Don&rsquo;t Repeat Yourself</em>. Rails has native, simple support for Helpers, Templates, Engines, Plugins — all well integrated so that you can be productive without being messy. Copy and paste is bad; we have to stop doing it, and Rails gives us the tools to do so.</p>
</li>
<li>
<p><em>Convention over Configuration</em>. Instead of editing dozens of XML and properties files, Rails architecture prefers conventions: put your controller with the right name in the right directory and it will be found. Put your model in the right place and it will be located.</p>
</li>
</ul>
<p>Two simple concepts, implemented throughout the entire framework, and they make all the difference.</p>
<p>And don&rsquo;t let prejudice lead you astray. I&rsquo;ve already heard people say: <em>&ldquo;Ruby is an uncompiled language, so it must be slow&rdquo;</em>, <em>&ldquo;everything Ruby does I can do in Java&rdquo;</em>, <em>&ldquo;Ruby doesn&rsquo;t scale well&rdquo;</em>, <em>&ldquo;I don&rsquo;t need to learn another language&rdquo;</em>, <em>&ldquo;Java will never go away&rdquo;</em>.</p>
<p>Replace <em>&ldquo;Ruby&rdquo;</em> with <em>&ldquo;Java&rdquo;</em> and <em>&ldquo;Java&rdquo;</em> with <em>&ldquo;C&rdquo;</em> — or any other language. Sounds like we&rsquo;re hearing the exact same things from 10 years ago, when Java was the novelty.</p>
<p>It seems there&rsquo;s a tendency to criticize first and only look at the good parts later — and when we finally do, we realize we&rsquo;ve arrived too late to the party.</p>
<p>How about doing something different this time, for a change?</p>
]]></content:encoded><category>rails</category><category>web-development</category><category>software-engineering</category></item></channel></rss>