<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Frontend | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/frontend/</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:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/frontend/index.xml" rel="self" type="application/rss+xml"/><item><title>We're Living in the Future: Cars Fly and We Don't Have to Write HTML/CSS Anymore</title><link>https://akitaonrails.github.io/en/2026/10/16/living-in-the-future-cars-fly-no-more-html-css/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/16/living-in-the-future-cars-fly-no-more-html-css/</guid><pubDate>Fri, 16 Oct 2026 02:00:00 -0300</pubDate><description>&lt;p&gt;It&amp;rsquo;s past two in the morning and I&amp;rsquo;m still awake because I just watched the new Tesla Roadster climb a vertical ramp live, stop upside down, then spin in midair and land in one piece on another run.&lt;/p&gt;
&lt;p&gt;I expected a fast car. I didn&amp;rsquo;t expect one with ten compressed-air propulsion nozzles using SpaceX-derived technology, plus an underbody suction system capable, &lt;a href="https://paultan.org/2026/10/16/tesla-roadster-revealed-flies-on-spacex-air-thrusters-0-96-km-h-in-0-81s-production-2028/"target="_blank" rel="noopener"&gt;according to Elon Musk, of pulling up to 2 G&lt;/a&gt;. During the demo, Tesla announced zero to 60 miles per hour, nearly 97 km/h, in 0.81 seconds. It did not disclose the measurement protocol.&lt;/p&gt;</description><content:encoded><![CDATA[<p>It&rsquo;s past two in the morning and I&rsquo;m still awake because I just watched the new Tesla Roadster climb a vertical ramp live, stop upside down, then spin in midair and land in one piece on another run.</p>
<p>I expected a fast car. I didn&rsquo;t expect one with ten compressed-air propulsion nozzles using SpaceX-derived technology, plus an underbody suction system capable, <a href="https://paultan.org/2026/10/16/tesla-roadster-revealed-flies-on-spacex-air-thrusters-0-96-km-h-in-0-81s-production-2028/"target="_blank" rel="noopener">according to Elon Musk, of pulling up to 2 G</a>. During the demo, Tesla announced zero to 60 miles per hour, nearly 97 km/h, in 0.81 seconds. It did not disclose the measurement protocol.</p>
<p>This is the future I wanted to see.</p>
<p>The entire demonstration is in <a href="https://livestream.tesla.com/"target="_blank" rel="noopener">Tesla&rsquo;s official livestream</a>. <a href="https://www.roadandtrack.com/news/a74151425/the-air-assisted-tesla-roadster-flies-in-demo-hits-0-60-in-08-seconds/"target="_blank" rel="noopener">Road &amp; Track measured and described the maneuvers</a>, while <a href="https://techcrunch.com/2026/10/15/tesla-reveals-spacex-powered-roadster-that-flies-and-falls-with-style/"target="_blank" rel="noopener">TechCrunch detailed the three versions</a>: Performance with active aerodynamics and suction, Thrust with rear thrusters, and Rocket with a ten-nozzle propulsion system and a single seat.</p>
<p>Of course, we watched a prototype in a controlled demonstration. Tesla expects production to begin in 2028, and the Rocket version is not approved for public-road use. Still, an actual car left the ground, rotated, and landed. I watched it live from my office. It was spectacular.</p>
<p>While Musk was landing rockets and making cars fly, I was doing something far smaller and more mundane: fiddling with my websites.</p>
<h2>Six sites in about a day<span class="hx:absolute hx:-mt-20" id="six-sites-in-about-a-day"></span>
    <a href="#six-sites-in-about-a-day" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before the Tesla event, I spent the entire day redesigning six sites:</p>
<ul>
<li><a href="https://ailair.akitaonrails.com/"target="_blank" rel="noopener">Akita&rsquo;s AI Lair</a>, now matching the look of my new book, <em>Competent Enough</em>;</li>
<li><a href="https://aimemory.sh/"target="_blank" rel="noopener">ai-memory</a>;</li>
<li><a href="https://aijail.io/"target="_blank" rel="noopener">ai-jail</a>;</li>
<li><a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a> newsletter;</li>
<li>the <a href="https://blog.themakitachronicles.com/"target="_blank" rel="noopener">newsletter archive</a>;</li>
<li>and this blog, <a href="https://akitaonrails.com/"target="_blank" rel="noopener">AkitaOnRails.com</a>.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/10/redesign-seis-sites-stitch.jpg" alt="The six redesigned sites in production: Akita’s AI Lair, ai-memory, ai-jail, The M.Akita Chronicles, the newsletter archive, and AkitaOnRails"  loading="lazy" /></p>
<p>They all started with variations of that default theme Claude makes. Dark background, purple or blue gradient, huge sans-serif type, rounded cards, a glow behind something. It works. It has also become so common that you can spot a &ldquo;site made by Claude&rdquo; before reading the title.</p>
<p>I had already tried asking Claude for different designs. It changed the color, moved the cards around, and delivered the same visual grammar. I posted <a href="https://x.com/AkitaOnRails/status/2110809199890300986"target="_blank" rel="noopener">some screens and the result of the change on Twitter</a>. I&rsquo;m no web designer, and for a while I accepted &ldquo;it works.&rdquo; Yesterday I got tired of it.</p>
<p>Someone suggested <a href="https://stitch.withgoogle.com/"target="_blank" rel="noopener">Stitch by Google</a>. It&rsquo;s an experimental Google Labs tool that turns instructions and images into editable interfaces and front-end code. The <a href="https://developers.googleblog.com/en/stitch-a-new-way-to-design-uis/"target="_blank" rel="noopener">first version launched in 2025</a> on top of Gemini; the <a href="https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-ai-ui-design/"target="_blank" rel="noopener">current version</a> has become a design canvas with an agent, prototypes, and export to a <code>DESIGN.md</code> that other agents can follow.</p>
<p><a href="https://blog.google/innovation-and-ai/models-and-research/google-labs/stitch-ai-ui-design/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/10/stitch-ai-native-canvas-google.webp" alt="The Google Stitch canvas with multiple interfaces, visual variations, and design-system controls"  loading="lazy" /></a></p>
<p>I signed up, got the API token, and went to work. Stitch generated visual directions. Claude Code and Codex brought those directions into the real repositories, adapted the components, fixed responsiveness, replaced images, adjusted SEO, tested, and deployed. When one quota ran out, I continued with the other.</p>
<p>The six sites come from five repositories. Looking at them side by side made one thing clear: Stitch responds much better when it receives a concrete metaphor, the audience, and what the product needs to communicate. ai-memory became a memory archive. ai-jail became a cinematic prison. AI Lair adopted a 1950s and 1960s world&rsquo;s fair, drawn from my book cover. The newsletter became a late-night broadcast, and this blog became an editorial archive. Five different identities instead of five variations of Claude&rsquo;s same gradient.</p>
<p>The pretty prototype is the easy part. The job only ends when the visual language reaches internal pages, the article, the book reader, email, legal pages, error states, and mobile. That&rsquo;s what the agents did inside the repositories. On ai-memory, for example, verification covered 102 pages in six languages, light and dark themes, desktop and mobile. On The M.Akita Chronicles, the same design had to work across signup, archive, newsletter detail, podcast, and the email template. A Stitch mockup solves none of that by itself.</p>
<p>Each site went through several rounds of prototypes. Some looked awful. Others had one good idea surrounded by bad decisions. I told the agents to throw them away, preserve one part, mix it with another attempt, and start over. Code is cheap now, so there is no reason to fall in love with the first version.</p>
<p>The result won&rsquo;t win a design award. A professional designer would still find hundreds of things to improve. I know I could take it further by spending a few more days. It looks much better than it did, though, and for now it is competent enough.</p>
<p>Six existing sites redesigned, responsive, with SEO, image generation and replacement, HTML, CSS, and JavaScript adjustments, testing, and production deployment. One person. About a day. A year ago, that package would have taken weeks or months.</p>
<p>Keep the scale in perspective: these are small sites, mostly landing pages and content pages. Nobody is rebuilding Amazon.com&rsquo;s entire collection of pages and systems in a day. The experiment shows how much repetitive work has already moved out of the way within a scope that one experienced person can understand and review. Large projects remain large, full of private rules, old integrations, strange states, and decisions that do not fit in a prompt.</p>
<h2>Even the first proofreading pass can be automated<span class="hx:absolute hx:-mt-20" id="even-the-first-proofreading-pass-can-be-automated"></span>
    <a href="#even-the-first-proofreading-pass-can-be-automated" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As part of the same process, I had agents review the sites as different readers. This wasn&rsquo;t grammar correction. I wanted to know what someone would understand in the first few seconds, where they would get lost, which promise would sound suspicious, and whether they could complete a task without already knowing the product.</p>
<p>On ai-jail, I used two isolated sessions. One played a skeptical engineer who approves every prompt and had never heard of the project. The other was a non-engineer managing eight developers, trying to decide whether to approve the tool for the company. On ai-memory, I used three: an engineer learning AI tools, a manager evaluating a pilot, and a parent with no agent experience who worried about AGI and jobs. Each one read the site without seeing the others&rsquo; reports.</p>
<p>Anyone can do the same thing. Run the application locally or open a preview deployment, start fresh sessions, and give each agent a concrete task. A basic prompt can look 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-text" data-lang="text"><span class="line"><span class="cl">Open this site on desktop and mobile: [URL].
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You are [persona], know [context], and have never seen this product.
</span></span><span class="line"><span class="cl">Your goal is [specific decision or task].
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Navigate as this person. Do not read code or internal documentation first.
</span></span><span class="line"><span class="cl">Record:
</span></span><span class="line"><span class="cl">- what you understood in the first ten seconds;
</span></span><span class="line"><span class="cl">- what you think the product does and who it is for;
</span></span><span class="line"><span class="cl">- where you did not know the next step;
</span></span><span class="line"><span class="cl">- contradictory, absolute, or hard-to-believe promises;
</span></span><span class="line"><span class="cl">- terms the site uses before explaining them;
</span></span><span class="line"><span class="cl">- reading, keyboard, contrast, touch, and responsiveness problems;
</span></span><span class="line"><span class="cl">- what would stop you from trying, buying, or approving it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For every finding, show the page and visible evidence.
</span></span><span class="line"><span class="cl">Separate factual bugs, usability problems, and subjective preferences.
</span></span><span class="line"><span class="cl">Do not propose a broad redesign. Suggest the smallest change that solves the problem.</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 prefer three independent sessions over an agent roundtable. Once one reads another&rsquo;s report, it starts agreeing by inertia. A separate session can then consolidate repeated points, discard matters of taste, and check every claim against the code and product documentation. A review can flag a sentence as wrong; it has no authority to invent the right one.</p>
<p>The most useful results tend to fall into four groups: positioning, information order, path to the first action, and trust. Traditional automated tests still handle broken links, contrast, accessibility, overflow, and error states. The persona tells you that a technically accessible button appeared five screens below where the person needed it.</p>
<p>During the <a href="https://github.com/akitaonrails/ai-jail-web/blob/main/docs/research/PROOFREAD-2026-10-15-BRIEF.md"target="_blank" rel="noopener">ai-jail proofreading</a>, both profiles agreed that the site took too long to show a concrete risk and started speaking like engineering documentation too early. The threat of a poisoned page moved directly below the hero. New sections explained &ldquo;What changes on day one&rdquo; and &ldquo;For the person who must approve this,&rdquo; including operating cost, limitations, and the keys a company actually exposes. The review also caught a technically incorrect claim repeated across several pages: the network does not stay entirely off by default. The agent&rsquo;s own API host and, on Linux, registries remain accessible. This kind of reading catches documentation bugs too.</p>
<p>The <a href="https://github.com/akitaonrails/ai-memory-web/blob/main/docs/research/persona-review.md"target="_blank" rel="noopener">ai-memory report</a> found permissions described in contradictory ways, privacy promises that were too absolute, and the basic explanation buried under advanced features. During the second round, the profiles noticed that remote-access instructions told users to use HTTP before requiring TLS, that an unauthenticated <code>curl</code> promised the wrong response, and that important choices appeared far too late on mobile. The collection guide moved from about 4,364 pixels from the top to 1,617; the individual and team paths moved from 5,300 to 2,489. That&rsquo;s an objective improvement born from simulated reading.</p>
<p>In the <a href="https://github.com/akitaonrails/ai-toolkit-web/pull/4"target="_blank" rel="noopener">AI Lair redesign</a>, the new-reader review reorganized the entry path. A <a href="https://github.com/akitaonrails/ai-toolkit-web/commit/c2a5237e876f662a8b53450f23f0162fdc1d4a76"target="_blank" rel="noopener">later revision</a> made it explicit that my setup with multiple agents and two monitors is a mature configuration, not a requirement for starting. One person, one project, and one agent are enough. It also produced a pilot plan for managers: a reversible task, non-sensitive data, an acceptance criterion, deadline, budget, and measurement. The next audit found invalid ARIA and an animation that hid the opening copy from assistive technologies; the <a href="https://github.com/akitaonrails/ai-toolkit-web/pull/5"target="_blank" rel="noopener">fix</a> took axe-core from four serious violations to zero.</p>
<p>Simulated personas still fall far short of human feedback. They carry the models&rsquo; same biases, may agree with the prompt, never feel the irritation of someone who lost twenty minutes, and never take the risk of entering their own credit card. The reports serve as hypotheses. A real user will always find the case that I, the agent, and the test suite failed to imagine.</p>
<p>Before the first user exists, however, the alternative is usually no review at all. Two or three independent readings already catch contradictory promises, unexplained jargon, buried information, and a flow that only makes sense to its author. These readings cost little and make the product less stupid before it reaches actual people.</p>
<h2>HTML was never programming<span class="hx:absolute hx:-mt-20" id="html-was-never-programming"></span>
    <a href="#html-was-never-programming" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For years, I watched people perform some remarkable mental gymnastics to argue that &ldquo;HTML is a programming language&rdquo; and therefore anyone who writes HTML is a programmer.</p>
<p>HTML is markup: it describes a document&rsquo;s structure. CSS describes presentation. That has value, requires knowledge, and can become terribly complex. Accidental complexity does not turn markup into programming.</p>
<p>There is no shame in that. I started building sites in the 1990s. I received a Photoshop layout and turned it into HTML. I sliced images, built tables, adjusted pixels, opened the result in multiple browsers, went back, and tried again. It was honest work and paid my bills.</p>
<p>It was also basically mechanical work: copy, paste, adjust, look, repeat. Exactly the category computers should automate.</p>
<p>Today I still chose the visual direction, rejected bad proposals, decided what each site needed to communicate, and recognized when the result was good enough. The agents handled the grunt work. This should have happened years ago.</p>
<h2>The bubble delayed automation<span class="hx:absolute hx:-mt-20" id="the-bubble-delayed-automation"></span>
    <a href="#the-bubble-delayed-automation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For much of the 2010s, <a href="https://fred.stlouisfed.org/series/EFFR"target="_blank" rel="noopener">US interest rates stayed near zero</a>. Cheap money flooded funds, startups, and large companies. Hiring more people was easy, and the market absorbed any amount of manual labor disguised as engineering.</p>
<p>Low rates explain only part of tech&rsquo;s expansion. A field breaking hiring records has little incentive to eliminate its own repetitive work. If you can put fifty more people to work assembling screens, you put fifty more people to work assembling screens.</p>
<p>Anyone who attended my talks at tech events in the 2010s may remember <strong>Constraint == Innovation</strong>. Yes, the original title used two equal signs. I gave that talk at DevConf 2019, the <a href="https://www.slideshare.net/slideshow/devconf-2019-so-carlos/143923367"target="_blank" rel="noopener">83 slides are on my SlideShare</a>, and I had already written the same argument in 2013 in <a href="https://startupi.com.br/restricoes-sao-libertadoras-menos-e-mais/"target="_blank" rel="noopener">&ldquo;Constraints are liberating: less is more&rdquo;</a>.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/V0D3PuTLOZ8"
    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>It&rsquo;s the old maxim: <strong>necessity is the mother of invention</strong>. Constraints force choices. Choices force priorities. When the conventional path cannot fit the money, deadline, or available capacity, someone has to find another path. That&rsquo;s where real innovation often comes from.</p>
<p>Money funds expensive research, buys equipment, and gives people time to experiment. Excess money without pressure for results does something else: it hides inefficiency with brute force. There is no need to automate, simplify, or discover a better solution while you can keep hiring and growing the spreadsheet. Too much abundance turns a technical problem into an operating expense and postpones the difficult decision.</p>
<p>I <a href="/en/2026/02/08/rant-ai-killed-programmers/">repeated this warning at the beginning of 2026</a>: the programming bubble managed to replace efficiency with volume. Constraints returned when cheap money ended. Coincidentally, agents arrived at the same time and gave companies a practical way to automate the mechanical work they had spent a decade scaling through hiring.</p>
<p>That&rsquo;s how we spent a decade recreating the same admin dashboard and calling it innovation. Every week brought the next front-end savior: React, Angular, Vue, Svelte, another <em>bundler</em>, another framework on top of the framework. They are useful tools in the right context, but none addressed the obvious question: why is a human still hand-writing the thousandth variation of the same form?</p>
<p>The hiring bubble burst in 2022 and ended the party. At the end of that year, ChatGPT brought language models to the public. GPT-2, just to keep the chronology straight, had come out in 2019. Four years after ChatGPT, agentic programming became a normal work tool.</p>
<p>Any task that consists of copying a pattern, adapting an example, running it, looking at the error, and trying again is already within these agents&rsquo; reach. Front-end is the most visible target because it accumulated an absurd amount of this work. The rest of programming has no mystical shield. Plenty of back-end work is CRUD, glue between APIs, and repetitive configuration. That will be automated too.</p>
<p>Fine by me. Programming was never about typing code. Programming is making a computer do something useful.</p>
<h2>VS Code is the new punch card<span class="hx:absolute hx:-mt-20" id="vs-code-is-the-new-punch-card"></span>
    <a href="#vs-code-is-the-new-punch-card" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I wrote about exactly this in April in <a href="/en/2026/04/11/vs-code-is-the-new-punch-card/">&ldquo;VS Code Is the New Punch Card&rdquo;</a>.</p>
<p>For decades, the interface between intention and computer kept changing. First you connected wires and switches. Then you punched cards. Then you wrote Assembly, C, high-level languages, frameworks, and configuration. Each generation looked at the previous one and automated another layer of manual work.</p>
<p>No sane person would argue today that operating a card punch was the essence of computer science. It was the available interface. Typing every tag and CSS rule into VS Code was also the available interface.</p>
<p>Now I describe the intention, show references, evaluate the result, and tell the agent to fix it. I still need to understand browsers, networks, caching, accessibility, semantics, responsiveness, and SEO. Without the fundamentals, I couldn&rsquo;t tell whether the agent delivered a solution or a pretty time bomb. The keyboard lost importance; judgment gained it.</p>
<p>Today, every Tier A model I test, from Claude to Kimi, can reliably execute the mechanical front-end work that appeared in these six projects. I can say without hesitation that any front-end task that merely turns an already-made decision into code is automatable. They needed direction to create the six designs. None required me to hand-write the flexbox, breakpoint, or component I had already explained.</p>
<p>That covers an enormous share of the work while leaving the most valuable part. Someone still has to know the framework&rsquo;s tradeoffs, understand the browser, choose the architecture, notice when a library is being forced against its design, and find out why an apparently correct solution breaks under a particular combination of device, language, state, and real data. The agent writes the code; an experienced professional provides direction, limits the damage, and owns the result.</p>
<p>Front-end still exists. The excess mechanical work and the people hired only to follow a finished recipe lose value. A great front-end professional can direct agents and deliver far more than they could alone. A mediocre professional who spent years mistaking component typing for engineering will compete for fewer openings. The market gets smaller and more selective, with its best professionals producing more. The goal remains what it always was: get close to the top, where judgment and experience still make a difference.</p>
<p>And it doesn&rsquo;t stop at the browser. <a href="/en/2026/10/11/are-there-better-languages-for-programming-with-llms/">Pick the framework</a>: Android with Flutter or Kotlin, iOS with Swift and SwiftUI, desktop with Rust/Tauri, Flutter, C#, or Electron. Modern agents generally know the components, build tools, tests, and peculiarities of these platforms. They still need direction, feedback, and verification, just as they do on the web front-end. The mechanical part is already automatable.</p>
<p>I tested this in my own small open-source projects. <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">FrankYomik</a> has a Flutter client for Android and Linux, plus the server and extension. <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a> is a Rust/Tauri desktop app that produces an AppImage for Linux, a DMG for macOS, and a <code>.exe</code> installer for Windows. I also built <a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">Frank Karaoke</a> in Flutter for Android and <a href="https://github.com/akitaonrails/frank_mangaplus"target="_blank" rel="noopener">FRANK MANGA+</a> in Rust/Tauri for desktop. There are several others on <a href="https://github.com/akitaonrails?tab=repositories"target="_blank" rel="noopener">my GitHub</a>.</p>
<p>None came out perfect from the first prompt. Flutter and Tauri gave me trouble, especially on the early projects. Reliable means the agent can move forward, receive a correction, create tests, fix what broke, and reach a working application without making me take over the manual typing. After testing web, Android, and desktop across different stacks, I&rsquo;m very satisfied with the result. The pattern repeats: mechanical work became agent work.</p>
<p>At two in the morning, I had agents making the final adjustments to this blog without losing pace. That&rsquo;s what I need.</p>
<h2>When the web still didn&rsquo;t know what it was<span class="hx:absolute hx:-mt-20" id="when-the-web-still-didnt-know-what-it-was"></span>
    <a href="#when-the-web-still-didnt-know-what-it-was" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>At the peak of the dot-com bubble, career designers were creating websites. The web had not yet decided how a page should work, so there was real room to experiment. It was often ugly and almost always heavy, but actual creation was happening.</p>
<p>The iPhone opened another window in 2007. Suddenly everything had to fit on a small screen, respond to touch, and work without a mouse. Years of experimentation followed before the market settled on a reasonably stable grammar for navigation, gestures, forms, and content.</p>
<p>Today nobody needs to rediscover a menu, a list, a checkout, or an article page. The patterns are documented, millions of examples are public, and models have seen more interfaces than any person could study in a lifetime.</p>
<p>The most valuable work remains: understanding the product, the person who will use it, the constraints, and what deserves to appear first. Converting a finished decision into hundreds of predictable lines has become a perfect candidate for automation.</p>
<h2>Jobs disappear. Others appear<span class="hx:absolute hx:-mt-20" id="jobs-disappear-others-appear"></span>
    <a href="#jobs-disappear-others-appear" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The world has never stopped to preserve a job function.</p>
<p>Punch-card operators entered data and programs on paper cards. Telephone operators physically connected calls. Typists kept offices running. Linotype operators cast lines of hot metal to print newspapers. Technology reduced or eliminated these occupations at scale. The Bureau of Labor Statistics&rsquo; <a href="https://www.bls.gov/emp/tables/fastest-declining-occupations.htm"target="_blank" rel="noopener">current list of fastest-declining occupations</a> still includes data entry keyers, typists, and telephone operators. Nobody proposes bringing them back to protect jobs.</p>
<p>At the same time, jobs appeared that a person in 1926 could not even name: mobile developer, SRE, experience designer, Internet video creator, cloud specialist, machine learning engineer. The web itself killed old work and opened an entire industry.</p>
<p>The real story is messier than the caricature. The <a href="https://www.ilo.org/publications/generative-ai-and-jobs-refined-global-index-occupational-exposure"target="_blank" rel="noopener">ILO shows that technology tends to automate tasks without necessarily eliminating entire occupations</a>. Some functions shrink, others change, and new jobs appear. That statistical consolation does nothing for the person who became obsolete today. The individual has to react before the economic average reacts for them.</p>
<p>Compare 2026 with 1926, three years before the crash of 1929. It looks like another planet, even though a century is very little on history&rsquo;s scale. No contract guarantees that your current function will survive the next ten years, much less the next forty.</p>
<h2>How does a junior gain experience now?<span class="hx:absolute hx:-mt-20" id="how-does-a-junior-gain-experience-now"></span>
    <a href="#how-does-a-junior-gain-experience-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It will get harder. The old ladder began with someone paying a beginner to assemble simple screens, fix CSS, and copy components. An agent does much of that faster and cheaper. No company has an obligation to preserve inefficient work as a training program.</p>
<p>Beginners will have to build their own ladder with real projects and problems they are responsible for. That includes reading the code the agent generated, breaking and repairing it, studying documentation and source code, writing tests, and contributing to open projects. An agent gives any student access to an amount of practice that once required a team. Anyone who uses that access only to generate code they do not understand will remain a junior for a long time.</p>
<p>The market will reward people who make that extra effort. It always has. When someone asks how a junior can gain experience without the easy tasks, the hidden question is often different: &ldquo;how do I get a better salary without doing the hard work of getting good?&rdquo; The easy position, with rising pay and little pressure, was a distortion of the bubble. Adapting includes accepting that entry has become more competitive.</p>
<h2>My career never stayed in one place<span class="hx:absolute hx:-mt-20" id="my-career-never-stayed-in-one-place"></span>
    <a href="#my-career-never-stayed-in-one-place" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="/2023/02/14/akitando-138-meu-comeco-de-carreira-durante-a-bolha-1997-a-2002-bug-do-milenio-crash/">I started with multimedia and static websites</a>. Then I worked in a digital agency, enterprise consulting, SAP integration, startups, video, education, and now AI. In my <a href="/2018/12/26/akitando-34-voce-nao-sabe-nada-de-enterprise-conhecendo-a-sap/">Akitando episode about SAP</a>, I tell part of that phase when I had to learn a world completely different from what I had done before.</p>
<p>I lived through the dot-com crash, the 2008 crisis, and the end of the zero-rate bubble. I survived because I never treated my current function as a permanent identity. I kept studying, gaining experience, and changing direction.</p>
<p>A productive career should not consist of repeating the same mechanical task for decades. If your value depends on the market continuing to pay someone to push tags and pixels by hand, the problem did not begin with AI. The problem was betting that the present would last forever.</p>
<p>Many people understand &ldquo;being prepared&rdquo; as studying the bare minimum required for today&rsquo;s open position. That&rsquo;s the fastest recipe for becoming obsolete. Do the short-term work, but study beyond it. Learn the things that seem useless today. Fundamentals, operating systems, networks, databases, business, economics, history. You don&rsquo;t know which combination will save your career five years from now.</p>
<p>I spent years talking about this on Akitando. Revisit the <a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUehzKjiRruy--gncHz9Injy"target="_blank" rel="noopener"><strong>I Warned You</strong></a> and <a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUd6wEL932R7dW9Zazt2oNWF"target="_blank" rel="noopener"><strong>Which course should I take?</strong></a> playlists. If you have to start with one video, start with the <strong>Definitive Guide to Learning How to Learn</strong>. No course can save someone who still outsources their own learning.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/oUPaJxk6TZ0"
    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>The world has no shortage of opportunity. Most people feel they never get one because they are always unprepared when it appears. Once the opportunity lands in front of you, it is too late to start preparing.</p>
<p>So I won&rsquo;t mourn the fact that this package of six small sites, which used to take weeks or months, fit into about a day. I&rsquo;m happy as hell. I gained time to think about what to build, experiment more, and throw away bad versions without wasting weeks of my life.</p>
<p>Today I watched a car spin in midair and land. In about a day, I put six sites with new designs into production without writing most of the HTML and CSS. The future I wanted did not arrive perfect, cheap, or evenly distributed. It arrived far enough for me to work with it.</p>
<p>Accept what ended. Study what comes next. Be ready before the next opportunity appears.</p>
]]></content:encoded><category>artificial-intelligence</category><category>frontend</category><category>automation</category><category>career</category></item><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>[Beginner] Trying out Rails 5.1.x</title><link>https://akitaonrails.github.io/en/2017/10/24/beginner-trying-out-rails-5-1-x/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/10/24/beginner-trying-out-rails-5-1-x/</guid><pubDate>Tue, 24 Oct 2017 16:29:00 -0200</pubDate><description>&lt;p&gt;One thing that is not so great about having to deal with client projects is that most of them take their time before doing a full framework upgrade. And you can&amp;rsquo;t blame them, as many of the dependencies in the ecosystem take their sweet, sweet time to upgrade as well.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://weblog.rubyonrails.org/2017/9/29/this-week-in-rails-getting-closer-to-rails-5-2-beta/"target="_blank" rel="noopener"&gt;Rails 5.2 Beta&lt;/a&gt; is almost upon us, and I would bet that most of the Rails projects out there are in the 4.2 version, still thinking about doing the jump to 5.0 (which is not so bad an upgrade).&lt;/p&gt;</description><content:encoded><![CDATA[<p>One thing that is not so great about having to deal with client projects is that most of them take their time before doing a full framework upgrade. And you can&rsquo;t blame them, as many of the dependencies in the ecosystem take their sweet, sweet time to upgrade as well.</p>
<p><a href="http://weblog.rubyonrails.org/2017/9/29/this-week-in-rails-getting-closer-to-rails-5-2-beta/"target="_blank" rel="noopener">Rails 5.2 Beta</a> is almost upon us, and I would bet that most of the Rails projects out there are in the 4.2 version, still thinking about doing the jump to 5.0 (which is not so bad an upgrade).</p>
<p>By the way, in case you didn&rsquo;t know, the official Rails Guides website has a section dedicated to upgrades. It&rsquo;s called <a href="http://guides.rubyonrails.org/upgrading_ruby_on_rails.html"target="_blank" rel="noopener">A Guide for Upgrading Ruby on Rails</a>. The process of upgrading is not actually so difficult <strong>if, and only if</strong>:</p>
<ul>
<li>
<p>You have a fairly good test coverage. You should have, at the very least, feature specs for the most important parts of your system.</p>
</li>
<li>
<p>You should upgrade to just one subsequent version at a time, never going straight to the most recent one. For example, If you&rsquo;re in Rails 4.1, you must first upgrade to 4.2, make sure everything works. Then go to 5.0. And only then go to 5.1. Each version has <strong>many</strong> deprecations, changes in behavior, changes in default boilerplate configuration, added new features. You must attend to each very carefully.</p>
</li>
</ul>
<p>So, what are you missing if you&rsquo;re still in Rails 4.2?</p>
<p>Again, in case you didn&rsquo;t know, the official Rails Guides website also compiles detailed and easy to read Release Notes for each new version. The most recent are the <a href="http://guides.rubyonrails.org/5_1_release_notes.html"target="_blank" rel="noopener">Ruby on Rails 5.1 Release Notes</a>.</p>
<h2>Starting a new project<span class="hx:absolute hx:-mt-20" id="starting-a-new-project"></span>
    <a href="#starting-a-new-project" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <code>rails new</code> command has a lot of new flags. And I think these are the ones most people will use:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rails new --skip-action-mailer --skip-coffee --webpack=react my_fancy_new_project</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>You can disable features you don&rsquo;t need such as ActionMailer or ActionCable. You can disable CoffeeScript. It was cool for a while but now that ES6 exists we should just use it.</p>
<p>Rails 5.1 comes with the support for webpack. But until Rails 5.2 comes up, we need to use the <code>webpack-dev-server</code> manually in development mode so we can hot reload assets. For that end, I recommend installing good old <a href="https://github.com/ddollar/foreman"target="_blank" rel="noopener">Foreman</a>. Install it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gem install foreman</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>And add a <code>Procfile.dev</code> file to your project with the following content:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: ./bin/rails s -b 0.0.0.0 -p 3000
webpack: ./bin/webpack-dev-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>
<p>And now you can start up your development server like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>foreman start -f Procfile.dev</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 production, you don&rsquo;t need the <code>webpack-dev-server</code> as the <code>bin/rails assets:precompile</code> task should be able to compile all the assets you need as static, timestamped files in <code>public/assets</code>.</p>
<h2>Front-end Specific Changes<span class="hx:absolute hx:-mt-20" id="front-end-specific-changes"></span>
    <a href="#front-end-specific-changes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now, you can rejoice with Yarn, Webpack, and no more jQuery pre-installed. Samuel Muller has a <a href="http://samuelmullen.com/articles/embracing-change-rails51-adopts-yarn-webpack-and-the-js-ecosystem/"target="_blank" rel="noopener">nice article</a> that summarizes most of these changes in more detail.</p>
<p>Now Ruby on Rails officially supports every bell and whistle from the Javascript ecosystem. There is nothing left behind that you might miss.</p>
<p>No more manually vendoring JS assets in <code>vendor/assets/javascripts</code> directory. No more need to use <a href="https://rails-assets.org/"target="_blank" rel="noopener">Rails Assets</a>, which was a secondary RubyGems source specific to package JS libraries into RubyGems.</p>
<p>Let&rsquo;s say that, for some reason, you want jQuery back. You can now just do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn add jquery</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>And, as usual, you can simply add it to your <code>app/assets/javascripts/application.js</code> manifest file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>// app/assets/javascripts/application.js
...
//= require jquery
...
//= require_tree .</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 so-called <a href="https://github.com/rails/rails/pull/26836"target="_blank" rel="noopener">&ldquo;support for Yarn in Rails 5.1&rdquo;</a> boils down to a few wrappers and boilerplate. So you can run <code>rails yarn:install</code> and have the dependencies installed, but you can just as easily type <code>yarn</code> and it will install everything you need that is declared in the <code>package.json</code> file, as with any other Javascript-only project.</p>
<p>Now, the whole magic of the integration comes from the inclusion of the <a href="https://github.com/rails/webpacker"target="_blank" rel="noopener">Webpacker</a> gem. It adds a bunch of boilerplate configuration for webpack. And, <strong>bonus fact</strong> you can include it today in your current Rails 4.2+ projects as well. Just start by adding the webpacker gem to your <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Gemfile
gem &#39;webpacker&#39;, &#39;~&gt; 3.0&#39;</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>Then <code>bundle install</code> as usual and run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>bundle exec rake webpacker:install</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>If you didn&rsquo;t use the <code>--webpack=react</code> flag to the <code>rails new</code> command, you can add it later. Or you can add Angular as well:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>bundle exec rake webpacker:install:angular
bundle exec rake webpacker:install:react </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>You should replace <code>bundle exec rake</code> for <code>bin/rails</code> if you&rsquo;re in Rails 5.1.</p>
<p>Adding proper React components is a bit more than the command above though. And the details are more than I want to cover in this post.</p>
<p>Andy Barnov has a <a href="https://x-team.com/blog/get-in-full-stack-shape-with-rails-5-1-webpacker-and-reactjs/"target="_blank" rel="noopener">good post</a> on how to follow the Rails 5.1 packs convention to add your React components in the pipeline. As he recommends, don&rsquo;t fight the conventions and you should be golden.</p>
<p>Oddly enough, even though I used the <code>--webpack-react</code> flag to the <code>rails new</code> I still had to run the following command to bootstrap the webpack and react supports:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>./bin/rails webpacker:install:react</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>But once you do, you will get a new place to add React components, and that&rsquo;s in a proper <code>app/javascripts/packs</code>. And in the views, instead of the usual <code>javascript_include_tag</code> you will use the brand new <code>javascript_pack_tag</code>. I believe you can figure out the rest from the example component <code>hello_react.jsx</code> that it generates for you.</p>
<h2>Final Thoughts<span class="hx:absolute hx:-mt-20" id="final-thoughts"></span>
    <a href="#final-thoughts" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You now have a whole lot more options for Javascript, as Rails 5.1 fully embraces the current standards.</p>
<p>Once upon a time, no one properly knew how to streamline an <a href="http://guides.rubyonrails.org/asset_pipeline.html"target="_blank" rel="noopener">Asset Pipeline</a>. It was very messy to compile all your assets in a single file with proper cache busting timestamps, but the <strong>Sprockets</strong> gem did it.</p>
<p>Once upon a time, Javascript 5 was a damn mess. Libraries such as jQuery actually &ldquo;fixed&rdquo; most of the browser DOM situation, and CoffeeScript normalized the language in a way that made it enjoyable. ES6 came to take over the reins but it&rsquo;s very unfair to criticize jQuery and Coffee as neither ES6 nor HTML5 were there to solve the situation almost 10 years ago. jQuery and Coffee were there, and they solved it.</p>
<p>When Angular, Ember, and React were starting, we already had a good enough solution for javascript-heavy websites with <a href="https://github.com/turbolinks/turbolinks"target="_blank" rel="noopener">Turbolinks</a>. This is still a very good solution that you should consider using instead of adding a full-blown (and sometimes unnecessary) React/Redux solution at once.</p>
<p>CSS is another matter entirely. The Rails community also created Sass (which then Twitter envied and copied to Less for the Bootstrap framework).</p>
<p>It&rsquo;s easy to see the current landscape and just repeat that Coffee is bad or Sprockets is bad. But for the past 10 years, those tools were the best of breed in a time where people didn&rsquo;t have Yarn, Webpack, React and all the tools that popped up in the past 3 years or so.</p>
<p>Rails 5.1 is a very good release to marry the best of Rails with the best that kinda stabilized today in the Javascript mad arena. And I highly recommend that any new, greenfield project, start with Rails 5.1 in mind.</p>
<p>One thing that led me to write this post is to highlight that many people ask questions that are mostly answered in the official <a href="http://guides.rubyonrails.org/"target="_blank" rel="noopener">Rails Guides</a>. Not only on how to install and use the many components of the framework but also how to upgrade and what the new features of each release are. So you should definitely take a better look there.</p>
]]></content:encoded><category>rails</category><category>javascript</category><category>frontend</category></item></channel></rss>