<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged framework</title>
<link>https://coderlegion.com/tag/framework</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building a static blog with Sitelo, one function per page</title>
<link>https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</link>
<description>&lt;p&gt;In my &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/paulbrowne/javascript-in-html-out-building-a-simpler-static-site-generator-5ln&quot;&gt;last post on DEV&lt;/a&gt; I explained why I built Sitelo: most websites are documents, and documents shouldn't need a JavaScript application to reach the browser. That post was the argument. This one is the practice.&lt;/p&gt;
&lt;p&gt;We'll build a small blog from an empty folder: a home page, post pages generated from JSON files, and a comments section that stays fresh on an otherwise static page. Along the way I'll point out what Sitelo is doing at each step, and what it deliberately doesn't do.&lt;/p&gt;
&lt;p&gt;The whole framework fits in one sentence, and it's worth keeping in mind as we go:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Files are routes. Functions return HTML. Browser code is explicit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything below is that sentence, applied.&lt;/p&gt;
&lt;h3 id=&quot;step-1-one-page-is-one-function&quot;&gt;Step 1: one page is one function&lt;/h3&gt;
&lt;p&gt;Start with an empty folder and install Sitelo. It needs Node 20.19+ or 22.12+, and Vite is bundled, so there's nothing else to install or configure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init -y
npm pkg set type=module
npm install -D sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add two scripts to &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;sitelo&quot;,
  &quot;build&quot;: &quot;sitelo build&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the first page. Any file in &lt;code&gt;src/&lt;/code&gt; ending in &lt;code&gt;.ht.js&lt;/code&gt; is a page, and its default export returns HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
export default () =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;title&amp;gt;My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;src/styles.css&lt;/code&gt; with a few rules, then run &lt;code&gt;npm run build&lt;/code&gt; and you get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist/
  index.html   ← your HTML, with &amp;lt;!DOCTYPE html&amp;gt; added
  styles.css   ← bundled and minified, because the page linked it
  404.html     ← a default one, until you write src/404.ht.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function ran once, at build time, in Node. The browser gets the string it returned and nothing else: no runtime, no hydration, no bundle of page code. If you want JavaScript in the browser, you add a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, the same as you would in a hand-written HTML file.&lt;/p&gt;
&lt;p&gt;Template literals aren't the only option. A page can also return JSX from a &lt;code&gt;.ht.jsx&lt;/code&gt; or &lt;code&gt;.ht.tsx&lt;/code&gt; file, which is rendered to a static string with &lt;code&gt;react-dom/server&lt;/code&gt;. Or you can use my companion library &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/javascript-to-html&quot;&gt;javascript-to-html&lt;/a&gt;, where every element is a function. I'll stick with template literals here because they need no explanation.&lt;/p&gt;
&lt;h3 id=&quot;step-2-a-layout-is-just-another-function&quot;&gt;Step 2: a layout is just another function&lt;/h3&gt;
&lt;p&gt;A blog needs the same &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and header on every page. There's no layout system to learn for this, because a layout is a function that takes some HTML and wraps it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/lib/layout.js
export const layout = ({ title, children }) =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
      &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;
      &amp;lt;title&amp;gt;${title} · My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;header&amp;gt;&amp;lt;a href=&quot;/&quot;&amp;gt;My blog&amp;lt;/a&amp;gt; · &amp;lt;a href=&quot;/blog&quot;&amp;gt;Posts&amp;lt;/a&amp;gt;&amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;${children}&amp;lt;/main&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
import { layout } from './lib/layout.js'

export default () =&amp;gt; layout({
  title: 'Home',
  children: '&amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Welcome to my blog.&amp;lt;/p&amp;gt;',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;layout.js&lt;/code&gt; doesn't end in &lt;code&gt;.ht.js&lt;/code&gt;, so it isn't a page. It also never ends up in &lt;code&gt;dist/&lt;/code&gt;. Sitelo only emits a JavaScript or TypeScript file when your HTML references it with a &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;; a module you only import from page code stays on the build side. That rule matters more later, when the helpers you import hold API keys or database code. They can't leak into the browser by accident.&lt;/p&gt;
&lt;p&gt;The same rule works in the other direction. Write &lt;code&gt;&amp;lt;script type=&quot;module&quot; src=&quot;/main.ts&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; and Sitelo bundles &lt;code&gt;src/main.ts&lt;/code&gt; with esbuild, emits &lt;code&gt;main.js&lt;/code&gt;, and rewrites the tag to match. Browser code exists only where you asked for it.&lt;/p&gt;
&lt;h3 id=&quot;step-3-posts-from-json-files&quot;&gt;Step 3: posts from JSON files&lt;/h3&gt;
&lt;p&gt;Now for the posts. I'll keep them in the repo as JSON, one file per post, outside &lt;code&gt;src/&lt;/code&gt; because they're content rather than code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// data/posts/hello-world.json
{
  &quot;title&quot;: &quot;Hello, world&quot;,
  &quot;date&quot;: &quot;2026-10-01&quot;,
  &quot;body&quot;: &quot;&amp;lt;p&amp;gt;My first post.&amp;lt;/p&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The post page lives at &lt;code&gt;src/blog/[slug].ht.js&lt;/code&gt;. The square brackets make it a dynamic route, and it exports two more functions next to the page itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generateStaticParams()&lt;/code&gt; lists the pages to build, one per post.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data()&lt;/code&gt; loads what one page needs. Whatever it returns arrives in the page function as &lt;code&gt;data&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

const posts = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export async function generateStaticParams() {
  return (await posts()).map((post) =&amp;gt; ({ slug: post.slug }))
}

export async function data({ params }) {
  return (await posts()).find((post) =&amp;gt; post.slug === params.slug)
}

export default ({ data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;${post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;time datetime=&quot;${post.date}&quot;&amp;gt;${post.date}&amp;lt;/time&amp;gt;
      ${post.body}
    &amp;lt;/article&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;readJsonCollection&lt;/code&gt; reads every &lt;code&gt;.json&lt;/code&gt; file in the folder and takes each slug from its file name. It parses each file once per build, however many pages ask for it, and two posts with the same slug fail the build instead of one silently overwriting the other.&lt;/p&gt;
&lt;p&gt;The index page is the same idea without the brackets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/index.ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

export const data = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export default ({ data: posts }) =&amp;gt; layout({
  title: 'Posts',
  children: `
    &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      ${posts.map((post) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;a href=&quot;/blog/${post.slug}&quot;&amp;gt;${post.title}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;`).join('')}
    &amp;lt;/ul&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A build now writes &lt;code&gt;dist/blog/index.html&lt;/code&gt; and one &lt;code&gt;dist/blog/&amp;lt;slug&amp;gt;/index.html&lt;/code&gt; per post. In the dev server (&lt;code&gt;npm run dev&lt;/code&gt;) pages render on request through Vite, so editing a JSON file reloads the browser, and an error points at the exact line of your page in the terminal.&lt;/p&gt;
&lt;p&gt;If your posts live in a CMS instead, &lt;code&gt;data()&lt;/code&gt; can &lt;code&gt;fetch&lt;/code&gt; them. Sitelo exports &lt;code&gt;fetchWithCache&lt;/code&gt;, a drop-in for &lt;code&gt;fetch&lt;/code&gt; that caches responses during the build. A 500-page site then doesn't make 500 identical requests to your API.&lt;/p&gt;
&lt;h3 id=&quot;step-4-fresh-comments-on-a-static-page&quot;&gt;Step 4: fresh comments on a static page&lt;/h3&gt;
&lt;p&gt;Comments are the classic reason static blogs reach for a client-side widget or give up and go server-rendered. Sitelo's answer is a server island: the page stays static, and one marked region is rendered on the server when someone views it.&lt;/p&gt;
&lt;p&gt;An island is a module in &lt;code&gt;src/islands/&lt;/code&gt; whose default export returns HTML. Islands are fragments rather than pages, so the file is plain &lt;code&gt;.js&lt;/code&gt;, not &lt;code&gt;.ht.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands/comments.js
import { getComments } from '../lib/comments.js'

const escape = (s) =&amp;gt; String(s).replace(/[&amp;amp;&amp;lt;&amp;gt;&quot;']/g, (c) =&amp;gt; `&amp;amp;#${c.charCodeAt(0)};`)

export default async function comments({ props }) {
  const list = await getComments(props.postId)
  if (!list.length) return '&amp;lt;p&amp;gt;No comments yet.&amp;lt;/p&amp;gt;'
  return `&amp;lt;ul&amp;gt;${list.map((c) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;b&amp;gt;${escape(c.author)}&amp;lt;/b&amp;gt; ${escape(c.text)}&amp;lt;/li&amp;gt;`).join('')}&amp;lt;/ul&amp;gt;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getComments&lt;/code&gt; is whatever your comment store is: a database query, a call to a hosted service. Comments are user input, hence the &lt;code&gt;escape&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Place the island in the post page with &lt;code&gt;island()&lt;/code&gt;. The third argument is the fallback HTML that ships in the static build, and &lt;code&gt;when: 'visible'&lt;/code&gt; waits until the reader scrolls near the comments before making the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js (the page function)
import { island } from 'sitelo/islands'

export default ({ params, data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;…&amp;lt;/article&amp;gt;
    ${island('comments', { postId: params.slug }, '&amp;lt;p&amp;gt;Loading comments…&amp;lt;/p&amp;gt;', { when: 'visible' })}
    &amp;lt;script type=&quot;module&quot; src=&quot;/islands.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script tag loads a two-line client that finds the placeholders and fetches them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands.js
import { mountIslands } from 'sitelo/islands/client'

mountIslands()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;npm run dev&lt;/code&gt; and &lt;code&gt;sitelo preview&lt;/code&gt; this already works: Sitelo serves islands at &lt;code&gt;/_sitelo/islands/&amp;lt;name&amp;gt;&lt;/code&gt;. In production your static host serves the pages, and you mount a small handler wherever you can run server code. &lt;code&gt;createIslandsHandler&lt;/code&gt; from &lt;code&gt;sitelo/islands/server&lt;/code&gt; takes a Web &lt;code&gt;Request&lt;/code&gt; and returns a &lt;code&gt;Response&lt;/code&gt;, so it fits a serverless or edge function. The repo has &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/islands&quot;&gt;Netlify and Vercel versions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A few details I cared about getting right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Props are untrusted.&lt;/strong&gt; They're embedded in the page and sent back on the request, so anyone can edit them. Set &lt;code&gt;SITELO_ISLANDS_SECRET&lt;/code&gt; and the build signs every placeholder with HMAC-SHA256, and the handler rejects edited props with a 403.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Islands are server-only.&lt;/strong&gt; The rule from Step 2 applies: nothing references &lt;code&gt;comments.js&lt;/code&gt; from HTML, so it never reaches &lt;code&gt;dist/&lt;/code&gt;, and neither does your database code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure is graceful.&lt;/strong&gt; If the island endpoint is down or slow (10 seconds by default), the fallback HTML stays where it was. The page still works.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-5-let-the-build-check-your-work&quot;&gt;Step 5: let the build check your work&lt;/h3&gt;
&lt;p&gt;The blog works. The last step is a config file, and it's where Sitelo earns its keep on a real site:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// sitelo.config.js
export default {
  site: 'https://myblog.example',
  rss: {
    site: 'https://myblog.example',
    title: 'My blog',
    description: 'Latest posts',
    routePrefix: '/blog',
  },
  images: true,
  linkCheck: 'error',
  lighthouse: {
    thresholds: { accessibility: 100, 'best-practices': 95, seo: 100 },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each line buys you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;site&lt;/code&gt;&lt;/strong&gt; generates &lt;code&gt;dist/sitemap.xml&lt;/code&gt; from every route, including the ones &lt;code&gt;generateStaticParams&lt;/code&gt; produced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rss&lt;/code&gt;&lt;/strong&gt; writes &lt;code&gt;dist/rss.xml&lt;/code&gt; with an item for every page under &lt;code&gt;/blog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;images&lt;/code&gt;&lt;/strong&gt; turns each &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; into a resized, WebP &lt;code&gt;srcset&lt;/code&gt; with &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; set, so a 3000px hero photo no longer ships to phones. It needs &lt;code&gt;sharp&lt;/code&gt; installed, and it works in the dev server too, so what you see is what you ship.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;linkCheck: 'error'&lt;/code&gt;&lt;/strong&gt; resolves every internal &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; against the files the build actually wrote, the way a static host would. A typo like &lt;code&gt;/blgo&lt;/code&gt; fails the build with the page it's on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lighthouse&lt;/code&gt;&lt;/strong&gt; gives &lt;code&gt;sitelo lighthouse&lt;/code&gt; thresholds to enforce. Any score under its threshold exits non-zero, so it can gate CI. Like sharp, Lighthouse is an optional install.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I left &lt;code&gt;performance&lt;/code&gt; out of the thresholds on purpose. It depends on the machine running the audit, and a page that scores 100 on a laptop can score around 60 on a shared CI runner. Accessibility, best practices and SEO are properties of the HTML itself, so they score the same anywhere.&lt;/p&gt;
&lt;p&gt;One check needs no config. If a page links a script or stylesheet that doesn't exist in &lt;code&gt;src/&lt;/code&gt; or &lt;code&gt;public/&lt;/code&gt;, the build fails and lists the paths it tried. That's why Step 1 asked you to create &lt;code&gt;styles.css&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;Every build ends with a report of what you're shipping: file counts and sizes per type, the largest files, and how long each phase took. On a static site, &quot;how much JavaScript am I sending?&quot; should be a number you see every time, not something you go looking for.&lt;/p&gt;
&lt;h3 id=&quot;building-it-with-a-coding-agent&quot;&gt;Building it with a coding agent&lt;/h3&gt;
&lt;p&gt;I designed Sitelo with coding agents in mind, and the small surface area matters even more for them than for people. Ask an agent to add a page to an unfamiliar framework and it reaches for whatever it has seen most: React components, Next.js conventions, Astro frontmatter. With Sitelo there's less to get wrong, because there's less of everything. A page is a function, a route is a file, and browser code is a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;p&gt;To close the remaining gap, Sitelo ships two &lt;a rel=&quot;nofollow&quot; href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt;: one for the framework (pages, routing, data, islands, deployment) and one for its component library. Install them into your project and Claude Code, Cursor, Codex and other agents load them when a task needs them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add Paul-Browne/sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skills also ship inside the npm package, matching the version you installed. Sitelo's test suite checks every API the skills mention against the package's real exports, so a renamed or removed API fails the tests before it reaches a release. For agents that prefer to read a URL, the docs publish &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/llms.txt&quot;&gt;sitelo.dev/llms.txt&lt;/a&gt;, plus a single-file &lt;code&gt;llms-full.txt&lt;/code&gt; with every guide in Markdown.&lt;/p&gt;
&lt;p&gt;The build checks from Step 5 help here too. An agent that invents a stylesheet path or links to a page that doesn't exist gets a failed build naming the problem, not a site that quietly ships broken.&lt;/p&gt;
&lt;h3 id=&quot;what-we-built-and-when-not-to-use-it&quot;&gt;What we built, and when not to use it&lt;/h3&gt;
&lt;p&gt;In five steps we went from an empty folder to a blog with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a home page, a post index and one page per JSON file&lt;/li&gt;
&lt;li&gt;comments rendered on the server at view time, on otherwise static pages&lt;/li&gt;
&lt;li&gt;a sitemap, an RSS feed and responsive images&lt;/li&gt;
&lt;li&gt;a build that fails on broken links and missing assets, plus a Lighthouse gate for accessibility and SEO&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The browser receives HTML, one stylesheet and a 1.7 kB island loader. That's all, because those are the only things we asked for.&lt;/p&gt;
&lt;p&gt;Sitelo isn't the right tool for everything. JSX pages render to static HTML only, so &lt;code&gt;onClick&lt;/code&gt; handlers and hooks do nothing in the browser. If you're building an application, with client-side state and routing, use an application framework. Astro, Next.js and Eleventy are all excellent at what they do. Sitelo is for docs, blogs, marketing sites and content sites, where &quot;view source&quot; should show what you wrote.&lt;/p&gt;
&lt;p&gt;If that sounds like your next project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/docs&quot;&gt;sitelo.dev/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (MIT licensed): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo&quot;&gt;github.com/Paul-Browne/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Package: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/sitelo&quot;&gt;npmjs.com/package/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repo includes a complete &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/blog&quot;&gt;blog example&lt;/a&gt; that goes further than this post, with Markdown posts. And if you find something that makes Sitelo more complicated than it needs to be, please open an issue. Keeping it boring is the whole point.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</guid>
<pubDate>Fri, 30 Oct 2026 14:35:56 +0000</pubDate>
</item>
<item>
<title>Framer vs Webflow for Beginners: Which No-Code Builder is Best?</title>
<link>https://coderlegion.com/29941/framer-vs-webflow-for-beginners-which-no-code-builder-is-best</link>
<description>&lt;p&gt;Choosing a website builder is no longer simply a question of finding a platform that lets you publish a page without writing code. Modern no-code builders now combine visual design, content management, hosting, SEO controls, analytics, forms, localization, collaboration, and increasingly, AI-powered tools.&lt;/p&gt;
&lt;p&gt;Two platforms that attract a large share of attention in this category are Framer and Webflow. Both allow users to build websites visually, but they approach the process differently. Framer is strongly oriented toward visual design and rapid publishing, while Webflow provides a more extensive system for structured website development, content management, collaboration, and larger projects.&lt;/p&gt;
&lt;p&gt;For a beginner deciding between the two, the important question is not which platform has more features. The better question is which platform makes the type of website you want to build easier to create and manage.&lt;/p&gt;
&lt;p&gt;Framer and Webflow Take Different Approaches&lt;br&gt;
Framer began with a strong connection to interface and interaction design, and its current website builder continues to emphasize visual creation. Users can build responsive pages on a visual canvas, create CMS-driven pages, manage SEO settings, add forms, use analytics, and publish through Framer's hosting infrastructure.&lt;/p&gt;
&lt;p&gt;Webflow also uses a visual development environment, but its system is designed around greater control over website structure, content, interactions, hosting, collaboration, and business requirements. Its CMS is positioned for content-rich websites, while its broader platform includes hosting, localization, SEO, analytics, optimization, and other tools.&lt;/p&gt;
&lt;p&gt;That distinction matters for beginners.&lt;/p&gt;
&lt;p&gt;If the main goal is to create a polished portfolio, landing page, personal website, agency site, or other design-led project quickly, Framer can be an attractive starting point.&lt;/p&gt;
&lt;p&gt;If the goal is to learn a more extensive visual website development system that can accommodate sophisticated CMS structures, larger content operations, localization, and team workflows, Webflow deserves serious consideration.&lt;/p&gt;
&lt;p&gt;Which Platform Is Easier for Beginners?&lt;br&gt;
Framer has a strong advantage when simplicity and visual design are the immediate priorities.&lt;/p&gt;
&lt;p&gt;Its workflow keeps much of the website creation process inside a visual canvas. Pages can be designed responsively, CMS pages can be generated from collections, and users can manage website content without moving into a completely separate publishing system. Framer's current documentation also describes support for web pages, CMS pages, and design pages.&lt;/p&gt;
&lt;p&gt;This makes Framer particularly attractive to someone who thinks visually and wants to see the result while building.&lt;/p&gt;
&lt;p&gt;Webflow is also a visual platform, but beginners may encounter more concepts before becoming comfortable. Website structure, classes, responsive behavior, CMS collections, interactions, hosting settings, SEO controls, and other platform functions can require more time to understand.&lt;/p&gt;
&lt;p&gt;That is not necessarily a weakness. Greater control can be valuable once a user moves beyond basic websites. The tradeoff is that Webflow can require a larger learning investment.&lt;/p&gt;
&lt;p&gt;For someone building their first website, Framer is generally the easier place to begin.&lt;/p&gt;
&lt;p&gt;For someone willing to spend more time learning the platform in exchange for deeper control, Webflow can provide a stronger long-term development environment.&lt;/p&gt;
&lt;p&gt;Design and Creative Control&lt;br&gt;
This is one of the clearest areas where the platforms differ in practical use.&lt;/p&gt;
&lt;p&gt;Framer is built around a design-first workflow. Its visual canvas makes it possible to create responsive pages, navigation, effects, and different breakpoints directly within the design environment.&lt;/p&gt;
&lt;p&gt;That approach works especially well for designers, freelancers, creators, startups, portfolio owners, and anyone who wants the website itself to have a strong visual identity.&lt;/p&gt;
&lt;p&gt;Webflow also provides extensive visual design control. However, its interface is closely connected to concepts associated with professional web development, including structured styling and reusable systems.&lt;/p&gt;
&lt;p&gt;For a beginner, Framer can therefore feel more direct.&lt;/p&gt;
&lt;p&gt;For a user who wants to understand how a complex website is constructed and controlled at a deeper level, Webflow may offer more room to grow.&lt;/p&gt;
&lt;p&gt;CMS and Content Management&lt;br&gt;
The choice becomes more complicated when content management is a major requirement.&lt;/p&gt;
&lt;p&gt;Framer has a CMS that supports collections and dynamic pages. Its official documentation describes CMS pages generated from collection content, with templates that can be used for blog posts, news articles, product pages, case studies, and similar content.&lt;/p&gt;
&lt;p&gt;Framer also provides visual content editing and automatic SEO options for CMS pages. Its CMS documentation highlights dynamic titles and descriptions, schema, Open Graph tags, drafts, publishing, and localization capabilities.&lt;/p&gt;
&lt;p&gt;Webflow has a more established reputation for structured content management. Its CMS is designed for content-rich websites and allows teams to manage structured content while controlling how that content is presented visually.&lt;/p&gt;
&lt;p&gt;For a simple blog or small content site, either platform can work.&lt;/p&gt;
&lt;p&gt;For a website expected to contain a substantial amount of structured content, multiple content types, and more complicated publishing requirements, Webflow's broader CMS environment may be more suitable.&lt;/p&gt;
&lt;p&gt;SEO: Framer vs Webflow&lt;br&gt;
Both platforms provide built-in SEO controls, meaning users do not have to rely on a separate WordPress-style SEO plugin simply to manage fundamental search settings.&lt;/p&gt;
&lt;p&gt;Framer states that its websites are optimized for SEO by default. Its SEO tools include sitemap and robots.txt generation, custom page titles and descriptions, indexing controls, redirects, and structured data options.&lt;/p&gt;
&lt;p&gt;Framer also provides built-in analytics and allows integration with Google Analytics.&lt;/p&gt;
&lt;p&gt;Webflow provides comparable fundamental SEO controls, including page titles, meta descriptions, URLs, Open Graph settings, and CMS-based SEO fields.&lt;/p&gt;
&lt;p&gt;Webflow goes further when localization becomes important. Its Localize system supports localized URLs, metadata, sitemaps, hreflang implementation, and visitor routing.&lt;/p&gt;
&lt;p&gt;For a beginner creating a single-language website, neither platform has an obvious fundamental SEO disadvantage.&lt;/p&gt;
&lt;p&gt;For larger multilingual websites, Webflow offers a more extensive localization system.&lt;/p&gt;
&lt;p&gt;Forms and Analytics&lt;br&gt;
A modern business website needs more than attractive pages. It also needs a way to collect leads and measure visitor activity.&lt;/p&gt;
&lt;p&gt;Framer includes native forms and built-in analytics. Its documentation covers contact forms for leads, bookings, signups, and inquiries, as well as spam protection and analytics.&lt;/p&gt;
&lt;p&gt;Framer also allows users to assign tracking IDs to links and forms and view those events through Analytics Overview. The platform supports funnels and A/B testing as part of its analytics and testing capabilities.&lt;/p&gt;
&lt;p&gt;Webflow provides forms and a broader set of marketing and optimization tools within its platform. Its current platform includes Analyze and Optimize products, while its paid Site plans include different levels of functionality depending on the selected plan.&lt;/p&gt;
&lt;p&gt;For a small website, Framer's built-in tools may be enough.&lt;/p&gt;
&lt;p&gt;For a company that expects website analytics, optimization, localization, collaboration, and other marketing functions to become increasingly important, Webflow's broader platform can be more compelling.&lt;/p&gt;
&lt;p&gt;Pricing Matters for Beginners&lt;br&gt;
Pricing can change, so it is important to compare current plans rather than rely on old articles.&lt;/p&gt;
&lt;p&gt;As of October 2026, Framer lists a free plan, Basic at $10 per month, and Pro at $30 per month, with annual billing displayed on its pricing page. The Basic plan includes a custom domain, two CMS collections, 50 GB of bandwidth, and built-in SEO. The Pro plan increases the CMS collections limit to 10 and includes features such as redirects, staging, and branching with previews.&lt;/p&gt;
&lt;p&gt;Webflow currently offers a free Starter plan. Its Basic Site plan is listed at $15 per month when billed yearly, while its Premium plan is listed at $25 per month when billed yearly. The Premium plan includes Webflow CMS, while the Basic plan is intended for simpler websites without CMS requirements.&lt;/p&gt;
&lt;p&gt;Webflow also separates Site plans from Workspace plans, which means users need to understand the distinction between hosting a website and managing projects, staging environments, collaboration, and related Workspace functions.&lt;/p&gt;
&lt;p&gt;For a beginner comparing only the entry-level paid plans, Framer currently has the lower listed starting price.&lt;/p&gt;
&lt;p&gt;However, price alone should not determine the decision. A cheaper plan that lacks a feature you later require can become more expensive if it forces a move to another tier or platform.&lt;/p&gt;
&lt;p&gt;Localization and International Websites&lt;br&gt;
International websites introduce another important difference.&lt;/p&gt;
&lt;p&gt;Framer supports localization and provides tools for translating and adapting sites. Its current platform documentation also describes localization as part of its broader website-building system.&lt;/p&gt;
&lt;p&gt;Webflow has invested heavily in localization. Webflow Localize supports multiple locales, localized content and styles, localized page settings, SEO metadata, URLs, and sitemap functionality.&lt;/p&gt;
&lt;p&gt;Webflow's localization system can therefore become particularly relevant to companies operating across different countries or language markets.&lt;/p&gt;
&lt;p&gt;A beginner building a personal portfolio probably does not need to prioritize this feature.&lt;/p&gt;
&lt;p&gt;A business planning a multilingual website should examine it carefully before choosing a platform.&lt;/p&gt;
&lt;p&gt;Collaboration and Growing Teams&lt;br&gt;
The platform choice can change when a solo website becomes a team project.&lt;/p&gt;
&lt;p&gt;Framer introduced a new billing system in 2026 that includes different editor types, including Content Editors for people who manage CMS content without requiring full editing access.&lt;/p&gt;
&lt;p&gt;Webflow has a broader Workspace system with collaboration features, staging environments, seats, permissions, publishing workflows, and team-oriented plans. Its current Team plan is designed for growing organizations and includes features such as Localize, publishing workflows, page branching, single-page publishing, and additional governance capabilities.&lt;/p&gt;
&lt;p&gt;For a solo creator, these differences may not matter much.&lt;/p&gt;
&lt;p&gt;For agencies, marketing departments, and organizations with multiple people involved in website production, Webflow's collaboration architecture deserves close attention.&lt;/p&gt;
&lt;p&gt;Which One Is Better for a Portfolio?&lt;br&gt;
For a personal portfolio, designer portfolio, freelancer website, or creative landing page, Framer is a strong choice.&lt;/p&gt;
&lt;p&gt;The visual workflow allows beginners to concentrate heavily on the appearance and presentation of the website. Its hosting, CMS, SEO, analytics, and publishing functions are integrated into the same platform.&lt;/p&gt;
&lt;p&gt;Webflow can also produce excellent portfolios, but it may introduce more concepts than a beginner needs for a relatively small project.&lt;/p&gt;
&lt;p&gt;If the objective is to launch a visually impressive portfolio without spending weeks learning a complex platform, Framer is the more straightforward recommendation.&lt;/p&gt;
&lt;p&gt;Which One Is Better for a Business Website?&lt;br&gt;
The answer depends on the business.&lt;/p&gt;
&lt;p&gt;A small company that needs a professional homepage, services page, contact page, and a few supporting pages can use either platform successfully.&lt;/p&gt;
&lt;p&gt;Webflow becomes more attractive when the website requires a structured CMS, multiple content types, localization, advanced collaboration, extensive marketing operations, or a larger team.&lt;/p&gt;
&lt;p&gt;Framer remains a strong choice when visual design, rapid publishing, and ease of use are the highest priorities.&lt;/p&gt;
&lt;p&gt;Which One Is Better for a Blog?&lt;br&gt;
Both platforms can support blogs through their CMS systems.&lt;/p&gt;
&lt;p&gt;Framer's CMS can generate dynamic pages from collections, making it possible to create blog posts and other recurring content from reusable templates.&lt;/p&gt;
&lt;p&gt;Webflow's CMS is also designed for structured content and content-rich websites.&lt;/p&gt;
&lt;p&gt;For a small independent blog, either platform can work.&lt;/p&gt;
&lt;p&gt;For a publication that expects complicated content structures, larger editorial operations, multiple content types, or more advanced team workflows, Webflow is likely the stronger candidate.&lt;/p&gt;
&lt;p&gt;Framer vs Webflow: The Beginner's Verdict&lt;br&gt;
There is no universal winner because the platforms prioritize different aspects of website building.&lt;/p&gt;
&lt;p&gt;Framer is the better choice for beginners who want:&lt;/p&gt;
&lt;p&gt;A highly visual website-building workflow&lt;br&gt;
Fast project creation&lt;br&gt;
Design-focused websites&lt;br&gt;
Portfolio websites&lt;br&gt;
Landing pages&lt;br&gt;
Small business websites&lt;br&gt;
Straightforward CMS projects&lt;br&gt;
Built-in SEO and analytics tools&lt;br&gt;
A lower starting price for paid plans&lt;br&gt;
Webflow is the better choice for beginners who want:&lt;/p&gt;
&lt;p&gt;A deeper website development environment&lt;br&gt;
More extensive CMS capabilities&lt;br&gt;
Content-heavy websites&lt;br&gt;
Larger business websites&lt;br&gt;
Advanced collaboration&lt;br&gt;
Multilingual and international websites&lt;br&gt;
More extensive marketing and optimization capabilities&lt;br&gt;
A platform that can accommodate increasingly complex projects&lt;br&gt;
The important point is that Webflow is not automatically better because it has more features, and Framer is not automatically better because it is easier to approach.&lt;/p&gt;
&lt;p&gt;For a first website, Framer is probably the better starting point for most beginners who prioritize simplicity and visual design.&lt;/p&gt;
&lt;p&gt;For someone who intends to build more complex websites and is prepared to spend more time learning the platform, Webflow is the stronger long-term candidate.&lt;/p&gt;
&lt;p&gt;The best choice is therefore determined by the website being built, not by which platform has the longer feature list.&lt;/p&gt;
&lt;p&gt;Before committing to either service, beginners should also check the current pricing and feature limits. Both companies continue to update their products and plans, and Webflow made significant pricing and plan changes during 2026.&lt;/p&gt;
&lt;p&gt;If the goal is to get a polished website online quickly, start with Framer.&lt;/p&gt;
&lt;p&gt;If the goal is to develop deeper skills for building structured, content-rich, collaborative websites, start with Webflow.&lt;/p&gt;
&lt;p&gt;For most first-time users, that distinction is more useful than simply asking which platform is &quot;best.&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29941/framer-vs-webflow-for-beginners-which-no-code-builder-is-best</guid>
<pubDate>Tue, 06 Oct 2026 05:40:50 +0000</pubDate>
</item>
<item>
<title>Stop Slow Laravel: Your Next Architectural Leap Is Here!</title>
<link>https://coderlegion.com/21582/stop-slow-laravel-your-next-architectural-leap-is-here</link>
<description>
&lt;h2 id=&quot;stop-slow-laravel-your-next-architectural-leap-is-here&quot;&gt;Stop Slow Laravel: Your Next Architectural Leap Is Here!&lt;/h2&gt;

&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;

&lt;p&gt;It&amp;rsquo;s 2026, and the landscape of web development has evolved dramatically. If your Laravel applications are still relying on traditional PHP-FPM for request handling, you might be leaving significant performance on the table. The conversation around application speed has moved beyond mere query optimization; it&amp;rsquo;s an architectural paradigm shift. Enter Laravel Octane, a powerful package that integrates your Laravel application with high-performance application servers like RoadRunner or Swoole. This isn&amp;rsquo;t just about making PHP &amp;ldquo;faster&amp;rdquo;; it&amp;rsquo;s about fundamentally transforming how your application processes requests, ushering in an era of unprecedented throughput, reduced latency, and blazing-fast API responses. Octane transitions your application from a stateless, &amp;ldquo;boot-on-every-request&amp;rdquo; model to a persistent, long-running process, eliminating the costly framework bootstrap for each incoming call.&lt;/p&gt;

&lt;h3 id=&quot;code-layout-and-architectural-walkthrough&quot;&gt;Code Layout and Architectural Walkthrough&lt;/h3&gt;

&lt;p&gt;Embracing Laravel Octane demands a shift in mindset, particularly regarding application state. Let&amp;rsquo;s walk through the essential steps and considerations to integrate Octane into your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Installation and Setup:&lt;/strong&gt;
Getting started with Octane is straightforward. You first install the package via Composer:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;composer require laravel/octane
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, you&amp;rsquo;ll install Octane into your application, choosing your preferred server driver (RoadRunner or Swoole). RoadRunner is often simpler to get started with due to fewer external dependencies for many environments.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;php artisan octane:install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command publishes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/octane.php&lt;/code&gt; configuration file, which allows you to fine-tune settings like the number of worker processes, maximum requests per worker before restarting, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Persistent Server Model:&lt;/strong&gt;
Unlike FPM, which reboots your entire application for every request, Octane leverages a persistent application server. When you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php artisan octane:start&lt;/code&gt;, a dedicated server process (RoadRunner or Swoole) is launched. This server loads your Laravel application once, keeping it in memory. Subsequent requests are then routed to this already-booted application instance. This fundamentally eliminates the overhead of bootstrapping the framework on &lt;em&gt;every single request&lt;/em&gt;, leading to dramatic performance improvements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Embracing the Stateful Mindset:&lt;/strong&gt;
This is the most critical aspect of adopting Octane. Because your application remains loaded in memory between requests, global state can persist, leading to unexpected behavior if not managed carefully.
Key areas to monitor:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Static Properties &amp;amp; Global Variables:&lt;/strong&gt; Any data stored in static properties or global variables will persist across requests. This can lead to data leaks or incorrect behavior if not reset.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service Container Bindings:&lt;/strong&gt; While Octane intelligently resets many core framework services, custom singleton bindings in your service container might retain state.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Opened Resources:&lt;/strong&gt; Database connections, file handles, or network sockets that are not properly closed might accumulate or become stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Octane&amp;rsquo;s Mitigation Strategies:&lt;/strong&gt;
Octane provides powerful hooks to manage state:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Octane::onRequest&lt;/code&gt;:&lt;/strong&gt; You can register callbacks to be executed at the beginning of each request. This is ideal for manually resetting any custom global state or re-binding services if necessary.
    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Laravel\Octane\Facades\Octane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Clear any custom static caches&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;MyCustomService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Rebind specific service if it holds request-specific state&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SomeService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SomeService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Octane::onWorkerStopping&lt;/code&gt;:&lt;/strong&gt; This hook can be used for cleanup tasks when a worker process is gracefully shutting down.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic Resetting:&lt;/strong&gt; Octane intelligently resets many core Laravel components, such as request input, session data, authentication state, and even clears resolved instances from the service container (unless explicitly configured otherwise).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most well-architected Laravel applications that rely heavily on dependency injection and avoid excessive global state, the transition is smoother than anticipated. Focus on ensuring your services and controllers are stateless or properly reset if they must hold state.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;Adopting Laravel Octane is no longer an edge-case optimization for the ultra-high-traffic elite; it&amp;rsquo;s becoming the baseline for modern, efficient Laravel applications. The architectural shift to a persistent application server, powered by RoadRunner or Swoole, fundamentally redefines performance. By eliminating per-request framework bootstrapping, Octane delivers 10x throughput, significantly reduced latency, and an incredibly responsive user experience. While it demands a careful consideration of application state, the performance gains and competitive advantage it provides are immense. Stop leaving raw performance on the table. Embrace Octane today and position your Laravel applications for peak efficiency and future success.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21582/stop-slow-laravel-your-next-architectural-leap-is-here</guid>
<pubDate>Wed, 01 Jul 2026 06:34:24 +0000</pubDate>
</item>
<item>
<title>Best Python AI Frameworks in 2026</title>
<link>https://coderlegion.com/20300/best-python-ai-frameworks-in-2026</link>
<description>&lt;p&gt;A comparison of seven major Python AI frameworks for 2026: TensorFlow, PyTorch, Keras, scikit-learn, XGBoost, LangChain, and Hugging Face. Each framework is evaluated by strengths, weaknesses, and ideal use cases. Deep learning frameworks (TensorFlow, PyTorch, Keras) suit neural network tasks; classical ML frameworks (scikit-learn, XGBoost) excel on tabular data; LLM/agent frameworks (LangChain, Hugging Face) power language model applications. A decision table helps match framework choice to data type, abstraction level, deployment target, and project objective. Many teams combine multiple frameworks in production.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20300/best-python-ai-frameworks-in-2026</guid>
<pubDate>Thu, 11 Jun 2026 12:57:42 +0000</pubDate>
</item>
<item>
<title>We finally broke the 500ms latency barrier for AI phone calls.</title>
<link>https://coderlegion.com/16611/we-finally-broke-the-500ms-latency-barrier-for-ai-phone-calls</link>
<description>&lt;p&gt;Hey CoderLegion! One of the biggest complaints with AI calling is the lag.&lt;br&gt;
We wanted to share a raw snippet of our open-source framework, Siphon, in action.&lt;br&gt;
By using WebRTC under the hood instead of WebSockets, we've gotten the voice-to-voice response time to feel incredibly human.&lt;br&gt;
Would love for you guys to test the repo and let us know how it feels on your local machines!&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=GkY42naY8lg&amp;amp;t=1s&quot;&gt;Siphon | Step-by-Step YT Tutorial&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;http://siphon.blackdwarf.in/docs&quot;&gt;Siphon Website&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon&quot;&gt;GitHub code&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16611/we-finally-broke-the-500ms-latency-barrier-for-ai-phone-calls</guid>
<pubDate>Mon, 04 May 2026 08:35:00 +0000</pubDate>
</item>
<item>
<title>The &quot;Context Gap&quot; in Voice AI: Why Chatbots Fail at Scheduling</title>
<link>https://coderlegion.com/10875/the-context-gap-in-voice-ai-why-chatbots-fail-at-scheduling</link>
<description>&lt;p&gt;Voice AI demos are everywhere.&lt;/p&gt;
&lt;p&gt;You call a number, an enthusiastic robot voice greets you, and you have a pleasant five-minute chat about the weather or the meaning of life. But the moment you ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;“Can you squeeze me in for a cleaning next Tuesday at 2 PM?”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;…the magic usually breaks.&lt;/p&gt;
&lt;h3 id=&quot;why-&quot;&gt;Why?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Because talking is easy. Context is hard.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most voice agents are great conversationalists, but they operate in a vacuum. They have no real understanding of time, availability, or the systems businesses actually run on.&lt;/p&gt;
&lt;p&gt;To build a truly useful AI receptionist, you don’t just need a good LLM—you need &lt;strong&gt;real integrations&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That’s why we’re introducing &lt;strong&gt;Native Google Calendar Integration&lt;/strong&gt; in &lt;strong&gt;SIPHON&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-context-gap-in-voice-agents&quot;&gt;The Context Gap in Voice Agents&lt;/h3&gt;
&lt;p&gt;Traditionally, building a scheduling voice agent looks something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Transcription&lt;/strong&gt; – Convert speech to text&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extraction&lt;/strong&gt; – Parse phrases like “next Tuesday at 2 PM”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Glue&lt;/strong&gt; – Authenticate with Google, fetch events, check conflicts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response Logic&lt;/strong&gt; – Manually construct confirmations and retries&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach is fragile, slow to build, and painful to maintain.&lt;/p&gt;
&lt;p&gt;SIPHON solves this by treating &lt;strong&gt;integrations as first-class citizens&lt;/strong&gt;—just like your LLM, STT, or TTS provider.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;native-google-calendar-integration&quot;&gt;Native Google Calendar Integration&lt;/h3&gt;
&lt;p&gt;With SIPHON, enabling Google Calendar support is a &lt;strong&gt;single line of configuration&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;agent = Agent(
    # ... standard config ...
    google_calendar=True
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it.&lt;/p&gt;
&lt;p&gt;No OAuth boilerplate.&lt;br&gt;
No custom API clients.&lt;br&gt;
No fragile glue code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-enables&quot;&gt;What This Enables&lt;/h3&gt;
&lt;p&gt;Once enabled, your SIPHON agent can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check real-time availability&lt;/strong&gt; before offering time slots&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create, update, and cancel events&lt;/strong&gt; directly on Google Calendar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid double-booking&lt;/strong&gt; and intelligently handle conflicts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reschedule appointments&lt;/strong&gt; in a natural conversation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All through voice.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-real-example-24-7-dental-clinic-receptionist&quot;&gt;A Real Example: 24/7 Dental Clinic Receptionist&lt;/h3&gt;
&lt;p&gt;To showcase this integration, we built a &lt;strong&gt;production-ready Dental Clinic Receptionist&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is not a toy demo—it behaves like a real front-desk employee:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inbound Call&lt;/strong&gt; – A patient calls to book an appointment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification&lt;/strong&gt; – The agent verifies whether the caller is a new or existing patient&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart Scheduling&lt;/strong&gt; – It checks the dentist’s &lt;em&gt;actual&lt;/em&gt; Google Calendar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Booking&lt;/strong&gt; – The agent books the slot and confirms it verbally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follow-ups&lt;/strong&gt; – Patients can call back to modify or cancel appointments&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon/tree/main/examples/Dental_Clinic_Receptionist&quot;&gt;Explore the full example on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;For Voice AI to move from &lt;strong&gt;novelty&lt;/strong&gt; to &lt;strong&gt;utility&lt;/strong&gt;, it must do real work.&lt;/p&gt;
&lt;p&gt;Scheduling is one of the highest-value automation targets for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clinics and hospitals&lt;/li&gt;
&lt;li&gt;Salons and service businesses&lt;/li&gt;
&lt;li&gt;Consultants and small teams&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By abstracting away OAuth flows, API management, and tool orchestration, SIPHON lets developers build powerful scheduling agents in &lt;strong&gt;minutes instead of weeks&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;get-started&quot;&gt;Get Started&lt;/h3&gt;
&lt;p&gt;Ready to give your voice agent a calendar?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://siphon.blackdwarf.in/docs&quot;&gt;Read the Documentation&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon/tree/main/examples/Dental_Clinic_Receptionist&quot;&gt;Run the Dental Receptionist Example&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;SIPHON is an open-source framework for building production-grade calling AI.&lt;/em&gt;&lt;br&gt;
⭐ &lt;strong&gt;Star us on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10875/the-context-gap-in-voice-ai-why-chatbots-fail-at-scheduling</guid>
<pubDate>Sun, 01 Feb 2026 13:15:33 +0000</pubDate>
</item>
<item>
<title>SIPHON: The Telephony-First Framework for Calling AI</title>
<link>https://coderlegion.com/10654/siphon-the-telephony-first-framework-for-calling-ai</link>
<description>&lt;p&gt;Building an AI that can chat via text is now a solved problem. Building an AI that can handle a &lt;strong&gt;real phone call&lt;/strong&gt; in production is still a nightmare of fragmented infrastructure.&lt;/p&gt;
&lt;p&gt;Developers typically face a &quot;plumbing&quot; crisis: they must manually stitch together SIP trunks, manage real-time media buffers, handle signal interruptions, and wire up multiple AI providers—all while trying to keep latency low enough for a human conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SIPHON&lt;/strong&gt; was built to end the plumbing. It is an open-source Python framework that provides a unified, telephony-first abstraction for building and operating &lt;strong&gt;Calling AI&lt;/strong&gt; agents.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-the-telephony-gap&quot;&gt;The Problem: The Telephony Gap&lt;/h3&gt;
&lt;p&gt;Traditional IVR systems and contact center platforms were never designed for the era of Large Language Models (LLMs). When developers try to build modern calling agents, they encounter three primary friction points:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Complexity:&lt;/strong&gt; Each project requires re-implementing SIP trunk provisioning, room orchestration, and VAD (Voice Activity Detection) tuning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vendor Lock-in:&lt;/strong&gt; Most &quot;Voice AI&quot; platforms tie you to a single stack, making it impossible to swap LLM or TTS providers as technology evolves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operation Hurdles:&lt;/strong&gt; Features like call recording, metadata persistence, and transcription handling are often treated as afterthoughts rather than first-class features.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-solution-a-unified-calling-abstraction&quot;&gt;The Solution: A Unified Calling Abstraction&lt;/h3&gt;
&lt;p&gt;SIPHON sits between the telephony world and the AI world, providing a coherent framework for developers to build production-ready agents in &lt;strong&gt;~30 lines of code&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;how-it-works-the-siphon-architecture&quot;&gt;How it Works: The SIPHON Architecture&lt;/h4&gt;
&lt;p&gt;Unlike generic voice frameworks, SIPHON is built on top of &lt;strong&gt;LiveKit&lt;/strong&gt;, leveraging its real-time media and SIP layer to ensure high reliability.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fthf6oldup4ak2oh1tip.png&quot; alt=&quot;siphon-high-level-architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;The diagram illustrates how SIPHON orchestrates the entire call lifecycle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PSTN/SIP Provider&lt;/strong&gt; ↔ &lt;strong&gt;LiveKit SIP Domain&lt;/strong&gt; (Real-time media)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LiveKit Rooms&lt;/strong&gt; ↔ &lt;strong&gt;SIPHON Agent Worker&lt;/strong&gt; (Orchestration)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SIPHON Worker&lt;/strong&gt; ↔ &lt;strong&gt;AI Providers&lt;/strong&gt; (OpenAI, Deepgram, Cartesia, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The framework is divided into two core modules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;siphon.telephony&lt;/code&gt;&lt;/strong&gt;: Handles inbound &lt;code&gt;Dispatch&lt;/code&gt; rules and outbound &lt;code&gt;Call&lt;/code&gt; initiation, allowing you to bind phone numbers to agents programmatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;siphon.agent&lt;/code&gt;&lt;/strong&gt;: The runner that manages the LiveKit Agent worker, entrypoint orchestration, and the dynamic construction of AI components.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-pillars-of-siphon&quot;&gt;Key Pillars of SIPHON&lt;/h3&gt;
&lt;h4 id=&quot;1-production-ready-at-scale&quot;&gt;1. Production-Ready at Scale&lt;/h4&gt;
&lt;p&gt;SIPHON is not a &quot;toy&quot; framework. It is designed for horizontal scalability; you can run your agent on one server or a thousand, and the architecture automatically balances the load.&lt;/p&gt;
&lt;h4 id=&quot;2-sub-500ms-latency&quot;&gt;2. Sub-500ms Latency&lt;/h4&gt;
&lt;p&gt;By utilizing WebRTC for real-time media interactions, SIPHON ensures voice interactions feel natural. It manages audio packet loss and interruptions to maintain a human-like flow.&lt;/p&gt;
&lt;h4 id=&quot;3-total-vendor-flexibility&quot;&gt;3. Total Vendor Flexibility&lt;/h4&gt;
&lt;p&gt;Through its plugin architecture, SIPHON is agnostic to your AI stack. You can swap between OpenAI, Gemini, Deepgram, or ElevenLabs with minimal code changes.&lt;/p&gt;
&lt;h4 id=&quot;4-integrated-data-persistence&quot;&gt;4. Integrated Data Persistence&lt;/h4&gt;
&lt;p&gt;Recording, transcription, and metadata persistence are enabled via simple environment flags. Whether you need to save to S3, Postgres, or Redis, SIPHON handles the storage logic for you.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;SIPHON is now open-source under the &lt;strong&gt;Apache 2.0 license&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Developers can install the framework and spin up a functioning agent worker in minutes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install siphon-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whether you are building an AI receptionist, an outbound notification system, or a contact-center triage bot, SIPHON provides the infrastructure to get you to production faster.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Stop building the plumbing. Start building the agent.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://siphon.blackdwarf.in/docs&quot;&gt; Read the Documentation&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon&quot;&gt;⭐ Star us on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;A BLACKDWARF Initiative.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10654/siphon-the-telephony-first-framework-for-calling-ai</guid>
<pubDate>Wed, 28 Jan 2026 17:36:40 +0000</pubDate>
</item>
<item>
<title>Hello, World. Meet SIPHON.</title>
<link>https://coderlegion.com/10444/hello-world-meet-siphon</link>
<description>&lt;p&gt;Building an AI that chats is easy. Building an AI that calls is painfully hard.&lt;/p&gt;
&lt;p&gt;If you have ever tried to build a production-ready voice agent, you know the struggle: fighting with SIP signaling, debugging audio buffers, and wrestling with 2-second latency.&lt;/p&gt;
&lt;p&gt;We got tired of the plumbing. So we built &lt;strong&gt;SIPHON&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;what-is-it-&quot;&gt;What is it?&lt;/h4&gt;
&lt;p&gt;SIPHON is an &lt;strong&gt;open-source Python framework&lt;/strong&gt; for building Calling AI agents.&lt;/p&gt;
&lt;p&gt;It is a &quot;telephony-first&quot; abstraction over LiveKit that handles the chaos of real-world phone networks for you. No more stitching together WebSockets or media servers. Just pure Python.&lt;/p&gt;
&lt;h4 id=&quot;why-use-it-&quot;&gt;Why use it?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Production Ready&lt;/strong&gt;:&lt;br&gt;
Built for real phone calls, not just demos.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Few Lines of Code&lt;/strong&gt;:&lt;br&gt;
Spin up an agent that handles inbound/outbound calls in minutes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vendor Agnostic&lt;/strong&gt;:&lt;br&gt;
Swap LLMs (OpenAI, Gemini) and Voice providers (Deepgram, Cartesia) with a single config line.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sub-500ms Latency&lt;/strong&gt;:&lt;br&gt;
Powered by WebRTC for conversations that feel human.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero-Config Horizontal Scaling&lt;/strong&gt;:&lt;br&gt;
Run your agent on multiple servers and it automatically load balances calls across them—no complex setup required.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;get-started-1&quot;&gt;Get Started&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Install it:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install siphon-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Build your agent:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from siphon.agent import Agent
from siphon.plugins import openai, cartesia, deepgram

agent = Agent(
    agent_name=&quot;Receptionist&quot;,
    llm=openai.LLM(),
    tts=cartesia.TTS(),
    stt=deepgram.STT(),
)

if __name__ == &quot;__main__&quot;:
    agent.dev()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The plumbing is done. Go build the agent.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/blackdwarftech/siphon&quot;&gt;⭐ Star us on GitHub&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://siphon.blackdwarf.in/docs&quot;&gt; Read the Docs&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10444/hello-world-meet-siphon</guid>
<pubDate>Sun, 25 Jan 2026 11:27:39 +0000</pubDate>
</item>
<item>
<title>Modern Frontend Frameworks Are Failing at Testing</title>
<link>https://coderlegion.com/9093/modern-frontend-frameworks-are-failing-at-testing</link>
<description>&lt;p&gt;A few weeks after releasing &lt;a rel=&quot;nofollow&quot; href=&quot;https://brikev.github.io/twd/&quot;&gt;TWD&lt;/a&gt;, a tool designed to integrate testing directly into the development workflow. I started creating testing recipes for different frontend frameworks. While doing that, a few patterns became very clear.&lt;/p&gt;
&lt;h3 id=&quot;what-works&quot;&gt;What works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Frontend-focused applications (SPAs) are relatively easy to test using tools like TWD and traditional runners such as Vitest.&lt;/li&gt;
&lt;li&gt;You can validate real user interactions, keep tests close to the UI, and iterate quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-doesn-t&quot;&gt;What doesn’t&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SSR frameworks make testing unnecessarily confusing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frameworks like &lt;strong&gt;Next.js, Astro, TanStack, Qwik, and Solid&lt;/strong&gt; provide little to no guidance on how to test applications in a reliable and maintainable way. Most of them simply &lt;strong&gt;delegate testing to external tools&lt;/strong&gt; like Playwright or Cypress (&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/testing&quot;&gt;example next.js docs&lt;/a&gt;), treating testing as an afterthought rather than part of the framework itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-ssr-frameworks&quot;&gt;The problem with SSR frameworks&lt;/h3&gt;
&lt;p&gt;SSR frameworks blur the line between backend and frontend. In theory, this sounds powerful. In practice, it creates serious testing problems.&lt;/p&gt;
&lt;p&gt;Yes, you need to test both frontend and backend behavior, but SSR frameworks don’t offer a clear or opinionated way to do that. If you look at their documentation, testing is either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Completely missing&lt;/li&gt;
&lt;li&gt;Mentioned briefly&lt;/li&gt;
&lt;li&gt;Outsourced to E2E tools like Playwright or Cypress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This effectively means: &lt;em&gt;“Here’s how to build fast. Testing is your problem.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From a company perspective—especially one building a &lt;strong&gt;long-lived product&lt;/strong&gt;—this is a big red flag. You want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests that are easy to write&lt;/li&gt;
&lt;li&gt;Tests that reliably fail when something is actually broken&lt;/li&gt;
&lt;li&gt;Tests that help reproduce bugs&lt;/li&gt;
&lt;li&gt;Tests that evolve with the product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most SSR frameworks don’t provide that. They optimize for speed of development, but you start accumulating &lt;strong&gt;testing debt from day one&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-but-you-can-unit-test-right-&quot;&gt;“But you can unit test, right?”&lt;/h3&gt;
&lt;p&gt;Technically, yes. But in practice, unit testing frontend code often becomes &lt;em&gt;testing for the sake of testing&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;What usually happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You mock half of the system&lt;/li&gt;
&lt;li&gt;You test implementation details&lt;/li&gt;
&lt;li&gt;You don’t test real user interactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, you fall back to Playwright. And now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests live outside your app&lt;/li&gt;
&lt;li&gt;They’re slower&lt;/li&gt;
&lt;li&gt;They’re harder to debug&lt;/li&gt;
&lt;li&gt;They don’t feel like part of your development workflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is extremely common in &lt;strong&gt;Next.js applications&lt;/strong&gt;. To this day, I haven’t seen a &lt;strong&gt;truly robust testing setup&lt;/strong&gt; for Next.js that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fails only when something meaningful is broken&lt;/li&gt;
&lt;li&gt;Avoids implementation details&lt;/li&gt;
&lt;li&gt;Is easy to write and maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By “robust,” I mean tests that actually protect the product—not just increase coverage numbers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;react-router-loaders-and-actions-an-exception&quot;&gt;React Router loaders and actions: an exception&lt;/h3&gt;
&lt;p&gt;In the React ecosystem, I found &lt;strong&gt;one notable exception: React Router&lt;/strong&gt;, specifically createRoutesStub.&lt;/p&gt;
&lt;p&gt;This API is interesting because it allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mock routes with loaders and actions&lt;/li&gt;
&lt;li&gt;Create different scenarios per page&lt;/li&gt;
&lt;li&gt;Test frontend behavior realistically&lt;/li&gt;
&lt;li&gt;test loaders and actions separately as pure functions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation of concerns is something many frameworks try to replicate—but React Router gets it right.&lt;/p&gt;
&lt;p&gt;The API design makes testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Straightforward&lt;/li&gt;
&lt;li&gt;Explicit&lt;/li&gt;
&lt;li&gt;Predictable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better, migrating from SPA data mode to framework (SSR) mode is &lt;strong&gt;incremental and low-risk&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After researching multiple alternatives, and considering that testing is one of the most critical aspects of a product, &lt;strong&gt;React Router is the only SSR-capable solution I’d personally choose today.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;using-twd-with-react-router&quot;&gt;Using TWD with React Router&lt;/h3&gt;
&lt;p&gt;When building TWD, we noticed how powerful &lt;code&gt;createRoutesStub&lt;/code&gt; was—and decided to use it in a different way.&lt;/p&gt;
&lt;p&gt;Instead of testing routes in isolation, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added a dedicated testing route&lt;/li&gt;
&lt;li&gt;Executed TWD tests inside the real application&lt;/li&gt;
&lt;li&gt;Rendered pages using route stubs&lt;/li&gt;
&lt;li&gt;Controlled loaders, actions, and scenarios per test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the setup.&lt;/p&gt;
&lt;h4 id=&quot;route-configuration&quot;&gt;Route configuration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { type RouteConfig, index, route } from &quot;@react-router/dev/routes&quot;;

export default [
  index(&quot;routes/home.tsx&quot;),
  route(&quot;todos&quot;, &quot;routes/todolist.tsx&quot;),
  route(&quot;testing&quot;, &quot;routes/testing-page.tsx&quot;),
] satisfies RouteConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;testing-page&quot;&gt;Testing page&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/routes/testing-page.tsx
let twdInitialized = false;

export async function clientLoader() {
  if (import.meta.env.DEV) {
    const testModules = import.meta.glob(&quot;../**/*.twd.test.{ts,tsx}&quot;);
    if (!twdInitialized) {
      const { initTWD } = await import('twd-js/bundled');
      initTWD(testModules, {
        serviceWorker: false,
      });
      twdInitialized = true;
    }
    return {};
  } else {
    return {};
  }
}

export default function TestPage() {
  return (
    &amp;lt;div data-testid=&quot;testing-page&quot; &amp;gt;
      &amp;lt;h1 style={{ opacity: 0.5, fontFamily: 'system-ui, sans-serif' }}&amp;gt;TWD Test Page&amp;lt;/h1&amp;gt;
      &amp;lt;p style={{ opacity: 0.5, fontFamily: 'system-ui, sans-serif' }}&amp;gt;
        This page is used as a mounting point for TWD tests.
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7x1ryhxb18xxu79hyiac.png&quot; alt=&quot;twd testing page&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;example-test&quot;&gt;Example test&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createRoot } from &quot;react-dom/client&quot;;
import { twd, screenDomGlobal } from &quot;twd-js&quot;;

let root: ReturnType&amp;lt;typeof createRoot&amp;gt; | undefined;

export async function setupReactRoot() {
  if (root) {
    root.unmount();
    root = undefined;
  }

  // Navigate to the empty test page
  await twd.visit('/testing');
  
  // Get the container from the test page
  const container = await screenDomGlobal.findByTestId('testing-page');
  root = createRoot(container);
  return root;
}

describe(&quot;Hello World Test&quot;, () =&amp;gt; {
  // root instance to re render the stub
  let root: ReturnType&amp;lt;typeof createRoot&amp;gt; | undefined;

  beforeEach(async () =&amp;gt; {
    // preparing the root instance
    root = await setupReactRoot();
  });

  it(&quot;should render home page test&quot;, async () =&amp;gt; {
    // mocking the component
    const Stub = createRoutesStub([
      {
        path: &quot;/&quot;,
        Component: () =&amp;gt; {
          const loaderData = useLoaderData();
          const params = useParams();
          const matches = useMatches() as any;
          return &amp;lt;Home loaderData={loaderData} params={params} matches={matches} /&amp;gt;;
        },
        loader() {
          return { title: &quot;Home Page test&quot; };
        },
      },
    ]);

    // Render the Stub
    root!.render(&amp;lt;Stub /&amp;gt;);
    await twd.wait(300);
    // Check for the element within our test container
    // We scope the search to the container to be safe, or just search globally since the harness is empty otherwise
    const h1 = await screenDom.findByRole('heading', { level: 1 });
    twd.should(h1, 'have.text', 'Home Page test');
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the test runs, the route is rendered inside the real application, fully interactive, with all assertions passing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/375uz5i9ru0tsi8r17zi.png&quot; alt=&quot;twd test&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;At the end of the day, you have a choice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep using tools that optimize for speed but accumulate testing debt&lt;/li&gt;
&lt;li&gt;Or adopt an approach where testing is part of everyday development&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For SSR applications, &lt;strong&gt;React Router + TWD&lt;/strong&gt; is the first setup I’ve seen that truly supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real UI testing&lt;/li&gt;
&lt;li&gt;Maintainability and easier to debug&lt;/li&gt;
&lt;li&gt;Long-lived products&lt;/li&gt;
&lt;li&gt;Fast feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you work on large applications with complex requirements, this approach scales far better than traditional SSR testing strategies.&lt;/p&gt;
&lt;p&gt;An example on how to integrate &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-react-router&quot;&gt;twd + react router&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Integration Testing</category>
<guid isPermaLink="true">https://coderlegion.com/9093/modern-frontend-frameworks-are-failing-at-testing</guid>
<pubDate>Tue, 30 Dec 2025 12:12:28 +0000</pubDate>
</item>
<item>
<title>Nexios (ASGI Python framework)</title>
<link>https://coderlegion.com/3989/nexios-asgi-python-framework</link>
<description>&lt;h2 id=&quot;nexios-a-modern-python-web-framework-built-for-speed&quot;&gt;Nexios – A Modern Python Web Framework Built for Speed&lt;/h2&gt;
&lt;h3 id=&quot;the-story-behind-nexios&quot;&gt;The Story Behind Nexios&lt;/h3&gt;
&lt;p&gt;Nexios was born out of frustration — frustration with bulky frameworks, unnecessary boilerplate, and the over-complication of simple tasks. I wanted a framework that had:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the simplicity of &lt;strong&gt;Express.js&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;the power of &lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;the flexibility of &lt;strong&gt;raw ASGI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;and none of the magic Django-style meta-classes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built &lt;strong&gt;Nexios&lt;/strong&gt; — a lightweight, async-first Python web framework designed to help developers ship fast, clean, and production-ready APIs with minimal setup.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-how-nexios-works&quot;&gt;⚙️ How Nexios Works&lt;/h3&gt;
&lt;p&gt;Nexios is built on ASGI and designed with speed and clarity in mind. You define routes using Python functions — no decorators, no hidden base classes, no magic. Just clean, readable code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from nexios import NexiosApp

app = NexiosApp()

@app.route(&quot;/hello&quot;)
async def hello(request, response):
    return {&quot;message&quot;: &quot;Hello, Nexios!&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Under the hood, Nexios handles routing, request parsing, async middleware, response formatting, and more — all in a modular and blazing-fast ASGI engine.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Async-first&lt;/strong&gt;: Built on ASGI for native async performance.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Minimal boilerplate&lt;/strong&gt;: Get up and running in 5 lines of code.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Custom Auth Backends&lt;/strong&gt;: JWT, API keys, token rotation, blacklisting — all pluggable.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;ORM-Agnostic&lt;/strong&gt;: Works with Tortoise, Prisma, SQLAlchemy, or no ORM at all.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero Magic&lt;/strong&gt;: No hidden behavior or metaclass gymnastics.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Fast Startup&lt;/strong&gt;: Instant dev server, instant reload.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Auto Docs&lt;/strong&gt;: Generate OpenAPI docs without extra setup.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Extensible Core&lt;/strong&gt;: Built to be hacked and extended.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-get-involved&quot;&gt;How to Get Involved&lt;/h3&gt;
&lt;p&gt;Nexios is open-source and community-friendly. You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;Star the repo&lt;/strong&gt; on GitHub&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report bugs&lt;/strong&gt; or request features&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contribute modules&lt;/strong&gt; or plugins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Join discussions&lt;/strong&gt; in the community&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nexios-labs/Nexios&quot;&gt;https://github.com/nexios-labs/Nexios&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;[https://nexios-docs.netlify.app&quot;&gt;https://nexios-docs.netlify.app&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ready-to-try-it-&quot;&gt;Ready to Try It?&lt;/h3&gt;
&lt;p&gt;Install it now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install nexios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a new project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nexios new  &amp;lt;project_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start building APIs your way.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Built by developers, for developers. Nexios is what Python web development &lt;em&gt;should&lt;/em&gt; feel like.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Open Source</category>
<guid isPermaLink="true">https://coderlegion.com/3989/nexios-asgi-python-framework</guid>
<pubDate>Mon, 14 Jul 2025 12:14:49 +0000</pubDate>
</item>
<item>
<title>Inversion of Control (IoC) Principle</title>
<link>https://coderlegion.com/3947/inversion-of-control-ioc-principle</link>
<description>&lt;p&gt;Nowadays, it is very common for applications to use some pre-written code to perform specific tasks. Typically used to structure or provide functionality, this code can be reused and repurposed, avoiding the need to write it from scratch, saving time and effort. These useful codes are usually called Libraries and Frameworks, although they have fundamental differences, they are often confused. Do you know what the biggest difference is between a Library and a Framework? In this article, I’m going to talk about that difference, called Inversion of Control Principle.&lt;br&gt;
Library vs Framework&lt;/p&gt;
&lt;p&gt;First things first, let’s talk about the differences. A library is a set of functions or class that provides specific functionality. You can call it to do some work in your code flow and then it returns control to you. You call it and control how and when it gets called. Below is a C code that calls the &amp;lt;math.h&amp;gt; library to calculate the square root. Since we have flow control, we call the method, the already implemented method does the calculation for us, and finally, we show the result.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt; 
#include &amp;lt;math.h&amp;gt;
  
int main() {
  double number = 16;
  double square_root = sqrt(number);
  printf(&quot;%.2lf&quot;, square_root);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a different way, a framework works, seen as a skeleton, with some custom behavior that we need to insert and it will call. We just implement a functionality and it calls, but we don’t know how and exactly when, for this reason we lose control, we are inverting control — the framework calls the implementation, instead of the application calling it. Below, we have an example of a framework called Flask, used to create a web server in a simple and extremely powerful way, abstracting aspects such as configurations, sockets and HTTP requests. We don’t worry about this, because the framework does it for us, providing a set of functionalities, widgets and ready-made structures to facilitate development.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return &quot;&amp;lt;h1&amp;gt;Hello, Flask!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Welcome to the home page.&amp;lt;/p&amp;gt;&quot;

@app.route('/about')
def about():
    return &quot;&amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This is a simple Flask app.&amp;lt;/p&amp;gt;&quot;

if __name__ == '__main__':
    app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you read, inversion of control is a fundamental characteristic of framework, now let’s talk more about this principle.&lt;br&gt;
Inversion of Control Principle (IoC)&lt;/p&gt;
&lt;p&gt;As I said before, inversion of control consists of changing the flow of control from the usual flow used in procedural programming and putting external code to manage it. This external code, usually a framework, calls user-provided methods to do something and these methods return control to the framework, or whatever. When you use this framework, it usually plays the role of the main one that defines the skeleton of the step sequence. This concept reminds me of the Template Method design pattern, which is a way to implement the inversion of control principle. In it we defines the template of algorithm in a superclass and subclasses override methods without changing its structure. Did you understand? We implemented it but we don’t know how the template will execute the algorithm.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abstract class Game {
    public final void play() {
        initialize();
        startPlay();
        endPlay();
    }

    protected abstract void initialize();
    protected abstract void startPlay();
    protected abstract void endPlay();
}

class Soccer extends Game {
    @Override
    protected void initialize() {
        System.out.println(&quot;Setting up Soccer field...&quot;);
    }

    @Override
    protected void startPlay() {
        System.out.println(&quot;Kickoff! Match started.&quot;);
    }

    @Override
    protected void endPlay() {
        System.out.println(&quot;Full time. Match ended.&quot;);
    }
}

public class Main {
    public static void main(String[] args) {
        Game soccer = new Soccer();

        System.out.println(&quot;\n=== Playing Soccer ===&quot;);
        soccer.play();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Above, the Game class creates the behavior model. Subclasses, such as Soccer, only implement the content but do not decide the order of execution, and then call the play method, which controls the flow of the application and calls the other methods.&lt;br&gt;
Framework examples&lt;/p&gt;
&lt;p&gt;In Flutter, a UI framework, we abstract a lot of things, as it provides a structure, only needing to fill in the customizable parts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import 'package:flutter/material.dart';

void main() =&amp;gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text(&quot;User Profile&quot;)),
        body: Center(child: Text(&quot;Hello, there!&quot;)),
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/vlopess/Petal-Test&quot;&gt;PetalTest&lt;/a&gt;, a testing framework I’m developing in Dart inspired by JUnit, we can implement the setUp and tearDown methods, however, we do not call this, the framework will call the implemetation that has been provided by the application. Again, the inversion of control used here is that instead of the application calling a framework method, we delegate this responsability to the framework. If you’re curious about JUnit, check out this article, and I’ll be writing about PetalTest soon.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;///PetalTest Framework Example: We don't call this, we just implement it
@ClassTestable()
class ClassTest {

  @BeforeEach()
  void setUp() {
    print(&quot;Before Each Test&quot;);
  }

  @AfterEach()
  void tearDown() {
    print(&quot;After Each Test&quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In conclusion, this principle gives us the power to separate components, making them more flexible, and also guarantees low coupling — Even more so using &lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/articles/injection.html&quot;&gt;dependency injection&lt;/a&gt;, another way to implement IoC.&lt;/p&gt;
&lt;p&gt;Did you get a better understanding of Inversion of Control? Do you have any questions or find any errors? Let me know, comment below. See you later!&lt;/p&gt;
</description>
<category>Full-stack Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3947/inversion-of-control-ioc-principle</guid>
<pubDate>Sun, 13 Jul 2025 21:33:50 +0000</pubDate>
</item>
<item>
<title>The Pygame Framework I Didn’t Plan: Building Tic-Tac-Toe with Asyncio and Events</title>
<link>https://coderlegion.com/3729/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events</link>
<description>&lt;p&gt;Over the last weekend, I spent some time mucking around with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pygame.org/news&quot;&gt;Pygame&lt;/a&gt;. Despite facing some challenges along the way, I ended up with a working prototype. There was neither design nor specification drafted for the project. Nonetheless, mid-development, a reusable design emerged, and we discussed that in last week’s article. Following that discovery, we will explore the implementation this week and answer the question ‚ “Can we make a framework out of it?”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*EFBQUlLgruEaJ-xBPLUYCQ.png&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Cute illustration from Copilot&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-unexpected-framework-from-pygame-hacks-to-a-reusable-design&quot;&gt;The Unexpected Framework: From Pygame Hacks to a Reusable Design&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As a brief recap to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/my-pygame-evolution-embracing-asyncio-and-immutability-for-scalable-design-g55&quot;&gt;last article&lt;/a&gt;, we know how Pygame is designed to be primitive, and lacks inherent structure. Building a Pygame application would therefore require a developer to begin by writing the main application loop. On the other hand, this also presents an opportunity to design a bespoke structure for graphical application development. By refactoring our work from last week, we will witness how a framework emerges, while adhering the original design goal to stay immutable and reliable.&lt;/p&gt;
&lt;p&gt;Adapting the synchronous main application loop to AsyncIO took quite some effort. Despite the complexity involved, it was worth it because it broke the tight coupling of display update and event dispatching loop. Previously, events were only fetched once per display update cycle. If the application was written in a pure event driven manner, delegating work into multiple events would introduce latency.&lt;/p&gt;
&lt;p&gt;Now that both event dispatching and display update run independently, ensuring synchronization of application states between the two components is also crucial. Immutability was therefore introduced such that application data can only be changed through publishing changes to relevant queues.&lt;/p&gt;
&lt;p&gt;With all the work laid out, this opens up an interesting potential, allowing the orchestration code for event dispatching and display updates to be split from the game logic. Application setup work, such as event handler registrations and the initialization of application-specific data, is the primary area that differs from one application to another.&lt;/p&gt;
&lt;p&gt;Sounds like a plan, let’s split it out, and complete the tic-tac-toe game we started.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*ZM4TOBEllAcdECvK&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Daniel McCullough on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;building-the-core-immutable-state-and-asynchronous-orchestration&quot;&gt;Building the Core: Immutable State and Asynchronous Orchestration&lt;/h4&gt;
&lt;p&gt;It shouldn’t be difficult, right? Given the design goal mentioned earlier, the splitting work was indeed not too difficult. On the other hand, keeping track of the declared dataclasses is another story. Regardless, we will uncover an architectural pattern that forms the framework core through some refactoring work. Some minor revisions will be made to the state management, as well as the main application loop. All this work yields a reusable framework for simple graphical applications.&lt;/p&gt;
&lt;p&gt;Previously we were creating multiple queues for different fields in our Application dataclass. Having multiple queues serving similar purpose to update values is fine, though I personally find it wasteful. Why not just consolidate them?&lt;/p&gt;
&lt;p&gt;And I did exactly that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*pbPfF-uj2A66b_aR&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by USGS on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Firstly, we start by introducing an Enum to represent the field we intend to update&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ApplicationDataField(Enum):
    STATE = auto()
    ELEMENTS = auto()
    EVENTS = auto()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Secondly, we update the DeltaOperation to always require the field we intend to update&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class DeltaOperation(ABC):
    field: ApplicationDataField
    item: Any
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, we need to update the application_refresh function, which is the only place where the Application object is updated throughout the framework.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def application_refresh(application: Application) -&amp;gt; Application:
    result = application

    with suppress(asyncio.queues.QueueEmpty):
        while delta := application.delta_data.get_nowait():
            field = application_get_field(application, delta.field)

            match delta:
                case DeltaAdd():
                    result = replace(
                        result,
                        **{field: getattr(result, field) + (delta.item,)},
                    )

                case DeltaUpdate():
                    result = replace(
                        result,
                        **{
                            field: tuple(
                                delta.new if item == delta.item else item
                                for item in getattr(result, field)
                            )
                        },
                    )

                case DeltaDelete():
                    result = replace(
                        result,
                        **{
                            field: tuple(
                                item
                                for item in getattr(result, field)
                                if not item == delta.item
                            )
                        },
                    )

    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actual implementation is a little bit longer, but I truncated for brevity. Logically, the current version is rather similar to the previous version, though it only consumes from one single queue (application.delta_data) to update the states in an Application object.&lt;/p&gt;
&lt;p&gt;After the minor refactoring to the state management, we shift our attention to the main application loop.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def main_loop(
    application_setup: Awaitable[Application],
) -&amp;gt; None:
    pygame.init()

    application = await application_setup

    pygame.event.post(pygame.event.Event(SystemEvent.INIT.value))

    tasks = []

    tasks.append(
        asyncio.create_task(
            display_update(
                application.delta_screen,
                application.clock,
            )
        )
    )
    tasks.append(
        asyncio.create_task(events_process(application))
    )

    await application.exit_event.wait()

    pygame.quit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is almost similar to the original run function in our original code, with just 2 small changes. Remember I mentioned about wanting a window.onload like system events? Here we are emitting an INIT event before we start the display update and event dispatching loops. Additionally, setup code is passed in as &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/p/ea7ed54ec705/edit&quot;&gt;an&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/p/ea7ed54ec705/edit&quot;&gt;Awaitable object&lt;/a&gt;, that is only scheduled to run after pygame.init().&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/understanding-awaitables-coroutines-tasks-and-futures-in-python-gk7&quot;&gt;Understanding Awaitables: Coroutines, Tasks, and Futures in Python&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That’s practically it, though extracting them out to another module, is a pain due to how these functions reference numerous dataclasses and other helpers. Alongside these changes, I also added some helpers to deal with in-game state management. Spiritually, the design of these helper functions loosely follows &lt;a rel=&quot;nofollow&quot; href=&quot;https://redux.js.org/&quot;&gt;Redux&lt;/a&gt;, though the implementation is a lot simpler in my implementation. We shall see them in action when we go through the game implementation next.&lt;/p&gt;
&lt;h4 id=&quot;framework-in-action-bringing-tic-tac-toe-to-life&quot;&gt;Framework in Action: Bringing Tic-Tac-Toe to Life&lt;/h4&gt;
&lt;p&gt;Framework is now done, what’s next?&lt;/p&gt;
&lt;p&gt;What’s a better way to test the framework, than building a game for real? Now that we delegate the orchestration work to another module, we can now focus purely on the game logic. Throughout the discussion, we can observe how the framework design enables building of a simple game with just dispatching events and implementing the handlers. Game state management as mentioned earlier will be discussed as well.&lt;/p&gt;
&lt;p&gt;Firstly, let’s set-up the game:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def setup() -&amp;gt; Application:
    application = Application(screen=pygame.display.set_mode((300, 300)))
    application = await add_event_listeners(
        application,
        (
            (SystemEvent.INIT.value, handle_init),
            (CustomEvent.RESET.value, handle_reset),
            (CustomEvent.MAP_UPDATE.value, handle_update),
            (CustomEvent.JUDGE.value, handle_judge),
        ),
    )

    return application
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new helper add_event_listeners was written for adding multiple event listeners, though they work similarly as the add_event_listener we discussed last week. In case you are wondering at this point, yes, this is all we needed to implement our tic-tac-toe game. Since we split out the orchestration code to another module, our run function now becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from core import main_loop

async def run() -&amp;gt; None:
    await main_loop(setup())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember main_loop() takes an Awaitable as argument? This is the reason why setup() was written as a coroutine function, and passed into main_loop function without the await keyword.&lt;/p&gt;
&lt;p&gt;Before we start the display update and event dispatching cycles, let’s initialize the game by drawing relevant elements to the application screen object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_init(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    for row, col in product(range(3), range(3)):
        element = await box_draw(target, row, col, 100)
        element = await add_event_listener(
            element,
            pygame.MOUSEBUTTONDOWN,
            handle_box_click,
        )
        await target.delta_data.put(DeltaAdd(ApplicationDataField.ELEMENTS, element))
        await screen_update(target, element) # type: ignore

    pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During the initialization phase, the handler draws the 3 x 3 tiles to the screen via box_draw, and then registers a mouse click event to each of them. Each tile is then sent to 2 queues, one to populate the application.elements tuple (via target.delta_data.put), and another to update the display (via screen_update). Once done, the handler invokes the RESET event to start a new game.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_reset(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    await state_merge(target, &quot;board&quot;, winner=None, state=GameState.RING, board=None)

    for element in target.elements.values():
        delta = await box_redraw(target, (255, 255, 255), element, Symbol.EMPTY) # type: ignore
        await target.delta_data.put(
            DeltaUpdate(ApplicationDataField.ELEMENTS, element, delta)
        )
        await screen_update(target, delta)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As the game is resetting, it first reinitializes the game state via state_merge.The helper sends a DeltaOperation to update application.state to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;board&quot;: {
        &quot;winner&quot;: None,
        &quot;state&quot;: GameState.END,
        &quot;board&quot;: None
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Elements in the game board also get redrawn (via box_redraw) to ensure they do not contain a ring or cross symbol. As we did earlier, all of them are then sent to the queues for application and display update.&lt;/p&gt;
&lt;p&gt;Our game is now accepting input. And once a click is registered, the registered event handler is called.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def handle_box_click(
    _event: pygame.event.Event,
    target: Box,
    application: Application,
    logger: BoundLogger,
    **detail: Any,
) -&amp;gt; None:
    element = None

    match state_get(application, &quot;board&quot;).get(&quot;state&quot;):
        case GameState.END:
            await logger.ainfo(&quot;END&quot;)
            pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))

        case GameState.TIE:
            await logger.ainfo(&quot;TIE&quot;)
            pygame.event.post(pygame.event.Event(CustomEvent.RESET.value))

        case GameState.RING if target.value == Symbol.EMPTY:
            element = await box_update(
                application,
                target,
                Symbol.RING,
            )
            await state_merge(application, &quot;board&quot;, state=GameState.CROSS)

        case GameState.CROSS if target.value == Symbol.EMPTY:
            element = await box_update(
                application,
                target,
                Symbol.CROSS,
            )
            await state_merge(application, &quot;board&quot;, state=GameState.RING)

        case _:
            await logger.aerror(&quot;Invalid click&quot;)

    if element:
        pygame.event.post(pygame.event.Event(CustomEvent.MAP_UPDATE.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Click events are handled differently, depending on the state of the board. If there is no valid moves available, in the events of an ended or tie game, then we log a message, and emit a RESET event. Otherwise, we update the clicked element to the symbol corresponding to the current state of the board, followed by a board state change for the next symbol. Lastly, we trigger a MAP_UPDATE event to update the current mapping of the board for judging.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@dataclass
class BoardMap:
    mapping: dict[tuple[int, int], Symbol]

async def handle_update(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    await state_merge(
        target,
        &quot;board&quot;,
        map=BoardMap(
            {
                (item.column, item.row): item.value
                for item in target.elements.values()
                if isinstance(item, Box)
            }
        ),
    )

    pygame.event.post(pygame.event.Event(CustomEvent.JUDGE.value))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BoardMap is just a dataclass keeping the current state of the board, what symbol is placed at each tile. As soon as it is updated, a JUDGE event is triggered to evaluate the game.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WIN_LINES = [
    # Rows
    {(0, 0), (0, 1), (0, 2)},
    {(1, 0), (1, 1), (1, 2)},
    {(2, 0), (2, 1), (2, 2)},
    # Columns
    {(0, 0), (1, 0), (2, 0)},
    {(0, 1), (1, 1), (2, 1)},
    {(0, 2), (1, 2), (2, 2)},
    # Diagonals
    {(0, 0), (1, 1), (2, 2)},
    {(0, 2), (1, 1), (2, 0)},
]

async def handle_judge(
    _event: pygame.event.Event, target: Application, logger: BoundLogger, **detail: Any
):
    if not (bmap := state_get(target, &quot;board&quot;).get(&quot;map&quot;)):
        return

    tiles, winner = (), None

    for symbol in (Symbol.RING, Symbol.CROSS):
        places = {coor for coor, item in bmap.mapping.items() if symbol == item}

        for line in WIN_LINES:
            if line.issubset(places):
                winner = symbol
                tiles = line
                break

        if winner:
            break

    if winner:
        await logger.ainfo(&quot;Winner is found&quot;, winner=winner)
        await state_merge(target, &quot;board&quot;, winner=winner, state=GameState.END)

        for element in target.elements.values():
            if (element.column, element.row) in tiles: # type: ignore
                delta = await box_redraw(target, (255, 255, 0), element, winner) # type: ignore
                await target.delta_data.put(
                    DeltaUpdate(ApplicationDataField.ELEMENTS, element, delta)
                )
                await screen_update(
                    target,
                    delta, # type: ignore
                )
    elif (
        len([symbol for _, symbol in bmap.mapping.items() if symbol == Symbol.EMPTY])
        == 0
    ):
        await logger.ainfo(&quot;End with a tie&quot;)
        await state_merge(target, &quot;board&quot;, state=GameState.TIE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not the most robust code I would proudly print on a t-shirt and parade with it. Essentially, it judges the game to find the winning line and symbol. Redrawing the winning tiles will take place whenever applicable. Besides that, the relevant state of the board is also queued for update (either an END or TIE) whenever needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/1*9hZL4Bm3asPWiieXpsof8Q.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;By utilizing events and state_merge strategically, the simple game of tic-tac-toe is implemented. All the event dispatching work and display update are completely handled by the framework itself. All we needed to do, when changes occur, is to submit the change to the relevant queue. While there are certainly potential improvements to be made, as a weekend project I deem it good enough.&lt;/p&gt;
&lt;h4 id=&quot;beyond-tic-tac-toe-lessons-learned-and-what-s-next&quot;&gt;Beyond Tic-Tac-Toe: Lessons Learned and What’s Next&lt;/h4&gt;
&lt;p&gt;Can we build more things with it?&lt;/p&gt;
&lt;p&gt;Short answer to the question is yes, but I don’t see much point in doing so for most people. Instead, we should reflect on what we learned so far. What is the purpose of the project? What have we achieved so far? What do we do with this piece of code?&lt;/p&gt;
&lt;p&gt;Extracting the orchestration code into a framework is a totally unexpected outcome. Yet, I am happy with the turns of events leading to what we have now. With some polish, the resulting framework should be able to be applied for other simple graphical applications. Personally, I am quite curious on seeing how the framework scales with more elements on screen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1024/0*fEbkPFllr5iRzZ_h&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;em&gt;Photo by Dan Freeman on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As an experimental project, it certainly is quite rough on the edges. Much thought and polish are still needed in the framework API design for consistency. There are also some experimental code snippets that are not covered in the article as they are irrelevant to the game featured this week. Some of the most interesting helpers I can’t wait to try out are the functions inspired by setTimeout and element.dispatchEvent().&lt;/p&gt;
&lt;p&gt;Perhaps a re-implementation of &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/code-rains-revelation-embracing-existence-before-perfection-60f5c641963a&quot;&gt;the code rain animation a-la the Matrix&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/jeffrey04/code-rains-revelation-embracing-existence-before-perfection-2mnc&quot;&gt;Code Rain’s Revelation: Embracing Existence Before Perfection&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, back to the original question asked earlier, at the beginning of this section. The framework is proven capable of building a simple game, as shown in this article. Despite that, do go for a proper game engine like Godot for serious projects. Treat the work presented here as a personal passion project, though I am open for feedback and collaboration. Seriously, this exploration makes me appreciate how game engines do all the heavy-lifting work.&lt;/p&gt;
&lt;p&gt;Finally, thanks for reading this far. If you are interested in checking out the project, it is still hosted on GitHub at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Jeffrey04/pygame-ttt/&quot;&gt;the same repository&lt;/a&gt;. Though I cannot commit to a fixed publication schedule due to my new job, I shall try writing again soon.&lt;/p&gt;
&lt;p&gt;A quick note on how this article came together. While Gemini helped with the drafting and refining of the language, all the core ideas, the technical insights, and every line of code you’ve seen are entirely mine. This project truly reflects my personal journey and passion for game development. If you enjoyed this deep dive and want to follow more of my explorations into coding and game development, consider subscribing or following me on &lt;a rel=&quot;nofollow&quot; href=&quot;https://kitfucoda.medium.com/&quot;&gt;Medium&lt;/a&gt;. Your support means a lot!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/3729/the-pygame-framework-i-didnt-plan-building-tic-tac-toe-with-asyncio-and-events</guid>
<pubDate>Sat, 05 Jul 2025 13:46:49 +0000</pubDate>
</item>
<item>
<title>DevLog 20250611: Audio API Design for Divooka Glaze!</title>
<link>https://coderlegion.com/3324/devlog-20250611-audio-api-design-for-divooka-glaze</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Glaze!&lt;/strong&gt; is an interactive media framework in Divooka that features a &lt;a rel=&quot;nofollow&quot; href=&quot;https://processing.org/&quot;&gt;Processing&lt;/a&gt;-like interface.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;Today, I'm laying the groundwork for audio APIs for our &lt;strong&gt;Glaze!&lt;/strong&gt; framework.&lt;/p&gt;
&lt;p&gt;Divooka &lt;strong&gt;Glaze!&lt;/strong&gt; draws inspiration from SFML and Processing, so we need to understand how those two handle audio first.&lt;/p&gt;
&lt;h3 id=&quot;how-sfml-handles-it-sfml-net-&quot;&gt;How SFML Handles It (SFML.Net)&lt;/h3&gt;
&lt;p&gt;To play audio in SFML.Net, we use the &lt;strong&gt;SFML.Audio&lt;/strong&gt; module, which provides two main classes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Sound&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;SoundBuffer&lt;/code&gt;&lt;/strong&gt; for short, fully-loaded sounds (like sound effects).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Music&lt;/code&gt;&lt;/strong&gt; for long, streamed audio (like background music).&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;playing-short-sounds&quot;&gt;Playing Short Sounds&lt;/h4&gt;
&lt;p&gt;Short audio files (WAV, OGG, FLAC, etc.) can be fully loaded into memory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;// Load the entire file into a buffer (only do this once per sound).
SoundBuffer buffer = new SoundBuffer(&quot;hit.wav&quot;);

// Create a Sound instance and assign the buffer.
Sound hitSound = new Sound(buffer);

// Optional settings
hitSound.Volume = 75f;    // 0–100
hitSound.Pitch  = 1.2f;   // playback speed
hitSound.Loop   = false;  // play once

// Play it
hitSound.Play();

// Make sure the program doesn't exit immediately,
// otherwise the sound will be cut off:
while (hitSound.Status == SoundStatus.Playing)
    System.Threading.Thread.Sleep(10);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reuse&lt;/strong&gt; the same &lt;code&gt;SoundBuffer&lt;/code&gt; if you need to play the same sound multiple times.&lt;/li&gt;
&lt;li&gt;Keep the &lt;code&gt;Sound&lt;/code&gt; (and its &lt;code&gt;SoundBuffer&lt;/code&gt;) in scope until playback finishes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;streaming-music&quot;&gt;Streaming Music&lt;/h4&gt;
&lt;p&gt;For large files (e.g., full-length tracks), use the &lt;code&gt;Music&lt;/code&gt; class, which streams from disk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It doesn't load the entire file into RAM.&lt;/li&gt;
&lt;li&gt;Suitable for long tracks, though with slightly higher CPU usage due to streaming.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;// Create a Music instance; it will stream the file on the fly.
Music backgroundMusic = new Music(&quot;background.ogg&quot;);

// Optional settings
backgroundMusic.Volume = 50f;
backgroundMusic.Loop   = true;

// Start playback
backgroundMusic.Play();

// In a game loop we'd typically:
// while (window.IsOpen()) { /* update, draw, etc. */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;common-pitfalls&quot;&gt;Common Pitfalls&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Missing Native DLLs&lt;/strong&gt;: If you get &lt;code&gt;DllNotFoundException&lt;/code&gt;, ensure all CSFML native libraries are present.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope &amp;amp; Lifetime&lt;/strong&gt;: If &lt;code&gt;Sound&lt;/code&gt; or &lt;code&gt;Music&lt;/code&gt; goes out of scope (is garbage collected), playback stops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Program Exit&lt;/strong&gt;: If the application ends before the audio finishes, you won't hear the full sound - keep the main thread running or integrate playback into the render loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-processing-handles-it&quot;&gt;How Processing Handles It&lt;/h3&gt;
&lt;p&gt;Processing offers sound support through the built-in Sound library and the legacy Minim library.&lt;/p&gt;
&lt;p&gt;Below are the two most common approaches.&lt;/p&gt;
&lt;h4 id=&quot;the-built-in-sound-library&quot;&gt;The Built-in Sound Library&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Installing/Importing&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the Processing IDE, go to &lt;strong&gt;Sketch → Import Library… → Add Library…&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;“Sound”&lt;/strong&gt;, then click &lt;strong&gt;Install&lt;/strong&gt; (if not already installed).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Placing Audio Files&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a folder named &lt;code&gt;data&lt;/code&gt; in the sketch's directory.&lt;/li&gt;
&lt;li&gt;Copy audio files (e.g., &lt;code&gt;music.mp3&lt;/code&gt; or &lt;code&gt;beat.wav&lt;/code&gt;) into that &lt;code&gt;data&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the sketch, import and declare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import processing.sound.*;
SoundFile song;

void setup() {
  size(400, 200);
  // Load a file named &quot;music.mp3&quot; from /data
  song = new SoundFile(this, &quot;music.mp3&quot;);
  song.play();        // start playing once
}

void draw() {
  background(0);
  // (Optionally visualize or control playback here)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Looping, Volume, Rate&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void setup() {
  size(400, 200);
  song = new SoundFile(this, &quot;music.mp3&quot;);
  
  song.loop();               // loop indefinitely
  song.amp(0.5);             // set volume (0.0–1.0)
  song.rate(1.2);            // change playback speed (1.0 = normal)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;minim&quot;&gt;Minim&lt;/h4&gt;
&lt;p&gt;Minim handles things similarly, albeit through &lt;code&gt;AudioPlayer&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import ddf.minim.*;
Minim minim;
AudioPlayer player;

void setup() {
  size(400, 200);
  
  minim = new Minim(this);
  // loadFile(filename, bufferSize)
  player = minim.loadFile(&quot;music.mp3&quot;, 2048); // Place MP3/WAV files inside the sketch's `data` folder.
  player.play();
  
  // Looping &amp;amp; Controlling
  player.loop();        // loop playback
  player.setGain(-10);  // lower volume: gain in dB
  player.setTempo(1.5); // speed multiplier
}

void draw() {
  background(50);
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Where to Store Files&lt;/strong&gt;: Always place audio files in the sketch's &lt;code&gt;data/&lt;/code&gt; folder so Processing can find them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Formats&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Sound library: supports WAV, AIFF, MP3 (depending on platform).&lt;/li&gt;
&lt;li&gt;Minim: supports MP3, OGG, WAV.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency / Real-Time&lt;/strong&gt;: For low-latency playback or live audio input/processing, Minim (or advanced libraries like &lt;strong&gt;Beads&lt;/strong&gt; or &lt;strong&gt;TarsosDSP&lt;/strong&gt;) may be preferable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visualizations&lt;/strong&gt;: Both libraries support real-time audio analysis (waveforms, FFT), which is great for interactive visuals.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;observations&quot;&gt;Observations&lt;/h3&gt;
&lt;p&gt;Those two statements are crucial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Load a soundfile from the /data folder of the sketch and play it back
file = new SoundFile(this, &quot;sample.mp3&quot;);
file.play();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can observe a few things regarding API design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It's procedural and imperative - we create resources first, then set properties, then play/stop via the resource reference. Importantly, it's necessary to keep a reference for later control.&lt;/li&gt;
&lt;li&gt;Resources might require explicit (or scope-based) release.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On the other hand, I'm more interested in a &quot;functional&quot; style API, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6mgkkm5rdmftf7f1g7r.png&quot; alt=&quot;Audio API Concept&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-approach&quot;&gt;The Approach&lt;/h3&gt;
&lt;p&gt;We could implicitly load and manage resources, releasing them at the end of the application lifetime (on window close), if no explicit reference is needed. However, distinguishing between &lt;code&gt;SoundBuffer&lt;/code&gt; and &lt;code&gt;Sound&lt;/code&gt; (both are &lt;code&gt;IDisposable&lt;/code&gt;) can be tricky when deciding which to persist. At the very least, this is what we should expose via &lt;code&gt;PlaySound()&lt;/code&gt; and &lt;code&gt;PlayMusic()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ifkkz6igrkzxiyhb31l.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;private void ReleaseResources()
{
    // Sounds
    foreach (Sound item in _soundInstances)
        item.Dispose();
    foreach ((string key, SoundBuffer item) in _soundBuffers)
        item.Dispose();
    foreach (Music item in _music)
        item.Dispose();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In fact in the current design we cannot share the underlying instance - since &lt;strong&gt;Glaze!&lt;/strong&gt; is implementation-agnostic - unless we introduce strongly typed types for &lt;code&gt;SoundBuffer&lt;/code&gt;, &lt;code&gt;Sound&lt;/code&gt;, and &lt;code&gt;Music&lt;/code&gt;, so all implementations can recognize them. Otherwise, we can't return any instance objects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;// IInteractiveServiceProvider, which is ignorant of SFML.Net's existence
#region Audio Engine
void PlaySound(string filePath);
void PlayMusic(string filePath, bool loop = true);
#endregion
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A slightly inelegant shortcut is to expose them as untyped &quot;object handles&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c#&quot;&gt;/// &amp;lt;summary&amp;gt;
/// One-shot sound.
/// &amp;lt;/summary&amp;gt;
object PlaySound(string filePath);
/// &amp;lt;summary&amp;gt;
/// Loopable BGM etc.
/// &amp;lt;/summary&amp;gt;
object PlayMusic(string filePath, bool loop = true);
/// &amp;lt;param name=&quot;volume&quot;&amp;gt;0–100f&amp;lt;/param&amp;gt;
void SetVolume(object soundOrMusic, float volume);
/// &amp;lt;param name=&quot;pitch&quot;&amp;gt;0–1f&amp;lt;/param&amp;gt;
void SetPitch(object soundOrMusic, float pitch);
void SetLoop(object music, bool loop);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While it might seem &quot;unsafe&quot; on the paper, it works well in Divooka's graphical interface:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wfrx2osj8j4zhwd8l1ue.png&quot; alt=&quot;Object Handle Looks Just Like Any Other Object on GUI - As If Strongly Typed&quot;&gt;&lt;/p&gt;
&lt;p&gt;(This is partly thanks to Divooka's support for return value naming via XML doc comments, e.g., &lt;code&gt;&amp;lt;returns name=&quot;Music&quot;&amp;gt;A music instance.&amp;lt;/returns&amp;gt;&lt;/code&gt; for &lt;code&gt;PlayMusic()&lt;/code&gt;.)&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;So far so good! There's a lot we can enhance in the audio engine - more parameters, DSP support, etc. For example, Processing has very neat functions for &lt;a rel=&quot;nofollow&quot; href=&quot;https://processing.org/reference/libraries/sound/index.html&quot;&gt;low-pass/high-pass filters&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But for now, we can play sounds - and that's a solid start!&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Processing Sound library documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://processing.org/reference/libraries/sound/SoundFile_play_.html&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Divooka Glaze! framework documentation on &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.methodox.io/en/Frameworks/Glaze&quot;&gt;Methodox Wiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/3324/devlog-20250611-audio-api-design-for-divooka-glaze</guid>
<pubDate>Wed, 11 Jun 2025 18:14:04 +0000</pubDate>
</item>
<item>
<title>How To Build A Raycast Extension ✨</title>
<link>https://coderlegion.com/3182/how-to-build-a-raycast-extension</link>
<description>&lt;p&gt;In this article, you are going to learn how to build a Raycast extension. We are going to split this tutorial into two. In the first tutorial you will learn:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What Raycast is?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Raycast APIs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the second tutorial, you will learn:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;How to build a Raycast extension.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to publish a Raycast extension.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to update a Raycast extension.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;raycast&quot;&gt;Raycast&lt;/h3&gt;
&lt;p&gt;Raycast is a productivity tool built for developers by developers. It helps developers save time by adding all the tools that they need in one place to avoid continuous context switching between browser tabs.&lt;/p&gt;
&lt;p&gt;Raycast's UI and UX are incredible, everything you need is in one interface and each tool one keyboard command away. One of the reasons that I as an engineer enjoy using Raycast apart from it's great UI and UX, is there openness and inclusion of the community. Instead of the Raycast team building the entire ecosystem of extensions on their own, they have a documentation guide on how developers can build their own Raycast extension and publish it to the Raycast store. The Raycast store has over 2.13K extensions that have been built by both the Raycast team and the community.&lt;/p&gt;
&lt;p&gt;In today's world AI is part of our lives and it makes our everyday tasks much simpler. Raycast has not been left behind in matters related to AI. As a matter of fact, AI integrates seamlessly into Raycast as if they were meant for one another. You can be able to schedule meetings, cancel meetings, add events and all the great stuff that would require 5 minutes of your life in 30 seconds by simply typing a command and voila! Everything has been set.&lt;/p&gt;
&lt;p&gt;Raycast's pricing model is insanely cheap, they have a free and two paid tiers. The free tier includes: core features, access to thousands of extensions, custom extensions, developer tool, Raycast AI ( Free 50 messages by trying any Pro model ) etc. The other two paid tiers are $8/month and $16/month respectively. You can check out their pricing model and what they offer here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.raycast.com/pricing&quot;&gt;Raycast pricing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently, Raycast is only available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.raycast.com/&quot;&gt;MacOs&lt;/a&gt; but they do have a waitlist if you are on windows. You can join the Windows waitlist here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.raycast.com/windows&quot;&gt;Raycast Windows waitlist&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;raycast-apis&quot;&gt;Raycast APIs&lt;/h3&gt;
&lt;p&gt;Raycast has powerful APIs that enable developers to tap into it's core engine. These APIs include but not limited to UI components, CLI etc. Let's look at each of them.&lt;/p&gt;
&lt;h4 id=&quot;ui-components&quot;&gt;UI components&lt;/h4&gt;
&lt;p&gt;Raycast provides developers with building blocks to build their extensions. These UI components have a similar look to React's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.radix-ui.com/&quot;&gt;Radix UI&lt;/a&gt;. The Raycast API works with React, take a look at this code: import { useState } from &quot;react&quot;; import { List } from &quot;@raycast/api&quot;;&lt;/p&gt;
&lt;p&gt;In the code above, we import &lt;strong&gt;useState&lt;/strong&gt; from React to handle our state and List component from the &lt;strong&gt;@raycast/api&lt;/strong&gt; for building our UI. The @raycast/api provides us with UIs and utilities for building our extension, these UIs and utilities include but not limited to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Detail component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Form component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grid component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Action component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Toast component.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;showToast function.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the next tutorial, we’ll explore and work with each one in detail.&lt;/p&gt;
&lt;h4 id=&quot;cli&quot;&gt;CLI&lt;/h4&gt;
&lt;p&gt;The Raycast CLI allows you to build, develop, and lint your extension. The CLI is part of the &lt;strong&gt;@raycast/api&lt;/strong&gt; package and is automatically installed in the extension directory during setup. The CLI has various commands for specific use cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Help command: To get a list of the available CLI commands.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build command: Creates an optimized production build of our&lt;br&gt;
extension for distribution.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Development command: Starts our extension in development mode.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lint command: Runs ESLint for all files in the src directory.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Migrate command: Migrates our extension to the latest version of the&lt;br&gt;
@raycast/api.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Publish command: Verifies, builds, and publishes an extension to the&lt;br&gt;
Raycast store.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the next tutorial, we are going to learn how to build and publish a Raycast extension. &lt;/p&gt;
&lt;p&gt;Till then, &lt;strong&gt;happy hacking ✨&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/3182/how-to-build-a-raycast-extension</guid>
<pubDate>Sat, 31 May 2025 21:22:15 +0000</pubDate>
</item>
<item>
<title>Quarkus Neo4j: How to build a Quarkus app with Neo4j</title>
<link>https://coderlegion.com/3181/quarkus-neo4j-how-to-build-a-quarkus-app-with-neo4j</link>
<description>&lt;p&gt;I've recently been working on expanding my horizons in different Java frameworks, and Quarkus was on my list to try.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/about/&quot;&gt;Quarkus&lt;/a&gt; is a Kubernetes-native Java framework designed to reduce an application's footprint, ideal for addressing challenges in deploying, running, and maintaining applications in cloud-based environments.&lt;/p&gt;
&lt;p&gt;It has become a major player in list of frameworks, so naturally, I wanted to try it out! I started by following the first few steps in &lt;a rel=&quot;nofollow&quot; href=&quot;https://myfear.substack.com/p/the-power-of-relationships-neo4j-quarkus&quot;&gt;this blog post&lt;/a&gt;, as it outlines how to get a project up and running and the dependency needed for Neo4j. I felt the Quarkus Neo4j documentation was a bit overly complicated and verbose for a starter app.&lt;/p&gt;
&lt;p&gt;All of the code for this blog post is available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/quarkus-coffee-shop&quot;&gt;quarkus-coffee-shop Github repository&lt;/a&gt;. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I also have a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/sdn-coffee-shop&quot;&gt;sister project using Spring Data Neo4j&lt;/a&gt;, for comparison. :)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;creating-a-project&quot;&gt;Creating a project&lt;/h3&gt;
&lt;p&gt;Similar to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://start.spring.io/&quot;&gt;Spring Initializr&lt;/a&gt; that offers a web interface for defining a project and its dependencies, Quarkus has &lt;a rel=&quot;nofollow&quot; href=&quot;https://code.quarkus.io/&quot;&gt;code.quarkus.io&lt;/a&gt;. Quarkus also offers a CLI, which would be especially helpful for creating multiple projects quickly.&lt;/p&gt;
&lt;p&gt;On code.quarkus.io, I filled out the project name, and then added three dependencies for the &lt;code&gt;Neo4j client&lt;/code&gt;, &lt;code&gt;RESTEasy Classic&lt;/code&gt;, and &lt;code&gt;RESTEasy Classic JSON-B&lt;/code&gt;. Once that is complete, click the &lt;code&gt;Generate your application&lt;/code&gt; blue button to download the .zip file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=943366570968875687&quot; alt=&quot;Quarkus online code generator&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next, unzip the folder and open it in your preferred IDE. The project template includes a &lt;code&gt;GreetingResource&lt;/code&gt; class with a default &lt;code&gt;/hello&lt;/code&gt; endpoint, so we can run the application and call the endpoint to see a message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mvnw quarkus:dev

http &quot;:8080/hello&quot;

Hello RESTEasy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;However, to build out our application, there are a couple of adjustments needed to the &lt;code&gt;pom.xml&lt;/code&gt;. To utilize object graph mapping capabilities, we need to include the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/releases/tag/3.14.0&quot;&gt;&lt;code&gt;neo4j-ogm-quarkus&lt;/code&gt;&lt;/a&gt; library, which currently (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/releases/tag/3.14.0&quot;&gt;v3.14.0&lt;/a&gt;) is only compatible with Quarkus 3.22.1.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; There is an open &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/pull/482&quot;&gt;Github pull request for 3.23&lt;/a&gt;, which may be released soon.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, we need to downgrade Quarkus's platform version in order to use OGM (for now). Open the &lt;code&gt;pom.xml&lt;/code&gt;, then navigate to the &lt;code&gt;&amp;lt;properties&amp;gt;&lt;/code&gt; section and change the &lt;code&gt;quarkus.platform.version&lt;/code&gt; property to version &lt;code&gt;3.22.1&lt;/code&gt;, as shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;properties&amp;gt;
    &amp;lt;quarkus.platform.version&amp;gt;3.22.1&amp;lt;/quarkus.platform.version&amp;gt;
&amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, add the following library to the &lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt; section.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.neo4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;neo4j-ogm-quarkus&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.14.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need a Neo4j database and to connect to the instance from our application.&lt;/p&gt;
&lt;h3 id=&quot;creating-and-connecting-to-an-instance-of-neo4j&quot;&gt;Creating and connecting to an instance of Neo4j&lt;/h3&gt;
&lt;p&gt;If you don't have one already, spin up a &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.neo4j.com/aura-java&quot;&gt;free cloud instance of Neo4j Aura&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once the instance is running, load the data by clicking on the &lt;code&gt;Query&lt;/code&gt; tool along the left console menu, then copy the contents of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/graph-demo-datasets/blob/main/coffee-shop/load-data.cypher&quot;&gt;coffee shop import script&lt;/a&gt; and paste into the input box at the top of the window. Click the &lt;code&gt;play&lt;/code&gt; button on the right of the input box and wait for all steps to complete.&lt;/p&gt;
&lt;p&gt;Back in the Quarkus application, open the &lt;code&gt;src/main/resources/application.properties&lt;/code&gt; file to add configuration for connecting to Neo4j.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Neo4j config
quarkus.neo4j.uri=&amp;lt;NEO4J_URI&amp;gt;
quarkus.neo4j.authentication.username=&amp;lt;NEO4J_USERNAME&amp;gt;
quarkus.neo4j.authentication.password=&amp;lt;NEO4J_PASSWORD&amp;gt;

# OGM config
org.neo4j.ogm.use-native-types=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first three properties should match the URI, username, and password for the Neo4j instance. The final property is so that OGM will map temporal data types (dates and times) correctly.&lt;/p&gt;
&lt;p&gt;The next step is to create the domain classes for &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Receipt&lt;/code&gt;, &lt;code&gt;Staff&lt;/code&gt;, and &lt;code&gt;Customer&lt;/code&gt; entities.&lt;/p&gt;
&lt;h3 id=&quot;domain-classes&quot;&gt;Domain classes&lt;/h3&gt;
&lt;p&gt;The domain model for this application contains 4 main entities and the relationships between them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12849477509161879787&quot; alt=&quot;Coffee shop graph data model&quot;&gt;&lt;/p&gt;
&lt;p&gt;We will map the &lt;code&gt;Order&lt;/code&gt; entity first. Create a new file named &lt;code&gt;Order.java&lt;/code&gt; and define the class as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.time.LocalDate;
import java.time.LocalTime;

import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity
public class Order {
    @Id
    private String transactionId;
    private String orderId;
    private LocalDate orderDate;
    private LocalTime orderTime;
    private String inStore;

    //make OGM happy
    public Order() {
    }

    public Order(String transactionId) {
        this.transactionId = transactionId;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@NodeEntity&lt;/code&gt; annotation is for OGM to map this class to an &lt;code&gt;Order&lt;/code&gt; node in Neo4j, and the &lt;code&gt;@Id&lt;/code&gt; annotation pinpoints the id field for the class. After the class variables for &lt;code&gt;orderId&lt;/code&gt;, etc, we have to create an empty constructor, as well as a required parameter constructor for OGM. Then, the getters and setters follow.&lt;/p&gt;
&lt;p&gt;Next, let's define the &lt;code&gt;Staff.java&lt;/code&gt; and &lt;code&gt;Customer.java&lt;/code&gt; classes.&lt;/p&gt;
&lt;p&gt;Staff.java:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Staff {
    @Id
    String staffId;
    String firstName;
    String lastName;
    LocalDate startDate;

    public Staff() {
    }

    public Staff(String staffId, String firstName, String lastName, LocalDate startDate) {
        this.staffId = staffId;
        this.firstName = firstName;
        this.lastName = lastName;
        this.startDate = startDate;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Customer.java:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Customer {
    @Id
    private String customerId;
    private String customerName;
    private String loyaltyId;

    public Customer() {
    }

    public Customer(String customerId, String customerName, String loyaltyId) {
        this.customerId = customerId;
        this.customerName = customerName;
        this.loyaltyId = loyaltyId;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These classes are similar to &lt;code&gt;Order&lt;/code&gt;, so the code shouldn't contain any surprises.&lt;/p&gt;
&lt;h3 id=&quot;relationships&quot;&gt;Relationships&lt;/h3&gt;
&lt;p&gt;The next step is to define relationships. While we can directly define the relationship by adding a new variable to point to the other entity, this domain also contains relationship properties between &lt;code&gt;Order&lt;/code&gt; and &lt;code&gt;Customer&lt;/code&gt; that hold information on how many items the customer ordered and the total amount for the order.&lt;/p&gt;
&lt;p&gt;To map these properties, we need another class that holds the receipt information.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.neo4j.ogm.annotation.EndNode;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.RelationshipEntity;
import org.neo4j.ogm.annotation.StartNode;

@RelationshipEntity(&quot;BOUGHT&quot;)
public class Receipt {
    @Id @GeneratedValue
    private Long id;

    private Integer itemsInOrder;
    private Double orderTotal;

    @StartNode
    private Customer customer;

    @EndNode
    private Order order;

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@RelationshipEntity&lt;/code&gt; maps this class to the &lt;code&gt;BOUGHT&lt;/code&gt; relationship in Neo4j. &lt;code&gt;@Id&lt;/code&gt; and &lt;code&gt;@GeneratedValue&lt;/code&gt; define the id and note that it's an internally-generated id. Two class variables hold the number of items and order total. Then, the &lt;code&gt;@StartNode&lt;/code&gt; and &lt;code&gt;@EndNode&lt;/code&gt; point to the starting and ending entities for the relationship.&lt;/p&gt;
&lt;p&gt;Finally, we can connect these pieces together by mapping the relationships from the &lt;code&gt;Order.java&lt;/code&gt; class.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Order {
    //previous class variables

    @Relationship(value = &quot;BOUGHT&quot;, direction = Relationship.Direction.INCOMING)
    private Receipt receiptAndCustomer;
    @Relationship(value = &quot;SOLD&quot;, direction = Relationship.Direction.INCOMING)
    private Staff staff;

    //constructors

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are two definitions - one for each relationship. The &lt;code&gt;@Relationship&lt;/code&gt; annotation defines the relationship type (&lt;code&gt;BOUGHT&lt;/code&gt; or &lt;code&gt;SOLD&lt;/code&gt;) and the direction (both incoming to the &lt;code&gt;Order&lt;/code&gt; node). Those are mapped to class variables that return the entity type (&lt;code&gt;Receipt&lt;/code&gt; and &lt;code&gt;Staff&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;These definitions map connections from the &lt;code&gt;Order&lt;/code&gt; class to related entities. Now we need to define a repository and resource to retrieve entities from the database and create endpoints for us to access it.&lt;/p&gt;
&lt;h3 id=&quot;repository-class&quot;&gt;Repository class&lt;/h3&gt;
&lt;p&gt;Create an &lt;code&gt;OrderRepository.java&lt;/code&gt; and populate it with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Map;
import org.neo4j.ogm.session.SessionFactory;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class OrderRepository {
    private final SessionFactory sessionFactory;

    OrderRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    
    Iterable&amp;lt;Order&amp;gt; findTenOrders() {
        return sessionFactory.openSession().query(Order.class, 
            &quot;MATCH (order:Order)&amp;lt;-[rel:BOUGHT]-(c:Customer), &quot; +
                &quot;(order)&amp;lt;-[rel2:SOLD]-(s:Staff) &quot; + 
                &quot;RETURN order, collect(rel), collect(c), collect(rel2), collect(s) LIMIT 10;&quot;, 
            Map.of());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/guides/cdi&quot;&gt;&lt;code&gt;@ApplicationScoped&lt;/code&gt; annotation&lt;/a&gt; will share this repository bean across the application. Inside the class, we inject the OGM &lt;code&gt;SessionFactory&lt;/code&gt; into the class to handle connections to Neo4j.&lt;/p&gt;
&lt;p&gt;Next, we define a method to find 10 orders. The query retrieves customers who bought orders, and staff who sold those orders. By returning the order nodes and collecting related entities, unique patterns (orders) are returned. OGM will map returned nodes and relationships accordingly.&lt;/p&gt;
&lt;p&gt;Finally, we need an endpoint to access this information.&lt;/p&gt;
&lt;h3 id=&quot;resource-i-e-controller-class&quot;&gt;Resource (i.e. Controller) class&lt;/h3&gt;
&lt;p&gt;Quarkus uses the term &lt;code&gt;Resource&lt;/code&gt; rather than &lt;code&gt;Controller&lt;/code&gt;, so the &lt;code&gt;OrderResource.java&lt;/code&gt; will handle the user interface by creating a REST api.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@RequestScoped
@Path(&quot;/orders&quot;)
public class OrderResource {
    private final OrderRepository orderRepository;

    @Inject
    public OrderResource(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Iterable&amp;lt;Order&amp;gt; getOrders() {
        return orderRepository.findTenOrders();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, the &lt;code&gt;@RequestScoped&lt;/code&gt; narrows this bean to each request, and the &lt;code&gt;@Path&lt;/code&gt; defines the root url of &lt;code&gt;/orders&lt;/code&gt;. Within the class, there is a variable for the repository and it's injected into the class constructor, so that we can call the query method we wrote earlier.&lt;/p&gt;
&lt;p&gt;Next, we define the &lt;code&gt;getOrders()&lt;/code&gt; method as a HTTP GET method that will return multiple orders (&lt;code&gt;Iterable&amp;lt;Order&amp;gt;&lt;/code&gt;) in JSON output &lt;code&gt;@Produces(MediaType.APPLICATION_JSON)&lt;/code&gt;. The method will call the repository's &lt;code&gt;findTenOrders()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Now we can test the application!&lt;/p&gt;
&lt;h3 id=&quot;run-and-test-the-application&quot;&gt;Run and test the application&lt;/h3&gt;
&lt;p&gt;Run the application in dev mode with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mvnw quarkus:dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can test the default &lt;code&gt;/hello&lt;/code&gt; endpoint (&lt;code&gt;GreetingResource&lt;/code&gt;), as well as the &lt;code&gt;/orders&lt;/code&gt; endpoint (&lt;code&gt;OrderResource&lt;/code&gt;), which retrieves 10 order entities (and their related entities) from Neo4j:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http &quot;:8080/hello&quot;

http &quot;:8080/orders&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;wrapping-up-&quot;&gt;Wrapping up!&lt;/h3&gt;
&lt;p&gt;In this blog post, we walked through the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How to create a Quarkus application using the Quarkus project online tool.&lt;/li&gt;
&lt;li&gt;How to connect to Neo4j.&lt;/li&gt;
&lt;li&gt;How to map domain entities to Neo4j with OGM.&lt;/li&gt;
&lt;li&gt;How to run queries in Neo4j and access the data via a REST endpoint.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This project is a jumping point into so many other facets, and I'll be sure to bring you along for the ride.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;h3 id=&quot;resources&quot;&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Github repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/quarkus-coffee-shop&quot;&gt;Accompanying code for this blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/about/&quot;&gt;What is Quarkus?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Data set: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/graph-demo-datasets/tree/main/coffee-shop&quot;&gt;Coffee shop graph data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.quarkiverse.io/quarkus-neo4j/dev/index.html&quot;&gt;Quarkus Neo4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Github repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus&quot;&gt;Neo4j Quarkus OGM library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Intro to DBs</category>
<guid isPermaLink="true">https://coderlegion.com/3181/quarkus-neo4j-how-to-build-a-quarkus-app-with-neo4j</guid>
<pubDate>Fri, 30 May 2025 20:23:12 +0000</pubDate>
</item>
<item>
<title>Healenium: Anti-flakiness potion</title>
<link>https://coderlegion.com/2837/healenium-anti-flakiness-potion</link>
<description>&lt;h2 id=&quot;healenium-anti-flakiness-potion&quot;&gt;Healenium: Anti-flakiness potion&lt;/h2&gt;
&lt;p&gt;Test automation flakiness is a long-standing headache in UI testing. Selectors break, tests fail, and debugging becomes a frustrating ritual. Enter &lt;strong&gt;Healenium&lt;/strong&gt; — a self-healing solution that enhances Selenium and Appium by automatically fixing broken locators at runtime.&lt;/p&gt;
&lt;p&gt;In this article, you'll learn how Healenium integrates with your Java-based Selenium or Appium test framework, and how it transforms brittle tests into robust, maintainable assets.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-healenium-&quot;&gt;What Is Healenium?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Healenium&lt;/strong&gt; is an open-source library designed to augment web and mobile automation frameworks by enabling automatic healing of element locators. When a locator fails (due to minor UI changes, for instance), Healenium uses machine learning and history-based matching to find the most likely alternative, allowing the test to continue without failure.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Reference:&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://healenium.io/docs&quot;&gt;healenium.io/docs&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;core-features&quot;&gt;Core Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automatic Locator Healing&lt;/strong&gt;: Fixes broken XPath or CSS selectors on-the-fly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ML-based Matching&lt;/strong&gt;: Uses previous DOM snapshots to find similar elements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal Code Change&lt;/strong&gt;: Simple integration into existing test frameworks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;: Works with Selenium WebDriver and Appium.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-Healing Report Dashboard&lt;/strong&gt;: Visualizes healed selectors and statistics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multilingual Support via Proxy&lt;/strong&gt;: Enables healing across various languages using Healenium-Proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-healenium-selenium-java-integration-guide&quot;&gt;☕ Healenium + Selenium-Java: Integration Guide&lt;/h3&gt;
&lt;h4 id=&quot;1-add-maven-dependencies&quot;&gt;1. &lt;strong&gt;Add Maven Dependencies&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.epam.healenium&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;selenium&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.5.5&amp;lt;/version&amp;gt;//Update accordingly
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-wrap-your-driver&quot;&gt;2. &lt;strong&gt;Wrap Your Driver&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;WebDriver delegate = new ChromeDriver();
SelfHealingDriver driver = SelfHealingDriver.create(delegate);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;Now &lt;code&gt;driver.findElement(...)&lt;/code&gt; is backed by Healenium's self-healing engine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;3-set-up-healenium-backend-optional-but-recommended-&quot;&gt;3. &lt;strong&gt;Set Up Healenium Backend (Optional but Recommended)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Healenium stores and compares locator history. To do this, run the backend via Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose -f docker-compose-local.yml up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Healing Service&lt;/li&gt;
&lt;li&gt;Postgres DB&lt;/li&gt;
&lt;li&gt;UI Dashboard at &lt;code&gt;http://localhost:7878&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;configure-healing-behavior-optional-&quot;&gt;Configure Healing Behavior (Optional)&lt;/h3&gt;
&lt;p&gt;Create a custom configuration file named &lt;code&gt;healenium.properties&lt;/code&gt; under the &lt;code&gt;test/resources&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;recovery-tries = 1
score-cap = 0.5
heal-enabled = true
hlm.server.url = http://localhost:7878
hlm.imitator.url = http://localhost:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;config-parameter-explanations-&quot;&gt;Config Parameter Explanations:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;recovery-tries&lt;/strong&gt; – Number of proposed healed locators to try.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;score-cap&lt;/strong&gt; – Healing only proceeds if the match score is &amp;gt;= this threshold (e.g., 0.5 means 50%).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;heal-enabled&lt;/strong&gt; – Enables/disables healing (can be overridden using &lt;code&gt;-Dheal-enabled=false&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hlm.server.url&lt;/strong&gt; – Address of the Healenium backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;hlm.imitator.url&lt;/strong&gt; – Address of the imitate service.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;disable-healing-for-specific-methods&quot;&gt;Disable Healing for Specific Methods&lt;/h4&gt;
&lt;p&gt;Use the &lt;code&gt;@DisableHealing&lt;/code&gt; annotation to skip healing for specific methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@DisableHealing
public boolean isButtonPresent() {
    try {
        return driver.findElement(By.cssSelector(&quot;.test-button&quot;)).isDisplayed();
    } catch (NoSuchElementException e) {
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;healenium-report&quot;&gt;Healenium Report&lt;/h3&gt;
&lt;p&gt;Healenium generates a detailed &lt;strong&gt;HTML report&lt;/strong&gt; after each Selenium session, accessible via browser.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Local URL:&lt;/em&gt; &lt;code&gt;http://localhost:7878/healenium/report/&lt;/code&gt;&lt;/p&gt;
&lt;h4 id=&quot;report-sections-include-&quot;&gt;Report Sections Include:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Path to healed locator&lt;/li&gt;
&lt;li&gt;Failed locator value&lt;/li&gt;
&lt;li&gt;Healed locator value&lt;/li&gt;
&lt;li&gt;Screenshot of healed locator on page&lt;/li&gt;
&lt;li&gt;Healing success confirmation (correct/not correct)&lt;/li&gt;
&lt;li&gt;Score of the healing result&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ Note: The Maven/Gradle report plugins are deprecated and no longer supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;healenium-appium&quot;&gt;Healenium + Appium&lt;/h3&gt;
&lt;p&gt;Healenium supports Appium too, using the same &lt;code&gt;SelfHealingDriver&lt;/code&gt;. Simply delegate your mobile driver (like &lt;code&gt;AndroidDriver&lt;/code&gt; or &lt;code&gt;IOSDriver&lt;/code&gt;) to &lt;code&gt;SelfHealingDriver.create(...)&lt;/code&gt;. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;AppiumDriver delegate = new AndroidDriver&amp;lt;&amp;gt;(url, capabilities);
SelfHealingDriver driver = SelfHealingDriver.create(delegate);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure that Appium and Healenium versions are compatible. Use native locators (like XPath, accessibility ID) for best results.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-usage-tips&quot;&gt;Real-World Usage Tips&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Store all your element locators centrally to monitor which ones heal frequently.&lt;/li&gt;
&lt;li&gt;Use Healenium’s dashboard to identify flaky elements and clean up your selectors.&lt;/li&gt;
&lt;li&gt;Combine with &lt;strong&gt;Allure Reports&lt;/strong&gt; to attach healed screenshots and diagnostics.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;beyond-java-other-language-support&quot;&gt;Beyond Java: Other Language Support&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Healenium-Proxy&lt;/strong&gt; enables healing across all Selenium-supported languages, such as &lt;strong&gt;Python&lt;/strong&gt;, &lt;strong&gt;JavaScript&lt;/strong&gt;, &lt;strong&gt;C#&lt;/strong&gt;, and more. &lt;/p&gt;
&lt;p&gt;To use Healenium in other languages, simply create a &lt;code&gt;RemoteWebDriver&lt;/code&gt; and connect it to the Healenium-Proxy instance. This allows clients in any language to benefit from Healenium's healing capabilities without needing native library support.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from selenium import webdriver

options = webdriver.ChromeOptions()
driver = webdriver.Remote(
    command_executor='http://localhost:7878/wd/hub',
    options=options
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach allows teams to integrate healing logic regardless of the language or test framework they use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;enable-console-debug-with-simplelogger&quot;&gt;Enable Console Debug with SimpleLogger&lt;/h3&gt;
&lt;p&gt;To see detailed Healenium logs during test execution, configure &lt;code&gt;simplelogger.properties&lt;/code&gt; as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;org.slf4j.simpleLogger.log.healenium=debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Place this file in your &lt;code&gt;test/resources&lt;/code&gt; directory. This enables console-level debug logging for Healenium's internal operations, helping with troubleshooting and development.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;last-thoughts&quot;&gt;Last Thoughts&lt;/h3&gt;
&lt;p&gt;Healenium empowers your automation framework with self-healing capabilities, helping reduce flaky failures, improve resilience, and speed up debugging. Whether you're working with Selenium for web or Appium for mobile, Healenium is a lightweight but powerful addition.&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://healenium.io/docs&quot;&gt;Official Docs&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/healenium&quot;&gt;GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Machine Learning</category>
<guid isPermaLink="true">https://coderlegion.com/2837/healenium-anti-flakiness-potion</guid>
<pubDate>Wed, 30 Apr 2025 01:40:18 +0000</pubDate>
</item>
<item>
<title>EF Core Interceptors: Supercharge Your Data Access with the Decorator Pattern</title>
<link>https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</link>
<description>&lt;p&gt;Learn how to hook into EF Core operations using interceptors, leverage the &lt;strong&gt;Decorator Pattern&lt;/strong&gt; for clean cross-cutting concerns, and see real-world examples like SQL logging and auditing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Brief History of EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What Are EF Core Interceptors?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EF Core Interceptors and the Decorator Pattern&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Top Use Cases for EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 1: Logging SQL Queries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-brief-history-of-ef-core-interceptors&quot;&gt;A Brief History of EF Core Interceptors&lt;/h3&gt;
&lt;p&gt;EF Core interceptors were first introduced in EF Core 3.0 (released in September 2019). This version marked a significant step forward in EF Core's extensibility, allowing developers to intercept low-level database operations like commands, connections, and transactions.&lt;/p&gt;
&lt;p&gt;Since then, Microsoft has expanded interceptor capabilities in newer versions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 5.0&lt;/strong&gt;: Introduced SaveChanges interceptors for tracking changes at the context level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 6.0+&lt;/strong&gt;: Continued performance improvements and better support for asynchronous interception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Are EF Core Interceptors?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Entity Framework Core (EF Core), interceptors allow developers to hook into the execution pipeline of EF Core's operations. Think of them as middleware for your database interactions. You can intercept events such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL command execution&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connection opening/closing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transactions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SaveChanges lifecycle&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This lets you inject custom logic like &lt;strong&gt;logging&lt;/strong&gt;, &lt;strong&gt;auditing&lt;/strong&gt;, &lt;strong&gt;validation&lt;/strong&gt;, or performance metrics without modifying your application’s core logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core Interceptors and the Decorator Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EF Core interceptors are a real-world application of the Decorator Pattern, a classic design pattern where functionality is wrapped around existing behavior to extend or alter it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The core EF operation is the base object.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Your interceptor is the decorator.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;You add behavior before or after EF Core’s operation — without altering EF’s core.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach promotes clean code, separation of concerns, and makes your custom behavior reusable and testable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Top Use Cases for EF Core Interceptors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;– SQL Query Logging &lt;/p&gt;
&lt;p&gt;– Track database queries for performance or debugging. &lt;/p&gt;
&lt;p&gt;– Auditing – Automatically log user actions and timestamps. &lt;/p&gt;
&lt;p&gt;– Soft Deletes – Intercept delete commands and convert them to updates. &lt;/p&gt;
&lt;p&gt;– Command Modification – Change or enrich raw SQL before execution. &lt;/p&gt;
&lt;p&gt;– Multi-Tenant Filtering – Inject filters dynamically for multi-tenant scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1: Logging SQL Queries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s log every SQL command executed by EF Core.&lt;/p&gt;
&lt;p&gt;Step 1: Create a SQL Logging Interceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Diagnostics;

public class SqlLoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, 
        CommandEventData eventData, 
        InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Debug.WriteLine($&quot;SQL Executed: {command.CommandText}&quot;);
        return base.ReaderExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL Executed: SELECT * FROM [Users] WHERE [IsActive] = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Register the Interceptor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(&quot;YourConnectionString&quot;)
            .AddInterceptors(new SqlLoggingInterceptor());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;p&gt;Let’s implement automatic auditing of entity modifications.&lt;/p&gt;
&lt;p&gt;Step 1: Define an Auditable Entity Base Class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AuditableEntity
{
    public DateTime ModifiedAt { get; set; }
    public string ModifiedBy { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 2: Implement a SaveChangesInterceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuditInterceptor : SaveChangesInterceptor
{
    private readonly string _currentUser;

    public AuditInterceptor(string currentUser)
    {
        _currentUser = currentUser;
    }

    public override int SavingChanges(DbContextEventData eventData, int result)
    {
        var context = eventData.Context;
        if (context == null) return base.SavingChanges(eventData, result);

        var entries = context.ChangeTracker
            .Entries&amp;lt;AuditableEntity&amp;gt;()
            .Where(e =&amp;gt; e.State == EntityState.Modified);

        foreach (var entry in entries)
        {
            entry.Entity.ModifiedAt = DateTime.UtcNow;
            entry.Entity.ModifiedBy = _currentUser;
        }

        return base.SavingChanges(eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;p&gt;EF Core interceptors are an underutilized gem in the .NET ecosystem. They let you cleanly implement cross-cutting concerns without muddying your core application code. And because they follow the Decorator Pattern, you get flexibility, maintainability, and testability.&lt;/p&gt;
&lt;p&gt;Whether you need to log queries, audit changes, enforce soft deletes, or handle custom behavior, interceptors offer a powerful and elegant solution.&lt;/p&gt;
&lt;p&gt;References&lt;br&gt;
EF Core Documentation – Interceptors&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core 3.0 Release Notes (Interceptors introduced)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&quot;&gt;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core SaveChanges Interceptor Docs&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Decorator Pattern - Refactoring Guru (for pattern explanation)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://refactoring.guru/design-patterns/decorator&quot;&gt;https://refactoring.guru/design-patterns/decorator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core GitHub Repository (for latest updates and issues)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/efcore&quot;&gt;https://github.com/dotnet/efcore&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</guid>
<pubDate>Thu, 20 Mar 2025 08:25:11 +0000</pubDate>
</item>
</channel>
</rss>