今日已更新 317 条资讯 | 累计 37222 条内容
关于我们

标签:#javascript

找到 1012 篇相关文章

AI 资讯

I built plugins for three editors. Everywhere, you're a guest in someone else's house

Over the last while I've built integrations for three places where people work with text and images: Obsidian , VS Code, and Figma. Doing a few of them back to back, I noticed something you don't see from a single one. They're all desktop apps. For your integration to exist at all, the person first installs a program on their machine, and then, inside it, your plugin. You're not writing for the web. You're writing code locked inside someone else's app — and each app has its own runtime, its own rules, and its own wall for you to walk into. The web trained us to think an HTTP request is one line. Inside someone else's sandbox, it turns out even that has to be earned. Figma was the strictest host of the three. I'll tell it through Figma, because it's locked down tighter than Obsidian or VS Code, and everything shows up on it at once. The task was almost comically simple: select a frame, write a caption, pick your social accounts, publish — without exporting the image and opening a second app. We already had the publishing API, so I expected the Figma side to be small. And it was: the main plugin file is 120 lines. The work wasn't in them. It was around them. Figma gives you bytes, not a file The first version came together easily. When the selection changes, the plugin checks whether there's one exportable node and tells the UI what it found. For the preview it exports a small copy; for publishing, separately, at 2×. const bytes = await nodes [ 0 ]. exportAsync ({ format : " PNG " , constraint : { type : " SCALE " , value : 2 }, }); 2× because the image still has a journey ahead of it: social networks recompress what you upload, and small text on a design goes noticeably softer by the time it lands in a feed. Then the first quirk of the foreign house. Figma hands the plugin not a file but raw PNG bytes — exportAsync() returns a Uint8Array . Our normal API won't eat that — it doesn't take a giant image stuffed into a JSON body. It creates a post first, hands the client

2026-08-26 原文 →
AI 资讯

How I "Vibe-Coded" a Privacy-First, Client-Side Base64 Tool (Deep-Dive into Unicode Handling in JS)

Hey DEV community! 👋 As developers, we handle Base64 encoding and decoding almost daily—whether we're debugging API payloads, formatting authorization headers, or embedding small graphic assets directly into stylesheets. However, many online translation utilities process your inputs on their backend servers. If you are dealing with sensitive configuration parameters, internal logs, or keys, pasting that data into a third-party web tool is a clear data privacy risk. To solve this, I decided to "vibe-code" a lightweight, strictly browser-based, privacy-oriented Base64 Encoder & Decoder . In this post, we will look at how this utility was built using AI assistance and vanilla JavaScript, along with the core logic to handle common encoding pitfalls. What is "Vibe Coding"? For those unfamiliar with the term, vibe coding is the practice of leveraging modern generative AI models to handle the bulk of the standard layout and event listeners, while you focus on the core logic, user experience, and privacy requirements. Instead of writing every CSS class and event listener manually, I guided an AI assistant to generate a clean, responsive layout using a standard grid framework, while ensuring that the core translation logic resides strictly in the user's browser. The Pitfall of Traditional JS Base64 (and How to Fix It) If you have ever used native JavaScript btoa() and atob() functions, you might know they struggle with Unicode/UTF-8 characters (like emojis or non-Latin scripts). Running this in your console will throw an error: btoa ( " Xin chào! 🚀 " ); // Throws "Uncaught DOMException" To resolve this during the development process, the utility implements modern TextEncoder and TextDecoder APIs. This approach converts strings into binary byte arrays before encoding them, avoiding exceptions. The Client-Side Implementation Here is the clean JavaScript snippet used for bidirectional encoding and decoding: function processBase64 ( action , inputValue ) { try { if ( action ===

2026-08-26 原文 →
AI 资讯

The function you wrote last month is a third-party API

There is a habit I have for other people's libraries that I do not have for my own code: before I call something, I read what it returns. With my own functions I skip that, because I wrote them, so I know. Three times in three days that turned out to be false, and the third time I caught it before it cost anything only because I had started treating my own modules like somebody else's. The version I had already been burned by twice I maintain qbofile , a set of browser-based converters between the file formats accounting software uses. It is a small codebase: a parser per input format, a generator per output format, and pages that wire one to the other. Wiring a new pair felt like plumbing, so I estimated it like plumbing. Two new pages, both reusing an existing parser and an existing generator: no new code. I said that out loud before opening either end. The generator had no column for the thing the parser produced. The parser could read the category a user had assigned to each transaction; the CSV generator emitted six fixed columns and category was not one of them. Not a bug — it had simply never needed one, because the format it was originally written for does not carry categories. That is a strange kind of wrong. Nothing was broken. The code did exactly what it always had. My model of it was built from the function name. The same evening, in the same pair of modules, the second one: L . push ( `P ${ sanitizeText ( tx . description )} ` ); P is the payee field in that output format. M is the memo. Two fields, and upstream, description was defined as memo || payee . So for any transaction that had a memo, the memo took the payee slot and the actual payee was dropped. Silently — the file is valid, it imports fine, and the missing name never announces itself. The two minutes that caught the third one After the second one I wrote down a rule and did not really believe I needed it: before wiring two components together, open both ends and read what actually crosses.

2026-08-26 原文 →
AI 资讯

Building a Unicode Text Transformer with Pure Character Maps

I built Unicode Text Tools , a free site with a bunch of text converters — superscript, subscript, bubble/circled text, upside-down text, small caps, and more. Type something, get it transformed, copy it out. The whole engine is one dependency-free JS file built entirely from character mapping tables . No AI, no server, no libraries. Here's why that's the right architecture for this class of tool, and how the trickier conversions work. The core idea: it's all just lookup tables Every conversion on the site is a function that maps each input character to a Unicode character (or does a small transform). The simplest cases are pure dictionaries: // Superscript (full a-z, 0-9) var SUP = { a : ' ᵃ ' , b : ' ᵇ ' , c : ' ᶜ ' , d : ' ᵈ ' , e : ' ᵉ ' , f : ' ᶠ ' , g : ' ᵍ ' , h : ' ʰ ' , i : ' ⁱ ' , j : ' ʲ ' , k : ' ᵏ ' , l : ' ˡ ' , m : ' ᵐ ' , n : ' ⁿ ' , o : ' ᵒ ' , p : ' ᵖ ' , q : ' ᵠ ' , r : ' ʳ ' , s : ' ˢ ' , t : ' ᵗ ' , u : ' ᵘ ' , v : ' ᵛ ' , w : ' ʷ ' , x : ' ˣ ' , y : ' ʸ ' , z : ' ᶻ ' , ' 0 ' : ' ⁰ ' , ' 1 ' : ' ¹ ' , ' 2 ' : ' ² ' , ' 3 ' : ' ³ ' , ' 4 ' : ' ⁴ ' , ' 5 ' : ' ⁵ ' , ' 6 ' : ' ⁶ ' , ' 7 ' : ' ⁷ ' , ' 8 ' : ' ⁸ ' , ' 9 ' : ' ⁹ ' , ' + ' : ' ⁺ ' , ' - ' : ' ⁻ ' , ' = ' : ' ⁼ ' , ' ( ' : ' ⁽ ' , ' ) ' : ' ⁾ ' }; The transform itself is trivial — walk the string, look up each char, append the mapped value (or the original char if unmapped). The work is in the tables: knowing which Unicode blocks exist, what's 1:1 reversible, and what's incomplete. The Unicode reality check Here's the thing nobody tells you about Unicode text transformation: the blocks are inconsistent. Superscript : complete for a-z and 0-9 — fully reversible. Subscript : incomplete — there's no subscript b , c , d , f , g , q , w , y , z . If you map an input with those letters, you have to decide what to do with them. Small caps : x has no small-cap form ( ꞯ is the closest, but it's a different character and looks wrong). j is a problem too — the Unicode small-cap ᴊ collides visually

2026-08-26 原文 →
AI 资讯

MyAnimeList-Module (NPM)

MyAnimeList Module This module is neither affiliated with nor endorsed by MyAnimeList. All data returned by this module is provided by MyAnimeList. Version 1.0.5 Installation Install myanimelist-module with npm npm install myanimelist-module Usage/Examples const { MyAnimeList } = require ( ' myanimelist-module ' ) const mal = new MyAnimeList ({ client_id : `YOUR_MAL_CLIENT_ID` // Get it here: https://myanimelist.net/apiconfig }) async function test () { const response = await mal . getAnimeInfo ({ name : " Anime name " }) if ( response . error ) { console . error ( response . error ) } else { console . log ( response . datas ) } } test () All functions new MyAnimeList() Parameter Type Description client_id string Required . Your MAL Client ID getAnimeInfo() Parameter Type Description name string Required . fields [array] Optional. More information in the "Available fields" section. limit number Optional. Number of items in the response. (Maximum of 100) offset number Optional. Default : 0 nsfw boolean Optional. Default: false getAnimeInfoByURL() Parameter Type Description api_url string Required . You must use any valid MyAnimeList API link. It also works with older responses via response.datas.paging.next and response.datas.paging.previous . getSpecificAnimeInfo() Parameter Type Description name string Required . fields [array] Optional. More information in the "Available fields" section. nsfw boolean Optional. Default: false getAnimeInfoByID() Parameter Type Description id string Required . fields [array] Optional. More information in the "Available fields" section. nsfw boolean Optional. Default: false getAnimeRanking() Parameter Type Description type string Optional. More information in the "Available ranking types" section. fields [array] Optional. More information in the "Available fields" section. limit number Optional. Number of items in the response. (Maximum of 500) offset number Optional. Default : 0 nsfw boolean Optional. Default: false getSeasonalAnime(

2026-08-26 原文 →
AI 资讯

Amazon, Temu, and AliExpress already have visual search. Desktop just hides it.

I shop on a laptop. A lamp on Amazon that costs too much. A jacket in a listing photo. Something I saw on eBay and wanted to check on Temu. On my phone, that is a camera tap. On desktop, the camera icon is mostly missing. So I built SameSame , a browser extension I still use every day. It does not send your photo to a third-party reverse-image API. It opens the visual search each store already runs - the same one their mobile apps and some out-of-stock flows use - from the page you are already on. The desktop gap Visual product search is not new. Amazon Lens, Temu camera search, AliExpress image search: they work because they search inside that store's catalog. That is different from Google Lens, which searches the open web and often returns a mix of blogs, pins, and shopping links. The catch is where those tools live. Amazon's image search is a first-class feature in the shopping app. On amazon.com in a browser, it is easy to miss or simply not there, depending on the page. Temu and AliExpress follow the same pattern: obvious on mobile, buried or absent on desktop. If you want to search by image from a laptop, the usual advice is: save the image, open the store app or a reverse-image site, upload, then repeat for the next store. That is a lot of friction for something the store already knows how to do. The searches were already there I did not invent a new matcher. Amazon, Temu, and AliExpress already run visual search against their own catalogs. On mobile that is the camera in the search bar. The same capability shows up in other places, including some out-of-stock and similar-items flows on the web. When a listing is unavailable, you have sometimes seen visually close alternatives. That is not a coincidence. The catalog search is already wired up. Desktop shopping just does not put a camera on every page. Those endpoints are not a public developer API you sign up for. They are the stores' own visual search, used by their apps and a handful of desktop pages, mostl

2026-08-25 原文 →
AI 资讯

React Form Backends Compared: Serverless Functions vs. Form-as-a-Service

React Form Backends Compared: Serverless Functions vs. Form-as-a-Service React makes building a form straightforward. What happens after onSubmit is a different question: you still need somewhere to validate, process, store, or forward the submission. Two common approaches are writing a serverless function yourself or using a hosted form backend such as onsubmit.dev (form backend). This article compares the two, using Vercel/Netlify-style functions for the DIY approach and onsubmit.dev with its React integration as the managed example. The basic problem Imagine a typical contact form: function ContactForm () { return ( < form > < input name = "email" type = "email" required /> < textarea name = "message" required /> < button type = "submit" > Send </ button > </ form > ); } The React component is only the UI. A real application usually needs backend behavior too: accepting the HTTP request validating and sanitizing input handling errors preventing abuse or spam delivering or storing the submission keeping credentials and other secrets off the client There are two broad ways to get that backend. Option 1: Build a serverless function With platforms such as Vercel and Netlify, you can create an HTTP function alongside your application and have your React form submit to it. Conceptually, the architecture looks like this: React form | v Your serverless function | +--> validation +--> email provider +--> database +--> other services The main advantage is control. Your function owns the request lifecycle, so you decide precisely how data is validated, transformed, authenticated, stored, and forwarded. If a submission needs to update PostgreSQL, call an internal API, enqueue a job, and return application-specific data, a custom backend is usually the natural solution. Serverless functions can also reduce product-level vendor lock-in. Although platforms have their own deployment conventions, HTTP handlers and their business logic are generally portable with some work. The tr

2026-08-25 原文 →
AI 资讯

I removed the LLM call and replaced it with 200 lines of template code

The feature was a letter generator. Somebody fills in a few fields and gets a finished letter of recommendation, resignation letter or notice letter, in plain text, ready to paste into an email. The obvious build is a prompt and a model call. I wrote the deterministic version instead: a pure function, about two hundred lines, no network, no key, no tokens. I want to lay out the reasoning, because "just call a model" is the default now and the default is not always right. The three reasons, in order of weight 1. The output is short and the shape is fixed. A recommendation letter is a date block, a greeting, three or four paragraphs, a sign off and a name. There is no structural variation to discover. Generation is valuable when the space of good outputs is large and you cannot enumerate it. Here the space is small enough to write down, and once you have written it down the model is doing an expensive approximation of a switch statement. 2. It is a legal-adjacent document. Not legal advice, but it goes into an employment record. A resignation letter that invents a notice period, or a reference that invents a fact about a person, is a real problem for the person who sent it. Templates cannot hallucinate. Everything specific in the output either came from a form field or is a sentence I wrote and can be held to. 3. Zero marginal cost changes what the product can be. This is the one that actually decided it. A model call costs money per use, and anything that costs money per use needs an account, a rate limit and eventually a card. A pure function costs nothing, so the tool can stay open with no signup, forever, without a business case. That is a product decision expressed as an architecture decision, and it only works if the code path is free. What the code looks like The whole engine is one exported function over one input type. export type LetterKind = ' resignation ' | ' notice ' | ' recommendation ' ; export type LetterTone = ' formal ' | ' warm ' | ' brief ' ; expo

2026-08-25 原文 →
AI 资讯

Why every BaZi calculator disagrees with the almanac

Every Four Pillars calculator — saju in Korea, BaZi in China — agrees on the easy 95% of the job. Feed it a birth date and it maps that instant onto a traditional calendar: four pillars, each a heavenly stem paired with an earthly branch. The remaining 5% is boundaries. And at the boundaries, nearly all of them quietly disagree with the printed almanac they claim to reproduce. I maintain a saju reading service, and getting these four cases right was most of the actual engineering. Here they are, with the failing inputs. 1. A solar term is an instant, not a date The year pillar does not turn on January 1, and not on lunar new year either. It turns at 입춘 (ipchun, "start of spring") — one of the 24 solar terms, defined by the sun's apparent longitude. In 2024 that moment was February 4, 16:27 KST . A calculator that applies solar terms at day granularity says "February 4 → new year pillar" and hands the wrong year to everyone born that morning. npx k-saju 2024-02-04 04:00 # year 癸卯 — still the old year pillar, because 04:00 < 16:27 The fix is unglamorous: store term boundaries as instants and compare instants. The subtlety is that this correction applies to the year and month pillars only — the day pillar runs on its own sexagenary count and must not be touched. 2. The 23:00 hour belongs to two days at once Traditional practice starts the day at 23:00, not midnight — the hour of the Rat (자시). So for a birth at 23:31, there are two defensible answers about which day's stem the hour pillar derives from, and schools split on it. The convention this engine declares: the day pillar keeps clock midnight , while the hour stem takes the next day's stem (the 야자시 rule). npx k-saju 2000-05-15 23:31 # day 癸酉, hour 甲子 I am not claiming this is the One True Rule. I am claiming it should be written down. Most tools pick a side in silence, which is how two calculators give one person two charts and neither can explain why. 3. The clock is not the sun Korea keeps time on the 135°E meri

2026-08-25 原文 →
开发者

Nuxt 4.5: Experimental SSR Streaming, Vite 8 and an Rsbuild-Powered Rspack Builder

Nuxt has released version 4.5, featuring updates such as a switch to Vite 8, a new Rspack 2 builder, and experimental SSR streaming. This streaming enhances Time to First Byte by flushing the HTML shell instantly. The release also includes a stable error code system and new composables, alongside important upgrade instructions for developers moving from earlier versions. By Daniel Curtis

2026-08-25 原文 →
AI 资讯

Why your hreflang tags are being ignored

Originally published on the WeLocale blog . Most SEO work is a matter of degree. You improve a title, you gain a little. hreflang is not like that. It either forms a valid set that search engines act on, or it does nothing at all, and the failure is completely silent. No warning, no penalty, no message in Search Console telling you the tags you carefully added are being discarded. We build a translation widget, which means we generate hreflang tags for other people's sites. This post is what we have learned about why they get ignored, including the parts where our own approach has real limits. The rule that breaks most setups hreflang is not a property of a page. It is a property of a set of pages, and every page in that set has to agree. If your English page says the German version is at /de/ , the German page has to say the English version is at / . If it does not, the declaration is one-way, and one-way declarations get dropped. Google calls these return links and treats their absence as a reason to distrust the whole set. This is why hreflang fails in a way that feels unfair. Every individual page looks correct when you inspect it. The problem only exists in the relationship between pages, which is exactly the thing you cannot see by viewing source on one URL. The corollary catches people too: each page must list itself . A German page whose tags mention English and French but not German is an incomplete set. Incomplete sets get dropped. The other four failure modes en-UK. The language code comes from ISO 639-1 and the region code from ISO 3166-1. In ISO 3166-1 the United Kingdom is GB. There is no UK. The tag is silently invalid, and it is easily the most common hreflang error on the web. Same class of mistake: lowercase regions, uppercase languages, and a region with no language at all. URLs that redirect. hreflang has to point at the final URL. If it points at http and you redirect to https, or it omits a trailing slash your server adds, the target is a redir

2026-08-25 原文 →
AI 资讯

Building High-Performance Web Systems & Mobile Apps: Lessons from Modern Software Engineering

Building web applications today often comes with a trade-off between feature velocity and performance. Over-reliance on heavy frameworks or unoptimized third-party plugins can quickly lead to bloated bundle sizes and poor user experience. As an engineer running DevLanka , a small web and app development studio in Sri Lanka, I’ve had the opportunity to build custom web systems and mobile applications. In this article, I want to share a few practical engineering insights on modern web performance, toolchain selection, and practical security. 1. Toolchain & Bundle Size Considerations Moving from legacy build setups to modern toolchains like Vite and React 19 significantly improves development DX (Developer Experience) and build output: Module Bundling: Vite leverages ES modules during development, resulting in faster startup times and optimized production builds. Tree-Shaking: Ensuring modern JavaScript imports are properly tree-shaken prevents unused code from shipping to the client. Rendering Strategy: For public-facing, SEO-critical pages, client-side rendering (CSR) alone may not always be ideal. Combining SSG (Static Site Generation) or SSR (Server-Side Rendering) with lightweight React components ensures proper HTML pre-rendering for search crawlers. 2. When to Use Custom Engineering vs. CMS Platforms There is no single "best" tech stack for every project. Choosing between a traditional CMS (like WordPress/Wix) and custom software engineering depends entirely on project requirements: Use a CMS when: You need rapid deployment, simple content publishing, or a standard marketing site with a limited budget. Use Custom Engineering when: You require tailored business logic, seamless API integrations, custom database schemas, or fine-grained control over execution environments. Note on Security: Custom development reduces dependency on third-party plugin vulnerability exploits, but it is not inherently immune to security risks. Custom code still requires strict adherenc

2026-08-25 原文 →
AI 资讯

A Simple CI/CD Pipeline That Actually Works

The Problem with Most CI/CD Tutorials Most tutorials show you a pipeline that deploys a "hello world" app to a free Heroku instance. They skip the messy parts: secrets, rollbacks, and the moment your pipeline breaks because a dependency changed. I've been there. After years of fighting with over-engineered setups, I settled on a minimal pipeline that's easy to understand, debug, and extend. It's not fancy, but it works. The Core Idea A CI/CD pipeline is just three stages: Test - run automated checks Build - create an artifact Deploy - push the artifact to a server We'll use GitHub Actions because it's free for public repos and integrates with everything. But the same concepts apply to GitLab CI, CircleCI, or Jenkins. The Pipeline File Here's the complete .github/workflows/deploy.yml : name : CI/CD on : push : branches : [ main ] pull_request : branches : [ main ] jobs : test : runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 - uses : actions/setup-node@v4 with : node-version : ' 20' - run : npm ci - run : npm test build-and-deploy : needs : test runs-on : ubuntu-latest if : github.ref == 'refs/heads/main' && github.event_name == 'push' steps : - uses : actions/checkout@v4 - run : npm ci - run : npm run build - name : Deploy to server uses : appleboy/scp-action@v0.1.7 with : host : ${{ secrets.SERVER_HOST }} username : ${{ secrets.SERVER_USER }} key : ${{ secrets.SSH_PRIVATE_KEY }} source : " dist/*" target : " /var/www/myapp" That's it. Let's break it down. Stage 1: Test The test job runs on every push and pull request. It checks out the code, installs dependencies with npm ci (which respects the lockfile), and runs your test suite. If a PR fails tests, the build-and-deploy job won't run because of the needs: test dependency. Stage 2: Build The build-and-deploy job only runs on pushes to main (not on PRs). It builds your app into a dist folder. For a Node.js app, npm run build might be a bundler like Vite or webpack. For a Python app, you'd replace with

2026-08-25 原文 →
AI 资讯

Comparing prices across retailers is a unit-normalization problem, not a scraping problem

Disclosure: I'm the founder of Popgot , which I use as the example below. The problem and the approach apply regardless of what you build on. Every price comparison project I've seen starts the same way: scrape a bunch of retailers, store the prices, sort ascending. And then it produces garbage rankings, because price is not a comparable field. Here's the classic failure. Three listings for AA batteries: Listing Price Count Brand A $5.99 16 Brand B $6.99 20 Brand C $11.94 40 Sort by price and Brand A "wins" at $5.99. Sort by cost per battery and the order flips completely: Brand C is ~29.9c per cell, Brand A is ~37.4c. The cheapest listing is the worst deal on the page. Why this is hard The naive fix is "just divide price by quantity." The problem is that quantity almost never exists as a clean number. It's buried in the title, and the title is written by whoever uploaded the listing: AA Batteries 24 Pack AA Alkaline Batteries, 1.5 Volts, 24 Count 48-Pack (2 x 24) Double A So you end up writing a title parser. Then you discover the same product needs a different unit depending on the category: per fluid ounce for detergent, per serving for protein powder, per 100g for coffee, per sheet for paper towels. Then you discover that some categories need a spec filter before unit price is even meaningful. A fish oil at 20c per serving isn't cheaper than one at 34c per serving if the first one has half the EPA+DHA. You're comparing two different products. That last part is the piece people underestimate. Normalization is only valid within a set of products that actually satisfy the same requirement, which means something has to read the label, not just the title. What a normalized record looks like This is the problem I ended up building Popgot around, so rather than describe it abstractly, here's the shape of the data. The developer API returns listings with the unit math already done: GET /api/developer-api/products?query=aa+batteries&limit=10 { "products" : [ { "display_t

2026-08-25 原文 →
开发者

JWT Authentication in Node.js: A Practical Guide (with Express)

Ever logged into an app, closed the tab, come back, and you're still logged in — no password needed? That's almost always JWT doing its job behind the scenes. JWT (JSON Web Token) is one of the most common ways to handle authentication in modern backends. But a lot of developers use it without really understanding what's happening — and that's exactly where security bugs sneak in. Let's fix that. By the end of this post you'll know what a JWT actually is, how to use it in a Node.js + Express app, and the mistakes that quietly break real apps. What is a JWT, really? A JWT is just a string with three parts , separated by dots: xxxxx.yyyyy.zzzzz │ │ │ header payload signature Header — says which algorithm signed the token (e.g. HS256 ). Payload — the actual data (like userId , role , and an expiry time). This is not encrypted — it's just Base64-encoded. Anyone can read it. Signature — a cryptographic stamp created using a secret only your server knows. This is what stops people from faking tokens. Want to see this for yourself? Paste any token into a free JWT decoder and you'll instantly see the header and payload. Notice you can read everything without the secret — that's the key lesson: never put passwords or sensitive data in a JWT payload. Creating a token (login) Install the library: npm install jsonwebtoken When a user logs in successfully, sign a token: import jwt from ' jsonwebtoken ' // On successful login: const token = jwt . sign ( { userId : user . _id , role : user . role }, // payload process . env . JWT_SECRET , // secret (keep it in .env!) { expiresIn : ' 7d ' } // auto-expiry ) res . json ({ token }) Three things to notice: Keep the payload small — just an id and role, not the whole user object. The secret lives in an environment variable, never hardcoded. Always set expiresIn . A token that never expires is a token that can be stolen forever. Verifying a token (protecting routes) Now create a middleware that checks the token on every protected request

2026-08-24 原文 →
AI 资讯

Building an ASCII Art Generator with AI: The Good, The Bad, and The Figlet

The Problem I was staring at my terminal during a deploy, waiting for the build to finish, when I realized something: I'd been typing figlet "Hello World" into my terminal for years to generate ASCII art for commit messages and README files. But every time I wanted to share that art with someone who wasn't a developer, I hit a wall. "Just install figlet," I'd say. "Install what now?" they'd reply. The problem wasn't that ASCII art tools don't exist online. The problem was that the ones I found were either bloated with ads, required JavaScript frameworks that made the page take forever to load, or couldn't handle non-Latin characters gracefully. I wanted something that just worked in a browser tab, no installation, no server, no fuss. So I decided to build my own. Because apparently I enjoy reinventing wheels. The AI-Assisted Development Journey Here's where things get interesting. I've been using AI pair programming for a while now, and this project felt like the perfect test case: it's well-defined, has clear requirements, and involves a lot of repetitive font data that would be tedious to type manually. The Initial Prompt I started by describing the requirements to an AI assistant in pretty specific terms: Build a single-file HTML tool that converts text to ASCII art. Must have multiple fonts (Block, Slant, Small, Standard, Mini). Real-time preview. Copy to clipboard. Download as .txt. Support dark mode. Chinese/English i18n. Vanilla JS only. The AI came back with something surprisingly decent. It had the basic structure right, the font data was embedded, and the rendering logic was clean. But there were issues. Where AI Got It Wrong The first problem was character handling . The AI assumed that all input would be uppercase English letters. When I tested with lowercase, numbers, and special characters, it just... broke. Not crashed, but silently dropped characters. // What the AI initially wrote (simplified) function getChar ( char , font ) { return font [ char .

2026-08-24 原文 →
AI 资讯

How to Build a Fair A/B Audio Preview for AI Processing

Two audio players do not make a fair before-and-after test. If the second player restarts from zero or takes half a second to load, the user is no longer comparing two versions of the same moment. They are comparing two memories. That is a weak way to evaluate any audio effect. It is especially weak for AI processing. A denoiser can remove a fan while softening consonants. A de-reverb model can reduce the room tail while making the voice sound less natural. The output may be cleaner without being better. The preview therefore has one job: let the listener switch quickly enough to hear both the improvement and the damage. The rule I use is deliberately boring. Both versions should contain the same edit and play from the same position. Switching should not restart playback or create a pause. The interface should not hint that one version is supposed to win. Two independent <audio> elements fail surprisingly quickly. Each owns its playback state, buffering behavior, clock, and seek operation. The user ends up finding the same position twice and comparing one sound with a memory of another. A better interface has one transport and one version control: [ Play ] [ Original | Processed ] 00:18 ━━━━━━━ 00:42 The transport decides where playback happens. The segmented control decides which signal is audible. One transport, two signals For a short preview, I decode both files into AudioBuffer s, start them at the same AudioContext time and offset, and route each through its own GainNode . Both sources run; only one gain is open. decodeAudioData() decodes complete file data and resamples it to the context's sample rate. The decoded buffers can then share the same audio clock. See the MDN documentation for format and loading details. The core is small: const context = new AudioContext (); const originalGain = context . createGain (); const processedGain = context . createGain (); originalGain . connect ( context . destination ); processedGain . connect ( context . destination )

2026-08-24 原文 →
AI 资讯

The Evolution of Web Forms — Part 3

The Evolution of Web Forms — Part 3: React Hook Form, Validation Libraries, and Zod In Part 2, we learned that React solved the problem of manually updating the DOM. Instead of writing: emailError . textContent = " Email already exists " ; emailInput . setAttribute ( " aria-invalid " , " true " ); React allowed us to describe the interface from state: < input aria-invalid = { Boolean ( errors . email ) } /> { errors . email && ( < p > { errors . email } </ p > )} However, React did not automatically manage: Form values Validation errors Touched fields Dirty fields Submission state Reset behavior Dynamic fields Backend errors Performance Developers still had to build those features manually. That created the need for form-management libraries. This part covers: React Hook Form’s philosophy and architecture React Hook Form’s core APIs Validation libraries React Hook Form with Zod and TypeScript By the end, we will build a production-style registration form using: React + TypeScript + React Hook Form + Zod + An API layer Stage 9: React Hook Form Deep Dive React Hook Form is not simply a shorter way to write controlled React forms. It uses a different architectural philosophy. A traditional controlled input stores its value in React state: const [ email , setEmail ] = useState ( "" ); < input value = { email } onChange = { ( event ) => { setEmail ( event . target . value ); } } /> Every keystroke produces a state update: User types ↓ onChange runs ↓ setEmail runs ↓ Component renders again ↓ Input receives the new value React Hook Form prefers native, uncontrolled inputs when possible. < input { ... register ( " email " ) } /> The browser stores the current value inside the input element. React Hook Form registers the input, listens to its events, tracks relevant form state, and reads its value when required. React Hook Form’s official documentation describes register() as the mechanism that connects an input to validation, value tracking, and submission. Controlled vers

2026-08-24 原文 →
AI 资讯

Your canvas.toBlob might be silently handing you a PNG

A user told me the .webp files my tool produced wouldn't open on their desktop. I opened one in a hex editor. First four bytes: 89 50 4E 47 . It was a PNG. With a .webp extension. The encoder wasn't broken. I had simply never checked whether the browser actually did what I asked. The spec says it's allowed to do this Here's the code. Nothing looks wrong with it: canvas . toBlob ( blob => { download ( blob , ' output.webp ' ); }, ' image/webp ' ); The callback fires. The blob isn't null. Its size looks reasonable. Everything succeeds — except it isn't WebP. This is not a bug. The HTML spec explicitly requires it: if the user agent doesn't support the requested type, it must create the file using the PNG format instead. No exception, no warning, no second argument telling you what happened. There's exactly one place that information exists — blob.type : canvas . toBlob ( blob => { console . log ( blob . type ); // iOS below 16.4: "image/png" }, ' image/webp ' ); toDataURL does the same thing, but at least there the fallback is visible to the naked eye, since the data URL literally starts with data:image/png;base64, . There is no capability query for this My first instinct was to special-case iOS. That falls apart quickly. Every browser on iOS is WebKit underneath, so "is this Safari" isn't a meaningful question. Embedded webviews inside apps track the system version in ways that don't always match the standalone browser. And a user can flip on "Request Desktop Website" and hand you a macOS user agent from an iPhone. More fundamentally: the user agent string answers "who are you" , and I need to know "can you encode WebP right now" . Between those two questions sit the engine version, OS version, host app, and build flags. Any mismatch in that chain and your lookup table lies to you. So I went looking for an official capability API. Media has them: MediaRecorder . isTypeSupported ( ' video/webm;codecs=vp9 ' ); // → boolean await navigator . mediaCapabilities . encoding

2026-08-24 原文 →