<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DevCharms</title><description>DevCharms - A software development blog and portfolio showcasing custom software solutions and development insights.</description><link>https://devcharms.com/</link><atom:link href="https://devcharms.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>A fresh start for DevCharms</title><link>https://devcharms.com/posts/a-fresh-start-for-devcharms/</link><guid isPermaLink="true">https://devcharms.com/posts/a-fresh-start-for-devcharms/</guid><description>Why this site got a clean slate, what you can expect from the blog from here on out, and how we are thinking about shipping useful stories—not theme demos.</description><pubDate>Sat, 01 Mar 2025 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This site spent a long time carrying demo posts from the original AstroPaper template. They were fine for scaffolding, but they were never really &lt;em&gt;DevCharms&lt;/em&gt;. So we cleared the slate.&lt;/p&gt;
&lt;p&gt;From here, this space is for &lt;strong&gt;honest engineering notes&lt;/strong&gt;: things we tried, what broke, what stuck, and the occasional opinion grounded in shipping real work. The stack stays familiar—Astro, Markdown, Tailwind—but the voice is ours.&lt;/p&gt;
&lt;p&gt;If you are reading this right after the purge, thanks for sticking around. The old URLs from the template era are gone; new posts will get their own slugs and their own reasons to exist.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-is-coming-next&quot;&gt;What is coming next&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-coming-next&quot;&gt;What is coming next&lt;/h2&gt;
&lt;p&gt;Expect a mix of short stories from the terminal and longer write-ups when a topic deserves depth. No promises on cadence—only that when something ships here, it should be worth your time.&lt;/p&gt;
&lt;p&gt;Welcome to the new chapter.&lt;/p&gt;</content:encoded><category>meta</category><category>astro</category></item><item><title>Before I hit publish on a static site</title><link>https://devcharms.com/posts/before-i-hit-publish-on-a-static-site/</link><guid isPermaLink="true">https://devcharms.com/posts/before-i-hit-publish-on-a-static-site/</guid><description>A short pre-flight checklist for shipping static sites—build, links, SEO basics, and the one grep that catches embarrassing typos in slugs.</description><pubDate>Thu, 26 Mar 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Shipping a static site can feel low risk until you remember there is &lt;strong&gt;no server to patch later&lt;/strong&gt; for a broken RSS path or a typo in canonical URLs. The safety net is discipline before deploy.&lt;/p&gt;
&lt;p&gt;Here is the checklist I actually run through—not exhaustive, but enough to catch the mistakes that waste an evening.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#build-and-smoke&quot;&gt;Build and smoke&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#links-and-slugs&quot;&gt;Links and slugs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#social-and-feeds&quot;&gt;Social and feeds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#content-passes&quot;&gt;Content passes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#done&quot;&gt;Done&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;build-and-smoke&quot;&gt;Build and smoke&lt;/h2&gt;
&lt;p&gt;Run a production build locally and fix every warning you agree matters. Open the generated &lt;code&gt;dist&lt;/code&gt; (or let CI artifacts do it) and click the pages that changed—not only the homepage.&lt;/p&gt;
&lt;h2 id=&quot;links-and-slugs&quot;&gt;Links and slugs&lt;/h2&gt;
&lt;p&gt;If you renamed posts or slugs, &lt;strong&gt;search the repo&lt;/strong&gt; for old paths and for internal links that still point at retired URLs. Static hosts will happily serve &lt;code&gt;404&lt;/code&gt; forever.&lt;/p&gt;
&lt;h2 id=&quot;social-and-feeds&quot;&gt;Social and feeds&lt;/h2&gt;
&lt;p&gt;Regenerate or inspect &lt;code&gt;rss.xml&lt;/code&gt; and any sitemap output. Feed readers do not forgive a bad link, and search engines notice when metadata drifts from the page title.&lt;/p&gt;
&lt;h2 id=&quot;content-passes&quot;&gt;Content passes&lt;/h2&gt;
&lt;p&gt;Read the new post aloud once. Fix awkward sentences and check code fences. If you reference “today” or “this week,” decide whether it will age badly—and trim if it will.&lt;/p&gt;
&lt;h2 id=&quot;done&quot;&gt;Done&lt;/h2&gt;
&lt;p&gt;The goal is not perfection on day one. It is &lt;strong&gt;confidence&lt;/strong&gt; that what you shipped matches what you think you shipped. The rest can ride in the next small change.&lt;/p&gt;</content:encoded><category>astro</category><category>workflow</category><category>quality</category></item><item><title>How CSS cascade layers let you win with lower specificity</title><link>https://devcharms.com/posts/css-cascade-layers-specificity/</link><guid isPermaLink="true">https://devcharms.com/posts/css-cascade-layers-specificity/</guid><description>Using the @layer at-rule to order resets, components, and utilities so layer order does the heavy lifting—fewer chained selectors and less need for !important.</description><pubDate>Mon, 30 Mar 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Specificity is a useful tool until it becomes a &lt;strong&gt;scoreboard&lt;/strong&gt;. You add a class, then a parent selector, then another class, and soon “winning” the cascade means out-gunning yesterday’s selectors instead of expressing intent. &lt;strong&gt;Cascade layers&lt;/strong&gt; (&lt;code&gt;@layer&lt;/code&gt;) change the game: they add a &lt;strong&gt;new step&lt;/strong&gt; in the cascade—&lt;strong&gt;layer order&lt;/strong&gt;—that is evaluated &lt;strong&gt;before&lt;/strong&gt; specificity. That lets you keep selectors shallow and predictable while still controlling what wins.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-layer-actually-does&quot;&gt;What &lt;code&gt;@layer&lt;/code&gt; actually does&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-main-benefit-order-beats-specificity-across-layers&quot;&gt;The main benefit: order beats specificity across layers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#fewer-reasons-to-use-important&quot;&gt;Fewer reasons to use &lt;code&gt;!important&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#clear-mental-model-for-teams&quot;&gt;Clear mental model for teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#how-this-pairs-with-modern-css-stacks&quot;&gt;How this pairs with modern CSS stacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#practical-cautions&quot;&gt;Practical cautions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-layer-actually-does&quot;&gt;What &lt;code&gt;@layer&lt;/code&gt; actually does&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;&lt;code&gt;@layer&lt;/code&gt;&lt;/strong&gt; at-rule groups styles into &lt;strong&gt;named layers&lt;/strong&gt;. You declare an order once—often from low-level to high-level:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;@layer&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; reset, tokens, components, utilities;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rules in &lt;code&gt;utilities&lt;/code&gt; are considered &lt;strong&gt;after&lt;/strong&gt; rules in &lt;code&gt;components&lt;/code&gt; in the cascade, &lt;strong&gt;regardless of selector specificity&lt;/strong&gt; (for normal declarations in author stylesheets). Only after layers are compared does the engine fall back to specificity, then source order.&lt;/p&gt;
&lt;p&gt;So layers are not a replacement for specificity &lt;strong&gt;inside&lt;/strong&gt; a layer; they are a way to &lt;strong&gt;structure conflict&lt;/strong&gt; between &lt;strong&gt;kinds&lt;/strong&gt; of styles (foundation vs. components vs. overrides) without turning every file into a specificity arms race.&lt;/p&gt;
&lt;h2 id=&quot;the-main-benefit-order-beats-specificity-across-layers&quot;&gt;The main benefit: order beats specificity across layers&lt;/h2&gt;
&lt;p&gt;Classic pain: a third-party or legacy block uses &lt;code&gt;.sidebar .nav li a.active&lt;/code&gt; and you need a one-off tweak. Without layers, you either match or beat that specificity, reach for &lt;code&gt;!important&lt;/code&gt;, or duplicate structure in your selector.&lt;/p&gt;
&lt;p&gt;With layers, you might put that legacy chunk in &lt;code&gt;@layer legacy&lt;/code&gt; and your app overrides in &lt;code&gt;@layer app&lt;/code&gt;. A single class in &lt;code&gt;app&lt;/code&gt; can override a long chain in &lt;code&gt;legacy&lt;/code&gt; because &lt;strong&gt;the layer wins first&lt;/strong&gt;. You are not “reducing specificity” in the mathematical sense for rules in the &lt;strong&gt;same&lt;/strong&gt; layer—you are &lt;strong&gt;making specificity less often the deciding factor&lt;/strong&gt; between &lt;strong&gt;layers&lt;/strong&gt; you own.&lt;/p&gt;
&lt;p&gt;That is the practical payoff: &lt;strong&gt;simpler selectors&lt;/strong&gt; for the code you write every day, because &lt;strong&gt;where&lt;/strong&gt; it lives in the stack is explicit.&lt;/p&gt;
&lt;h2 id=&quot;fewer-reasons-to-use-important&quot;&gt;Fewer reasons to use &lt;code&gt;!important&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;!important&lt;/code&gt; often appears when specificity and source order leave no clean escape hatch. Layers give you a &lt;strong&gt;structured&lt;/strong&gt; escalation path: add a higher layer (for example &lt;code&gt;utilities&lt;/code&gt; or &lt;code&gt;overrides&lt;/code&gt;) instead of marking every declaration important.&lt;/p&gt;
&lt;p&gt;You still should not create dozens of micro-layers without reason—&lt;strong&gt;broad, stable layers&lt;/strong&gt; (reset → base → components → utilities) are easier to reason about than a layer per file.&lt;/p&gt;
&lt;h2 id=&quot;clear-mental-model-for-teams&quot;&gt;Clear mental model for teams&lt;/h2&gt;
&lt;p&gt;Layers encode &lt;strong&gt;intent&lt;/strong&gt; in the stylesheet architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reset&lt;/code&gt; / &lt;code&gt;normalize&lt;/code&gt;&lt;/strong&gt; — browser quirks, low precedence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;base&lt;/code&gt;&lt;/strong&gt; — element defaults and typography.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;components&lt;/code&gt;&lt;/strong&gt; — BEM-style blocks, cards, nav patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;utilities&lt;/code&gt;&lt;/strong&gt; — small, single-purpose helpers (spacing, visibility).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New contributors can read &lt;code&gt;@layer&lt;/code&gt; declarations like a &lt;strong&gt;table of contents&lt;/strong&gt; for the cascade. That reduces “why did this style lose?” debugging sessions that end in a 0–4–2 specificity lecture.&lt;/p&gt;
&lt;h2 id=&quot;how-this-pairs-with-modern-css-stacks&quot;&gt;How this pairs with modern CSS stacks&lt;/h2&gt;
&lt;p&gt;Frameworks and build tools increasingly respect layers (for example, importing Tailwind with layered preflight and utilities). Whether you author layers by hand or adopt a layered preset, the underlying idea is the same: &lt;strong&gt;separate concerns by layer&lt;/strong&gt;, keep selectors inside each concern as simple as the design allows, and let &lt;strong&gt;layer order&lt;/strong&gt; carry cross-cutting priority.&lt;/p&gt;
&lt;h2 id=&quot;practical-cautions&quot;&gt;Practical cautions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unlayered&lt;/strong&gt; author styles still sit &lt;strong&gt;above&lt;/strong&gt; layered styles in the cascade for normal rules—so stray global CSS outside &lt;code&gt;@layer&lt;/code&gt; can still surprise you. Prefer moving everything into layers once you adopt them, or be deliberate about what stays unlayered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Same-layer&lt;/strong&gt; conflicts still use specificity and source order; layers do not remove the need for sensible selectors inside a layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scoped styles&lt;/strong&gt; (for example in Shadow DOM) follow their own encapsulation rules; layers apply within the same origin and tree as usual, but component boundaries still matter for what actually applies.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Cascade layers are not magic—they are &lt;strong&gt;structure&lt;/strong&gt;. If your project fights the cascade with ever-longer selectors, &lt;code&gt;@layer&lt;/code&gt; gives you a way to &lt;strong&gt;lower the temperature&lt;/strong&gt;: define a small number of layers, put resets and utilities in the right slots, and let &lt;strong&gt;order&lt;/strong&gt; do what you used to do with specificity gymnastics.&lt;/p&gt;</content:encoded><category>css</category><category>cascade</category><category>web-standards</category></item><item><title>@container: responsive layout that follows the component, not the viewport</title><link>https://devcharms.com/posts/css-container-queries-at-container/</link><guid isPermaLink="true">https://devcharms.com/posts/css-container-queries-at-container/</guid><description>Container queries and the @container rule let you style elements from their parent&apos;s width—so cards, sidebars, and embeds adapt inside columns and grids without viewport media queries.</description><pubDate>Tue, 07 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Media queries answer a single question: &lt;strong&gt;how wide is the viewport?&lt;/strong&gt; That is the right default for page-level concerns, but components rarely care about the window—they care about &lt;strong&gt;the space they actually get&lt;/strong&gt;. A card might sit in a narrow sidebar or a full-width column; &lt;strong&gt;container queries&lt;/strong&gt; let styles react to &lt;strong&gt;that&lt;/strong&gt; box instead of guessing from &lt;code&gt;100vw&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#from-viewport-to-containment&quot;&gt;From viewport to containment&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-container-rule&quot;&gt;The &lt;code&gt;@container&lt;/code&gt; rule&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#named-containers&quot;&gt;Named containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#how-this-differs-from-clamp-and-flex&quot;&gt;How this differs from &lt;code&gt;clamp()&lt;/code&gt; and flex&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#practical-pattern-one-card-many-homes&quot;&gt;Practical pattern: one card, many homes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#support-and-progressive-enhancement&quot;&gt;Support and progressive enhancement&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;from-viewport-to-containment&quot;&gt;From viewport to containment&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Container queries&lt;/strong&gt; need an element marked as a &lt;strong&gt;query container&lt;/strong&gt;. You declare what the browser should track (usually &lt;strong&gt;inline size&lt;/strong&gt;) with &lt;code&gt;container-type&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.card-grid&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container-type: inline-size;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;inline-size&lt;/code&gt;&lt;/strong&gt; — the container’s width in horizontal writing modes (what you want for most “responsive component” patterns).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;size&lt;/code&gt;&lt;/strong&gt; — both inline and block dimensions participate (heavier; use when you truly need height-based rules).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optionally name the container so nested layouts stay clear:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.card-grid&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container-type: inline-size;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container-name: cards;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shorthand:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.card-grid&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container: cards / inline-size;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-container-rule&quot;&gt;The &lt;code&gt;@container&lt;/code&gt; rule&lt;/h2&gt;
&lt;p&gt;Inside a subtree, you write &lt;strong&gt;&lt;code&gt;@container&lt;/code&gt;&lt;/strong&gt; blocks that mirror &lt;code&gt;@media&lt;/code&gt;, but the condition is evaluated against &lt;strong&gt;ancestor query containers&lt;/strong&gt;—typically the nearest one unless you target a name.&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; (min-width: 28rem) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;  .card&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    display: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;grid&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    grid-template-columns: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;auto&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;fr&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    gap: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the &lt;strong&gt;container&lt;/strong&gt; (not the viewport) is at least &lt;code&gt;28rem&lt;/code&gt; wide, those rules apply. The same &lt;code&gt;.card&lt;/code&gt; markup can &lt;strong&gt;stack&lt;/strong&gt; in a narrow rail and &lt;strong&gt;split&lt;/strong&gt; in a wide main column, with &lt;strong&gt;one stylesheet&lt;/strong&gt; and no duplicate breakpoints keyed to page templates.&lt;/p&gt;
&lt;h3 id=&quot;named-containers&quot;&gt;Named containers&lt;/h3&gt;
&lt;p&gt;If multiple wrappers could qualify, pin the query to a specific container:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; cards (min-width: 40rem) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;  .card-grid&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    grid-template-columns: &lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;repeat&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;fr&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That reads cleanly in design systems: **“at this card track width, use three columns”—**not &lt;strong&gt;“at this screen width, maybe three columns if we are on the right page.”&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-this-differs-from-clamp-and-flex&quot;&gt;How this differs from &lt;code&gt;clamp()&lt;/code&gt; and flex&lt;/h2&gt;
&lt;p&gt;Fluid type and spacing (&lt;code&gt;clamp()&lt;/code&gt;, &lt;code&gt;min()&lt;/code&gt;, viewport units) smooth scales &lt;strong&gt;globally&lt;/strong&gt;. Flex and grid distribute &lt;strong&gt;siblings&lt;/strong&gt; inside a parent. Container queries add &lt;strong&gt;contextual breakpoints&lt;/strong&gt;: “when &lt;strong&gt;this&lt;/strong&gt; layout region is wide enough, change &lt;strong&gt;this&lt;/strong&gt; subtree.” They complement media queries—you still use &lt;code&gt;@media&lt;/code&gt; for navigation patterns, page gutters, and coarse layout—but &lt;strong&gt;component internals&lt;/strong&gt; often belong in &lt;code&gt;@container&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;practical-pattern-one-card-many-homes&quot;&gt;Practical pattern: one card, many homes&lt;/h2&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.product-card&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container-type: inline-size;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  container-name: product;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; product (max-width: 20rem) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;  .product-card__actions&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    flex-direction: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;column&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;@container&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; product (min-width: 36rem) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;  .product-card__media&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    aspect-ratio: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;16&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; / &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;9&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop that card into a marketing grid, a cart drawer, or a comparison table—the &lt;strong&gt;local&lt;/strong&gt; width drives the &lt;strong&gt;local&lt;/strong&gt; layout.&lt;/p&gt;
&lt;h2 id=&quot;support-and-progressive-enhancement&quot;&gt;Support and progressive enhancement&lt;/h2&gt;
&lt;p&gt;Container queries and &lt;code&gt;@container&lt;/code&gt; are &lt;strong&gt;widely available in current browsers&lt;/strong&gt;. For older environments, treat container-driven rules as &lt;strong&gt;enhancement&lt;/strong&gt;: base styles work everywhere; &lt;code&gt;@container&lt;/code&gt; blocks refine layout where supported. You can mirror critical breakpoints in &lt;code&gt;@media&lt;/code&gt; only when you must support legacy clients—but many static and content sites can ship container queries alone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If your breakpoints are labeled &lt;code&gt;sm:&lt;/code&gt;, &lt;code&gt;md:&lt;/code&gt;, &lt;code&gt;lg:&lt;/code&gt; but the real variable is &lt;strong&gt;“how wide is this widget,”&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;@container&lt;/code&gt;&lt;/strong&gt; is the at-rule that finally matches how you think about the UI.&lt;/p&gt;</content:encoded><category>css</category><category>layout</category><category>responsive-design</category></item><item><title>Why CSS logical properties are the efficient default</title><link>https://devcharms.com/posts/css-logical-properties-efficiency/</link><guid isPermaLink="true">https://devcharms.com/posts/css-logical-properties-efficiency/</guid><description>How margin-inline, padding-block, and friends replace left/right gymnastics with fewer declarations and better RTL and writing-mode behavior—without extra media queries.</description><pubDate>Fri, 27 Mar 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For years we reached for &lt;code&gt;padding-left&lt;/code&gt; and &lt;code&gt;padding-right&lt;/code&gt; because the screen felt like “horizontal.” That works until you support &lt;strong&gt;right-to-left&lt;/strong&gt; layouts, vertical writing modes, or you simply want one declaration instead of two. &lt;strong&gt;Logical properties&lt;/strong&gt; describe space along the &lt;strong&gt;inline&lt;/strong&gt; and &lt;strong&gt;block&lt;/strong&gt; axes—where text flows and where blocks stack—not “west” and “east” of the viewport.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#one-declaration-instead-of-two&quot;&gt;One declaration instead of two&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-mental-model-inline-vs-block&quot;&gt;The mental model: inline vs block&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rtl-without-duplicate-rules&quot;&gt;RTL without duplicate rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#efficiency-in-the-real-sense&quot;&gt;Efficiency in the real sense&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;one-declaration-instead-of-two&quot;&gt;One declaration instead of two&lt;/h2&gt;
&lt;p&gt;If you want equal horizontal padding, this is familiar:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.card&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  padding-left: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  padding-right: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With logical properties you express the same intent in one line:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;css&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;.card&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;  padding-inline: &lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;rem&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You write less CSS, and the meaning is clearer: “padding on the inline axis,” not “padding on two arbitrary sides.” That is a small win in every component file—and it adds up across a design system.&lt;/p&gt;
&lt;h2 id=&quot;the-mental-model-inline-vs-block&quot;&gt;The mental model: inline vs block&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inline&lt;/strong&gt; is the direction text runs in a line (usually left-to-right in English; right-to-left in Arabic or Hebrew).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block&lt;/strong&gt; is the direction blocks stack (usually top-to-bottom).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So &lt;code&gt;margin-inline&lt;/code&gt; covers start and end along the line; &lt;code&gt;margin-block&lt;/code&gt; covers the perpendicular direction. When you use &lt;strong&gt;physical&lt;/strong&gt; properties, you are naming compass directions. When you use &lt;strong&gt;logical&lt;/strong&gt; ones, you are naming &lt;strong&gt;flow&lt;/strong&gt;, which is what layout is actually about.&lt;/p&gt;
&lt;p&gt;Common pairs:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Physical (two values)&lt;/th&gt;&lt;th&gt;Logical (one value)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;padding-left&lt;/code&gt; / &lt;code&gt;padding-right&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;padding-inline&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;padding-top&lt;/code&gt; / &lt;code&gt;padding-bottom&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;padding-block&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;margin-left&lt;/code&gt; / &lt;code&gt;margin-right&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;margin-inline&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt; (in flow context)&lt;/td&gt;&lt;td&gt;often &lt;code&gt;inline-size&lt;/code&gt; / &lt;code&gt;block-size&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Shorthands like &lt;code&gt;inset-inline&lt;/code&gt; for &lt;code&gt;left&lt;/code&gt;+&lt;code&gt;right&lt;/code&gt; (or their logical equivalents) follow the same idea.&lt;/p&gt;
&lt;h2 id=&quot;rtl-without-duplicate-rules&quot;&gt;RTL without duplicate rules&lt;/h2&gt;
&lt;p&gt;The big payoff is &lt;strong&gt;internationalization&lt;/strong&gt;. If you mirror a layout for RTL, physical properties force you to duplicate or override rules: one block for LTR, another for RTL. Logical properties flip with the writing direction, so the same rule can serve both—no &lt;code&gt;[dir=&quot;rtl&quot;]&lt;/code&gt; patch for every component that used &lt;code&gt;margin-left&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is not just “accessibility”—it is &lt;strong&gt;less code to maintain&lt;/strong&gt; and fewer bugs when someone adds a new panel and forgets the RTL override.&lt;/p&gt;
&lt;h2 id=&quot;efficiency-in-the-real-sense&quot;&gt;Efficiency in the real sense&lt;/h2&gt;
&lt;p&gt;“Efficiency” here is not only bytes on the wire (though fewer properties help). It is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fewer declarations&lt;/strong&gt; for symmetric spacing and sizing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One source of truth&lt;/strong&gt; for layout that should follow writing mode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clearer intent&lt;/strong&gt; for the next person reading the stylesheet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logical properties are well supported in modern browsers. For legacy constraints, you can still layer physical fallbacks or use tools that emit both; for greenfield components, defaulting to logical naming keeps the cascade simpler from day one.&lt;/p&gt;
&lt;p&gt;If you are still typing &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; for symmetric gutters, try &lt;code&gt;inline&lt;/code&gt; once—your RTL future self (and your line count) will notice.&lt;/p&gt;</content:encoded><category>css</category><category>layout</category><category>web-standards</category></item><item><title>Ditch the post-it notes: protect your business secrets with a password vault</title><link>https://devcharms.com/posts/ditch-the-post-it-notes-protect-your-passwords/</link><guid isPermaLink="true">https://devcharms.com/posts/ditch-the-post-it-notes-protect-your-passwords/</guid><description>Why every company—from two-person shops to enterprises—should stop scribbling credentials on sticky notes and start using a password vault like 1Password.</description><pubDate>Mon, 11 May 2026 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/contrast-shot.png&quot; alt=&quot;A cluttered desk with post-it note passwords on one side contrasted with a clean workspace and a digital vault on the other&quot;&gt;&lt;/p&gt;
&lt;p&gt;A password on a post-it note stuck to a monitor is not a security strategy. Neither is a shared spreadsheet, a notebook in a desk drawer, or a Slack message that says “hey, what’s the AWS login again?” Yet these habits persist in companies of every size, from startups to publicly traded firms.&lt;/p&gt;
&lt;p&gt;The reasoning usually sounds practical: “We only have five people,” or “I’ll remember it,” or “We’ve never had a problem.” That last one is survivorship bias wearing a name badge. The problem has not happened &lt;em&gt;yet&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-real-cost-of-just-remembering-it&quot;&gt;The real cost of “just remembering it”&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#ai-has-changed-the-math-on-password-guessing&quot;&gt;AI has changed the math on password guessing&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#what-a-vault-like-1password-actually-gives-you&quot;&gt;What a vault like 1Password actually gives you&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#secure-sharing-without-exposure&quot;&gt;Secure sharing without exposure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#watchtower-breach-detection-you-would-never-do-manually&quot;&gt;Watchtower: breach detection you would never do manually&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#secrets-automation-for-development-teams&quot;&gt;Secrets automation for development teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#travel-mode&quot;&gt;Travel Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#passkey-and-mfa-management&quot;&gt;Passkey and MFA management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#were-too-small-for-this&quot;&gt;”We’re too small for this”&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;#the-habit-shift&quot;&gt;The habit shift&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-real-cost-of-just-remembering-it&quot;&gt;The real cost of “just remembering it”&lt;/h2&gt;
&lt;p&gt;When credentials live in someone’s head—or worse, on a scrap of paper—three things quietly go wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;People reuse passwords.&lt;/strong&gt; If the company WiFi password is also someone’s email password, one breach unzips everything. Verizon’s 2024 Data Breach Investigations Report found that stolen or weak credentials were involved in nearly half of all breaches. That statistic has barely moved in a decade.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Onboarding and offboarding become dangerous.&lt;/strong&gt; A new hire asks around for the staging server password. Someone texts it. When that person leaves six months later, nobody rotates the credential. The door stays open and everyone forgets it exists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no audit trail.&lt;/strong&gt; If something goes wrong, you cannot answer the most basic question: who had access to what, and when?&lt;/p&gt;
&lt;h2 id=&quot;ai-has-changed-the-math-on-password-guessing&quot;&gt;AI has changed the math on password guessing&lt;/h2&gt;
&lt;p&gt;Brute-forcing a short, predictable password has always been feasible. What has changed is that large language models and purpose-built cracking tools can now make &lt;em&gt;educated&lt;/em&gt; guesses at scale.&lt;/p&gt;
&lt;p&gt;Researchers at Home Security Heroes demonstrated in 2023 that an AI-powered tool called PassGAN could crack over half of common passwords in under a minute and 71 percent of them in less than a day. It works by learning the patterns humans fall into—capitalizing the first letter, appending a year, substituting &lt;code&gt;@&lt;/code&gt; for &lt;code&gt;a&lt;/code&gt;—and generating candidates that feel random to us but are statistically predictable to a model.&lt;/p&gt;
&lt;p&gt;If your company’s convention is &lt;code&gt;CompanyName2025!&lt;/code&gt; or &lt;code&gt;Summer2026#&lt;/code&gt;, an attacker armed with a fine-tuned model, a list of leaked hashes, and commodity GPUs does not need luck. They need patience measured in hours, not years.&lt;/p&gt;
&lt;p&gt;A password vault solves this at the source. It generates long, truly random strings—&lt;code&gt;x7$qL!9mWv@kR2pZ&lt;/code&gt;—that no human would choose and no model can predict from patterns, because there are no patterns to learn.&lt;/p&gt;
&lt;h2 id=&quot;what-a-vault-like-1password-actually-gives-you&quot;&gt;What a vault like 1Password actually gives you&lt;/h2&gt;
&lt;p&gt;A password vault is not just a digital notebook with a lock. It is an infrastructure layer. Here is what it provides that you would never build or sustain on your own.&lt;/p&gt;
&lt;h3 id=&quot;secure-sharing-without-exposure&quot;&gt;Secure sharing without exposure&lt;/h3&gt;
&lt;p&gt;Need to give a contractor access to a staging API key? In 1Password, you share a vault or a single item. The contractor sees the credential inside the app but never copies it into a text file. When the engagement ends, you revoke access in one click. Compare that to the alternative: sending a password over email, hoping they delete it, and wondering six months later if they did.&lt;/p&gt;
&lt;h3 id=&quot;watchtower-breach-detection-you-would-never-do-manually&quot;&gt;Watchtower: breach detection you would never do manually&lt;/h3&gt;
&lt;p&gt;1Password’s Watchtower feature cross-references your stored credentials against known breach databases (like Have I Been Pwned) and flags weak, reused, or compromised passwords automatically. No human is going to check 200 service accounts against a breach list every morning. Watchtower does it continuously and silently, surfacing only the items that need attention.&lt;/p&gt;
&lt;h3 id=&quot;secrets-automation-for-development-teams&quot;&gt;Secrets automation for development teams&lt;/h3&gt;
&lt;p&gt;For engineering teams, 1Password offers a Secrets Automation platform and a CLI that inject credentials into builds, CI/CD pipelines, and server environments without ever writing them to disk or committing them to a repository. A &lt;code&gt;.env&lt;/code&gt; file checked into Git is an open invitation. A reference to a 1Password secret that is resolved at runtime is not.&lt;/p&gt;
&lt;h3 id=&quot;travel-mode&quot;&gt;Travel Mode&lt;/h3&gt;
&lt;p&gt;Crossing an international border where devices may be inspected? 1Password’s Travel Mode lets you temporarily remove sensitive vaults from a device. Only vaults marked “safe for travel” remain visible. Once you arrive, you toggle Travel Mode off and everything reappears. Try replicating that with a notebook.&lt;/p&gt;
&lt;h3 id=&quot;passkey-and-mfa-management&quot;&gt;Passkey and MFA management&lt;/h3&gt;
&lt;p&gt;Passwords are already giving way to passkeys in many services. 1Password stores and autofills passkeys alongside traditional credentials, so the transition does not mean juggling yet another tool. It also serves as an authenticator for TOTP two-factor codes, consolidating what would otherwise be a mess of separate apps and backup codes.&lt;/p&gt;
&lt;h2 id=&quot;were-too-small-for-this&quot;&gt;”We’re too small for this”&lt;/h2&gt;
&lt;p&gt;You are not. A freelancer with a handful of client logins is exactly the profile that suffers most from a single breach—because there is no security team to contain the damage and no PR department to manage the fallout.&lt;/p&gt;
&lt;p&gt;1Password’s pricing starts at a few dollars per user per month. The time you save not resetting forgotten passwords, not onboarding people through DMs, and not lying awake wondering if the intern still has the production database password will pay for itself before the first quarterly review.&lt;/p&gt;
&lt;h2 id=&quot;the-habit-shift&quot;&gt;The habit shift&lt;/h2&gt;
&lt;p&gt;Moving to a vault is less a technology decision than a habit change. The hardest part is the first week: importing existing passwords, agreeing on a vault structure, and convincing the person who “has a system” that their system is a liability.&lt;/p&gt;
&lt;p&gt;After that, the daily experience is &lt;em&gt;easier&lt;/em&gt; than what it replaces. You stop typing passwords. You stop resetting passwords. You stop wondering if that Google Doc full of credentials is shared with the right people.&lt;/p&gt;
&lt;p&gt;Post-it notes are great for reminding yourself to buy milk. They are not where your business secrets belong.&lt;/p&gt;</content:encoded><category>security</category><category>productivity</category><category>teamwork</category></item><item><title>Hunting Dictionary Bots: Inside a Sophisticated WordPress Membership Attack</title><link>https://devcharms.com/posts/hunting-dictionary-bots-inside-wordpress-membership-attack/</link><guid isPermaLink="true">https://devcharms.com/posts/hunting-dictionary-bots-inside-wordpress-membership-attack/</guid><description>Modern &quot;dictionary bots&quot; can bypass CAPTCHAs and WAFs to spam WordPress membership signups. Learn how the attack works—and how to block it in code.</description><pubDate>Wed, 27 May 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/og-dictionary-bots.png&quot; alt=&quot;A screenshot of a WordPress membership signup form with a CAPTCHA and a WAF blocking a dictionary bot&quot;&gt;&lt;/p&gt;
&lt;p&gt;Automated registration spam is shifting away from traditional brute-force tactics. Standard security layers often fail against modern bot scripts that pass visual challenges and bypass baseline web application firewalls. These advanced campaigns mimic legitimate human patterns to compromise open registration endpoints.&lt;/p&gt;
&lt;p&gt;This post examines the mechanics of &lt;strong&gt;Dictionary Attack Bots&lt;/strong&gt;, details the concrete risks they pose to membership platform integrity, and provides implementation code to secure your user tables.&lt;/p&gt;
&lt;h2 id=&quot;the-membership-threat-impact-by-the-numbers&quot;&gt;The Membership Threat: Impact by the Numbers&lt;/h2&gt;
&lt;p&gt;Allowing automated registrations to pool in your database creates immediate operational hazards:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;42% Database Inflation:&lt;/strong&gt; Automated accounts distort core engagement metrics. They skew active user KPIs, churn rates, retention tracking, and email marketing segmentation analytics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Origin Server Overhead:&lt;/strong&gt; Bulk account creation triggers repetitive &lt;code&gt;INSERT&lt;/code&gt; queries, invalidates user cache clusters, and drives high CPU utilization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8.3x Email Delivery Risk:&lt;/strong&gt; Automated sign-ups generate high bounce rates and trigger spam complaints during onboarding automation. This degrades your transactional email domain authority and lands critical system emails in user spam folders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Downstream Vulnerabilities:&lt;/strong&gt; Attackers weaponize these dormant accounts later for credential stuffing, automated API scraping, or content/comment injection.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;anatomy-of-the-exploit-dictionary-word-pairing&quot;&gt;Anatomy of the Exploit: Dictionary Word Pairing&lt;/h2&gt;
&lt;p&gt;Traditional registration scripts rely on randomized character strings (e.g., &lt;code&gt;xj947_ktr&lt;/code&gt;). Modern security filters identify and block these strings easily. To circumvent detection, this script uses coordinated dictionary pairings.&lt;/p&gt;
&lt;p&gt;The bot engine generates identifiers by combining two distinct English dictionary terms, appended with a single or double digit: &lt;code&gt;[Noun/Adj] + [Noun] + [Digit]&lt;/code&gt;. This structure targets validation suites that assume standard dictionary terms indicate human input.&lt;/p&gt;
&lt;p&gt;Data from an active database trace reveals clear structural patterns across separate registration batches:&lt;/p&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;User ID&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Registered Username&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Target Domain Name&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Structural Formula&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;4372&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;basketoption7&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;koiot.de&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;3537&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;basketperch1&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;koiot.de&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;4341&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;basketriver9&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;koiot.de&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;4481&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;basketwomen0&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;koiot.de&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;4528&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;actman4&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;koiot.de&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;adultmiddle4&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;calculator.city&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Adj] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;N/A&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;actormuseum8&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;bitingmites.org&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[Noun] + [Noun] + [Digit]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The script rotates target domains across separate waves, altering infrastructure footprints while maintaining the underlying username generation logic. The email handles often use standard human name formats (e.g., &lt;code&gt;johne@calculator.city&lt;/code&gt;) to blend in with valid registrations.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;defensive-engineering-programmatic-remediation&quot;&gt;Defensive Engineering: Programmatic Remediation&lt;/h2&gt;
&lt;p&gt;Because these registration payloads exist entirely within the body of incoming &lt;code&gt;POST&lt;/code&gt; requests, standard proxy layers cannot always parse or drop them based on payload content alone. Remediating this threat requires a defense-in-depth approach across your technical stack.&lt;/p&gt;
&lt;h3 id=&quot;1-application-layer-domain-restrictions&quot;&gt;1. Application Layer Domain Restrictions&lt;/h3&gt;
&lt;p&gt;The fastest method for stopping active registration waves involves validating the registration array directly inside WordPress. Hooking into the &lt;code&gt;registration_errors&lt;/code&gt; filter intercepts the registration process before the application executes a database transaction.&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7F848E;font-style:italic&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7F848E;font-style:italic&quot;&gt; * Restrict known malicious domains from creating WordPress user accounts.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7F848E;font-style:italic&quot;&gt; * Drops the registration request before hitting the database.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7F848E;font-style:italic&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt;add_filter&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;&apos;registration_errors&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt; &apos;devcharms_restrict_spam_domains&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt; devcharms_restrict_spam_domains&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;$errors&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;$sanitized_user_login&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;$user_email&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;    $banned_domains&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;24faw.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;igtikrenai.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;koiot.de&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;temz.net&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;emailmin.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;        &apos;calculator.city&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    ];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;    $email_parts&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt; explode&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;&apos;@&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt; $user_email&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;    $domain&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;      =&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt; end&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;$email_parts&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;in_array&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;strtolower&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;$domain&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;),&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt; $banned_domains&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;        $errors&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt;add&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#98C379&quot;&gt;            &apos;banned_domain_error&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt;            __&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;&apos;&amp;#x3C;strong&gt;ERROR&amp;#x3C;/strong&gt;: Registration parameters rejected.&apos;&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;        );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C678DD&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt; $errors&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-remediate-existing-users-purge-the-spam-already-inside&quot;&gt;2. Remediate Existing Users (Purge the Spam Already Inside)&lt;/h3&gt;
&lt;p&gt;Once these accounts exist, blocking new registrations won’t fix the damage already done to your tables and analytics. The simplest remediation path is to go to &lt;strong&gt;Admin → Users&lt;/strong&gt;, search by the offending domain (for example &lt;code&gt;@koiot.de&lt;/code&gt;), and bulk-delete the matching accounts.&lt;/p&gt;
&lt;p&gt;This approach (Admin UI bulk delete or WP-CLI) is almost always safer than “just deleting rows” in the database. User emails and IDs can be duplicated across core and plugin tables, and a manual DB purge can leave orphaned metadata, broken relationships, or stale activity records—especially on community sites running BuddyPress, bbPress, membership plugins, or LMS/ecommerce add-ons.&lt;/p&gt;
&lt;p&gt;Common places these users can persist include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Core WordPress&lt;/strong&gt;: &lt;code&gt;wp_users&lt;/code&gt;, &lt;code&gt;wp_usermeta&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BuddyPress&lt;/strong&gt;: &lt;code&gt;wp_bp_activity&lt;/code&gt;, &lt;code&gt;wp_bp_activity_meta&lt;/code&gt;, &lt;code&gt;wp_bp_friends&lt;/code&gt;, &lt;code&gt;wp_bp_groups*&lt;/code&gt;, &lt;code&gt;wp_bp_notifications&lt;/code&gt;, &lt;code&gt;wp_bp_messages*&lt;/code&gt;, &lt;code&gt;wp_bp_xprofile*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bbPress&lt;/strong&gt;: forum/topic/reply content in &lt;code&gt;wp_posts&lt;/code&gt; plus related post meta in &lt;code&gt;wp_postmeta&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Other plugins&lt;/strong&gt;: their own custom tables and user meta keys (memberships, LMS, ecommerce, forms, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have WP-CLI access, you can do the same cleanup much faster from the command line by deleting users whose email matches a domain pattern:&lt;/p&gt;
&lt;pre class=&quot;astro-code one-dark-pro&quot; style=&quot;background-color:#282c34;color:#abb2bf; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt; delete&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt; $(&lt;/span&gt;&lt;span style=&quot;color:#61AFEF&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt; --search=&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;&quot;*@koiot.de&quot;&lt;/span&gt;&lt;span style=&quot;color:#D19A66&quot;&gt; --field=ID&lt;/span&gt;&lt;span style=&quot;color:#ABB2BF&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content:encoded><category>security</category><category>wordpress</category><category>automation</category></item><item><title>Reasons to set up a Cloudflare Tunnel</title><link>https://devcharms.com/posts/reasons-to-set-up-a-cloudflare-tunnel/</link><guid isPermaLink="true">https://devcharms.com/posts/reasons-to-set-up-a-cloudflare-tunnel/</guid><description>Why outbound-only Cloudflare Tunnels beat port forwarding for self-hosted apps, homelabs, and services you want online without opening your network to the internet.</description><pubDate>Sat, 20 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/reasons-to-set-up-a-cloudflare-tunnel.webp&quot; alt=&quot;A diagram illustrating how Cloudflare Tunnel works, showing a local service connected outbound to Cloudflare&amp;#x27;s edge network, with traffic flowing securely between them&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let’s say you are running a service on your home network—maybe a smart home dashboard (like Home Assistant), a private website you’re coding, or a home file server. You want to be able to access it when you are away from home, or you want a friend to look at it.&lt;/p&gt;
&lt;p&gt;To let the outside internet in, the old-school method is called &lt;strong&gt;port forwarding&lt;/strong&gt;. This is the digital equivalent of unlocking a specific window in your house, publishing your home address to the world, and hoping no burglars notice the open window.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; changes the game. Instead of unlocking any windows, your home computer reaches out to Cloudflare’s secure servers in the cloud and establishes a private, encrypted hallway between them. When someone wants to visit your site, they talk to Cloudflare, and Cloudflare passes the message back to your computer through this private hallway. No windows are unlocked, and your home address remains hidden.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;do-i-need-this&quot;&gt;&lt;strong&gt;Do I need this?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If you are running a self-hosted app, home server, or development site and need it safely accessible from the internet, a Cloudflare Tunnel is the gold standard. If you’d rather not deal with the configuration, DNS routing, or firewall settings yourself, &lt;strong&gt;Shrinkray Interactive&lt;/strong&gt; can handle the entire setup for you.&lt;/p&gt;
&lt;p&gt;To learn how tunnels work and why they are so secure, see below.&lt;/p&gt;
&lt;p&gt;Ready to secure your network? &lt;a href=&quot;#custom-tunnel&quot;&gt;Reach out to Greg to get your own custom tunnel configured today&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#you-stop-opening-doors-on-your-router&quot;&gt;You stop opening doors on your router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#if-they-cant-find-your-door-they-cant-knock-on-it&quot;&gt;If they can’t find your door, they can’t knock on it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#changing-home-addresses-stops-being-a-headache&quot;&gt;Changing home addresses stops being a headache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#free-security-padlocks-without-the-headaches&quot;&gt;Free security padlocks without the headaches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#your-small-project-gets-enterprise-grade-shields&quot;&gt;Your small project gets enterprise-grade shields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#add-a-bouncer-to-your-site-in-two-clicks&quot;&gt;Add a bouncer to your site in two clicks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#easy-integrations-for-developers-webhooks&quot;&gt;Easy integrations for developers (Webhooks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#it-works-even-when-your-internet-provider-blocks-you&quot;&gt;It works even when your internet provider blocks you&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-free-plan-is-actually-free-and-powerful&quot;&gt;The free plan is actually free and powerful&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#when-is-a-tunnel-not-the-right-tool&quot;&gt;When is a tunnel &lt;em&gt;not&lt;/em&gt; the right tool?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#time-to-lock-the-doors&quot;&gt;Time to lock the doors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#let-me-build-your-own-custom-tunnel&quot;&gt;Let me build your own Custom Tunnel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;you-stop-opening-doors-on-your-router&quot;&gt;You stop opening doors on your router&lt;/h2&gt;
&lt;p&gt;Ports are like doors on your internet router. By default, your router blocks all incoming traffic to keep you safe. Port forwarding means you carved a small pet door into your firewall and pointed it directly at your device (like a home storage drive or computer).&lt;/p&gt;
&lt;p&gt;The problem is that automated bots crawl the internet 24/7, shaking every door handle they can find. If you leave a port open, those bots will find it and try to exploit outdated software or guess your passwords.&lt;/p&gt;
&lt;p&gt;With a tunnel, you don’t open any doors. The tunnel software (a tiny program running on your computer) makes an &lt;strong&gt;outbound&lt;/strong&gt; connection. It’s like your computer calling out to Cloudflare and keeping the phone line open. Traffic comes in through that phone line, meaning your router’s doors remain locked tight.&lt;/p&gt;
&lt;p&gt;For homelabs, side projects, and small business gear sitting on a consumer connection, that is a meaningful security upgrade without buying enterprise hardware.&lt;/p&gt;
&lt;h2 id=&quot;if-they-cant-find-your-door-they-cant-knock-on-it&quot;&gt;If they can’t find your door, they can’t knock on it&lt;/h2&gt;
&lt;p&gt;Attackers can’t hack what they can’t see. When you use port forwarding, your public IP address is exposed, meaning anyone can scan your connection. If there is a bug (a security vulnerability) in your home software, they can exploit it.&lt;/p&gt;
&lt;p&gt;With a tunnel, your home setup is invisible to the public internet. Visitors only see Cloudflare’s massive network. Think of Cloudflare as a highly trained security guard standing at the entrance of a gated community. If a bot or hacker tries to spam your site or send malicious traffic, the guard blocks them at the gate before they ever get near your actual home network.&lt;/p&gt;
&lt;h2 id=&quot;changing-home-addresses-stops-being-a-headache&quot;&gt;Changing home addresses stops being a headache&lt;/h2&gt;
&lt;p&gt;Every home internet connection has an IP address—think of it as your house’s mailing address. However, home internet providers constantly change this address every few days or weeks (this is called a “dynamic IP”). If you are hosting a website, this means your site’s address is constantly changing, making it impossible for people to reliably connect to it.&lt;/p&gt;
&lt;p&gt;Normally, you have to buy a costly “static IP” or set up complicated updater tools (Dynamic DNS) that break when you least expect it.&lt;/p&gt;
&lt;p&gt;Because a Cloudflare Tunnel keeps a constant connection open from your computer &lt;em&gt;out&lt;/em&gt; to Cloudflare, it doesn’t matter if your home’s mailing address changes. Cloudflare always knows where your computer is because the computer is the one keeping the connection alive. Your website name (like &lt;code&gt;myproject.com&lt;/code&gt;) stays online without you needing to lift a finger.&lt;/p&gt;
&lt;h2 id=&quot;free-security-padlocks-without-the-headaches&quot;&gt;Free security padlocks without the headaches&lt;/h2&gt;
&lt;p&gt;When you visit a website, you expect to see the secure padlock icon in your browser address bar. This is powered by HTTPS and an SSL/TLS certificate—a digital security badge proving your site is secure.&lt;/p&gt;
&lt;p&gt;Setting up these certificates on your own home computer is notoriously frustrating. You have to install renewal tools, make sure they run every few months, and configure complex server software. If something breaks, visitors get a scary “This site is not secure” warning.&lt;/p&gt;
&lt;p&gt;When you use a tunnel, Cloudflare automatically provides and manages the SSL certificate for you. Visitors get a secure, encrypted connection to Cloudflare, and Cloudflare encrypts the rest of the journey to your home computer. You get the padlock icon instantly without configuring any certificate tools at home.&lt;/p&gt;
&lt;h2 id=&quot;your-small-project-gets-enterprise-grade-shields&quot;&gt;Your small project gets enterprise-grade shields&lt;/h2&gt;
&lt;p&gt;Your home computer or a cheap virtual server was never designed to handle thousands of visits at once. If your site gets popular, or if a malicious group tries to overwhelm it with fake traffic (a &lt;strong&gt;DDoS attack&lt;/strong&gt;), your home internet will crash.&lt;/p&gt;
&lt;p&gt;By routing your traffic through Cloudflare’s tunnel, you get to borrow their massive global network. If a flood of traffic hits your site, Cloudflare absorbs it and only sends the legitimate visits down the tunnel to your home computer. It’s like having a giant buffer that prevents your home internet from being knocked offline.&lt;/p&gt;
&lt;h2 id=&quot;add-a-bouncer-to-your-site-in-two-clicks&quot;&gt;Add a bouncer to your site in two clicks&lt;/h2&gt;
&lt;p&gt;Just because your project is online doesn’t mean you want the public to see it. Maybe it’s a private family photo album or a dashboard for your home smart devices.&lt;/p&gt;
&lt;p&gt;Normally, keeping these secure means setting up a complex VPN (Virtual Private Network) or managing logins inside your app.&lt;/p&gt;
&lt;p&gt;With Cloudflare, you can turn on a feature called &lt;strong&gt;Cloudflare Access&lt;/strong&gt;. This acts like a bouncer at the door. Before anyone is even allowed to see your website, they have to log in using an approved method—like their Google account, GitHub, or a one-time email code. If they aren’t on the list, Cloudflare turns them away before they ever reach your computer.&lt;/p&gt;
&lt;h2 id=&quot;easy-integrations-for-developers-webhooks&quot;&gt;Easy integrations for developers (Webhooks)&lt;/h2&gt;
&lt;p&gt;If you are building apps, you often need external services to send messages to your local computer. For example, when someone pays you on &lt;strong&gt;Stripe&lt;/strong&gt; or pushes code to &lt;strong&gt;GitHub&lt;/strong&gt;, those services need to send a notification (a “webhook”) to your project.&lt;/p&gt;
&lt;p&gt;Getting these notifications to reach a computer in your living room is incredibly hard because home networks block outside requests by default.&lt;/p&gt;
&lt;p&gt;A tunnel gives you a simple, public link that connects directly to the app running on your computer. You can test payment systems, chat bots, and other tools on your own computer without having to pay for online servers or mess with insecure router settings.&lt;/p&gt;
&lt;h2 id=&quot;it-works-even-when-your-internet-provider-blocks-you&quot;&gt;It works even when your internet provider blocks you&lt;/h2&gt;
&lt;p&gt;Many modern internet providers use a technology called &lt;strong&gt;CGNAT&lt;/strong&gt; (Carrier-Grade NAT). Essentially, they share a single public IP address among hundreds of different homes in your neighborhood. Because of this, traditional port forwarding is physically impossible—you don’t have your own “front door” to unlock.&lt;/p&gt;
&lt;p&gt;Similarly, if you are at an office, a university, or a coffee shop, their firewalls will block any incoming connections.&lt;/p&gt;
&lt;p&gt;But because a Cloudflare Tunnel is an &lt;strong&gt;outbound&lt;/strong&gt; connection (your computer initiating the call to Cloudflare), CGNAT and strict network firewalls don’t block it. If your computer can browse the web, it can host a tunnel.&lt;/p&gt;
&lt;h2 id=&quot;the-free-plan-is-actually-free-and-powerful&quot;&gt;The free plan is actually free and powerful&lt;/h2&gt;
&lt;p&gt;Usually, security tools like this are expensive or have severely limited free plans. Fortunately, Cloudflare’s tunnel feature is completely free for personal use, side projects, and small teams. You can run multiple tunnels, connect different projects, and secure them with login screens without paying a cent.&lt;/p&gt;
&lt;h2 id=&quot;when-is-a-tunnel-not-the-right-tool&quot;&gt;When is a tunnel &lt;em&gt;not&lt;/em&gt; the right tool?&lt;/h2&gt;
&lt;p&gt;Tunnels are amazing for making a specific app or website reachable. But they aren’t a cure-all:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If you need access to your entire home network:&lt;/strong&gt; If you want to connect to your home printer, your smart TV, and all your computers at once while away, a VPN (like WireGuard or Tailscale) is a better choice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you have strict data privacy needs:&lt;/strong&gt; Since all traffic goes through Cloudflare’s servers to be inspected for security, companies with strict government compliance rules may not want their data passing through a third party.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;time-to-lock-the-doors&quot;&gt;Time to lock the doors&lt;/h2&gt;
&lt;p&gt;Setting up a tunnel usually takes under ten minutes. You install a small helper program, log in to your Cloudflare account, name your website, and point it at the app running on your computer.&lt;/p&gt;
&lt;p&gt;Once you do, you can log into your home router and lock every single inbound port. You stop thinking about your home’s public IP address as your front door. Cloudflare becomes your front door, your bouncer, and your security system—giving you peace of mind that your home network is completely secure.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;custom-tunnel&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;let-me-build-your-own-custom-tunnel&quot;&gt;Let me build your own Custom Tunnel&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 sm:grid-cols-[1fr_2fr] gap-6 items-center mt-8&quot;&gt;
  &lt;div class=&quot;flex justify-center sm:justify-start&quot;&gt;
    &lt;img src=&quot;/assets/customtunnel.webp&quot; class=&quot;w-48 sm:w-full h-auto rounded&quot; alt=&quot;Secure your website with Cloudflare Tunnels&quot;&gt;
  &lt;/div&gt;
  &lt;p class=&quot;w-full&quot;&gt;
    If you want to secure your self-hosted websites, home servers, or private projects but don&apos;t want to wrestle with command line tools, DNS setups, or firewall settings, &lt;a href=&quot;https://shrinkraylabs.com/contact-me/&quot; class=&quot;underline decoration-dashed underline-offset-8 hover:text-skin-accent&quot;&gt;&lt;strong&gt;Shrinkray Interactive&lt;/strong&gt;&lt;/a&gt; is here to help. We can design and implement secure outbound tunnel architectures, configure authentication bouncers, and audit your remote access setups to make sure your home or business network stays completely protected.
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Ready to lock down your network? Reach out to Greg to get your own custom tunnel configured today!&lt;/p&gt;</content:encoded><category>cloudflare</category><category>security</category><category>devops</category></item><item><title>Shipping small wins</title><link>https://devcharms.com/posts/shipping-small-wins/</link><guid isPermaLink="true">https://devcharms.com/posts/shipping-small-wins/</guid><description>How tiny, reviewable changes beat big bang releases for morale, quality, and sleep.</description><pubDate>Tue, 18 Mar 2025 16:45:00 GMT</pubDate><content:encoded>&lt;p&gt;Big launches look heroic in retrospectives. In real time they are often late, brittle, and stressful. I have come to prefer &lt;strong&gt;small wins&lt;/strong&gt;: changes that can be explained in one sentence, reviewed without a map, and rolled back without a ceremony.&lt;/p&gt;
&lt;p&gt;That does not mean avoiding ambition. It means cutting work so that each slice is real, observable progress—a bug fixed, a metric improved, a rough edge smoothed.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-small-actually-means&quot;&gt;What “small” actually means&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-small-actually-means&quot;&gt;What “small” actually means&lt;/h2&gt;
&lt;p&gt;Small is not trivial. It is &lt;strong&gt;bounded&lt;/strong&gt;. A small win has a clear finish line and leaves the codebase easier to change tomorrow, not harder.&lt;/p&gt;
&lt;p&gt;This blog refresh is an example: remove dead content, publish a few posts that reflect how we actually work, ship it, iterate. No manifesto required—just momentum you can feel.&lt;/p&gt;</content:encoded><category>productivity</category><category>teamwork</category></item><item><title>The bug that taught me to read the docs</title><link>https://devcharms.com/posts/the-bug-that-taught-me-to-read-the-docs/</link><guid isPermaLink="true">https://devcharms.com/posts/the-bug-that-taught-me-to-read-the-docs/</guid><description>A short story about chasing a &quot;mystery&quot; production issue that turned out to be documented behavior—and what I changed in how I work afterward.</description><pubDate>Wed, 05 Mar 2025 14:30:00 GMT</pubDate><content:encoded>&lt;p&gt;I once burned most of a day on a bug that felt like a framework betrayal. Symptoms only showed up in production. Local looked perfect. Logs were noisy but unhelpful. I rewrote a hook, then a data layer, then blamed the CDN.&lt;/p&gt;
&lt;p&gt;The fix was a single line in the official docs, in a section I had skimmed because the heading looked boring.&lt;/p&gt;
&lt;p&gt;The behavior was &lt;strong&gt;exactly&lt;/strong&gt; what the docs described: a default I had assumed was universal was only true in development. Production did the stricter, spec-correct thing. I had been “debugging” my own confirmation bias.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#what-i-do-differently-now&quot;&gt;What I do differently now&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-i-do-differently-now&quot;&gt;What I do differently now&lt;/h2&gt;
&lt;p&gt;Before I refactor or rip out a dependency, I open the docs for the failing path and search for the error string or the API name. If the framework authors already named the edge case, I want to see that paragraph first—not after I have painted myself into a corner.&lt;/p&gt;
&lt;p&gt;The bug was embarrassing. The habit stuck.&lt;/p&gt;</content:encoded><category>debugging</category><category>habits</category></item><item><title>Why static sites still win for sites like this</title><link>https://devcharms.com/posts/why-static-sites-still-win-for-sites-like-this/</link><guid isPermaLink="true">https://devcharms.com/posts/why-static-sites-still-win-for-sites-like-this/</guid><description>A practical take on why we keep this blog static-first with Astro, and when you might reach for something heavier.</description><pubDate>Wed, 12 Mar 2025 09:15:00 GMT</pubDate><content:encoded>&lt;p&gt;Not every project needs a server at request time. For a blog and portfolio whose content changes when &lt;em&gt;you&lt;/em&gt; publish—not when a user clicks—&lt;strong&gt;static generation&lt;/strong&gt; is still one of the best bargains in web development.&lt;/p&gt;
&lt;p&gt;Astro fits that model without pretending you never need interactivity. You can hydrate the pieces that matter and leave the rest as HTML that loads fast everywhere, including on cheap hosting and flaky networks.&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;&lt;details&gt;&lt;summary&gt;Open Table of contents&lt;/summary&gt;&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#when-static-is-enough&quot;&gt;When static is enough&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;&lt;/details&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2 id=&quot;when-static-is-enough&quot;&gt;When static is enough&lt;/h2&gt;
&lt;p&gt;If your content is mostly Markdown, your personalization needs are light, and you care about Core Web Vitals without a dedicated performance team, a static-first site is a feature—not a limitation.&lt;/p&gt;
&lt;p&gt;We still reach for APIs, edge functions, and databases when the product demands it. For DevCharms, the default is &lt;strong&gt;simple, fast, and boring&lt;/strong&gt; in the best sense of the word.&lt;/p&gt;</content:encoded><category>astro</category><category>performance</category><category>architecture</category></item></channel></rss>