开发者
Cómo solucionar el error \"Text content does not match server-rendered HTML\" en Next.js App Router
Cómo solucionar el error "Text content does not match server-rendered HTML" en Next.js App Router Este error ocurre cuando el HTML generado en el servidor (SSR/SSG) no coincide con el árbol de React que se construye durante la primera renderización en el navegador (hydration). Es un problema crítico de consistencia de estado que rompe la experiencia de usuario y puede causar comportamientos impredecibles. 🔍 Causa raíz (diagnóstico técnico) En tu caso, el error está relacionado con contenido dinámico que varía entre renderizado del servidor y renderizado del cliente , probablemente causado por: Uso de Date() , Math.random() , localStorage , window , o APIs del navegador directamente en el render . Uso de typeof window !== 'undefined' como condición de renderizado (no es idempotente entre SSR y CSR). Metaetiquetas de detección automática de iOS ( format-detection ) que inyectan nodos <a> en tiempo de ejecución. Extensiones del navegador (especialmente en desarrollo) que modifican el DOM. Librerías CSS-in-JS mal configuradas que inyectan clases o estilos dinámicos en CSR. ⚠️ Nota crítica : Next.js App Router no permite el uso de useEffect para evitar el mismatch en el primer render — el mismatch debe prevenirse , no suprimirse . ✅ Solución definitiva (pasos verificados) Paso 1: Elimina toda lógica no determinista del render NUNCA uses lo siguiente directamente en el cuerpo del componente: // ❌ Evitar const now = new Date (); // ❌ const isClient = typeof window !== ' undefined ' ; // ❌ const randomId = Math . random (); // ❌ const theme = localStorage . getItem ( ' theme ' ); // ❌ ✅ Reemplaza con: // ✅ Usar `useEffect` para *actualizar* el estado, no para *determinar* el render inicial import { useState , useEffect } from ' react ' ; export default function Component () { const [ time , setTime ] = useState < string > ( '' ); // Inicializa con valor seguro (ej. string vacío o placeholder) useEffect (() => { setTime ( new Date (). toISOString ()); }, []); return < time d
AI 资讯
Building a Live, User-Controlled Canvas Background System That Doesn't Kill Low-End Phones
The idea Most apps give you a static background. I wanted Pairly to feel alive instead, so I built "Atmosphere": a real-time animated Canvas layer that sits behind every chat, fully tunable by the user, speed, density, opacity, brightness, saturation, all live. There are currently over 40 atmospheres in the system, from calm ones like Snow and Fireflies to more elaborate ones like a black hole accretion disk called Abyss. The interesting part wasn't drawing pretty particles. It was making that work smoothly on a five-year-old Android phone without draining the battery in ten minutes. Two rendering paths, not one Atmosphere isn't a single renderer, it's a small internal package ( @pairly/atmospheres ) with two shared engines that every individual atmosphere builds on: ParticleCanvas , a generic particle system for anything made of many independent objects: snow, fireflies, sakura petals. useCanvasLoop , a raw draw-loop hook for continuous scenes that aren't particle-based, like Abyss's swirling accretion disk. Both engines centralize every "don't destroy the device" concern in one place, so individual atmospheres never have to think about it. Here's useCanvasLoop 's frame loop: const frameInterval = 1000 / perf . fps ; let raf = 0 ; let last = performance . now (); let acc = 0 ; const loop = ( now : number ) => { if ( ! running ) return ; raf = requestAnimationFrame ( loop ); const elapsed = now - last ; last = now ; acc += elapsed ; if ( acc < frameInterval ) return ; const dt = acc / 1000 ; acc = 0 ; draw ( ctx , width , height , elapsedTime , perf ); }; requestAnimationFrame fires at the display's native rate (often 90-120Hz on phones now), but that doesn't mean you should draw every single time it fires. This accumulator pattern throttles actual drawing down to the target FPS from the device's performance profile, instead of trusting rAF's raw rate. Profiling the device before drawing anything Before any atmosphere renders a single frame, it checks the device: ex
AI 资讯
I wrote the privacy rule, enforced it, commented it, and shipped the leak anyway
This is a submission for DEV's Summer Bug Smash : Smash Stories. TL;DR. I wrote a scrubbing policy before writing any instrumentation code. I enforced it in a beforeSend hook. I unit tested it. I wrote a comment above the one obviously sensitive line saying exactly what it must never do. Then I intercepted the actual bytes leaving the browser and found a stranger's shoulder injury in them. Every guarantee I had written was about data my code hands to the SDK. None of them were about data the SDK collects on its own. The setup WhyRep is a workout tracker built local-first. Training data is created and read on the device, the tracker works offline with no account, and that is not a marketing line, it is the architecture. It is also the thing people decide to trust or not trust in about four seconds on the landing page. So when I added Sentry, the scrubbing policy came before the code. Written down, in the repo, as a list of things that may never appear in an event: exercise names, weights, reps, RIR, session notes, chat content. Never. On Android I enforced it twice. A beforeSend hook that strips the forbidden fields, and a unit test that constructs an event carrying each one and asserts it comes out stripped. @Test fun `beforeSend strips every field the policy forbids` () { val event = SentryEvent (). apply { setExtra ( "exerciseName" , "Incline Barbell Bench" ) setExtra ( "weightKg" , 82.5 ) setExtra ( "notes" , "left shoulder clicks past parallel" ) } val scrubbed = ScrubbingPolicy . scrub ( event , Hint ()) assertNull ( scrubbed ?. getExtra ( "exerciseName" )) assertNull ( scrubbed ?. getExtra ( "weightKg" )) assertNull ( scrubbed ?. getExtra ( "notes" )) } Green. Good. Then I wired up the landing site's share-link page. It decodes whyrep.com/t#<payload> , where the payload is somebody's entire workout template, base64 in the URL fragment. I was careful there too. On a decode failure it reports a coarse reason tag and never the payload: // NEVER send the payload i
AI 资讯
Someone forked my React component instead of opening an issue
I maintain a small comic and manga viewer component for React called react-comic-viewer . The other day I was poking around npm and noticed something odd — there were three other packages with basically my package's name, published by other people. All three were forks of mine. Same description, same repository URL pointing back at my repo. None of the three authors had ever opened an issue or a pull request on my side. What the fork changed The oldest fork was made about three months after I first published, and it kept going for almost a year. Its version number ran ahead of mine at the time — I was on 0.3.5 while the fork was on 0.6.3. So I read the diff. Honestly, it was more useful than any issue would have been. The commit messages alone told the whole story: remove sass fix: support className props use Hotkeys and a new example file called controlled.tsx The sass one I'd already fixed. The className one I'd fixed too, about a year later. The controlled.tsx one I had never fixed. Not in four years. The part I never fixed Here's what their example looked like: < ComicViewer currentPage = { currentPage } isExpansion = { false } onTryMoveNextPage = { ( nextPage ) => { /* ... */ } } onChangedCurrentPage = { ( page ) => setCurrentPage ( page ) } pages = { pages } /> And here's what my component actually accepted: < ComicViewer initialCurrentPage = { 0 } initialIsExpansion = { false } onChangeCurrentPage = { ( page ) => { /* ... */ } } pages = { pages } /> The initial prefix is the whole problem. My component would take a starting page from you, and then never let you touch it again. It owned that state for the rest of its life. That's fine for a demo. It's pretty bad for anything real. You can't jump to a page from a table of contents. Syncing the current page with the URL doesn't work either. And if a chapter needs to be purchased first, there's no way to step in and stop the move. Every one of those needs the parent to be in charge, and the parent never was. Maki
开发者
Forms in React : From Inputs to Controlled Components
You have probably written HTML forms before, and so the structure below resonates with you. Perhaps you even smile because, this one, you understand. <form> <input type= "text" /> <button> Submit </button> </form> If you have done this, you know what happens when you click the button. The whole form reloads, the changes or inputs are cleared. This is the default behavior of forms in HTML. In React, we handle every step and every stage so that we have control over the data and the behavior of the form and data. The above signature represents what we call UNCONTROLLED INPUT . This means that there isn't a single source of truth to the value of this field, hence it can change to anything, and any value In addition to the above attributes, we will add value and onChange props to the input element as below: <input type= "text" value= {} onChange= {}/ > value represents the content of the input field e.g. the name text that the user enters in a Name field. onChange is the function that will be triggered everytime the input changes. Whenever a key is pressed within this field, this function will be invoked. Controlled inputs have their values set and manipulated by states, as we saw in Part 1 of the series. Uncontrolled inputs on the other hand do not have a manager that will dictate what goes into the field and when. Now let's write our first React Input, we'll keep it simple. import { useState } from " react " function Form (){ const [ name , setName ] = useState ( "" ) return ( < input type = " text " value = { name } onChange = {( event ) => setName ( event . target . value )} / > ) Let's look at what happens in the above. We have declared a state [name,setName] . name is the state variable setName is a function used to update the variable We then initialized an input element with properties value and onChange Note that, when the value of an input is set, that will always be the value even if you type something into the box. That is the essence of controlled input. The
开发者
Hello Everyone
Hello everyone, I am new to coding just begun to learn the ins and outs of coding and what it can do. I am in the process of getting my Full Stack Developer certificates. I have always wanted to do something that has to do with computers because I needed something to pass the time when I hurt myself playing football. I am looking forward to chatting with all of you about the struggles you had and what you found that you liked within the development realm.
开发者
Your birth time is lying to you: a time-zone rabbit hole in a Chinese astrology calculator
I built a calculator for BaZi — Chinese "Four Pillars" birth charts. Whatever you think of the interpretive tradition (and I'll get to that), the input math turned out to be a genuinely deep time-zone problem, and that's what this post is about. If you've ever thought "time zones, how hard can it be" — this is a tour of exactly how hard, with working TypeScript. The problem BaZi divides the day into twelve two-hour "branches", so your birth hour is one of the chart's four pillars. Get the hour wrong and you get a different chart — not slightly different, categorically different. Every calculator I could find feeds the system the wall-clock time from your birth certificate. But the tradition predates time zones by about two thousand years; it obviously means solar time — where the sun actually was over your birthplace. Clock time and solar time differ by more than most people think, and the difference decomposes into exactly three parts: 1. Daylight saving time — and it's historical. You need the DST rules in force on the birth date , not today's. China ran a now-forgotten DST experiment from 1986–91; Harbin kept its own zone before 1949. If you were born in Beijing in July 1988, your certificate is an hour ahead of standard time and no modern-day lookup will tell you that. 2. Longitude. Solar time shifts 4 minutes per degree from your zone's standard meridian. China spans five geographic zones but uses one clock — born in Ürümqi, your clock runs about two hours ahead of the sun. It's not just a China quirk: Vancouver sits at 123°W in a zone whose meridian is 120°W, so that's another 12 minutes, everywhere, always. 3. The equation of time. The sun itself runs up to ±16 minutes fast or slow over the year, thanks to orbital eccentricity and axial tilt. NOAA publishes an approximation that's accurate to under a minute: /** Equation of time (minutes), NOAA approximation */ export function equationOfTimeMinutes ( dayOfYear : number ): number { const b = ( 2 * Math . PI *
开源项目
🔥 dbgate / dbgate - Database manager for MySQL, PostgreSQL, SQL Server, MongoDB,
GitHub热门项目 | Database manager for MySQL, PostgreSQL, SQL Server, MongoDB, SQLite and others. Runs under Windows, Linux, Mac or as web application | Stars: 7,269 | 6 stars today | 语言: JavaScript
AI 资讯
The multilingual bugs that never throw: hreflang, JSON-LD and a site in 12 languages
I run a search engine that publishes in twelve languages from one static site on Cloudflare Pages. Last week I audited its machine-readable layer — the part crawlers and answer engines read rather than humans — and found four problems. None of them threw an error. None appeared in logs. Every page rendered perfectly. That is the whole point of this post: the multilingual layer fails in a register where nothing tells you. 1. The homepage was serving the wrong language to everyone abroad The site's primary market speaks Hebrew, so / is Hebrew and /en/ , /ar/ , /de/ and nine others sit alongside it. A middleware rule redirected visitors from one specific region to their language. Everyone else — including every English speaker on earth — landed on Hebrew. My first instinct was to fix it with a broader geo-redirect: detect English-speaking countries, send them to /en/ . This would have been a bad idea, and it is worth saying why. Googlebot crawls predominantly from US IPs. A geo-redirect on / that keys off country would take the crawler off the Hebrew homepage and onto the English one almost every time it visited. You do not want your primary-market homepage to become the page the crawler can never reach. The correct tool is hreflang , and it is what search engines built for exactly this. Checking the page, the tags were already there and already right: <link rel= "alternate" hreflang= "he" href= "https://example.com/" > <link rel= "alternate" hreflang= "en" href= "https://example.com/en/" > <link rel= "alternate" hreflang= "ar" href= "https://example.com/ar/" > <!-- …ten more… --> <link rel= "alternate" hreflang= "x-default" href= "https://example.com/en/" > Two things make this work, and both are easy to get wrong: The set must be reciprocal. Every page in the group lists every other page including itself . If /en/ does not point back at / , search engines are entitled to ignore the whole cluster. x-default is not "the default language" — it is the fallback for users
开源项目
Four things SVG and CSS did that I did not expect
I spent a while building an icon editor that runs entirely in the browser (icons.jamuny.com, free, no account). Here is what cost me the most time. A presentation attribute loses to any author CSS rule I was scaling handle stroke widths by 1 / zoom and writing the result as an attribute. The value was never used. handle . setAttribute ( ' stroke-width ' , String ( 0.35 / zoom )); .handle { stroke-width: 0.35 } in the stylesheet outranks it, because a presentation attribute sits at the very bottom of the cascade. Measured in Chromium: an attribute of 0.05 computed as 0.35px . Every handle thickened on screen as you zoomed in, for months, with no error anywhere. The fix is a custom property, which is an ordinary declaration and wins where an attribute cannot: layer . style . setProperty ( ' --px ' , String ( 1 / zoom )); /* .handle { stroke-width: calc(0.35 * var(--px)) } */ Geometry attributes like r and width are unaffected. They have no CSS counterpart here, so nothing was ever overriding them. A focused SVG element gets a focus ring measured in user units My canvas is 24 units wide and about 620 pixels. Chrome drew its default focus ring at outline-width: 2.72727px in user units, which is about 24 screen pixels. A fat blue disc appeared around every point you clicked. It was reported to me four times, and four times I thinned something of my own that was not the cause. getComputedStyle ( document . activeElement ). outline That one line found it. My rule only covered :focus-visible , which is the keyboard case, and the keyboard case is the one where I draw a ring of my own. var() does work in a presentation attribute, and I wrote down that it doesn't I needed a segment colour that changes with the theme, so the value is oklch(var(--band-l) var(--band-c) 47) . I applied it through a style and put a comment beside it saying var() is not substituted in presentation attributes. It is. Both forms compute to the same colour, including on an element built detached and ap
AI 资讯
I built an OLX scraper for 24 countries — the boring version that actually ships
I built an OLX scraper for 24 countries — the boring version that actually ships OLX runs classifieds in about two dozen countries. Same brand, different domains, different anti-bot setups. Everyone scraping it does one country at a time. I got tired of forking. So I put 24 countries behind one input. country: "id" or country: "pl" or country: "br" — same schema out. It's live on Apify as primesieve/olx-global-scraper . One file. No browser. Here is the boring part that matters. What it does Input: { "country" : "id" , "keywords" : [ "iphone 13" ], "maxResults" : 50 , "maxPages" : 3 , "proxyConfiguration" : { "useApifyProxy" : true , "apifyProxyGroups" : [ "RESIDENTIAL" ] } } country — two-letter code ( id , pl , in , br , ua , pt , ro , bg , kz , uz , pk , za , ng , ke , eg , lb , ph , co , ar , pe , ec , gt , az , ma ). Default id . keywords — one or more search terms. Each runs sequentially. maxResults / maxPages — caps. Defaults 50 / 3, max 1000 / 30. proxyConfiguration — optional for Indonesia, required for the other 23. Output — same shape every country: { "listingId" : "123456789" , "title" : "iPhone 13 128GB mulus" , "price" : 6500000 , "priceText" : "Rp 6.500.000" , "currency" : "IDR" , "city" : "Jakarta Selatan" , "location" : "Tebet, Jakarta Selatan, DKI Jakarta" , "images" : [ "https://...jpg" ], "thumbnailUrl" : "https://...jpg" , "listingUrl" : "https://www.olx.co.id/item/123456789" , "country" : "id" } Title, price (numeric plus display text), currency, location, images, URL. No seller PII beyond what the listing page shows. No tricks. Try: https://apify.com/primesieve/olx-global-scraper The boring stack // no playwright, no puppeteer // apify + fetch + cheerio. That's it. The scraper is one file. Apify SDK for input, dataset, and pay-per-event. Native fetch for HTTP. cheerio for the HTML path. Undici ProxyAgent when a proxy is configured. Node 20, 512 MB, 600s timeout. I check the endpoint before I write the scraper. Indonesia answered with clean JSO
AI 资讯
Why free chess analysis is always capped at one game a day
Most free chess game review gives you one game per day. Chess.com works that way, and so does almost every smaller site offering the feature. I assumed for a long time that this was just a paywall placed where it hurts. It is partly that. But there is a real cost sitting behind the cap, and once I worked out what the cost was, I built my own analysis site differently. The cost of one game review Reviewing a 40 move game means evaluating about 80 positions. Give the engine two seconds on each one and you have spent close to three minutes of CPU. None of it is cacheable, because your game is not anyone else's game. Run that on your own hardware and you pay for every minute. A thousand people reviewing one game a day is roughly 50 CPU hours daily, for a feature you are giving away. The quota is not greed. It is the number that stops the free tier from eating the company. Which raises a more interesting question than "how do I price this". What happens if you delete the cost instead of rationing it? Move the engine to the client Stockfish compiles to WebAssembly. Put it in a Web Worker and the visitor's own processor spends those three minutes. Your server ships static files and never sees a chess position. The whole free tier problem disappears, because there is no per-user cost left to control. Nothing to meter, so nothing to cap. Getting started is unremarkable: const engine = new Worker ( " stockfish.js " ); engine . postMessage ( " uci " ); engine . postMessage ( " isready " ); After that you speak UCI over postMessage . Set a position, ask the engine to think, and read results off the message stream: engine . postMessage ( `position fen ${ fen } ` ); engine . postMessage ( `go depth 15 movetime 2000` ); That is the pitch. Now the parts nobody mentions. The protocol is strings, and it is asynchronous UCI was designed for a pipe between two processes. You get that pipe, faithfully, with all of its ergonomics intact. The engine answers with lines like this: info dept
AI 资讯
Cómo solucionar el error “Enable JavaScript and cookies to continue”
Cómo solucionar el error “Enable JavaScript and cookies to continue” Este mensaje aparece cuando Cloudflare (u otro proxy de seguridad similar) bloquea la solicitud porque detecta que el cliente no cumple con los requisitos mínimos de seguridad: JavaScript deshabilitado o cookies deshabilitadas/expiradas . 🔍 Causa técnica Cloudflare implementa mecanismos de protección como: JavaScript Challenge : El navegador debe ejecutar un script para demostrar que no es un bot. Cookie de verificación : Tras superar el desafío, Cloudflare emite una cookie ( __cf_bm o cf_clearance ) que valida la sesión. Si el cliente (navegador o cliente HTTP personalizado) no ejecuta JavaScript o no maneja cookies correctamente, la validación falla y se muestra este mensaje. ✅ Solución definitiva (por escenario) 🌐 Si eres un usuario final (navegador) Habilita JavaScript : Chrome: Configuración > Privacidad y seguridad > Sitios web no seguros > Habilitar JavaScript . Firefox: Preferencias > Privacidad y seguridad > Permisos > Habilitar JavaScript . Habilita cookies de terceros (si usas extensiones como uBlock Origin o Privacy Badger): Añade el dominio a la lista blanca. Desactiva temporalmente los bloqueadores para probar. Borra cookies y caché del dominio afectado. Reinicia el navegador y vuelve a cargar la página. 🧪 Si eres desarrollador (automatización / scraping / cliente HTTP) ❌ No uses requests o curl sin soporte JS/cookies → fallarán siempre . ✅ Opción recomendada: Usa un navegador headless con soporte JS y cookies # Ejemplo con Playwright (recomendado) from playwright.sync_api import sync_playwright with sync_playwright () as p : browser = p . chromium . launch ( headless = True ) context = browser . new_context () page = context . new_page () # Navega a la URL (Cloudflare se resolverá automáticamente) page . goto ( " https://ejemplo.com " , wait_until = " networkidle " ) # Si aún falla, fuerza espera tras el desafío try : page . wait_for_selector ( " #challenge-error-text " , timeout = 5
AI 资讯
VoidZero Releases Vite+ Beta: A Unified Web Toolchain Behind a Single Command
VoidZero has launched the beta of Vite+, a unified web development toolchain. It combines runtime, package management, and essential frontend tools under a single command. Vite+ supports various projects and is open source. The platform enhances workflow through features such as hot-reloading, format checking, and testing. The team emphasizes community feedback for future updates. By Daniel Curtis
AI 资讯
I Built Browser-Local File Tools So Files Don't Need to Be Uploaded
A lot of small file jobs still follow the same awkward pattern: choose a file, upload it to someone else's server, wait for processing, download the result. For some tasks, that server round trip is unnecessary. I have been building FileNest Worktools , a browser-based toolkit for repetitive file work, around a simple constraint: If a task can reasonably be done inside the browser, the file contents should stay on the user's device. What currently runs locally The current FileNest tools cover: batch file renaming sequential, reverse, and custom-order renaming JPG / PNG / WebP conversion image resizing and compression image-to-text OCR with editable review PDF merge, split, extract, and images-to-PDF text export to DOCX, PDF, TXT, Markdown, and HTML CSV / TSV / JSON conversion duplicate-file detection For these current browser-local workflows, the file contents are processed on the device rather than being sent to a conversion server. Why local processing matters Privacy is one reason, but it is not the only one. Many file operations do not actually need server-side infrastructure. Renaming a file is mostly about filenames, order, extensions, and conflict checks. Image resizing can be handled with browser APIs. CSV and JSON conversion is essentially local parsing and serialization. Duplicate detection can compare file fingerprints locally. If those jobs can stay inside the browser, there is less network overhead and one less copy of the user's files being created somewhere else. I also wanted the risky parts to stay visible One thing I dislike about many online file tools is the "click and hope" workflow. So I tried to make FileNest show more information before or after processing: renamed files can be previewed before packaging original files are not silently overwritten image compression reports actual output bytes duplicate detection uses content matching rather than filename guesses OCR output can be reviewed and edited the image enhancer does not claim that shar
开源项目
🔥 cloudflare / security-audit-skill - A coding-agent skill for multi-phase security audits with in
GitHub热门项目 | A coding-agent skill for multi-phase security audits with independently verified, machine-readable findings | Stars: 3,004 | 29 stars today | 语言: JavaScript
开发者
The Optimization That Was Too Good: Why Our Push Notifications Only Worked When You Weren't Looking
This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry. When I was...
AI 资讯
How We Handle Client-Side CSV Merging Without Server Processing
When merging CSVs in the browser, handling mismatched columns and quoted cells changes everything. Here's how filetools does it. Last week we shipped CSV merge/split/transpose tools for filetools, and the most interesting challenge wasn't CSV parsing - it was handling real-world data without a server. Here's how we handle the hard cases. The Problem: CSV files in the wild are messy. Columns don't always match. A cell value contains a comma and that comma is quoted. Headers are sometimes case-sensitive, sometimes not. When you build on a server, you can run a fast library and stream the result. In the browser, you have to make your merge operation deterministic from first load. Our approach: Column matching: Users specify which columns to merge on (e.g., "id" or "email"). We do a case-insensitive first pass, then check for exact matches. If no match exists, we warn the user and ask them to pick from the detected headers. This upfront clarity saves merge errors later. Quoted cell handling: We follow RFC 4180 strictly - a quote inside a quoted field is escaped as a double quote. Most CSV parsers get this wrong when they're quick. We use the csv-parse library (MIT) vendored into the site, same way we do with PDF and ZIP libraries. Column order: The merge operation respects column order from the first file, then appends any new columns from subsequent files. This is deterministic and reproducible. Why this matters for a browser tool: Server-based CSV tools hide their assumptions - you upload, they merge, you download. If a merge fails, you get an error message and no insight into why. Client-side, the user can see the detected headers, approve or correct them, and re-try immediately. That transparency matters when you're dealing with data that represents real records or transactions. What shipped this week: We added merge, split (by row count or column value), transpose, and comparison tools. The same deterministic, transparent approach applies to each one. Next question
AI 资讯
Four places ffmpeg.wasm fails silently in a Next.js app (and the fixes)
I shipped four browser-only video tools with ffmpeg.wasm: trim, compress, video-to-GIF and MP3 extraction. Files never leave the browser, nothing to install. Trim · Compress · GIF · MP3 (Korean UI, but the buttons are obvious) Getting there, I hit four walls. Every one of them surfaced as a single "conversion failed" line in the UI and nothing in the console . Writing them down for the next person. Stack: Next.js App Router + webpack, @ffmpeg/ffmpeg 0.12, self-hosted core. 1. webpack hijacks the dynamic import inside the worker @ffmpeg/ffmpeg spawns its worker like this: new Worker ( new URL ( " ./worker.js " , import . meta . url ), { type : " module " }); webpack recognises the pattern and bundles the worker. Fine. But it also rewrites the import(coreURL) inside that worker to go through its own module loader. The core URL arrives at runtime as a blob: URL, which webpack's loader has never heard of, so it dies with Cannot find module 'blob:...' . The error is thrown inside the worker, so the main-thread console stays empty. Fix: keep the worker out of the bundle. Copy node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js to public/ffmpeg/<version>/lib/ and pass it via classWorkerURL in load() . Now the untouched worker runs. 2. classWorkerURL needs the origin Passing a path like /ffmpeg/0.12.x/lib/worker.js is not enough. The library resolves it with new URL(classWorkerURL, import.meta.url) , and inside the bundle import.meta.url is a build-time file:///C:/... path. So it goes looking for file:///C:/ffmpeg/... and fails. const BASE = `/ffmpeg/ ${ FFMPEG_VERSION } ` ; await ffmpeg . load ({ coreURL : ` ${ location . origin }${ BASE } /core/ffmpeg-core.js` , wasmURL : ` ${ location . origin }${ BASE } /core/ffmpeg-core.wasm` , classWorkerURL : ` ${ location . origin }${ BASE } /lib/worker.js` , }); Prefix location.origin and it works. 3. You cannot build a GIF palette with -vf For decent GIF quality you run palettegen first and paletteuse second. Doing it in one pass needs
AI 资讯
A rule-based Korean dialect converter with no LLM: eight guards that keep standard Korean intact
I built a Korean dialect converter that runs entirely in the browser and uses no LLM. Type a standard Korean sentence and it rewrites it the way people speak in Gyeongsang, Jeolla, Chungcheong, Gangwon or Jeju. Paste a dialect sentence and it converts back to standard Korean. Nothing leaves the browser. Try it: https://toolnjoy.com/dialect-converter (Korean UI) The hard part was not producing dialect. It was not breaking standard Korean . Every time I widened a rule, a perfectly normal sentence got mangled somewhere. Here is what I ended up with. Most of it applies to any rule-based text rewriter (honorific converters, profanity filters, spelling normalizers). Shape of the thing Four modules: file job core sentence splitting, sentence-type detection, ending replacement endings per-region ending tables (declarative / interrogative / propositive / imperative × politeness) words per-region vocabulary reverse dialect → standard Forward conversion goes "decide the sentence type, then pick the region's ending". Reverse goes the other way: "look at the ending, infer the sentence type". You cannot just invert the forward table, so reverse has its own table. Each reverse rule carries a type , which removes the detection step entirely. In reverse mode the user does not pick a region. I run all five and keep the one with the highest score (4 points per matching ending + syllable count of matched words). People who paste dialect usually do not know which region it is from. That is why they came. Eight guards, each from a real failure 1. Never reverse a form that appears on the standard side of any table. The Gyeongsang table had 함께 → 같이 (together). Reverse flipped it and rewrote "같이 보게" into "함께 보게", except 같이 is perfectly standard. Fix: anything that ever appears in a standard column anywhere is excluded from reverse candidates. 2. Question endings only count when there is a question mark or a wh-word. Gyeongsang -나 / -노 mark questions. But 하나 ("one") also ends in -나 . Without