<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://angelblanco.dev/blog/</id>
    <title>Ángel Blanco · Blog</title>
    <updated>2026-11-05T23:00:00.000Z</updated>
    <author>
        <name>Ángel Blanco</name>
        <uri>https://angelblanco.dev/about/</uri>
    </author>
    <link rel="alternate" href="https://angelblanco.dev/blog/"/>
    <link rel="self" href="https://angelblanco.dev/atom.xml"/>
    <subtitle>Latest blog entries of the full-stack engineering blog</subtitle>
    <logo>https://angelblanco.dev/android-chrome-512x512.png</logo>
    <icon>https://angelblanco.dev/favicon.ico</icon>
    <rights>CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ © Ángel Blanco</rights>
    <entry>
        <title type="html"><![CDATA[Install PHP 8.5 with apt on Ubuntu 26.04]]></title>
        <id>https://angelblanco.dev/blog/apt-install-php-85-on-ubuntu-26/</id>
        <link href="https://angelblanco.dev/blog/apt-install-php-85-on-ubuntu-26/"/>
        <updated>2026-11-05T23:00:00.000Z</updated>
        <summary type="html"><![CDATA[Install PHP 8.5 and Composer on Ubuntu 26.04 LTS with apt from Ondřej Surý's packages.sury.org repository, and keep them patched with apt upgrade.]]></summary>
        <content type="html"><![CDATA[<p>Yesterday I posted how to <a href="https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/">install Node.js 26 with apt</a> so the operating system keeps it patched after October's security releases. Today it's PHP's turn, for the same reason: security breaches keep coming, and PHP has had its fair share.</p><p>On September 24, <a href="https://www.php.net/ChangeLog-8.php#8.5.11">PHP 8.5.11</a> fixed more than ten CVEs in OpenSSL, FPM, SOAP, Phar, and the HTTP stream wrapper, including a heap buffer overflow and a TLS hostname verification bypass. On October 21, <a href="https://www.php.net/archive/2026.php#2026-10-21-4">PHP 8.5.12</a> went further: an emergency release for two critical vulnerabilities (CVE-2026-109618 and CVE-2026-109619, CVSS 9.2) that allow unauthenticated remote code execution. In php.net's own words, &quot;updating is the only safe choice&quot;. The 8.4, 8.3, and 8.2 branches got the same fixes.</p><p>Here's the catch: <a href="https://documentation.ubuntu.com/release-notes/26.04/">Ubuntu 26.04 LTS</a> (<em>Resolute Raccoon</em>) ships PHP 8.5 in its archive, but it freezes the version at release time and backports the fixes it considers important. At the time of writing, <code>apt</code> gives you PHP <code>8.5.4</code> with patches on top, while <a href="https://www.php.net/">php.net</a> is already at <code>8.5.12</code>. To be fair, Ubuntu's security team handles the urgent stuff well: it shipped the October RCE fixes the same day as php.net. But the September batch took 25 days to land, and upstream's regular bug fixes rarely make it at all.</p><p>If you want the full upstream release, the easiest way is still to let the operating system handle it. Install PHP with <code>apt</code> from <a href="https://deb.sury.org/">Ondřej Surý's repository</a>, the same packages that have powered PHP on Debian and Ubuntu for more than a decade, and let <code>sudo apt upgrade</code> take care of the rest.</p><h3 id="support-dates-at-a-glance">Support dates at a glance</h3><p>Each PHP branch gets two years of active support followed by two years of security-only fixes. This is where things stand according to the <a href="https://www.php.net/supported-versions.php">official supported versions page</a>:</p><table><thead><tr><th>Version</th><th>Released</th><th>Active support until</th><th>Security support until</th></tr></thead><tbody><tr><td>8.2</td><td>Dec 8, 2022</td><td>Dec 31, 2024</td><td>Dec 31, 2026</td></tr><tr><td>8.3</td><td>Nov 23, 2023</td><td>Dec 31, 2025</td><td>Dec 31, 2027</td></tr><tr><td>8.4</td><td>Nov 21, 2024</td><td>Dec 31, 2026</td><td>Dec 31, 2028</td></tr><tr><td>8.5</td><td>Nov 20, 2025</td><td>Dec 31, 2027</td><td>Dec 31, 2029</td></tr></tbody></table><p>If you're still on PHP 8.2, you have less than two months left. <a href="https://wiki.php.net/todo/php86">PHP 8.6</a> is scheduled for November 19, 2026, but PHP 8.5 is the safe bet to pair with an LTS distribution today. You can always try 8.6 side by side (see <a href="https://angelblanco.dev/blog/apt-install-php-85-on-ubuntu-26/#what-about-other-php-versions">below</a>).</p><p>On the OS side, <a href="https://ubuntu.com/about/release-cycle">Ubuntu 26.04 LTS</a> was released in April 2026 and gets standard security maintenance until May 2031, extended to 2036 with Ubuntu Pro. So this setup will last for a while.</p><h3 id="whats-new-in-php-85">What's new in PHP 8.5</h3><p>If you're coming from PHP 8.4 or older, here are a few reasons to upgrade beyond security. The <a href="https://www.php.net/releases/8.5/en.php">release announcement</a> has the full list, but my favorites are:</p><ul><li>The pipe operator, to chain callables from left to right without temporary variables.</li><li><code>clone()</code> with properties, which turns the &quot;with-er&quot; pattern for <code>readonly</code> classes into one line.</li><li>The <code>#[\NoDiscard]</code> attribute, which warns when someone ignores a return value that matters.</li><li>A native URI extension, with URL parsing that follows RFC 3986 and WHATWG, no more <code>parse_url()</code> surprises.</li><li><code>array_first()</code> and <code>array_last()</code>. Finally.</li></ul><p>Here's the pipe operator turning a title into a slug:</p><pre><code class="language-php">$slug = $title
    |&gt; trim(...)
    |&gt; (fn (string $str): string =&gt; str_replace(' ', '-', $str))
    |&gt; strtolower(...);</code></pre><h3 id="what-changed-goodbye-ppa-hello-packagessuryorg">What changed: goodbye PPA, hello packages.sury.org</h3><p>If you've installed PHP on Ubuntu before, you probably ran <code>add-apt-repository ppa:ondrej/php</code>. That's no longer the way to go: Ondřej Surý is moving his Ubuntu packages from <a href="https://launchpad.net/~ondrej/+archive/ubuntu/php">Launchpad</a> to <a href="https://packages.sury.org/php/">packages.sury.org</a>, the server that already hosts the Debian ones. The <code>resolute</code> packages for Ubuntu 26.04 are <strong>only</strong> published there.</p><p>If you still have the PPA configured, you may have seen <code>apt update</code> complain this summer that the repository changed its 'Label' value, an error that made <code>apt-get update</code> fail in scripts until you accepted the change. For now the PPA still gets updates for Ubuntu 22.04 and 24.04, but it's being merged into packages.sury.org. And if both are configured, apt keeps preferring the PPA builds, so remove it before following the steps below:</p><pre><code class="language-bash">sudo add-apt-repository --remove ppa:ondrej/php</code></pre><p>The new setup also improves how the signing key is handled. Instead of downloading a raw <code>.gpg</code> file, you install the official <code>debsuryorg-archive-keyring</code> package.</p><h3 id="the-easy-way">The easy way</h3><p>As usual, you can install everything with a single command. The script adds the Sury repository, installs PHP 8.5 with the most common extensions, makes it the default <code>php</code>, and optionally installs <a href="https://getcomposer.org/">Composer</a>.</p><pre><code class="language-bash">curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-php-85.sh | bash</code></pre><p>The script asks for confirmation at each major step. As always, read <a href="https://angelblanco.dev/blog/apt-install-php-85-on-ubuntu-26/#the-script">the script</a> before you pipe it into <code>bash</code>. Trusting random URLs with <code>sudo</code> is a great way to learn about incident response.</p><p>If you don't have <code>curl</code> installed:</p><pre><code class="language-bash">sudo apt update
sudo apt install curl</code></pre><h3 id="the-step-by-step-guide">The step-by-step guide</h3><p>Here's what the script does, in case you want to run it step by step.</p><h4 id="_1-install-the-prerequisites">1. Install the prerequisites</h4><pre><code class="language-bash">sudo apt update
sudo apt install -y ca-certificates curl unzip</code></pre><p>PHP doesn't need <code>unzip</code>, but Composer uses it to extract packages much faster than its PHP fallback.</p><h4 id="_2-install-the-sury-signing-key">2. Install the Sury signing key</h4><pre><code class="language-bash">curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb \
  &amp;&amp; sudo dpkg -i /tmp/debsuryorg-archive-keyring.deb \
  &amp;&amp; rm /tmp/debsuryorg-archive-keyring.deb</code></pre><p>This drops the key in <code>/usr/share/keyrings/debsuryorg-archive-keyring.gpg</code> (plus, for now, a global copy in <code>/etc/apt/trusted.gpg.d/</code>). On Debian the repository also ships the keyring package, so key rotations arrive with <code>apt upgrade</code>. The Ubuntu suites don't ship it yet, so if the key is ever rotated, download and install the <code>.deb</code> again.</p><h4 id="_3-add-the-sury-repository">3. Add the Sury repository</h4><p>Since 24.04, Ubuntu uses the <a href="https://manpages.ubuntu.com/manpages/resolute/man5/sources.list.5.html">deb822 format</a> (<code>.sources</code> files) for its own repositories, and the classic one-line <code>.list</code> format is now officially deprecated. The codename is read from <code>/etc/os-release</code>, so the same snippet also works on Ubuntu 22.04 and 24.04, and on derivatives like Linux Mint.</p><pre><code class="language-bash">sudo tee /etc/apt/sources.list.d/php.sources &gt; /dev/null &lt;&lt;EOF
Types: deb
URIs: https://packages.sury.org/php/
Suites: $(. /etc/os-release &amp;&amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;)
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: /usr/share/keyrings/debsuryorg-archive-keyring.gpg
EOF

sudo apt update</code></pre><h4 id="_4-install-php-85">4. Install PHP 8.5</h4><pre><code class="language-bash">sudo apt install -y php8.5-cli php8.5-mbstring php8.5-curl php8.5-zip php8.5-xml</code></pre><p>Notice that we install <code>php8.5-cli</code> instead of the bare <code>php8.5</code> metapackage. The metapackage pulls in <a href="https://httpd.apache.org/">Apache</a> and its PHP module, which you probably don't want on your development machine. If you need a web server, add <a href="https://www.php.net/manual/en/install.fpm.php"><code>php8.5-fpm</code></a> and pair it with <a href="https://nginx.org/">Nginx</a> or <a href="https://caddyserver.com/">Caddy</a>. Need a database? Add <code>php8.5-mysql</code>, <code>php8.5-pgsql</code>, or <code>php8.5-sqlite3</code>.</p><p>You don't need an apt pin: Sury's <code>8.5.12</code> has a higher version than Ubuntu's <code>8.5.4</code>, so apt picks it automatically. You can check where each package comes from with:</p><pre><code class="language-bash">apt policy php8.5-cli</code></pre><h4 id="_5-make-php-85-the-default">5. Make PHP 8.5 the default</h4><p>The <code>php</code> command is managed by <a href="https://manpages.ubuntu.com/manpages/resolute/man1/update-alternatives.1.html"><code>update-alternatives</code></a>, so you can choose which version answers to it:</p><pre><code class="language-bash">sudo update-alternatives --set php /usr/bin/php8.5
php --version</code></pre><h4 id="_6-install-composer">6. Install Composer</h4><p>The <a href="https://getcomposer.org/download/">official installer</a> publishes a SHA-384 checksum, so verify it before running anything:</p><pre><code class="language-bash">EXPECTED_CHECKSUM=&quot;$(curl -fsSL https://composer.github.io/installer.sig)&quot;
curl -fsSLo composer-setup.php https://getcomposer.org/installer
ACTUAL_CHECKSUM=&quot;$(php -r &quot;echo hash_file('sha384', 'composer-setup.php');&quot;)&quot;

if [ &quot;$EXPECTED_CHECKSUM&quot; = &quot;$ACTUAL_CHECKSUM&quot; ]; then
    php composer-setup.php --quiet
    sudo install -m 0755 composer.phar /usr/local/bin/composer
    rm composer.phar
    composer --version
else
    &gt;&amp;2 echo &quot;ERROR: Invalid Composer installer checksum&quot;
fi
rm composer-setup.php</code></pre><p>Using <code>install</code> instead of <code>mv</code> makes <code>root</code> the owner of the binary, which matters because you'll run <code>sudo composer self-update</code> later.</p><h3 id="what-about-other-php-versions">What about other PHP versions?</h3><p>This is where the Sury repository really shines. It publishes every branch from <code>php5.6</code> to <code>php8.5</code>, plus the PHP 8.6 release candidates, and they can all live side by side:</p><pre><code class="language-bash">sudo apt install -y php8.4-cli php8.4-mbstring php8.4-curl php8.4-zip php8.4-xml
sudo update-alternatives --config php</code></pre><p><code>update-alternatives --config php</code> shows an interactive menu to switch the default <code>php</code>. You can also call a specific version directly with <code>php8.4</code> or <code>php8.5</code>, which is handy for running a legacy project's tests without touching your default.</p><p>If you try 8.6 before its final release on November 19, check <code>apt policy php8.6-cli</code>: builds older than RC4 don't include the October RCE fixes.</p><p>Keep in mind that every version you install is one more you have to keep patched. With Sury, at least, <code>apt upgrade</code> patches all of them at once.</p><h3 id="the-script">The script</h3><p>This is the complete script. It includes confirmations, uses a temporary directory that is removed on exit, and reads your answers from the terminal, so it works when you pipe it from <code>curl</code>. You can change the PHP version and the extensions with the variables at the top.</p><p>This script comes from my <a href="https://github.com/angelblanco/kickstart.nvim">Neovim configuration repo</a>, where I keep my <a href="https://github.com/angelblanco/kickstart.nvim/blob/master/install/install-php.sh">install scripts</a>. The editor needs <code>php</code> and <code>composer</code> for the PHP language servers and formatters to work, so I update them regularly. If you want the latest version, look there first.</p><pre><code class="language-bash">#!/bin/bash
#
# Author: Ángel Blanco
# License: MIT
#
# This script is provided &quot;as is&quot;, without warranty of any kind. Use at your own risk.
#
# Source: https://angelblanco.dev/blog/apt-install-php-85-on-ubuntu-26
#
# Usage:
#   curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-php-85.sh | bash
#
# Install PHP (from packages.sury.org) and Composer on Ubuntu.
# Normally you will call it from your non-root user; elevated permissions
# will be granted with sudo.
#

set -euo pipefail

# You can configure these variables if needed
PHP_VERSION=&quot;8.5&quot;
PHP_EXTENSIONS=&quot;cli mbstring curl zip xml&quot;
KEYRING=&quot;/usr/share/keyrings/debsuryorg-archive-keyring.gpg&quot;
SOURCE_FILE=&quot;/etc/apt/sources.list.d/php.sources&quot;

# Basic confirmation module before executing the script.
confirm_action() {
    while true; do
        read -r -p &quot;$1 (y/n): &quot; choice &lt; /dev/tty
        case &quot;$choice&quot; in
            [Yy]|[Yy][Ee][Ss]) return 0 ;;
            [Nn]|[Nn][Oo])     return 1 ;;
            *)                 echo &quot;Invalid choice. Please enter 'y' or 'n'.&quot; ;;
        esac
    done
}

WORKDIR=&quot;$(mktemp -d)&quot;
trap 'rm -rf &quot;$WORKDIR&quot;' EXIT

if ! confirm_action &quot;Install PHP $PHP_VERSION from packages.sury.org?&quot;; then
    echo &quot;Skipping PHP installation. Exiting.&quot;
    exit 0
fi

# --- Prerequisites ---------------------------------------------------------
sudo apt update
sudo apt install -y ca-certificates curl unzip

# --- Sury signing key (official keyring package) ---------------------------
curl -fsSLo &quot;$WORKDIR/debsuryorg-archive-keyring.deb&quot; https://packages.sury.org/debsuryorg-archive-keyring.deb
sudo dpkg -i &quot;$WORKDIR/debsuryorg-archive-keyring.deb&quot;

# --- Sury repository --------------------------------------------------------
CODENAME=&quot;$(. /etc/os-release &amp;&amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;)&quot;

sudo tee &quot;$SOURCE_FILE&quot; &gt; /dev/null &lt;&lt;EOF
Types: deb
URIs: https://packages.sury.org/php/
Suites: $CODENAME
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: $KEYRING
EOF

sudo apt update

# --- PHP --------------------------------------------------------------------
# Use -cli instead of the bare &quot;phpX.Y&quot; metapackage, which pulls in Apache.
PACKAGES=&quot;&quot;
for EXTENSION in $PHP_EXTENSIONS; do
    PACKAGES=&quot;$PACKAGES php${PHP_VERSION}-${EXTENSION}&quot;
done

# shellcheck disable=SC2086
sudo apt install -y $PACKAGES

sudo update-alternatives --set php &quot;/usr/bin/php${PHP_VERSION}&quot;

echo &quot;&quot;
php --version
echo &quot;&quot;

# --- Composer ---------------------------------------------------------------
if confirm_action &quot;Install Composer?&quot;; then
    cd &quot;$WORKDIR&quot;

    EXPECTED_CHECKSUM=&quot;$(curl -fsSL https://composer.github.io/installer.sig)&quot;
    curl -fsSLo composer-setup.php https://getcomposer.org/installer
    ACTUAL_CHECKSUM=&quot;$(php -r &quot;echo hash_file('sha384', 'composer-setup.php');&quot;)&quot;

    if [ &quot;$EXPECTED_CHECKSUM&quot; != &quot;$ACTUAL_CHECKSUM&quot; ]; then
        &gt;&amp;2 echo &quot;ERROR: Invalid Composer installer checksum&quot;
        exit 1
    fi

    php composer-setup.php --quiet
    sudo install -m 0755 composer.phar /usr/local/bin/composer

    echo &quot;&quot;
    composer --version
fi

echo &quot;&quot;
echo &quot;Installation process finished.&quot;
echo &quot;Run 'sudo apt update &amp;&amp; sudo apt upgrade' periodically to get PHP security releases.&quot;</code></pre><h3 id="keep-everything-up-to-date">Keep everything up to date</h3><p>Once it's installed, keeping it up to date takes just a couple of commands:</p><pre><code class="language-bash"># Operating system and PHP
sudo apt update &amp;&amp; sudo apt upgrade

# Composer
sudo composer self-update</code></pre><p>Even on 8.5.12, php.net <a href="https://www.php.net/archive/2026.php#2026-10-23-1">recommends</a> disabling session upload progress until the next release, to fully close an unlikely RCE path (CVE-2026-115560). If you serve PHP with FPM, it's a one-liner:</p><pre><code class="language-bash">echo 'session.upload_progress.enabled=0' | sudo tee /etc/php/8.5/fpm/conf.d/99-session-upload-progress.ini
sudo systemctl restart php8.5-fpm</code></pre><p>And don't forget your project dependencies: <a href="https://getcomposer.org/doc/03-cli.md#outdated"><code>composer outdated</code></a> and <a href="https://getcomposer.org/doc/03-cli.md#audit"><code>composer audit</code></a> tell you what needs updating and which packages have known vulnerabilities.</p><p>Two rounds of PHP security releases in less than a month, one of them an emergency, is a good reminder that you can't predict which patch will matter. What you can do is make applying them boring: one <code>apt upgrade</code> for PHP, one <code>composer self-update</code> for Composer, and <code>composer audit</code> for your projects. Set it up once and stop thinking about it.</p>]]></content>
        <category label="php" term="php"/>
        <category label="ubuntu" term="ubuntu"/>
        <category label="apt" term="apt"/>
        <category label="composer" term="composer"/>
        <category label="bash" term="bash"/>
        <category label="script" term="script"/>
        <published>2026-11-05T23:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Install Node.js 26 with apt on Ubuntu 26.04]]></title>
        <id>https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/</id>
        <link href="https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/"/>
        <updated>2026-11-04T23:00:00.000Z</updated>
        <summary type="html"><![CDATA[On October 21, 2026, Node.js released security patches for ten vulnerabilities affecting its supported versions, four of them rated high, including a remote code execution issue. Unsupported versions, such as Node.js 20 and 25, won't be updated.]]></summary>
        <content type="html"><![CDATA[<p>On October 21, 2026, Node.js released <a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases">security patches</a> for ten vulnerabilities affecting its supported versions, four of them rated high, including a remote code execution issue. Unsupported versions, such as Node.js 20 and 25, won't be updated.</p><p>Keeping Node.js up to date isn't optional: it's worth spending some time on it, both on your personal computer and on your server. The easiest way is to let the operating system handle those updates for you. To do that, install Node.js with <code>apt</code> from the <a href="https://nodesource.com/">NodeSource</a> repository and let <code>sudo apt upgrade</code> take care of the rest.</p><p>Now that <a href="https://nodejs.org/">Node.js 26</a> (<em>Lithium</em>) has been LTS since October 28, 2026 and <a href="https://documentation.ubuntu.com/release-notes/26.04/">Ubuntu 26.04 LTS</a> (<em>Resolute Raccoon</em>) has been out for months, it's a good time to get it properly installed. The recipe is the same as in my <a href="https://angelblanco.dev/blog/install-node-24-on-ubuntu-24/">Node.js 24 on Ubuntu 24 guide</a>.</p><h3 id="support-dates-at-a-glance">Support dates at a glance</h3><p>Both releases have long support windows, so this setup will last for a while:</p><table><thead><tr><th>Release</th><th>Released</th><th>LTS / standard support</th><th>End of life</th></tr></thead><tbody><tr><td>Node.js 26 (Lithium)</td><td>May 5, 2026</td><td>Active LTS from Oct 28, 2026</td><td>April 30, 2029</td></tr><tr><td>Ubuntu 26.04 LTS</td><td>April 23, 2026</td><td>Standard support until April 2031</td><td>ESM with Ubuntu Pro for 10 years</td></tr></tbody></table><p>Node.js 26 moves to maintenance on October 20, 2027 (see the <a href="https://github.com/nodejs/Release#release-schedule">official release schedule</a>), and it's the last release under the odd/even model: from <a href="https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule">Node.js 27</a> onwards, the project ships one major release per year and every release becomes LTS. If you're still on Node.js 24, don't panic: it's supported until April 2028 (and Node.js 22 until April 2027).</p><h3 id="what-changed-since-nodejs-24">What changed since Node.js 24</h3><p>Most of the steps are the same, but there is one important change: <strong>Corepack is no longer bundled with Node.js</strong>. It was removed in Node.js 25, so <code>corepack enable</code> doesn't work on a fresh Node.js 26 install. The <a href="https://nodejs.org/docs/latest-v25.x/api/corepack.html">Node.js docs</a> point to the userland <code>corepack</code> package if you still want it.</p><p>I took the simplest route and now install <a href="https://pnpm.io/">pnpm</a> with its own standalone installer. If you use <a href="https://yarnpkg.com/">Yarn</a>, you can install the <code>corepack</code> package globally (see <a href="https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/#what-about-yarn">below</a>).</p><h3 id="the-easy-way">The easy way</h3><p>As before, you can install everything with a single command. The script installs Node.js 26 from NodeSource, configures a global <a href="https://docs.npmjs.com/">npm</a> prefix (so you don't need <code>sudo</code> for global packages), and can optionally install pnpm and <a href="https://bun.sh/">Bun</a>.</p><pre><code class="language-bash">curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-node-26.sh | bash</code></pre><p>The script asks for confirmation at each major step. As always, read <a href="https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/#the-script">the script</a> before you pipe it into <code>bash</code>. Running random scripts from the internet is a fun hobby until it isn't.</p><p>If you don't have <code>curl</code> installed:</p><pre><code class="language-bash">sudo apt update
sudo apt install curl</code></pre><h3 id="the-step-by-step-guide">The step-by-step guide</h3><p>Here's what the script does, in case you want to run it step by step.</p><h4 id="_1-update-dependencies-and-add-the-nodesource-repository">1. Update dependencies and add the NodeSource repository</h4><p>First, install the tools needed to download and verify packages:</p><pre><code class="language-bash">sudo apt update
sudo apt install -y ca-certificates curl gnupg</code></pre><p>Then add the NodeSource repository for Node.js 26:</p><pre><code class="language-bash">curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -</code></pre><p>NodeSource uses a single <code>nodistro</code> suite for every Debian-based distribution, so it works on Ubuntu 26.04 without waiting for a codename-specific repository. The setup script also adds an apt pin with priority <code>600</code>, so the NodeSource package always wins over the <code>nodejs</code> package from the Ubuntu archive. Without it, you could end up with an older Node.js from the Ubuntu repositories.</p><h4 id="_2-install-nodejs-and-npm">2. Install Node.js and npm</h4><pre><code class="language-bash">sudo apt-get install -y nodejs
node --version</code></pre><p>This installs both the <code>node</code> runtime and <code>npm</code>.</p><h4 id="_3-configure-npms-global-prefix">3. Configure npm's global prefix</h4><p>To install global packages without <code>sudo</code>, set up a directory in your home folder:</p><pre><code class="language-bash">mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'</code></pre><p>Then add it to your <code>PATH</code> in <code>~/.bashrc</code> or <code>~/.zshrc</code>:</p><pre><code class="language-bash">export PATH=~/.npm-global/bin:$PATH</code></pre><h4 id="_4-install-pnpm">4. Install pnpm</h4><p>Since Corepack is no longer included, install pnpm with its <a href="https://pnpm.io/installation">standalone script</a>:</p><pre><code class="language-bash">curl -fsSL https://get.pnpm.io/install.sh | sh -</code></pre><p>The installer adds pnpm to your <code>PATH</code> in your shell config, so open a new terminal (or <code>source</code> your config file) before you run <code>pnpm --version</code>. From then on, <code>pnpm self-update</code> keeps it up to date.</p><h4 id="_5-install-bun-optional">5. Install Bun (optional)</h4><p>If you want to try <a href="https://bun.sh/">Bun</a>, use its official installer, which needs <code>unzip</code>:</p><pre><code class="language-bash">sudo apt install -y unzip
curl -fsSL https://bun.sh/install | bash</code></pre><h3 id="what-about-yarn">What about Yarn?</h3><p>If your projects use Yarn, you can still use Corepack. You just need to install it yourself:</p><pre><code class="language-bash">npm install -g corepack
corepack enable</code></pre><p>Because we configured a global npm prefix, this works without <code>sudo</code>. Keep in mind that the <a href="https://github.com/nodejs/corepack">Corepack</a> package is now maintained outside the Node.js release cycle, so check its README if something behaves unexpectedly on Node.js 26.</p><h3 id="the-script">The script</h3><p>This is the complete script. It includes confirmations, uses a temporary directory that is removed on exit, and reads your answers from the terminal, so it works when you pipe it from <code>curl</code>.</p><p>This script comes from my <a href="https://github.com/angelblanco/kickstart.nvim">Neovim configuration repo</a>, where I keep my <a href="https://github.com/angelblanco/kickstart.nvim/blob/master/install/install-node.sh">install scripts</a>. The editor needs Node.js for several language servers and plugins, so I update them regularly. If you want the latest version, look there first.</p><pre><code class="language-bash">#!/bin/bash
#
# Author: Ángel Blanco
# License: MIT
#
# This script is provided &quot;as is&quot;, without warranty of any kind. Use at your own risk.
#
# Source: https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26
#
# Usage:
#   curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-node-26.sh | bash
#
# This script installs Node.js and several package managers.
# Run it as your non-root user: it asks for elevated permissions
# with sudo when needed.
#

set -e

# Basic confirmation module before executing the script.
confirm_action() {
    read -p &quot;$1 (y/n): &quot; choice &lt; /dev/tty
    case &quot;$choice&quot; in
        [Yy]|[Yy][Ee][Ss])
            return 0
            ;;
        [Nn]|[Nn][Oo])
            return 1
            ;;
        *)
            echo &quot;Invalid choice. Please enter 'y' or 'n'.&quot;
            confirm_action &quot;$1&quot;
            ;;
    esac
}

# You can configure these variables if needed
NODE_VERSION=26
NPM_PREFIX=&quot;~/.npm-global&quot;
DEPS=&quot;ca-certificates curl gnupg&quot;

TMP_DIR=$(mktemp -d)
trap 'rm -rf &quot;$TMP_DIR&quot;' EXIT

echo &quot;This script will install Node.js and optionally other package managers.&quot;
echo &quot;&quot;

if ! confirm_action &quot;Install Node.js version $NODE_VERSION?&quot;; then
    echo &quot;Skipping Node.js installation. Exiting.&quot;
    exit 0
fi

echo &quot;Installing the dependencies needed to install Node.js...&quot;

sudo apt update
sudo apt install -y $DEPS

echo &quot;Installing Node.js...&quot;
curl -fsSL https://deb.nodesource.com/setup_$NODE_VERSION.x -o $TMP_DIR/nodesource_setup.sh
sudo -E bash $TMP_DIR/nodesource_setup.sh

sudo apt-get install -y nodejs

mkdir -p ~/.npm-global
npm config set prefix $NPM_PREFIX
echo &quot;npm prefix set to $NPM_PREFIX&quot;

echo &quot;Node.js installation completed.&quot;
echo &quot;Node version: &quot;
node --version

echo &quot;&quot;

if confirm_action &quot;Install pnpm?&quot;; then
    echo &quot;Installing pnpm...&quot;
    curl -fsSL https://get.pnpm.io/install.sh | sh -
    echo &quot;pnpm installation completed. Open a new terminal (or source your shell config) to use it.&quot;
fi

echo &quot;&quot;

if confirm_action &quot;Install Bun?&quot;; then
    echo &quot;Installing Bun...&quot;
    sudo apt install -y unzip
    curl -fsSL https://bun.sh/install | bash
    export BUN_INSTALL=&quot;$HOME/.bun&quot;
    export PATH=&quot;$BUN_INSTALL/bin:$PATH&quot;
    echo &quot;Bun version: &quot;
    bun --version
fi

echo &quot;&quot;
echo &quot;Installation process finished.&quot;
echo &quot;&quot;
echo &quot;Add the global npm bin directory to your PATH by adding this line to your .bashrc or .zshrc:&quot;
echo 'export PATH=~/.npm-global/bin:$PATH'
echo &quot;&quot;
echo &quot;Node.js now comes from apt, so keep it patched with:&quot;
echo &quot;sudo apt update &amp;&amp; sudo apt upgrade&quot;
echo &quot;&quot;</code></pre><h3 id="keep-everything-up-to-date">Keep everything up to date</h3><p>Once it's installed, keeping it up to date takes just a couple of commands:</p><pre><code class="language-bash"># Operating system and Node.js
sudo apt update &amp;&amp; sudo apt upgrade

# Package managers
pnpm self-update
bun upgrade
npm update -g</code></pre><p>And don't forget your project dependencies: <code>pnpm outdated</code> and <code>pnpm audit</code> tell you what needs updating.</p><p>That's also why I still don't use <a href="https://github.com/nvm-sh/nvm">NVM</a> day to day, as I explained in the <a href="https://angelblanco.dev/blog/install-node-24-on-ubuntu-24/#what-about-nvm">previous post</a>. It's great if you really need several Node.js versions side by side, but every version you install with NVM is one more you have to remember to patch.</p><p>With AI speeding up how fast vulnerabilities are found and exploited, and security breaches that keep popping up, staying up to date has to be as easy as possible. Because now, more than ever, it's not optional.</p>]]></content>
        <category label="nodejs" term="nodejs"/>
        <category label="ubuntu" term="ubuntu"/>
        <category label="apt" term="apt"/>
        <category label="javascript" term="javascript"/>
        <category label="bash" term="bash"/>
        <category label="script" term="script"/>
        <published>2026-11-04T23:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Create your own ESLint config and share it across projects]]></title>
        <id>https://angelblanco.dev/blog/create-your-eslint-config/</id>
        <link href="https://angelblanco.dev/blog/create-your-eslint-config/"/>
        <updated>2025-11-01T23:00:00.000Z</updated>
        <summary type="html"><![CDATA[If you've ever worked on multiple projects, you know the pain of copying ESLint configurations between repositories. Every time you set up a new project, you either start from scratch or copy the eslint.config.js file from your last one. There's a better way: publish your own shareable ESLint config package.]]></summary>
        <content type="html"><![CDATA[<p>If you've ever worked on multiple projects, you know the pain of copying <a href="https://eslint.org/">ESLint</a> configurations between repositories. Every time you set up a new project, you either start from scratch or copy the <code>eslint.config.js</code> file from your last one. There's a better way: publish your own shareable ESLint config package.</p><h2 id="why-i-love-antfueslint-config">Why I love antfu/eslint-config</h2><p>Discovering <a href="https://github.com/antfu/eslint-config">@antfu/eslint-config</a> changed how I configure ESLint. The move to &quot;Flat Config&quot; seemed unnecessary to me, until <a href="https://antfu.me/">Anthony Fu</a> built a config that makes ESLint simple to set up and extend.</p><p>Instead of juggling dozens of plugins, configurations and dependencies, you install one package and you're done. It's zero config by default: you import it, export it, and it respects your <code>.gitignore</code>. It looks at your project and enables the right rules for <a href="https://vuejs.org/">Vue</a>, <a href="https://www.typescriptlang.org/">TypeScript</a> or plain JavaScript, with opinionated defaults you can override.</p><p>It also formats your code, so for most projects you don't need <a href="https://prettier.io/">Prettier</a>. Besides JavaScript and TypeScript, it handles JSON, Markdown and YAML, so ESLint manages everything.</p><p>For my usual stack (Vue and TypeScript), it's the best starting point for configuring ESLint.</p><h2 id="why-create-your-own-eslint-config-package">Why create your own ESLint config package?</h2><p>Extending <code>antfu</code>'s config with my own preferences and plugins is simple, since it's designed for composing your own configuration. I'm used to ending lines with semicolons, indenting with 2 spaces and putting the <code>&lt;template&gt;</code> at the top of my <code>.vue</code> files.</p><p>But the real value isn't personal taste. It's <strong>consistency and reuse</strong>. Once you publish your ESLint config as an <a href="https://www.npmjs.com/">npm</a> package, you can:</p><ul><li>Add plugins for specific libraries that can't be part of <code>antfu</code>'s core.</li><li>Use it in all your repositories, with the same rules everywhere.</li><li>Version your linting rules: update the package, bump it in your projects, and they all get the new rules.</li><li>Share it with your team or make it public.</li><li>Maintain it in one place instead of updating the same rules in 10 projects.</li></ul><h2 id="my-config-angelblancoeslint-config">My config: @angelblanco/eslint-config</h2><p>My configuration, <a href="https://www.npmjs.com/package/@angelblanco/eslint-config">@angelblanco/eslint-config</a>, is available on npm, and the source code is on GitHub:</p><p><a href="https://github.com/angelblanco/eslint-config">github.com/angelblanco/eslint-config</a> · Ángel Blanco's ESLint configuration, built on @antfu/eslint-config for Vue + TypeScript projects.</p><h2 id="how-to-create-your-own">How to create your own</h2><p>Creating your own ESLint config is straightforward. Instead of starting from scratch, you can fork mine:</p><ol><li>Fork <a href="https://github.com/angelblanco/eslint-config">@angelblanco/eslint-config</a>.</li><li>Register on npm with your GitHub username.</li><li>Update <code>package.json</code> with your own package name, for example <code>@yourname/eslint-config</code>, and the rest of the details.</li><li>Install the dependencies with <code>pnpm install</code>.</li><li>Customize the rules in <code>index.ts</code>.</li><li>Build the JavaScript output with tsup by running <code>pnpm build</code>.</li><li>Publish it with <code>npm publish --access public</code>.</li><li>Install <code>@yourname/eslint-config</code> in your other repositories and import it in their ESLint config.</li></ol><p>The <a href="https://github.com/angelblanco/eslint-config">README in my repository</a> has up-to-date installation instructions, usage examples and every rule I changed. Open an issue if you want a hand.</p><h2 id="make-it-yours">Make it yours</h2><p>You don't need to reinvent the wheel. Start from something solid like @antfu/eslint-config, adjust it to your taste and publish it. The whole process takes about an hour, and you end up with one versioned linting configuration for all your projects.</p><p>If you work with Vue and TypeScript, try antfu's config or mine. If neither is exactly what you want, fork it and make it yours.</p>]]></content>
        <category label="eslint" term="eslint"/>
        <category label="typescript" term="typescript"/>
        <category label="vue" term="vue"/>
        <category label="tooling" term="tooling"/>
        <category label="npm" term="npm"/>
        <published>2025-11-01T23:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Install Node.js 24 on Ubuntu 24.04]]></title>
        <id>https://angelblanco.dev/blog/install-node-24-on-ubuntu-24/</id>
        <link href="https://angelblanco.dev/blog/install-node-24-on-ubuntu-24/"/>
        <updated>2026-11-04T23:00:00.000Z</updated>
        <summary type="html"><![CDATA[Getting Node.js 24 running on Ubuntu 24 should be a straightforward process. While Ubuntu comes with a version of Node.js in its default repositories, you might want to use Node.js 24, which became LTS on October 28, 2025 and is supported until April 2028. For a new setup, install Node.js 26 on Ubuntu 26.04 instead. This guide offers two simple methods to get you set up quickly.]]></summary>
        <content type="html"><![CDATA[<p>Getting Node.js 24 running on Ubuntu 24 should be a straightforward process. While Ubuntu comes with a version of Node.js in its default repositories, you might want to use Node.js 24, which became LTS on October 28, 2025 and is supported until April 2028. For a new setup, <a href="https://angelblanco.dev/blog/apt-install-node-26-on-ubuntu-26/">install Node.js 26 on Ubuntu 26.04</a> instead. This guide offers two simple methods to get you set up quickly.</p><p>We'll cover a one-liner script that automates everything, including setting up a proper global prefix for npm and enabling <a href="https://yarnpkg.com/">Yarn</a> and <a href="https://pnpm.io/">pnpm</a> via Corepack. For those who prefer manual control, we'll also walk through each step in detail.</p><h3 id="the-easy-way">The easy way</h3><p>I've created a script, so you can install everything by running a single command. This script will set up Node.js 24, configure a global npm prefix (so you don't need <code>sudo</code> for global packages), and optionally enable <code>corepack</code> (for Yarn and pnpm) and install Bun.</p><p>Just open your terminal and run this:</p><pre><code class="language-bash">curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-node-24.sh | bash</code></pre><p>The script will ask for confirmation at each major step, so you still have control over what gets installed. You can also review the <a href="https://angelblanco.dev/blog/install-node-24-on-ubuntu-24/#the-script">full script content</a> before running it or customize it for your own needs.</p><p>If you don't have <code>curl</code> installed:</p><pre><code class="language-bash">sudo apt update
sudo apt install curl</code></pre><h3 id="the-step-by-step-guide">The step-by-step guide</h3><p>For those who prefer a hands-on approach, here’s a breakdown of what the script does. You can follow these steps to perform the installation manually.</p><h4 id="_1-update-dependencies-and-add-the-nodesource-repository">1. Update dependencies and add the NodeSource repository</h4><p>First, we need to make sure our system has the necessary tools to fetch and install Node.js.</p><pre><code class="language-bash">sudo apt update
sudo apt install -y ca-certificates curl gnupg</code></pre><p>Next, we'll add the <a href="https://nodesource.com/">NodeSource</a> repository, which is a trusted source for up-to-date Node.js versions on Debian and Ubuntu.</p><pre><code class="language-bash">curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -</code></pre><h4 id="_2-install-nodejs-and-npm">2. Install Node.js and npm</h4><p>With the repository in place, installing Node.js is as simple as running:</p><pre><code class="language-bash">sudo apt-get install -y nodejs</code></pre><p>This command installs both the <code>node</code> runtime and the <code>npm</code> package manager.</p><h4 id="_3-configure-npms-global-prefix">3. Configure npm's global prefix</h4><p>By default, <code>npm</code> installs global packages in a system directory that requires <code>sudo</code>. This can lead to permission issues down the line. To avoid this, it's a best practice to configure a local directory for your global packages.</p><pre><code class="language-bash">mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'</code></pre><p>You'll also need to add this directory to your <code>PATH</code>. Open your <code>~/.bashrc</code> or <code>~/.zshrc</code> file and add the following line:</p><pre><code class="language-bash">export PATH=~/.npm-global/bin:$PATH</code></pre><h4 id="_4-enable-corepack-for-yarn-and-pnpm">4. Enable Corepack (for Yarn and pnpm)</h4><p><a href="https://nodejs.org/docs/latest-v24.x/api/corepack.html">Corepack</a> is an experimental tool included with Node.js that simplifies managing different package managers. With it, you don't need to install <a href="https://yarnpkg.com/">Yarn</a> or <a href="https://pnpm.io/">pnpm</a> manually.</p><p>To enable it, just run:</p><pre><code class="language-bash">sudo corepack enable</code></pre><p>Now you can use <code>yarn</code> and <code>pnpm</code> in your projects without any extra installation steps.</p><h4 id="_5-install-bun-optional">5. Install Bun (optional)</h4><p><a href="https://bun.sh/">Bun</a> is a fast, all-in-one JavaScript toolkit that has been gaining a lot of traction. If you want to give it a try, you can install it with its official script, which needs <code>unzip</code>:</p><pre><code class="language-bash">sudo apt install -y unzip
curl -fsSL https://bun.sh/install | bash</code></pre><h3 id="the-script">The script</h3><p>This is the complete script that automates the steps above. It asks for confirmation before each step and removes its temporary files on exit.</p><pre><code class="language-bash">#!/bin/bash
#
# Author: Ángel Blanco
# License: MIT
#
# This script is provided &quot;as is&quot;, without warranty of any kind. Use at your own risk.
#
# Source: https://angelblanco.dev/blog/install-node-24-on-ubuntu-24
#
# Usage:
#   curl -fsSL https://angelblanco.dev/scripts/ubuntu-install-node-24.sh | bash
#
# This script installs Node.js and several package managers.
# Run it as your non-root user: it asks for elevated permissions
# with sudo when needed.
#

set -e

# Basic confirmation module before executing the script.
confirm_action() {
    read -p &quot;$1 (y/n): &quot; choice &lt; /dev/tty
    case &quot;$choice&quot; in
        [Yy]|[Yy][Ee][Ss])
            return 0
            ;;
        [Nn]|[Nn][Oo])
            return 1
            ;;
        *)
            echo &quot;Invalid choice. Please enter 'y' or 'n'.&quot;
            confirm_action &quot;$1&quot;
            ;;
    esac
}

# You can configure these variables if needed
NODE_VERSION=24
NPM_PREFIX=&quot;~/.npm-global&quot;
DEPS=&quot;ca-certificates curl gnupg&quot;

TMP_DIR=$(mktemp -d)
trap 'rm -rf &quot;$TMP_DIR&quot;' EXIT

echo &quot;This script will install Node.js and optionally other package managers.&quot;
echo &quot;&quot;

if ! confirm_action &quot;Install Node.js version $NODE_VERSION?&quot;; then
    echo &quot;Skipping Node.js installation. Exiting.&quot;
    exit 0
fi

echo &quot;Installing the dependencies needed to install Node.js...&quot;

sudo apt update
sudo apt install -y $DEPS

echo &quot;Installing Node.js...&quot;
curl -fsSL https://deb.nodesource.com/setup_$NODE_VERSION.x -o $TMP_DIR/nodesource_setup.sh
sudo -E bash $TMP_DIR/nodesource_setup.sh

sudo apt-get install -y nodejs

mkdir -p ~/.npm-global
npm config set prefix $NPM_PREFIX
echo &quot;npm prefix set to $NPM_PREFIX&quot;

echo &quot;Node.js installation completed.&quot;
echo &quot;Node version: &quot;
node --version

echo &quot;&quot;

if confirm_action &quot;Enable Yarn and pnpm via corepack?&quot;; then
    echo &quot;Enabling corepack...&quot;
    sudo corepack enable
    echo &quot;Corepack enabled.&quot;
    echo &quot;Yarn version: &quot;
    yarn --version
    echo &quot;PNPM version: &quot;
    pnpm --version
fi

echo &quot;&quot;

if confirm_action &quot;Install Bun?&quot;; then
    echo &quot;Installing Bun...&quot;
    sudo apt install -y unzip
    curl -fsSL https://bun.sh/install | bash
    export BUN_INSTALL=&quot;$HOME/.bun&quot;
    export PATH=&quot;$BUN_INSTALL/bin:$PATH&quot;
    echo &quot;Bun version: &quot;
    bun --version
fi

echo &quot;&quot;
echo &quot;Installation process finished.&quot;
echo &quot;&quot;
echo &quot;Add the global npm bin directory to your PATH by adding this line to your .bashrc or .zshrc:&quot;
echo 'export PATH=~/.npm-global/bin:$PATH'
echo &quot;&quot;</code></pre><h3 id="what-about-nvm">What about NVM?</h3><p><a href="https://github.com/nvm-sh/nvm">Node Version Manager (NVM)</a> is a fantastic tool for managing multiple Node.js versions. However, I personally prefer to stick to a single, system-wide installation of the latest LTS version. This approach forces me to keep my projects updated. Besides, Node.js rarely introduces major breaking changes in its LTS releases, so the upgrade process is usually straightforward.</p><p>If you prefer the flexibility of NVM, you can still use it, but you would likely choose one method or the other. After installing NVM, you would manage your package managers like <code>yarn</code> and <code>pnpm</code> by installing them globally via <code>npm</code> for each Node version you use. You could then use aliases to switch between different versions of your tools. Be aware that this can sometimes lead to path conflicts or unexpected behavior, so be mindful of which versions are active in your shell.</p>]]></content>
        <category label="nodejs" term="nodejs"/>
        <category label="ubuntu" term="ubuntu"/>
        <category label="javascript" term="javascript"/>
        <category label="bash" term="bash"/>
        <category label="script" term="script"/>
        <published>2025-10-11T22:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[From Svelte to Nuxt: Rebuilding my portfolio in 2025]]></title>
        <id>https://angelblanco.dev/blog/hello/</id>
        <link href="https://angelblanco.dev/blog/hello/"/>
        <updated>2025-09-26T22:00:00.000Z</updated>
        <summary type="html"><![CDATA[It's 2025, and I've just rolled out a complete rebuild of my personal website. I've migrated from a Svelte and Sapper stack to Nuxt, Tailwind CSS and a Turborepo monorepo. It wasn't a cosmetic update but a full rewrite, driven by five years of changes in the frontend landscape and by the wish to use the same tools at home as at work.]]></summary>
        <content type="html"><![CDATA[<p>It's 2025, and I've just rolled out a complete rebuild of my personal website. I've migrated from a <a href="https://svelte.dev/">Svelte and Sapper</a> stack to <a href="https://nuxt.com/">Nuxt</a>, <a href="https://tailwindcss.com/">Tailwind CSS</a> and a <a href="https://turborepo.dev/">Turborepo</a> monorepo. It wasn't a cosmetic update but a full rewrite, driven by five years of changes in the frontend landscape and by the wish to use the same tools at home as at work.</p><h2 id="the-2020-decision-why-svelte">The 2020 decision: Why Svelte?</h2><p>Back in 2020, when I built the first version of this site, the Vue ecosystem was in the middle of a big transition. Vue 3 was in beta, and it was released right after I had finished my website. The first two years of Vue 3 were painful, and Nuxt 3 wasn't stable until November 2022. The migration path from Vue 2 was uncertain, and frankly, the future felt a bit shaky.</p><p>At that time, Svelte and its companion framework, Sapper, were a compelling alternative: fast, simple and stable. It was the right choice for its time.</p><h2 id="the-tipping-point-ecosystem-vs-diy">The tipping point: Ecosystem vs. DIY</h2><p>Five years later, the tables have turned. Svelte is still a great framework, but the Nuxt ecosystem has grown to cover almost everything a site like this needs.</p><p>With my old Sapper site, I had to build many features from scratch: SEO meta tags, content management for the blog and image optimization. I learned a lot, but maintaining that custom code became a chore. On top of that, the migration from Sapper to SvelteKit was a big project that I simply didn't have the time for.</p><p>Nuxt, on the other hand, has modules that handle these concerns out of the box:</p><ul><li><a href="https://content.nuxt.com/">@nuxt/content</a> for file-based content management.</li><li><a href="https://nuxt.com/modules/seo">@nuxtjs/seo</a> for SEO and metadata, with defaults that work without any configuration.</li><li><a href="https://nuxt.com/modules/image">@nuxt/image</a> for image optimization.</li><li><a href="https://nuxt.com/modules/icon">@nuxt/icon</a> for using any icon set without shipping the whole set.</li></ul><p>With that covered, I can focus on writing.</p><h2 id="aligning-with-my-daily-workflow">Aligning with my daily workflow</h2><p>The frontend side of my day job is Vue 3 with the Composition API, TypeScript and Tailwind. It makes sense for my personal site to use the same technologies I use every day. Working with Nuxt feels natural and simple, once you get its philosophy.</p><h2 id="a-quick-look-at-the-new-stack">A quick look at the new stack</h2><p><a href="https://nuxt.com/">Nuxt</a> is the core of the application. <a href="https://tailwindcss.com/">Tailwind CSS</a> handles the styling, and <a href="https://daisyui.com/">daisyUI</a> adds components and the light and dark themes. <a href="https://turborepo.dev/">Turborepo</a> manages the monorepo, which keeps the main app, the shared configurations and the internal tools in one place.</p><h2 id="from-bulma-to-tailwind-css-daisyui">From Bulma to Tailwind CSS &amp; daisyUI</h2><p>In the previous version of my site, I used <a href="https://bulma.io/">Bulma</a>, and it's a great CSS library. However, my day-to-day work is rooted in Tailwind CSS. Switching between the template and a separate CSS file now feels like double the work, since you rarely reuse classes across <code>.vue</code> or <code>.svelte</code> components.</p><p>Because I use Tailwind every day, I've started to &quot;think&quot; in Tailwind when it comes to styling, and once you think that way, it's hard to go back. For this rebuild, I've paired it with <a href="https://daisyui.com/">daisyUI</a>. It lets me opt in to pre-styled components when I don't want to overcomplicate things, and opt out to build something fully custom without losing any of Tailwind's benefits. It also comes with a theme system designed for Tailwind.</p><h2 id="the-role-of-ai-in-development">The role of AI in development</h2><p>This rebuild was also a chance to try the latest generation of AI coding assistants. I've tried (too many) tools, like the <a href="https://github.com/google-gemini/gemini-cli">Gemini CLI</a>, <a href="https://github.com/Aider-AI/aider">Aider</a> and <a href="https://github.com/features/copilot">GitHub Copilot</a>. Some days they felt like superpowers, and others like the flu. If I stick to my goal of writing more, I'll tell you more about each of them soon.</p><h2 id="whats-next">What's next?</h2><p>I want to write more, share some ideas I've been thinking about for a long time, and maybe start contributing to an open source project.</p><p>Thanks for reading. See you around.</p>]]></content>
        <category label="nuxt" term="nuxt"/>
        <category label="vue" term="vue"/>
        <category label="svelte" term="svelte"/>
        <category label="turborepo" term="turborepo"/>
        <category label="tailwindcss" term="tailwindcss"/>
        <category label="daisyui" term="daisyui"/>
        <published>2025-09-26T22:00:00.000Z</published>
    </entry>
</feed>