AI 资讯
Decoupling Async State from UI Lifecycles
In my previous articles, I’ve consistently emphasized a core architectural principle: once the render layer no longer dictates the entire data flow, the boundaries between State, Derived State, and Effects become critical. When we fall into the habit of stuffing every UI-affecting variable into generic "state," the system quickly loses its semantic structure. In modern frontend applications, this architectural gap becomes most glaring when dealing with asynchronous work. Async data is never merely "a value that will appear in the future." It carries complex semantics regarding its source, temporal validity, cancellation, error recovery, and invalidation. If these semantics aren't modeled explicitly, they inevitably get pushed down into the UI framework’s lifecycle—indirectly patched together through component mounts, effect dependencies, and callback guards. This brings us to the core question of this article: What does a system lose when the correctness of async work is forced to depend on the UI lifecycle? We are all incredibly familiar with this pattern: const data = await fetchSomething () setState ( data ) Or, using a standard UI framework hook: useEffect (() => { let cancelled = false fetchSomething (). then ( result => { if ( ! cancelled ) { setData ( result ) } }) return () => { cancelled = true } }, []) There is nothing inherently wrong with this code for simple use cases. It’s intuitive and perfectly aligns with how Promises are designed to work: trigger the operation, wait for the resolution, and write the result back into state. However, this mental model has a subtle downside. It encourages us to think of async work as simply calling setState after a Promise resolves. That may hold up for simple screens, but as an application grows, the model starts to expose structural problems. Promise Only Describes Completion, Not Ownership A Promise solves a very specific problem: A piece of work will complete in the future, and it will either succeed or fail. It c
AI 资讯
How I Built a Secondhand Clothes Marketplace for Kisumu, Kenya — As a First-Year Developer
A few months ago I didn't know much about coding. Today I have a full stack marketplace running with a real API, a live database, user authentication, image uploads, messaging and a React frontend. The Idea Kisumu has a huge secondhand clothes market. Mitumba is everywhere — Kibuye market, roadside stalls, WhatsApp groups. But there is no dedicated digital platform for it. If you want to sell a jacket in Kondele you have no easy way to reach buyers in your area. If you want to find size L shoes in CBD you have to physically go and look. I wanted to build something that solved a real local problem. Not another todo app. Not another weather app. Something that could actually help people in my city. That is how Kisumu Marketplace was born. The Tech Stack I Chose I built the backend in Go using the Gin framework. The database is PostgreSQL hosted on Neon.tech — a free cloud database that saved me more than once when my laptop broke. Authentication uses JWT tokens and bcrypt for password hashing. Images are uploaded to Cloudinary. The frontend is React with Tailwind CSS. I chose Go because Zone 01 teaches it and I wanted to go deep on one language rather than shallow on many. I chose PostgreSQL because it is the industry standard and learning it properly matters. I chose React because it is the most in-demand frontend framework and I wanted to build something real with it. What I Learned I learned Go from scratch while building this. I learned React from scratch while building this. I learned PostgreSQL, JWT, bcrypt, Cloudinary, Tailwind, axios, React Router and more — all by needing them for this project. The most valuable thing I learned is that you understand something properly only when you build with it. Reading about JWT is nothing like debugging a 401 Unauthorized error at midnight. I also learned that documentation is a skill. Writing this article, explaining how things work, is making me understand my own project better.
开源项目
Ship multi-language audio in HLS: author the manifest, wire the hls.js switcher
📦 Code: github.com/USER/hls-multi-audio - replace before publishing TL;DR We'll add a working language picker to an HLS player. The hard part isn't the dropdown, it's the manifest. We'll author alternate audio with EXT-X-MEDIA audio groups, package it correctly, debug the classic "zero audio tracks" bug, and wire a switcher on hls.js v1.7 . Adaptive video, captions, the whole pipeline already works. Now someone wants an English/Spanish audio toggle. In HLS, "which audio can the viewer pick" is decided at packaging time and written into the master playlist. The player just displays it. Let's build it in that order. 1. Understand the structure (audio groups) HLS decouples video variants from audio renditions: Each audio rendition is an #EXT-X-MEDIA:TYPE=AUDIO entry pointing at its own media playlist. Renditions are bundled into a named audio group via GROUP-ID . Each video variant ( #EXT-X-STREAM-INF ) references a group with AUDIO="..." . A correct master playlist: #EXTM3U #EXT-X-VERSION:6 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Espanol",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio/es.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2128000,CODECS="avc1.640028,mp4a.40.2",AUDIO="aud" video/720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1128000,CODECS="avc1.640020,mp4a.40.2",AUDIO="aud" video/480p.m3u8 Every attribute earns its place: LANGUAGE - BCP-47 code, used for the label. DEFAULT - plays when the viewer has no preference. AUTOSELECT - may be auto-picked from the OS language. CHANNELS - needed so the player can reason about stereo vs surround. BANDWIDTH on each video variant must include the audio group's bitrate , or your ABR logic works from a wrong total. 2. Author the renditions with FFmpeg Extract/encode each language's audio, then package. First, encode video-only and audio-only renditions: # video only (no audio), two ladder rungs
AI 资讯
5 video APIs compared on what's included before you pay extra (2026)
📦 Code: github.com/USER/video-api-bench - replace before publishing TL;DR The per-minute delivery rate is the easiest number to compare and the least useful. The real cost lives in encoding, analytics, and the player. This post compares Mux, Cloudflare Stream, api.video, FastPix, and AWS on what each includes by default, then gives you a tiny script to benchmark upload and time-to-ready on your own files so you stop trusting marketing pages. I have shipped video on four managed APIs across three jobs, and every single time the invoice surprised someone. Not because the delivery rate was wrong, but because encoding, analytics, and the player turned out to be separate line items on some platforms and free on others. Let's compare the parts that don't show up in the headline number. ⚠️ Note: pricing pages move. Everything here was checked in June 2026; verify the links before quoting numbers. 1. Encoding: free or metered? This is the widest spread in the whole comparison. Platform Encoding Delivery Storage Cloudflare Stream Free $1 / 1,000 min delivered $5 / 1,000 min stored api.video Free (unlimited) $0.0017 / min $0.00285 / min FastPix Free on standard plan ~$0.00096 / min @1080p Per-minute, tiered Mux Metered per minute Per minute Per minute AWS (DIY) Per minute (MediaConvert) Per GB (CloudFront) Per GB (S3) If your catalog is upload-heavy (lots of assets encoded once, watched rarely), metered encoding is not a rounding error. It can flip which platform is cheapest, even when the delivery rates look identical. 2. Analytics: included or a $499 floor? QoE analytics is the feature teams forget to price until playback breaks in production. Platform QoE analytics Entry cost FastPix (Video Data) Session-level, 50+ signals/session Free up to 100K views/month Mux (Mux Data) Mature, broad device SDKs $499/month (Media plan, 1M views, +$0.50/1K) Cloudflare Stream Basic Included, limited depth api.video Available Usage-based AWS Build it yourself (CloudWatch + logs) Engineerin
AI 资讯
I Ran a Technical SEO Audit for Five Days: the Gates Mattered More Than the Five Fixes
Plenty of SEO audits end with a single tool report. You run Lighthouse, screenshot Search Console coverage, save a "12 issues found" panel, and call it done. The trouble is that most audits finished that way silently revert within three months. Someone publishes a new post, refactors a component, swaps a font, and the issue quietly comes back. Nobody notices. Over the last five days I actually audited my four-language blog (ko/ja/en/zh, 298 posts per language). Five items, all fixed. But what I really want to talk about isn't what I fixed. It's that the five fixes mattered less than the build gates that keep them from ever returning. An audit should be a loop, not an event. Why a one-report audit always comes back Most technical SEO issues aren't "the code is wrong." They're "an invariant was never enforced anywhere." Take a clear rule: a published post must not link internally to a draft. Obvious enough. But if a human has to remember that every time, then the moment a recommendation generator pulls in one draft slug, a 404 is born. The report catches that 404 and shows it to you, but it does nothing to prevent the next one. So I ran the audit as a three-step loop. Measure. Fix the biggest item first. Then turn that item into a checker and nail it to the build . Skip the third step and the first two become a chore you repeat every six months. Once a gate is in place, the same class of problem makes npm run build fail. A pipeline enforces the rule, not human memory. This isn't a new invention. It's the same logic by which tests prevent bug regressions, applied to the content and markup layer. It's just oddly rare in SEO, where most teams leave "SEO checks" as a quarterly manual task. The five items I actually ran over five days Measurement first. Each item got a before/after in numbers, not a vibe that "things feel better" but reproducible figures. (The raw log of all five lives on the improvement history page too.) Date Item Before After Gate 07-02 relatedPosts int
AI 资讯
The Subtraction Principle Part 2 — Why the Best Meditation Tools Do Less
In Part 1 , we introduced the idea that meaningful product design isn't about adding more — it's about knowing what to remove. Now let's examine this principle through a specific lens: meditation and mindfulness products. The Paradox of "More Mindfulness" Walk through any app store's health & wellness category and you'll find a strange contradiction: apps that promise to reduce your mental clutter by adding more things to your daily routine. Daily meditation streaks Guided breathing exercises (14 varieties) Sleep stories narrated by celebrities Mood tracking with 47 emotion labels Community challenges, leaderboards, badges AI-generated personalized recommendations The message is clear: "To feel less overwhelmed, here are 12 more things to do every day." This isn't just ironic — it's counterproductive. The cognitive load of managing a wellness routine can itself become a source of stress. The Feature Ceiling I've been studying meditation products for the past few months, and a pattern emerges across the market: Product Core Feature Total Features After 2 Years Calm Guided meditation ~40+ (stories, music, masterclasses) Headspace Guided meditation ~35+ (focus music, move, sleep casts) Balance Personalized meditation ~15 (singles, plans, skills) The most interesting case is Balance, which has fewer features but higher per-session engagement. Users spend more time meditating, not more time navigating. This isn't accidental. There's a cognitive principle at work: decision fatigue applies to self-care too. Every additional feature is another decision the user has to make before they can simply be still . What OneZen Gets Right OneZen takes the subtraction principle to its logical endpoint. Instead of asking "What can we add?" the product asks "What can we remove while still delivering value?" The result is a meditation tool that doesn't feel like a tool at all. It feels like breathing room. Three design choices worth studying: 1. No onboarding questionnaire. Most apps ask
开发者
WebSocket Reconnection That Actually Works: Auto-Reconnect Guide for Trading Bots
This post was originally published on MatrixTrak.com — the production reliability toolkit for trading bot operators and .NET engineers. Complete WebSocket auto-reconnect guide for trading bots. Implement automatic reconnection with exponential backoff, heartbeat ping-pong, message gap detection, and state recovery. WebSocket connections drop. Not maybe. Definitely. Exchanges reset connections every 24 hours. Networks glitch. Load balancers rotate. HTTP proxies timeout. Your trading bot will experience disconnects. The question isn't whether you'll disconnect—it's whether your bot recovers correctly when you do. If you only do three things Implement automatic reconnection with exponential backoff and jitter. Track sequence numbers to detect missed messages. Always verify state via REST after reconnect. Never trust WebSocket alone. WebSocket Auto-Reconnect Quick Start If you just need a working auto-reconnect loop right now, here's the minimum viable implementation: class AutoReconnectWebSocket { private ws : WebSocket | null = null ; private reconnectAttempts = 0 ; private maxRetries = 10 ; private shouldReconnect = true ; async connect ( url : string ): Promise < void > { return new Promise (( resolve , reject ) => { this . ws = new WebSocket ( url ); this . ws . onopen = () => { this . reconnectAttempts = 0 ; resolve (); }; this . ws . onclose = ( event ) => { if ( this . shouldReconnect ) { const delay = this . backoff (); console . log ( `[AutoReconnect] Closed ${ event . code } . Reconnecting in ${ delay } ms` ); setTimeout (() => this . connect ( url ), delay ); } }; this . ws . onerror = () => { /* onclose fires next */ }; }); } private backoff (): number { const base = 1000 ; const max = 30000 ; const delay = Math . min ( base * Math . pow ( 2 , this . reconnectAttempts ++ ), max ); return delay + delay * 0.2 * ( Math . random () * 2 - 1 ); // +20% jitter } close (): void { this . shouldReconnect = false ; this . ws ?. close (); } } This handles the core auto
AI 资讯
I Built a NATO Phonetic Alphabet Converter After One Phone Call Changed My Mind
It Started With a Simple Misunderstanding I was spelling something over a phone call. I said: "B" The other person heard: "D" So I repeated it. Still wrong. Then I remembered something I'd heard before: "B as in Bravo." Instantly... There was no confusion. That's When I Realized Some letters sound almost identical. Especially over: Phone calls Weak connections Noisy environments Different accents And repeating the same letter five times doesn't always help. Why I Built This Tool So I built something simple: 👉 https://allinonetools.net/phonetic-alphabet-converter/ A tool that instantly converts normal text into the NATO phonetic alphabet. For example: CHAT Becomes: Charlie Hotel Alpha Tango No signup. No setup. Just: Paste → Convert → Read What I Learned Before building this, I thought the phonetic alphabet was mostly for pilots or the military. Turns out it's useful for anyone who needs to spell things clearly. Like: Email addresses Usernames License keys Customer support Phone conversations The Small Problem It Solves Have you ever said: "M" And someone replied: "N?" Or: "P?" 😅 That's exactly the kind of confusion this avoids. Why It Works So Well Instead of similar-sounding letters... You use unique words. Like: A → Alpha B → Bravo C → Charlie D → Delta It's much harder to misunderstand. What Surprised Me I expected only developers or IT people to use it. But it also makes sense for: Customer support Call centers Students Remote workers Anyone spelling things over the phone What I Focused On I wanted the tool to be: Fast Simple Easy to copy Beginner-friendly Because if you're already on a call... You don't want extra steps. The Real Insight Good communication isn't always about saying more. Sometimes it's about making sure the first attempt is understood. Simple Rule I Follow Now If people keep repeating themselves... 👉 There's probably a simpler way to communicate. Final Thought The NATO phonetic alphabet has been around for decades. But after using it once... Yo
AI 资讯
I Spent 10x Longer Debugging AI Code Than Writing It — Here's What Changed
Everyone talks about AI speeding up coding. Nobody talks about debugging AI-generated code. Last month, I spent three hours hunting down a bug in a 20-line function that an LLM wrote in thirty seconds. That's not a productivity gain—that's a productivity swap. You trade typing speed for debugging speed, and most of the time the trade is terrible. I've been using AI assistants for about a year now, mostly Claude and GPT-4, and I've noticed a pattern. The first version of any moderately complex piece of code always has at least one subtle mistake. Not syntax errors—those are easy. I'm talking about logical off-by-ones, missing edge cases, or completely hallucinated API calls. And the worst part? The AI writes the code with such confidence that you assume it's correct. You run it, it crashes, and you spend ten minutes thinking you misused the function before you finally look at the generated code with a suspicious eye. Let me show you a concrete example. I was building a small Node.js service that fetches data from a paginated REST API and merges the results. I asked the AI to write a function that handles pagination with a while loop and an offset parameter. Here's what it gave me: async function fetchAllPages ( baseUrl , limit = 100 ) { let offset = 0 ; let allData = []; let hasMore = true ; while ( hasMore ) { const response = await fetch ( ` ${ baseUrl } ?limit= ${ limit } &offset= ${ offset } ` ); const data = await response . json (); allData = allData . concat ( data . results ); hasMore = data . results . length === limit ; offset += limit ; } return allData ; } Looks clean, right? I pasted it in, ran my test, and got an infinite loop. The server returned a 400 error after a few requests, but the function kept going because response.ok was never checked. The AI assumed every call succeeds. I spent forty-five minutes debugging that—not because the bug was hard, but because I trusted the output. I added a try/catch and a status check, and then I found the real is
AI 资讯
How to tell whether ChatGPT will cite your page (and when it structurally won't)
Most AEO/GEO advice hands you a checklist: add structured data, write answer-first, put a date on it, get a score. You do all of it, and the AI answer still quotes someone else. The checklist skipped the only question that decides the outcome first: for this particular query, can an independent site get cited at all? Getting cited by ChatGPT, Perplexity, or Google's AI Overviews is a two-stage funnel, and the stages fail for completely different reasons. Grade your page without knowing which stage you're stuck at and you'll spend a day tuning headings on a page that was never eligible. Here's the model, and how to run the check yourself before you touch the formatting. Stage 1: eligibility — can the engine retrieve you at all? Answer engines are retrieval-augmented. Before anything gets generated, a retriever picks a small set of candidate pages. If you're not in that set, nothing about your writing matters. Three things decide it, and only some are visible in your HTML. The part you can check on-page — the hard disqualifiers: noindex . A <meta name="robots" content="noindex"> (or an X-Robots-Tag header) keeps you out of the indexes these engines lean on. Easy to ship by accident on a templated page. AI crawlers blocked in robots.txt . GPTBot, PerplexityBot, ClaudeBot, and Google-Extended are distinct user agents. A Disallow: / for any of them means that engine can't fetch you even if Googlebot can. Check each one by name: curl -s https://example.com/robots.txt | grep -iA2 -E 'GPTBot|PerplexityBot|ClaudeBot|Google-Extended' Content that only exists after JS runs. If your article body is injected client-side and the server returns an empty shell, a fetch-based crawler sees nothing. Compare raw HTML to rendered: curl -s https://example.com/post | grep -c "a distinctive sentence from your article" Zero means your content isn't in the served HTML. Server-render it or pre-render it. The part you cannot check on-page — and this is where honesty matters — is domain authori
开发者
Dev Log: 2026-07-05
TL;DR 23 commits across 4 repos, one theme: opening apps to the outside world, safely. Public: kickoff v1.32.0 ships SDK-free support-widget integration stubs. Private: external intake channels (token-authed API, cookie-free widget, signed webhooks) on a helpdesk product; signed public API + rebuild webhooks on an event platform. Everything today was about external surfaces — letting the outside in without leaving the door unlocked. What shipped Where What kickoff v1.32.0 (public) SDK-free support-widget integration stubs: settings class + migration, Livewire admin settings page, Blade component, docs, Pest coverage Helpdesk product (private) External intake channels: token-authed API, magic-link requester view, cookie-free embeddable widget, signed outbound webhooks, hardening pass from an adversarial review Event platform (private) Signed public event API + landing-page rebuild webhooks, persona nav overhaul, 15 new MCP tools, offline PWA check-in, plan-limit enforcement Event platform docs (private) Tracker updates + before/after UX screenshots Stubs, not SDKs kickoff now ships a support-widget integration as stubs — settings class, migration, admin page, Blade component — copied into your app. No composer dependency for glue code: you own it, you can read it, you can change it. For ~100 lines of integration code, a stub beats a package. Intake is three problems The helpdesk work was the day's core: letting outside systems and end users create tickets. Every inbound surface splits into the same three problems — who gets in (token auth, magic links), what they can do (rate limits, severity clamps, single-use entry), and what you send back out (signed, idempotent webhooks). An adversarial review caught four real issues before launch; that story gets its own post, next. Static pages, fresh data The event platform got a signed public API plus webhooks that fire on content changes — so landing pages can be static builds that rebuild themselves when an event changes. C
AI 资讯
A Cookie-Free Embeddable Support Widget: What Adversarial Review Caught
TL;DR Built an embeddable support widget for a helpdesk product: no cookies — a short-lived bearer token in a header, hashed at rest. Entry is an HMAC-signed assertion from the host page. An adversarial review caught four real holes before launch. Outbound webhooks: sign the exact bytes, dedupe key for idempotency, SSRF guard on destination URLs. The requirement: end users file tickets from pages the product doesn't own. That means an embeddable widget — and embeddable means everything you know about sessions stops working. Why cookie-free The widget lives on customers' domains, so any cookie it sets is a third-party cookie — blocked or partitioned by modern browsers. Fighting that means flaky sessions, so: no cookies at all. The entry exchange mints a short-lived session token the widget sends in a header, and the server caches the session keyed by sha256(token) — a cache dump yields nothing replayable. Sessions last 60 minutes, and expiry shows a real recovery path in the UI instead of dying silently. customer backend widget (on customer page) helpdesk API | signs ref|email|name | | | into HMAC assertion ---> |-- redeem assertion (single use) ->| | |<-- session token (60-min TTL) ---| | |-- X-Widget-Token: ... ---------->| What the adversarial review caught Finding Fix Replay burn keyed by client-chosen nonce Burn by HMAC signature — a leaked assertion can't mint extra sessions `\ ` accepted inside signed fields Origin check failed open when Origin/Referer absent Fall back to the unspoofable Sec-Fetch-Dest header to enforce embedding Widget could request critical severity Clamp effective severity (including the channel default) to the widget's allowlist My favourite is the delimiter one. If you sign ref|email|name and accept | inside a field, two different identity tuples can share one valid signature. Canonicalization bugs, not crypto bugs. Webhooks out: sign the exact bytes Outbound webhooks get composed once at enqueue time and stored; the delivery job re-encod
AI 资讯
From Angular.js to Fine-Grained Reactivity: Part 2 — The JS Proxy Runtime
In the first article of this series, we saw how a custom build-time compiler can transform a legacy Angular.js template into raw, optimized JavaScript. To recap, starting from this template: <!-- simple.html --> <p> Hello {{ name }}! </p> Our Go compiler generates the following JavaScript module: // simple.js export function template () { const p_0 = document . createElement ( " p " ); const text_1 = document . createTextNode ( "" ); p_0 . append ( text_1 ); return { mount ( container ) { container . append ( p_0 ); }, update ( change ) { if ( " name " in change ) { text_1 . data = " Hello " + change . name + " ! " ; } } } } This is incredibly clean. By running template() , we get an object with mount and update methods. Using mount is fully intuitive: we pass a reference to a DOM element, and it injects our empty paragraph ( p_0 ) into it: import { template } from ' ./simple.js ' ; const { mount , update } = template (); const container = document . getElementById ( ' view-container ' ); mount ( container ); // The DOM now contains: <p></p> (waiting for data) However, the paragraph remains empty until we call update with a change object like this: let changes = { name : " Mario " , }; update ( changes ); // The DOM surgically updates to: <p>Hello Mario!</p> But who is responsible for tracking changes in our application state, building this changes object, and calling update ? The answer lies in marrying the legacy Angular.js $scope with the modern JavaScript Proxy API . The Legacy State Pattern In a traditional Angular.js application, developers mutate the state directly inside a controller by assigning properties to the $scope object: // simple-controller.js export function SimpleController ( $scope ) { $scope . name = " Mario " ; } To bridge the gap between this legacy controller and our new build-time template, we need a way to automatically capture the assignment $scope.name = "Mario" and translate it into a structured update: let changes = { name : " Mario " }
AI 资讯
10 Website Performance Optimization Tips Every Developer Should Know
Website performance is no longer just a nice-to-have feature—it's a critical factor for user experience, SEO, and business success. Even a one-second delay in page load time can reduce conversions and increase bounce rates. Whether you're building a portfolio, SaaS application, eCommerce platform, or business website, these optimization techniques can make a significant difference. Optimize Images Images are often the largest assets on a webpage. Use modern formats like AVIF or WebP, compress images, and serve responsive image sizes to reduce bandwidth usage. Self-Host Fonts Third-party font requests add latency. Self-hosting fonts, preloading critical font files, and serving only the required character subsets can dramatically improve loading performance. Remove Unused CSS & JavaScript Shipping unnecessary code increases download size and execution time. Tree shaking, code splitting, and removing unused styles help keep your bundle lean. Enable Caching Configure long-term browser caching for static assets and use hashed filenames for cache busting. This allows returning visitors to load your website much faster. Use Lazy Loading Images, videos, and iframes that aren't immediately visible should load only when needed. Native lazy loading is supported by modern browsers and is easy to implement. Optimize Core Web Vitals Google's Core Web Vitals measure how users experience your website. Focus on: Largest Contentful Paint (LCP) Interaction to Next Paint (INP) Cumulative Layout Shift (CLS) Improving these metrics benefits both SEO and user satisfaction. Minify Assets Minify HTML, CSS, and JavaScript files before deployment. Smaller files transfer faster and improve overall performance. Use a CDN Serving assets from edge locations around the world reduces latency and improves loading times for global visitors. Prioritize Accessibility Accessible websites provide a better experience for everyone and often align with SEO best practices. Use semantic HTML, descriptive labe
AI 资讯
Fable 5 Hype: Fangirling with Datasets to Build a Lakers Dashboard
This is the story of a for-fun project, Luka Fit Index that started with me typing "ai for fun?...
AI 资讯
Building in public, week 17: turning one feature into a page cluster (and the internal-linking layer nobody sees)
Week 16 shipped the AI background remover: Rust-native, ort + ISNet + libvips, no Python. That was the feature. Week 17 was not about writing more of it. It was about the boring, high-leverage part that most side projects skip: turning one working feature into pages that can actually rank, and wiring those pages together so search engines can find them. No new engine code this week. Just leverage on what already existed. Here is what that actually looked like. The problem: a hub with nothing pointing at it The background remover lives at /remove-background . That is the hub. The plan was classic hub-and-spoke: one general tool page, then use-case spokes that each target a specific intent (removing a signature background, prepping an Amazon product photo, and so on). I built two spokes this week. But halfway through, I looked at how internal links actually worked on the site and found the real problem: nothing linked from the hub to the spokes. The spokes linked back to the hub in their body text, but the hub had no idea they existed. Neither did the ~180 converter pages. Tool links on the site were hardcoded in a frontend constant, roughly: export const IMAGE_TOOLS = [ { label : " Compress JPG " , href : " /compress/jpg " , tool : " compress " }, { label : " Resize Image " , href : " /resize-image " , tool : " resize " }, { label : " Crop Image " , href : " /crop-image " , tool : " crop " }, { label : " Images to PDF " , href : " /images-to-pdf " , tool : " convert " }, ] as const ; That list covered the converter tools. It did not include the background remover or its spokes at all. So the new pages were orphans: reachable only through the sitemap, with no internal links carrying any signal to them. For a domain that is still young and still earning Google's trust, orphan pages get discovered slowly and rank even slower. The fix: one constant as the source of truth Instead of hardcoding links in three different places, I made a single constant describe the whole cl
创业投融资
Amazon will stop accepting new customers for Mechanical Turk
These may be the last days of Amazon’s Mechanical Turk.
AI 资讯
I Contain Multitudes (and Also Three Git Repos)
A tour of the stack behind mattstratton.com and speaking.mattstratton.com: a monorepo holding two Astro sites and a dev.to sync tool, twenty years of blog posts, and the pipeline that crossposts posts like this one.
AI 资讯
3 Supabase security incidents, one shared root cause: SECURITY DEFINER inherits EXECUTE TO PUBLIC
Episode 1/4 of the mini-series The week Supabase lied to me 4 times . The three following episodes cover a mutation silently swallowed by the SDK [CANONICAL URL EPISODE 2: to fill in after push], an RLS recursion resolved by a JWT hook [CANONICAL URL EPISODE 3: to fill in after push], and a query that stops at exactly 1000 rows without saying so [CANONICAL URL EPISODE 4: to fill in after push]. The Tuesday the security probe spoke It's 9:12am on a Tuesday in May. The daily drift probe has been running automatically for three weeks — an aclexplode query across all public objects, filtered on anon . I don't open it every morning. That morning, it's waiting for me with a row that has no business being there. Niran sets a coffee on the corner of my desk without a word. He reads the output over my shoulder. A PII backup table — personal data in plaintext, created two days earlier for a bulk reclassification — shows up in the list with SELECT , INSERT , UPDATE , DELETE granted to anon . Accessible to any unauthenticated curl request. He lets three seconds pass and says: "It's not RLS." Then he goes back to his hoodie. He's right. It's not an RLS bug. The table itself is open, at the GRANT layer, before RLS even applies. Three objects, three doors, one mechanism That week, I realize I'm not dealing with an isolated incident. Three distinct objects, in three different migrations, each open a door nobody thought they'd opened. The backup table first. Then a policy set TO public because the public landing page needs it, which lets a POST {} from anon through with an HTTP 400 NOT NULL response instead of 401 Unauthorized . And finally four SECURITY DEFINER functions written to execute transactional operations with their owner's privileges, all invocable by anon because EXECUTE defaults to TO PUBLIC at CREATE time. Three objects, three superficially distinct mechanisms, yet one shared root. At every CREATE , Postgres completes the migration with an implicit GRANT the author nev
AI 资讯
What 74 ADRs in 70 days actually buy a solo dev (no hire, no clients, just the file)
The question you don't dare ask out loud It's 10:40 PM on a Tuesday, I just closed an ADR — the seventy-fourth in this setup, written conscientiously, dated, cross-referenced with its migration, its contract test, and the commit that triggered it. And the question rises, the way it always rises at that hour when you've been coding alone for ten hours: who did I just write this for . No tech lead to convince, no PR review that'll catch it, no hypothetical acquirer to reassure, no architecture committee to brief tomorrow. Just the file, just me, just the doubt. It's the question of a solo dev at 70 days of serious practice. It has an honest answer, and that answer is neither "it'll pay when you sell" nor "it'll pay when you hire". Those two ROIs belong to other trajectories. The ROI of the solo dev who documents is an ROI he buys himself — deferred, intangible at moments, but materially countable if you force yourself to measure it in the first person. Here's mine, over 74 ADRs and 18 doctrine rules accumulated in 70 days, with no external observer to validate the grid. The false economy of "I'll remember" First trap, the one that cost me three weeks before I learned the lesson. The solo dev believes he doesn't need to write down what he decided because he decided it himself — his memory is worth an ADR. False at 14 days, systematically false at six weeks. Not because general memory fails, but because technical memory has a deceptive shape: you remember perfectly that you decided , you no longer remember why you decided that way. Three weeks after the May 5 session where I wrote ADR-0051 (FK ON DELETE SET NULL + CHECK NOT NULL incompatible, DELETE failing silently), I reopen the migration to add a column. I reread the diff, I don't understand why a certain CHECK constraint is phrased like this — the alternative I mentally dismiss today seems simpler, and I'm two clicks from refactoring. I go check the ADR. The answer is there, dated, sourced, in three lines. The simpl