# Syntackle > Syntackle is a developer blog by Murtuzaali Surti, featuring technical articles on web development, JavaScript, React, Node.js, CSS, DevOps, AI, and modern development tools. Built with Eleventy (11ty) static site generator and deployed on Vercel. Syntackle covers a wide range of topics for developers, including tutorials, guides, opinions, and news about web technologies. The blog emphasizes practical, hands-on content with code examples and real-world applications. Topics span frontend and backend development, with particular focus on: - **JavaScript & TypeScript**: Core language concepts, frameworks, and best practices - **React**: Hooks, components, state management, and ecosystem tools - **CSS & Styling**: Modern CSS techniques, Sass/SCSS, animations, and design patterns - **Node.js & Backend**: Server-side development, Docker, databases, and deployment - **AI & Developer Tools**: AI integration, VS Code extensions, productivity tools - **Static Site Generators**: Eleventy (11ty), Astro, and related tooling The site is built using Eleventy (11ty), with Gulp for asset processing, Rollup for JavaScript bundling, and Pagefind for search functionality. Content is written in Markdown and rendered through Nunjucks/Liquid templates. Creator: [Murtuzaali Surti](https://murtuzaalisurti.com) ## All Posts This section contains the text content of all published blog posts on Syntackle for LLM consumption. --- ## Stacked PRs in GitHub, Gemma 4 on Mac & Kimi K3 - The Weekly Diff #8 - **URL**: https://syntackle.com/blog/github-stacked-prs-gemma-4-on-mac-kimi-k3-the-weekly-diff-8/ - **Updated On**: August 1, 2026 - **Description**: GitHub introduces stacked pull requests, Gemma 4 uses roughly 2 GB of memory for weights and KV cache on Apple Silicon, and Kimi K3 offers an unusual open-weight model architecture. Portable AI sessions, PostgreSQL 3D visualization, and why faster coding does not always produce better software. - **Tags**: post, postgres, AI, github, mac, apps, git, news, sql, opensource, weekly-diff, opinion - **Author**: Murtuzaali Surti Table of Contents GitHub Introduces Stacked Pull Requests GitHub has put stacked pull requests into public preview, but what are stacked PRs exactly? A stack is an ordered set of pull requests where each layer targets the one below it, so a large change can be split into small, focused pieces without manually maintaining a pile of dependent branches. You can create and manage stacks from GitHub.com, the GitHub CLI, the mobile app, or a coding agent using GitHub's stack skill. The CLI extension is available with: gh extension install github/gh-stack AI coding tools have made large changes easier to produce, which has shifted the bottleneck to review. A large generated diff still needs human understanding. Smaller dependent changes give reviewers a clearer path through the work than handing an AI reviewer an even larger diff. Tools such as Graphite and ghstack have been making stacked changes workable for years. GitHub is integrating that workflow with the code hosting, review policies, and checks most teams already use, which removes a lot of adoption friction. It has received strong praise, though early users reported rough edges around squash merges, re-approvals, stale local branches, and merging an entire stack. The preview currently stays within a repository, so teams that need cross-fork stacks will still need another tool. Gemma 4 on a 2 GB Memory Budget Source: Gemma 4 TurboFieldfare is a custom Swift and Metal runtime for running Gemma 4 26B-A4B on Apple Silicon Macs. It keeps roughly 2 GB of model weights and KV cache in memory, on an 8 GB Mac. Gemma 4 uses a Mixture-of-Experts (MoE) design, where a router selects only a few specialized model modules for each token instead of running the entire model. TurboFieldfare takes advantage of that design with a shared 1.35 GB core and SSD streaming for the modules it needs. Those modules are kept in a bounded cache, and the runtime uses Metal to do the work on the GPU. The runtime's 2 GB figure covers memory usage, the complete model installation still occupies about 14.3 GB on disk. The repository reports 5.1 to 6.3 tokens per second on an 8 GB M2 MacBook Air and 31 to 35 tokens per second on a 24 GB M5 Pro. It includes a native Mac app, a CLI, and an experimental loopback OpenAI-compatible server. The current runtime is text-only, and the server does not give tools permission to do anything automatically. A client still has to authorize and execute tool calls. INFO The project requires macOS 26, Metal 4, and Swift 6.2. It is a research runtime with a narrow platform target, so it cannot replace MLX or llama.cpp on older Macs. What I find interesting is the design constraint. The runtime treats the SSD as a slow extension of memory and makes the model's sparse modular design work harder. It accepts the hardware you have and redesigns the runtime around its memory hierarchy. It would be great to know about SSD wear, sustained thermals, and how much of the result comes from techniques that ordinary memory-mapped runtimes already use. Those factors decide whether the project is suitable for daily use rather than an impressive demo. Kimi K3's Smarter Design Kimi K3 is a new open-weight model from Moonshot, and it is enormous. Moonshot's model card lists 2.8 trillion total parameters. Like Gemma 4, it uses a Mixture-of-Experts design, so it runs only a small slice of itself for each word, and activates 104 billion parameters at a time rather than all 2.8 trillion. Sebastian Raschka's architecture notes walk through the changes, and the pattern behind them is a willingness to rethink areas which are otherwise known to be fixed. Some of them are about running cheaper, replacing standard building blocks with lighter versions that do the same job with less compute, while others question the defaults entirely. The best example is the word order. Most models tag every word with a small signal that says where it sits in the sentence. Kimi K3 removes that signal and lets other parts of the model track word order on their own. That is a strange choice for a model this large, and it only works if the rest of the design carries the weight. Moonshot reports the sparse mixture-of-experts setup is roughly 2.5 times more efficient at scaling than the previous Kimi K2, which is how a 2.8 trillion parameter model stays practical to run at all. K3 also ships with a 1-million-token context window and native support for images and video, both of which help with long coding sessions and large repositories. It holds up on the benchmarks too. On coding and agentic tasks such as SWE-Marathon, Terminal-Bench, and BrowseComp, Kimi K3 lands in the same range as leading closed models like Claude Opus 4.8 and GPT-5.6 Sol, and edges ahead on a few. Training a bigger model on more data is no longer the whole story, since a lot of the recent gains come from rethinking the pieces inside the model rather than only scaling them up. Whether those swaps hold up, or how much you can trust an architecture you cannot fully reproduce, remains to be seen. The issue is, open weights let you download and fine-tune the model, though you still do not get the original training data or the exact recipe used to build it. PGSimCity Makes PostgreSQL Look Like a Game Source: PGSimCity PGSimCity is an independent, non-commercial visualization of PostgreSQL internals in 3D. It turns processes such as the checkpointer, background writer, autovacuum, WAL writer, and client backends into a city that you can explore in the browser. I like this kind of explanation because database internals are usually presented as a collection of boxes and arrows. A visual model gives those parts a place in your head. You may not remember every detail of MVCC after one visit, but you are more likely to remember that several background processes are constantly doing work around the query you just ran. However, the project labels itself an early, unreviewed prototype and openly warns that its explanations may contain inaccuracies. That disclaimer is important for an educational tool about a system as subtle as PostgreSQL. I love the idea, and the improvement I would want is to submit a query, watch it move through the system, pause it, and inspect the relevant process without having to decode a busy city first. Vendor Lock-In of AI Sessions Source: Earendil "The Session You Cannot Take With You" by Earendil argues that AI providers are quietly moving the contents of your sessions onto their own servers, leaving you holding a receipt instead of the conversation. The original inference API was easy to understand. Send input, receive output, and keep the conversation yourself. That record could be inspected, archived, replayed, or handed to another model. Modern agent APIs increasingly add provider-bound state: encrypted reasoning, hosted search results, opaque context compaction, hidden subagent messages, file and vector-store references, and response IDs that only the original provider can resolve. The author proposes five practical tests for ownership: inspection, export, replay, audit, and deletion. Can you see what the model saw? Can you export a self-contained record? Can another implementation continue from it? Can a human explain why an action happened? Can you find and delete every server-side copy on which the session depends? Those questions are more useful than arguing about whether a particular provider is good or bad. To debug an agent that changes the wrong file, you need to inspect the instructions, tool results, hidden summaries, and delegated tasks that led it there. Provider-managed state is reasonable when it makes an agent faster or cheaper to continue and the user still has a readable handoff. An exportable tool log and an audit trail for subagent messages should exist alongside an encrypted fast path for the provider's own models. If Coding Is Solved, Why Does Software Keep Getting Worse? Source: ptrchm.com “Nothing Works and Everyone Is Euphoric” by Piotr Chmolowski asks why everyday products keep getting worse when coding is supposedly solved. A banking app needs several Face ID attempts before 3D Secure appears. Slack steals focus from a terminal at exactly the wrong moment. A warranty form fails after the final step with no indication. A car interface becomes slower and less reliable after an update. Software has grown more complex, product teams are measured on quantitative features, and bug-fixing work rarely looks exciting in a quarterly presentation. AI can make that problem worse by increasing the amount of code and the speed at which it arrives without improving what's already present. Teams should spend some of the capacity AI creates on the work they avoid: reducing complexity, fixing old bugs, testing real user flows, and removing features that are not meant to exist. AI models cannot tell if stability matters more than another redesign, unless the people running the project make that decision. Organizational misalignment, distributed-system complexity, weak ownership, and bad incentives were present even before coding agents arrived. AI gives those systems a faster way to produce more of the same mistakes. The decline in quality occurs when those systems are fueled by AI even more than before. --- ## Why Some AI Models Work Great On Low Effort And Others Don't - **URL**: https://syntackle.com/blog/why-some-ai-models-work-great-on-low-effort-and-others-don-t/ - **Updated On**: July 27, 2026 - **Description**: In this post, I explain what effort levels actually are, why more thinking costs more money, why some models are great on low effort while others fall apart, and what really happens when you pick an effort level in Claude Code, opencode, or Codex. - **Tags**: post, AI, workflow, guide, opinion - **Author**: Murtuzaali Surti Table of Contents Every coding agent I use has a setting for this now. Claude Code has /effort, Codex has model_reasoning_effort, and OpenCode has /variants. In opencode I cycle through variants with a keybind. Low, medium, high, and then something at the top like xhigh or max. And for the longest time I had no idea what it was doing when I turned that up. My assumption was that I'm giving the model more brainpower. More CPU, a bigger engine. Higher effort, smarter model. That's what the word "effort" makes you think, right? Nope. The model doesn't get bigger or faster or smarter. Every layer, every weight, all of it stays exactly the same. The only thing that changes is how long it talks to itself before it answers you. Once it clicked for me, a bunch of stuff I had been confused about for a while made sense. Things like, "Why my cheap fast model sometimes keeps up with the expensive one?", "Why cranking effort to max sometimes makes things worse?", and "Why the same setting acts completely different depending on what's behind it?" started making sense. What An Effort Level Actually Is Firstly, what does "thinking" even mean here? When you ask a reasoning model something, it doesn't answer you straight away. It writes out a pile of text you sometimes see (depending on the harness/agent you use), working the problem, second-guessing itself, trying something and backing out of it. That's the "thinking", or the reasoning trace. Then it writes the answer you actually get based on that. So thinking is the model writing more text before the text you asked for. That's all it is. Which means an effort level controls how much of that scratch text gets written. The interesting thing is how it controls it, and this is where I was wrong. I figured there was a real limit in there somewhere. A counter, a ceiling, something in the code cutting it off. There isn't. And we know there isn't, because OpenAI released a model family openly (gpt-oss) and you can just go look at the plumbing. The effort level is a line of English in the system prompt: Reasoning: high Their model card says it plainly, that they "train the models to support three reasoning levels: low, medium, and high. These levels are configured in the system prompt by inserting keywords such as "Reasoning: low"." The model reads "Reasoning: high" the same way it reads everything else you sent it. Nothing special fires in the code. It just goes along with the instruction, because during training it saw thousands of examples where those words came before long, careful thinking. It's closer to telling a coworker "hey, take your time on this one" than to turning up a thermostat. Anthropic's older API had budget_tokens, where you pass a real number, which really sounds like a hard limit. Buried in those same docs is a line about how changing that number breaks prompt caching "because the budget value is rendered into the prompt." So you thought you were setting a limit, but in fact, you were editing text. INFO Anthropic has since dropped token budgets in favor of a named effort setting, and their docs are blunt about what it is: "Effort is a behavioral signal, not a strict token budget." Google went the same way, moving from a raw number to named thinking levels in Gemini 3, where the old thinking_budget still works but is now called legacy. Turns out the numbers made it look more exact than it ever was. Why It Can't Be A Hard Limit Why don't the labs just enforce it properly? Count the tokens, stop at the limit, done. Two reasons, and the first one really surprised me. Models can't count their own tokens. The people behind the s1 paper actually tried training a model to hit exact token targets, asking for a thousand tokens of thinking, then sixteen thousand. What came back was roughly the same length every time. There's no odometer running in there. The model has no more idea it's on word 4,000 of 8,000 than you know which syllable you're on right now. So they tried limiting steps instead, and the model got around that almost immediately. When told to use few steps, it wrote a handful of huge ones. Told to use many, it wrote a pile of tiny ones. Same amount of thinking either way. The paper's own words are that the model "learns to hack its way around the compute constraint." The other reason is that a hard cutoff would be useless anyway. Stop a model mid-thought and you don't even get a shorter answer, you get no answer. So every lab landed on the same design. Train the habit, don't enforce the limit. Teach it to be short or thorough when asked, then hope for the best. Which is why these settings feel a bit fragile in practice. You're leaning on a habit, not flipping a switch. Anthropic's docs even mention in passing that Opus 4.7 "respects effort levels more strictly" than Opus 4.6, and nothing about the API changed between the two. They just got better at training it to listen. Why More Thinking Costs More Money Those hidden thinking tokens are still tokens. You pay for them at the output rate, same as the answer, they just never get shown to you (although you can see thinking in some harnesses). So a model that thinks for three pages before writing one paragraph bills you for three pages and a paragraph. More thinking means more waiting. When an agent is coding for you and the harness is looping the model over and over, the gap between low and high can easily be three times the wait (per turn). In a loop that might run for dozens of turns. Artificial Analysis makes this easy to see, because they now score every effort level as if it were a separate model. Here's Claude Opus 5 across all five of its settings: Effort Intelligence Index Cost per task low 51 $0.36 medium 56 $0.62 high 59 $1.06 xhigh 60 $1.56 max 61 $2.03 Low to high buys you 8 points and roughly triples the bill. High to max buys 2 more points and nearly doubles it again. The bottom of the ladder is where the value is, and the top is where the money goes. Anthropic isn't hiding this either. In Artificial Analysis' writeup of the launch, the spread across Opus 5's effort settings on one agentic benchmark came to "407 Elo points, with output token usage ranging around 8x from low to max effort." Eight times the tokens between the cheapest setting and the dearest one, on the same model, doing the same work. And then there's a third cost. Since effort is baked into the prompt, switching it mid-conversation wipes your prompt cache. Anthropic spells this out, that "because effort shapes the rendered prompt, changing it between requests does not preserve cached prefixes from earlier turns." So if you get clever and drop to low effort for the easy turns of a long session, you can end up paying more, because every switch throws the cache away. Pick a level, and stay there. CAUTION Be suspicious of the top of the ladder. Anthropic's own docs say that for max, "on most workloads max adds significant cost for relatively small quality gains, and on some structured-output or less intelligence-sensitive tasks it can lead to overthinking." OpenAI says roughly the same about their highest levels, telling you to use them "only... when your evals show a clear benefit that justifies the extra latency and cost." When both vendors are telling you not to max out their own product, that's worth listening to. Why Some Models Are Great On Low Effort Okay, the actual question. If thinking is where the reasoning happens, a model that barely thinks should be way worse than one that thinks a lot. And yet I notice some models perform better with thinking turned way down, while some fall apart. Two things going on here, and they're related. The thinking already happened Some labs literally train the fast mode on the slow mode's homework. NVIDIA's Nemotron 3 Nano does it by cutting the thinking short on purpose during training. They "randomly truncate 3% of reasoning traces to different reasoning budgets, before continuing with the original post-reasoning response", so the model gets practice at producing the full answer off a half-finished thought. DeepSeek's V4 work goes further and trains separate models for different effort levels, each with its own length penalty, then combines them all into one through distillation. Either way, the long careful thinking happened once, back in training, and got squashed into the weights. Think of a senior engineer glancing at a stack trace and naming the bug in four seconds. They're not skipping the reasoning. They did that reasoning years ago across a few hundred similar bugs, and now it's instinct. A junior dev looking at the same trace has to talk it through, check the docs, walk the call stack, and get to the same place out loud. Some models never needed the trace much anyway The second thing is a pattern that's hard to unsee once you've spotted it. The worse a model is, the more it gains from higher effort. The better it already is, the less the setting does. GPT-5.6 makes this easy to check, because OpenAI shipped it in three sizes and Artificial Analysis scored every one at every effort level. Same generation, same training, same ladder. The only real difference is how big the model is: Model Low effort Max effort Gain GPT-5.6 Luna (smallest) 33 51 +18 GPT-5.6 Terra 40 55 +15 GPT-5.6 Sol (biggest) 49 59 +10 Claude Opus 5 51 61 +10 The smaller the model, the more the effort setting is carrying it. Luna nearly doubles its score across the ladder. Opus 5, which starts where Luna finishes, gains barely half as much. Because a small model at max effort costs about the same as a big model at low effort, you can compare them directly: Model and effort Intelligence Index Cost per task GPT-5.6 Luna (max) 51 $0.21 Claude Opus 5 (low) 51 $0.36 GPT-5.6 Sol (low) 49 $0.20 Same score, and the small model thinking hard is the cheaper way to get there. Model size and effort are two separate knobs that partly do the same job, and past a point it's cheaper to buy the thinking than the parameters. For a strong model, thinking is a small fix on top of good instincts. For a weak one, thinking is where most of the work happens, so take it away and you've taken away most of the model. Which gives you a handy rule when you're picking a model. If something needs high effort before it's usable, that's not really about the effort setting. The ability isn't in the weights, and you're paying tokens every single turn to make up for it. Anthropic pointed out that Opus 4.5 at medium effort "matches Sonnet 4.5's best score on SWE-bench Verified, but uses 76% fewer output tokens." When High Effort Makes Things Worse A paper from Anthropic Fellows and Anthropic staff built tasks where thinking longer drives accuracy down, and found that different model families fail in their own particular ways. Claude models get "increasingly distracted by irrelevant information". OpenAI's o-series models "resist distractors but overfit to problem framings", which means latching onto an approach they've memorised that doesn't fit the actual question. A separate group of researchers, in a paper actually called "Do NOT Think That Much for 2+3=?", found reasoning models burning silly amounts of thinking on basic sums, working out the same answer over and over, writing a dozen separate solutions to something a child could answer. On the easiest problems, almost none of those thinking tokens did anything useful. It's the junior dev handed a two-line fix and three days to do it. Work expands to fill the space you gave it. Anthropic ran this experiment on real users by accident, and the writeup is worth reading properly. In their April 2026 postmortem on Claude Code quality complaints, they explain that they'd lowered the default effort from high to medium to fix speed. Their internal tests said the quality drop was slight. Users noticed straight away and started reporting that Claude Code "felt less intelligent", and Anthropic admits "neither our internal usage nor evals initially reproduced the issues identified." They called it "the wrong tradeoff" and put it back. What I take from that is that benchmarks average away exactly the rare, hard work where effort matters most. Your own gut on this is better evidence than a leaderboard. What Actually Happens When You Pick An Effort Level So you type /effort high in Claude Code. What actually leaves your machine? I went digging through the source of these tools rather than guess, and the answer is almost disappointingly small. One string changes in the request. Usually nothing else. In opencode the system prompt gets put together before your chosen variant is even looked up, and the variant only ever gets merged into the options. It never touches your prompt or your tool definitions. Codex builds its instructions and tools from the prompt, then works out the reasoning setting separately in a function called build_reasoning. Claude Code writes one field and leaves everything else alone. opencode has to translate, and you can read the whole mapping in reasoningEffort() in transform.ts. "high" needs to become reasoning.effort for OpenAI, output_config.effort for Anthropic, thinkingConfig.thinkingLevel for Google, and something else again on Bedrock. Same word you picked, four different shapes going out. Codex is tidier, since it only talks to one API. Its ReasoningEffort enum is worth a look though, because it carries more levels than the docs admit to, including Ultra and a Custom(String) catch-all for effort levels the server knows about and the client doesn't yet. Effort settings don't carry over across providers, and going by Anthropic's own advice, not even across model generations. The high you carefully tuned for one model is a different amount of thinking on another. Swap models while keeping your settings and you've quietly changed two things at once. The magic words are just words now. Claude Code's ultrathink doesn't set a budget anymore. In the current build it adds a note to your message saying the user asked for deeper reasoning this turn, and that's all it does. think harder and megathink, which used to map to different budgets, don't exist at all. Claude Code ships minified so there's no source to link, but the model config docs cover how /effort, the --effort flag, CLAUDE_CODE_EFFORT_LEVEL, and the effortLevel setting all stack up. The very top of the ladder isn't really an effort level. Claude Code's ultracode sets effort high and adds instructions changing how it hands work to subagents. Anthropic's own slash command reference gives it away, noting that "Ultracode is not a distinct level and reports as xhigh." Codex's ultra does something similar, sending max and switching subagents from opt-in to on by default. Those two are modes dressed up as effort levels, which is why they feel different rather than just slower. And the prompt does get rewritten, only not on your computer. Anthropic says effort "shapes the rendered prompt" on their end. Your client sends a short string, and that string becomes prompt text inside their systems. Same trick gpt-oss showed us in the open, you just can't watch it happen. Pro Tip Before you touch the effort knob at all, check something else. In their GPT-5 prompting guide, OpenAI reported that they "observed Tau-Bench Retail score increases from 73.9% to 78.2% just by switching to the Responses API and including previous_response_id to pass back previous reasoning items into subsequent requests." Four points, for doing nothing except letting the model keep its train of thought across tool calls. Codex does this by re-sending encrypted reasoning blobs. Anthropic's postmortem describes the opposite, a bug that dropped earlier thinking every turn, which made Claude seem "forgetful and repetitive" and left it "increasingly without memory of why it had chosen to do what it was doing." If your setup is dropping reasoning between turns, you're running at low effort no matter what you picked. How I Pick Now I've stopped treating this as a quality slider and started treating it as a question about the task. Medium is my default and I mostly leave it alone. The jump from medium to high is the worst value on the ladder, costing a lot more for a small gain on most work. Both OpenAI and Anthropic now say roughly this in their own docs, which is telling, given they'd both make more money if I maxed everything out. I go up when the work needs real step-by-step reasoning. A nasty debugging session where the cause is nowhere near the symptom, a migration touching many layers, anything I'd sketch on paper before starting. I go down for mechanical stuff like renames, boilerplate, formatting, simple lookups. One habit that's saved me more than any amount of effort tuning is that if a model is going in circles at high effort, the fix is almost never more effort, it's a smaller task. Both the research and my own experience point the same way, that breaking work into separate focused turns beats asking one turn to think harder. Better tools and clearer instructions beat it too. Sometimes, fixing the setup around the model wins over turning up the model. Conclusion Effort levels look like plumbing and behave like a suggestion. You're changing a phrase in a prompt that the model was trained to take seriously, and it answers by writing more or less to itself first. Which is why the same setting acts so differently from one model to the next. How much the thinking helps comes down to whether the ability got baked into the weights during training, or has to be worked out loud every single time. Models that did their thinking during training barely need it. The ones that didn't can't work without it. Not unlike what I found digging into context windows, where a big number on the spec sheet turned out to say very little about whether the model could actually use it. So pick a level on purpose and leave it there, instead of letting it drift or trusting whatever the default happens to be this month. And before you reach for max on everything, run the same task twice, once low and once high, then compare what the second one cost you. Going by the numbers above, there's a fair chance you're paying double for two points. CAUTION The internals here move fast, especially the CLI behavior. Effort ladders gain and lose levels, defaults change between releases, and Claude Code's magic keywords have already been rewired at least once. Everything above reflects the tools and docs as of writing, so check your own version's changelog before leaning on any specific detail. --- ## TypeScript 7, $10,000 AI Slop Removal and Claude Code's Token Bloat - The Weekly Diff #7 - **URL**: https://syntackle.com/blog/typescript-7-10k-ai-slop-cleanup-claude-code-s-token-bloat-the-weekly-diff-7/ - **Updated On**: July 23, 2026 - **Description**: TypeScript 7's native compiler lands, a firm charges $10,000 a week to delete AI generated slop, Clawk provides disposable Linux VMs for coding agents, and Claude Code sends 33k tokens before it even reads your prompt. - **Tags**: post, AI, opinion, news, typescript, claude, opensource, weekly-diff - **Author**: Murtuzaali Surti Table of Contents "LLMs are sponges that soak up everything you do and repeat it back to you." - Scott Robinson TypeScript 7 Is Here, and It's Fast TypeScript 7 is a native port of the compiler, rewritten in Go, and it lands somewhere between 8x and 12x faster on full builds. The team's own table has the VS Code codebase dropping from 125.7s to 10.6s, and bumping the new --checkers 8 flag pushes that to 7.5s, close to a 17x speedup. Memory use went down at the same time, and that usually doesn't happen when you chase raw speed. The old pain of opening a large project and waiting for the language server to wake up is mostly gone. Opening a file with an error in the VS Code repo went from about 17.5 seconds to under 1.3. Slack's engineers reported type checking in CI falling from roughly 7.5 minutes to 1.25, and said local type checking in the editor went from effectively unusable at their scale to feasible again. The TypeScript team is explicit that the port is done "as faithfully as possible," keeping the structure and logic of the original so the results stay consistent between the two compilers. A couple of things worth knowing before you npm install -D typescript@next and expect wonders. TypeScript 7 adopts 6.0's stricter defaults and turns a pile of deprecated flags into hard errors, so target: es5, baseUrl, and the classic module resolution modes are gone. strict is now on by default. Also, TypeScript 7 doesn't ship a stable programmatic API yet. That means tools that embed the compiler still lean on 6.0, with webpack loader authors and others noting they're stuck waiting on 7.1 for the API. If you work in Vue, Svelte, Astro, or MDX, the guidance is to keep using 6.0 in the editor and run 7 at the CLI for fast project wide checks. Claude Code Sends 33k Tokens Before It Reads Your Prompt When you ask Claude Code to reply with a single "OK," it sends roughly 33,000 tokens of system prompt, tool schemas, and injected scaffolding before your actual prompt even arrives. OpenCode, running the same model on the same machine, sends about 7,000 for the same request. That's a 4.7x gap, and most of it, roughly 24,000 of Claude Code's tokens, is tool definitions for an entire background agent and orchestration suite you probably aren't using on a one line reply. The way the authors arrived at it is that they spliced a logging proxy between each harness and the model endpoint and captured the exact request payloads, which a gateway can't distort. Claude Code rewrites tens of thousands of cache tokens (prompt tokens) mid-session, run after run, at one point writing up to 54x more cache tokens than OpenCode on an identical task. Cache writes bill at a premium. A 72KB CLAUDE.md from a real repo adds another ~20,000 tokens to every request, and five modest MCP servers add 5,000 to 7,000 more. By the time you fire your first request, it can be 75,000 to 85,000 tokens deep, even before you've typed a word. I want to be fair about the nuance the study itself is careful to include, because it's easy to turn this into a hit piece, and it isn't one. Both harnesses completed every scored task correctly, so this is the cost of an identical outcome, not proof that one produces worse code. On a multi step task, Claude Code actually came out lower on total tokens, since it batches tool calls into fewer round trips while OpenCode repays its smaller baseline turn after turn. That advantage didn't hold when the task was re-run on a newer model though, which tells you the batching win is model behavior rather than a fixed property of the harness. This connects directly to what I wrote about in the last issue, where Claude Code was caught steganographically fingerprinting prompts. A proprietary harness is a black box doing more than you can see, and the rational response is to own more of the stack. If your usage meter climbs faster than the work seems to justify, this is very likely why, and it's a solid reason to try running your models through something leaner like OpenCode with your own API key or Copilot with a custom key. I keep a running list of open source coding agents worth trying for exactly this reason. A Team That Charges $10k a Week to Delete Your AI Slop Slow down. This is the story that made me laugh and then made me think. A team of three senior engineers launched Slopfix, a service that refactors vibecoded codebases back to maintainability. One week, three senior engineers, $10,000. They commit to a line reduction target up front and charge you in proportion to how much of it they hit. What makes this more than a gag is the mechanism, since it's clearly built by people who've done real cleanup work. Before touching anything they write out exactly what the app does screen by screen and endpoint by endpoint, and that checklist becomes the safety net for a refactor that's mostly deletion. You keep the smaller codebase, the QA checklist, and a set of guardrails, a CLAUDE.md, lint rules, and CI checks, meant to slow the slop down when you go back to building. They admit they use Claude Code too, on a very short leash, and the line that stuck with me was, the difference is thirty years of combined experience about what maintainable code looks like, "and the agent doesn't get a vote." The discussion had the best one-liner of the week, from someone recalling their barber, "you don't pay me for what I cut, you pay me for what I leave behind." The skeptical takes were interesting too. If a client can't specify what their app does, how do they sit through the screen by screen inventory that the whole method depends on, and what stops the codebase from filling back up with slop the moment the engagement ends? A business can now exist purely to undo the output of the "tests pass, ship it" workflow, which tells you that the workflow is producing enough garbage to sustain a market. That's the natural consequence of the problems with AI-generated code I've written before, and it's a neat transition into the next piece below, which is really about how you avoid ever needing to call these guys. Write Code Like a Human Will Maintain It On an AI-built project, Scott Robinson caught himself getting lazy. He needed the same access check in four places, a route handler, a background job, an API endpoint, and a webhook. Each time, he'd describe what he wanted, the model would hand back a working four-condition if statement, and he'd merge it. The code worked, the tests passed, and he wasn't the one who'd have to touch it again, so why bother extracting a shared helper. The LLM doesn't write in a vacuum. It reads your codebase, the open files, the existing patterns, and the recent changes. Every shortcut you merge is a signal about how things are done here, so the next time you ask for an endpoint with the same access rules, the model doesn't start from first principles, it starts from the four copies already sitting in your repo. Ask for a fifth and you get a fifth duplicated conditional. Ask for a refactor and the model faithfully preserves all five, because that's your style now. As he puts it, "LLMs are sponges that soak up everything you do and repeat it back to you." The line that reframed it for me was that he thought he was outsourcing maintenance to the LLM, when what he was doing was training it to have bad habits. That's the trap in one sentence, and it's the mechanism underneath the whole AI slop cleanup economy from the previous piece. The slop isn't a one-time accident you can prompt your way out of later. It compounds. LLMs don't produce maintainable code unless you slow down and keep caring about the output. And it's the same argument I made in the problem with AI-generated code and back when vibe coding first became a thing. LLMs will cheerfully help you dig a hole you'll pay someone $10K to climb out of. Give Your Coding Agent a Disposable VM, Not Your Laptop Clawk gives coding agents a disposable Linux VM instead of direct access to your laptop, with a simple CLI to forward ports and allowlist the network, so an agent can do its work inside a sandbox you can throw away rather than in the same environment that holds your SSH keys, your .env files, and your shell history. This landed the same week people were passing around reports that xAI's Grok CLI uploads your entire repository, every tracked file plus git history, regardless of what the agent reads, and a separate account of Grok reading a user's whole home directory. When native coding agents are shipping that kind of behavior, "run it in a box that can't hurt you" stops being paranoid and starts being basic hygiene. Why not just use a Docker container? The honest answer is that a VM buys you stronger isolation than a container for something you actively expect to misbehave, though everyone's confident right up until the agent finds a container/VM escape hatch exploit. Enough developers are independently building disposable-VM wrappers for coding agents that it's clearly becoming a default rather than a niche, and that shift says a lot about how much we trust these tools with unrestricted access. --- ## Inference vs Harness: Why Every AI Lab Now Ships Its Own Harness - **URL**: https://syntackle.com/blog/why-every-ai-lab-now-ships-its-own-harness/ - **Updated On**: July 27, 2026 - **Description**: AI models are becoming a commodity while the "harness" wrapped around them turns into the real moat. I break down what inference and a harness actually are, how they differ, and why every AI lab is now shipping its own, using a self-driving car analogy. - **Tags**: post, AI, guide, opinion - **Author**: Murtuzaali Surti Table of Contents Almost every conversation about AI tools starts and ends with the same question, "which model is the best"? We argue about benchmark scores, we watch the leaderboards, and we switch tools the moment a new frontier model drops. Here's the thing though, the model tells you only one part of the story. The other part is something called the harness, and lately that's the half the entire industry is quietly fighting over. Anthropic, OpenAI, Google, and even the smaller Chinese labs are all racing to ship their own harness, and the reason has very little to do with raw intelligence and a lot to do with business. So let me break down what inference and a harness actually are, how they differ, and why owning the harness has become the real moat. The Self-Driving Car Analogy Picture a self-driving car. The engine is the raw power. It turns fuel into motion, and a better engine means more speed and more capability. In terms of AI, the model running is the engine turning over. That's inference, the model actually thinks in real time to answer you. But, an engine alone doesn't make a difference. Around it you need a steering system, brakes, sensors, cameras, a map, and the software that reads the road and decides when to turn, when to stop, and when it's safe to change lanes. That whole apparatus, everything that turns raw horsepower into a car that can actually get you somewhere without crashing, is the harness. Two things fall out of this picture. A monster engine with no steering or brakes is useless, and a brilliant self-driving stack wrapped around a lawnmower engine won't win a race either. You need both. And the part most people miss, is, the engine is tuned for that exact chassis. What Inference Actually Is There are two big phases in a model's life. Training is when the model learns, consuming mountains of data to shape its weights. That happens once (per version) and costs a fortune. Inference is everything after that, every single time the model runs to respond to you. When you send a prompt, the model reads your input and generates the output, one token at a time. Every token in and every token out is a bit of compute running on a GPU somewhere. That's inference, and it's what you're really paying for when you pay per token or burn through your subscription credits. Now here's the business aspect. Selling inference is selling tokens, and tokens are turning into a commodity. Open-weight models like DeepSeek, GLM, and Qwen have caught up close enough, that for a lot of everyday tasks, one model is roughly as good as another. DeepSeek's API costs roughly a quarter of what comparable models charge, and they've published the tricks (like speculative decoding) that let them run inference that cheaply. When the thing you're selling is basically the same as your competitor's and is getting cheaper every quarter, you're in a price war. That's a rough business to be in, and it's exactly why the labs went looking for a different place to make their money. Cheaper, faster inference is genuinely great for us as users. It's just a harder place to build a defensible business, since there's always someone willing to sell the same tokens for less. That said, I don't want to paint inference as a bad business, because it isn't. It just wins on a different axis. It's a volume game, not a differentiation game. Nvidia's Jensen Huang has spent much of 2026 reframing data centers as "AI factories" whose whole purpose is to churn out tokens, and his bet is that demand for inference is about to explode as agents run longer and reason more before they answer. When a single coding task chews through millions of tokens, a falling per-token price doesn't shrink the market, it grows it, because cheap tokens get used far more freely. Inference is also where AI actually gets monetized day to day, since training is a cost you pay once and inference is the meter that runs every time someone uses the thing. And serving those tokens cheaply is a moat of its own. Whoever runs inference most efficiently at scale, through custom silicon, smarter batching, and tricks like speculative decoding, keeps a margin nobody else can match. Seen that way, DeepSeek's rock bottom pricing isn't a weakness, it's a weapon. So inference is closer to running an electricity grid than selling a boutique product, a brutal, high volume business where efficiency decides who wins. The catch for the model labs is that inference doesn't hand you much pricing power or a direct relationship with the user, and that's exactly the gap the harness fills. What a Harness Actually Is A harness is the entire system wrapped around the model that turns it from a text generator into something that gets real work done. The cleanest definition I've seen comes from LangChain, and that is, "Agent = Model + Harness. If you're not the model, you're the harness." The harness is everything that isn't the weights: The tools the model can call, like read, write, edit, bash, grep, and glob. The memory that persists what it learned across sessions. The system prompt and the rules about what it's allowed to do without asking. Context management, deciding what to keep, what to compact away, and what to feed back in on each turn. Sub agents, sandboxes, permission policies, and the loop that ties it all together. Mechanically, that loop is almost dumb. The model asks to run a tool, the harness runs it, the result gets fed back, and the loop repeats until the task is done. Anthropic literally describes their runtime as a "dumb loop" where all the intelligence sits in the model. The craft is in everything the loop manages, and that's what decides whether your agent finishes the job or burns a hundred thousand tokens going in circles. When you use Claude Code, Codex, Cursor, or OpenCode, you're using a harness. What you're really choosing is the car. You Can't Just Swap Engines Here's where it gets interesting, and where the business angle really starts. You'd assume you could take the best model, drop it into any harness, and get the best result. You can't, at least not cleanly. Modern models are post-trained against a specific harness. During that final training stage, the model learns one exact tool vocabulary, one schema shape, one way of formatting a plan, one memory ritual. Those habits get baked into the weights. The clearest example comes from Cursor's harness team, quoted in Nicolas Bustamante's writeup on model-harness-fit: "OpenAI's models are trained to edit files using a patch-based format, while Anthropic's models are trained on string replacement. Either model could use either tool, but giving it the unfamiliar one costs extra reasoning tokens and produces more mistakes. So in our harness, we provision each model with the tool format it had during training." Feed a model the wrong tool format and it doesn't fail outright, it just quietly gets worse, spending more tokens and making more mistakes. The wire format is effectively part of the model itself. And the numbers back this up. On Terminal-Bench 2.0, a benchmark for coding agents, the same Claude Opus model scored around 4.5 points apart depending only on which harness ran it. LangChain reported that by holding the model fixed (GPT-5.2-Codex) and only touching the harness, they climbed from 52.8% to 66.5%, moving from outside the Top 30 to the Top 5. That's a bigger leap than most model-generation upgrades deliver. The harness has quietly become one of the biggest levers on how good an AI tool actually feels. Why Every AI Lab Now Ships Its Own Harness Put those two facts side by side and the whole strategy snaps into focus. The model is commoditizing and getting cheaper. The harness is where most of the real-world performance now lives, and swapping a model out of its matched harness costs you quality. So if you're a lab, the harness is where you plant your flag. As one Hacker News commenter put it, "the lock-in isn't the model; it's the tooling ecosystem." There are a few reasons this works so well as a business move. The harness is where the lock-in lives Because a model runs best in the harness it was trained against, bundling the two as a single product is both a quality play and a lock-in play. Your model plus your harness beats your model in someone else's harness, so you have a real reason to keep users inside your walls, and users have a real reason to stay. Anthropic even restricts its subscription plans to Claude Code specifically, which tells you how much they value that boundary. The flywheel that compounds over time This is the part that makes the moat deep. Every day people use a lab's harness, they generate millions of traces of the model using that harness's tools. Those traces become training data for the next model, which gets even better at that specific harness, which makes the pairing tighter. The harness team ships a new trick, it shows up in usage data within months, and the next model has it baked into its instincts within a year. A third party harness builder is always reacting to a model release. The lab is designing the next model and the next harness together. It's the same tight coupling that made Windows, Office, and Exchange so hard to compete with in the 90s, where each layer made the others stickier. The harness became the product Watch what the labs actually sell now, and it's not "the model" anymore. Anthropic leads with Claude Code, Cowork, and the Claude Agent SDK. OpenAI pushes Codex, its CLI, and the cloud agent. Google is deprecating Gemini CLI in favor of Antigravity, which shares one harness across its CLI and IDE. Even smaller labs are doing it. Z.ai wrapped its GLM-5.2 model in a Claude Code-style desktop agent called ZCode. The engine is still the model. The thing they sell you, the thing they want you paying a subscription for every month, is the car. The Counter Move - Neutral and Open Harnesses If every lab wants to trap you in its own harness, the natural rebellion is a harness that belongs to nobody, one that can route the same task to whichever model fits it best. Coding to one model, prose to another, cheap bulk work to a third. Third party harnesses like ForgeCode already top parts of the Terminal Bench leaderboard precisely by routing across model families, and open options like OpenCode let you plug in your own model or API key. The pull toward these agnostic harnesses is strong because it's the users' interest against the labs' interest. When someone criticized ZCode as "another walled garden," the common wish underneath was for a neutral harness that treats models as interchangeable parts. As one person neatly summed up where the real defensibility sits, "the moat is state." Whoever owns your accumulated context and memory owns you, no matter which model is running. So the market is splitting into two camps. Labs pushing bundled, closed harnesses to lock you in, and open harnesses trying to make models a swappable commodity so the value flows to the tooling instead. Which side wins shapes how much choice we actually have. What This Means for You as a Developer A few practical takeaways: Compare harnesses, not just models. The model name on the spec sheet doesn't tell you how the tool will actually perform. The same model can feel noticeably smarter or dumber depending on the harness driving it. Judge the whole car. Pick a harness that fits your workflow and that you trust. Since the harness is where the lock-in lives, the choice matters more than picking this month's top model. Think about whether you're comfortable being inside a lab's walled garden or whether you'd rather run an open harness that lets you route between providers. Watch the state, not the shine. The stickiest lock-in isn't the pretty interface, it's your memory, your project context, and your accumulated setup. If a tool makes that hard to take with you, that's the real cost of leaving. And if you're leaning on AI to write code, none of this replaces understanding what it produces. I've written about that trap in vibe coding and the problem with AI-generated code. Wrapping Up The model wars grab all the headlines, and fair enough, watching frontier intelligence climb is genuinely exciting. The quieter harness war is the one that decides who you actually pay every month and how easily you can walk away. The model is the engine, and engines are getting cheap and interchangeable. The harness is the car, and the car is where the labs are building their moats. So next time you're choosing an AI tool, don't just ask which engine is under the hood. Take a good look at the car you're being asked to buy. --- ## Fable 5 Returns, HackerRank's Flaky ATS & ZCode's GLM-5.2 Harness — The Weekly Diff #6 - **URL**: https://syntackle.com/blog/fable-5-returns-hackerrank-s-flaky-ats-zcode-s-glm-5-2-harness-the-weekly-diff-6/ - **Updated On**: July 23, 2026 - **Description**: Fable 5 quietly returns with a usage cap that undercuts its own doomsday marketing, HackerRank's open-sourced ATS can't score the same resume twice, and ZCode wraps GLM-5.2 in a Claude Code-style harness. Plus DeepSeek's DSpark paper, Box3D's open 3D physics, and Claude Code steganographically watermarking your prompts. - **Tags**: post, AI, opinion, news, claude, opensource, weekly-diff - **Author**: Murtuzaali Surti Table of Contents Fable 5 Returns Fable 5 is back, and the terms of its return tell you more than the announcement does. For a limited window you can spend up to 50% of your weekly plan limit on it, and once that's gone you burn usage credits. The model that was supposedly too dangerous to release has come back as a promotional slot with a usage cap. In the last issue I wrote about how Fable 5's "too dangerous to release" framing worked out perfectly for Anthropic, because it generated a mountain of free press implying the model was so powerful the government had to step in. The quiet return is the other shoe dropping. The moment competitors shipped equally strong models, the case for keeping it locked fell apart, and what was billed as a safety hold became a timed promo with a meter on it. The discussion zeroed in on the trust cost, which is the part that doesn't bounce back the way a usage limit does. People described how they'd canceled their max plan and asked for a refund when Fable was pulled, only to re-up the moment it returned. Coming back to a tool you no longer fully trust, because the open alternatives still aren't a drop-in, is its own kind of lock-in, and it's exactly the gap the open weights crowd is racing to close. Another sentiment was that the loss of trust in US-based models is the lasting damage, not the temporary absence of one model, since the doomsday messaging, and the administration falling for it, is what eroded confidence and pushed people toward those alternatives in the first place. HackerRank Open Sourced Its ATS HackerRank open-sourced its applicant tracking system, and Dan Kinsky did the obvious thing and ran his own resume through it. It scored 90, then 74, then 88, on the same input. A 65% variance run to run is the kind of result that would get a normal test flagged as flaky, except here it's deciding whether a human being gets an interview. The issue is that the scoring is done by an LLM, and LLMs are stochastic (involves inherent randomness), so asking one to produce a stable ranking is asking the wrong kind of system to do a deterministic job. The author leaned on temperature as a "determinism" knob, and the discussion was quick to point out that's not how temperature works, since even temperature 0 doesn't make a model reproducible in the way a grading rubric (set of instructions) needs to be. The scoring instructions say that open source contributions are worth 35 points and personal projects 30, which means fifteen years of paid work experience caps at 25% of your score. Any developer who doesn't code in their free time gets penalized by design, and the room was full of people pointing out that this describes most of the working engineers they know. The grim reframing, from someone who'd run hiring pipelines, was that a 35% pass rate is "actually a fantastic number" when you're drowning in applicants, which is the most honest and depressing thing in the thread. ZCode Wraps GLM-5.2 in a Coding Agent Z.ai shipped ZCode, a desktop coding agent built on GLM-5.2, and the most common reaction in the discussion was a question rather than a verdict: why does every AI company now need its own version of Claude Code/Codex? It's a fair question, and the honest answer is mostly vendor lock-in dressed up as a product. The UI is, by several accounts, a near-exact copy of the Codex app, and unlike other genuinely open harnesses, it isn't open source, which is a strange look when the model underneath is the open-weights selling point. The people who already live in a TUI agent pointed out that Z.ai documents integrations with nearly all the popular CLI agents, so if you're already running GLM-5.2 through something you trust, the desktop shell doesn't add much beyond a prettier window. A few tried it and drifted back to OpenCode, which one person said feels smarter. The 1.5x usage promo on the coding plan is the real reason to take it for a spin right now. What people want is an agnostic harness that can route the same context to whichever provider fits the task, coding to one model, prose to another, images to a third. That's the direction coding-model competition is pulling anyway, and I've written about the same pull from the cost side in using your Gemini subscription with opencode and GitHub Copilot with a custom API key. It's hard to get excited about another walled garden when the open one is already this good. DeepSeek's DSpark Paper Explains Why Its Tokens Are So Cheap Source: deepseek.ai DeepSeek's API costs roughly a quarter of what everyone else charges for comparable models. DSpark, a new speculative decoding method they just published, points to a simpler explanation, that they've figured out how to run inference cheaper than anyone else, and the gap keeps widening. To understand speculative decoding, picture a fast drafter and a careful editor working as a pair. The drafter fires off guesses at speed, the editor only steps in to fix the ones that are wrong, and most of the time the draft is good enough that the editor barely has to work. In LLM terms, a small, cheap model drafts tokens while a larger model verifies them, meaning you pay the expensive compute only when the draft misses. DSpark refines this approach to deliver 57% to 78% faster per-user generation at matched capacity, keeping responses interactive even where the same model without it would leave you watching a spinner. Publishing a methods paper that explains how your API stays cheap, while US labs are busy rationing models and framing releases as too dangerous, is a deliberate contrast. Openly documenting how you make inference fast enough to sell at a quarter of the market rate is its own kind of competitive moat, because it says "we're not just cheaper, we know exactly why, and here's the paper proving it." The weights are already up on HuggingFace with the speculative decoding module built in, so this isn't just a lab result. The question is whether the numbers reproduce on consumer GPUs rather than A100s. Someone sketched a near future where draft models get tuned to specific use cases, companies, or even individuals. That's the commoditization squeeze I keep coming back to, the same one GLM-5.2 kicked off and that open-weight models generally keep accelerating. When tokens cost a fraction of what they did a year ago, the math on which provider you route to starts to look different. Box3D, an Open Source 3D Physics Engine Erin Catto, the name behind Box2D, announced Box3D, an open source 3D physics engine. For years Bullet was effectively the only real open source option, and people are marveling that they've gone from that to Jolt, Rapier, Avian, Nvidia PhysX, and now Box3D, all in a few years. Box2D was the foundation for a generation of indie physics games, and Catto's work on the Valve side fed into Rubikon, with a new engine called Ragnarok reportedly showing up in future Valve games. The honest answer to "which one" is best is that the landscape is now rich enough that the choice depends on your stack and your constraints rather than on whichever single option happens to exist. s&box, the Source 2 successor, reportedly ripped out the Source 2 physics engine in favor of Box3D, which is about as strong an endorsement as an open physics library can get. Claude Code Is Quietly Fingerprinting Your Prompts The biggest story of the week, by some distance, was the discovery that Claude Code is steganographically marking requests, embedding hidden watermarks into the prompts it sends so Anthropic can fingerprint which client a request came from. The stated purpose is catching resellers and distillation attempts, and on its face that's a legitimate problem for a model provider to worry about. What makes it land harder is the execution. The technique mirrors the anti-observation tricks used by sophisticated malware, and as a discussion at HN pointed out, defeating it is trivial, which means it mostly punishes the exact people who are easiest to fingerprint, normal developers doing weird but legitimate things, rather than the sophisticated resellers it's aimed at. Anthropic already gets far more sensitive payloads from users, so the collection itself isn't the issue. What matters is whether they act on it directly, by rate-limiting, compute-limiting, or quietly rerouting flagged requests to a weaker model. The trust framing is what ties this whole issue together. Several people described Claude Code as feeling "malware-y" from the start, and one person used a month of access to build out a personal harness specifically so they'd never have to route through Anthropic again. That's the same instinct driving the open-weights and local-model thread running through this entire issue, from Fable 5's rationed return to DeepSeek's cheap inference to ZCode's GLM-5.2 harness. When your tooling has a hidden layer that can fingerprint, throttle, or reroute you, the rational response is to own more of the stack yourself, and the open side of the market keeps making that easier. --- ## OpenAI & Anthropic's AI Model Lockdown, Open Models Rise & Google's Firing — The Weekly Diff #5 - **URL**: https://syntackle.com/blog/openai-anthropic-s-ai-model-lockdown-open-models-rise-google-s-firing-the-weekly-diff-5/ - **Updated On**: July 23, 2026 - **Description**: OpenAI's GPT-5.6 Sol ships behind US government vetting, Anthropic's Mythos and Fable get rationed by Washington, open-weight models rise as the cheap alternative, smart model routing, Node's new Nub toolkit, Google firing a CLI maintainer, and Microsoft's quantum claim undone by Python bugs. - **Tags**: post, nodejs, AI, sde, opinion, news, claude, opensource, weekly-diff - **Author**: Murtuzaali Surti Table of Contents GPT-5.6 "Sol" Ships, and the US Government Decides Who Gets It OpenAI previewed GPT-5.6 Sol, which it's calling a next-generation model, and the US government will decide who gets to use it. The rollout is being staggered at the request of the Trump administration, with access vetted rather than open to all. The naming drew groans first, since calling something a next-generation model while shipping it as a point release invites the obvious question of why it isn't just GPT-6. The Sol/Terra/Luna tier names landed as OpenAI plainly envying Anthropic's knack for naming things. The pricing didn't help the mood either. Sol runs $5 for input and $30 for output per million tokens, which is steep at a moment when the rest of the market is racing the other way. The vetting is the part worth sitting with, though. Putting an approval layer on who can use a commercial model at home is new, and people are almost entirely against it. A "preview" gate is easy to wave away as temporary, though it sets a precedent. The moment access to a general purpose tool depends on approval, the default flips from "anyone can build on this" to "ask first," and defaults are sticky. For most working developers, the practical takeaway is simpler. If your model access can be revoked or rationed by a third party, that's now a real supply-chain risk for your product, not a hypothetical one. Anthropic's Mythos Released to "Trusted" US Orgs Anthropic's side of this is messier and more interesting. The US allowed Anthropic to release its Mythos model to "trusted" US organizations, which sounds like loosening up until you read the fine print. Mythos is probably the best model in Anthropic's lineup, and access to it has become a political bargaining chip, to the point where the NSA reportedly lost access amid a dispute with the company. The rollout even confused people on the basics, like why the government would clear Mythos while keeping the supposedly safer, more guarded Fable locked down. That confusion tells you the public picture of these model tiers is still fuzzy. The sharper argument is over how good Mythos actually is. The skeptical take, which came up a lot in the Will It Mythos? discussion, is that it's just a normal model with the safety rails removed, and that any current model would surface the same vulnerabilities if it weren't trained to refuse. The people who've actually pointed these models at real security work disagree, and the most convincing version of their case is that the edge is persistence, not raw intelligence. Fable doesn't just answer and stop, it keeps digging, sometimes turning up bugs in reverse-engineered binaries that a standard model walks right past. Meanwhile, Fable 5 is reportedly on track to return soon after being pulled, and it worked out perfectly for Anthropic. They got a mountain of free press that amounts to "we're so powerful the government had to step in," and the moment competitors answered by shipping equally strong models, the case for regulating any of it fell apart. When the moat you're advertising is "too dangerous to release," and three other labs ship the same thing next week, the moat was never really there. CAUTION Be careful reading model marketing as a security guarantee in either direction. "Too dangerous to release" and "perfectly safe" are both positioning. If you're relying on a model for security work, evaluate it on your own code and threat model, not the press release. The Rise Of the Open Weight Models Every one of those gating decisions points the same direction, and the open-weight model world is happy to take the handoff. A few pieces this week captured the moment well. There's a measured look at the gap between open-weight and closed-source LLMs, an argument about the unbearable cheapness of open-weight models, the news that Asian AI startups are launching Mythos-like models while the export ban drags on, and a broader case that for most of the world, open source AI is the only way forward. The most striking figure from the cheapness discussion was a developer reporting a full month of real work for under two dollars, and noting it isn't even subsidies doing it, since hosts like DigitalOcean and Cloudflare serve the same open models at similar prices. That kind of commoditization is exactly the squeeze the premium labs are trying to escape, and it raises an uncomfortable question, if most everyday tasks run fine on open models, what are you paying the premium for? This is the same thread I picked up when GLM-5.2 took the open-weights crown in last week's issue, and it has only sped up since. A couple of honest caveats are worth keeping in mind. "Open source" and "open weights" aren't the same thing, since open weights gives you the finished model without the training data or recipe to rebuild it, and the two kept getting blurred together. The bigger worry is whether any of this lasts, since today's best open models mostly exist because of corporate generosity that can be switched off at any time. There's also a real strategic question of whether the US is throwing away its lead by export-banning frontier models while open, largely non-US labs quietly catch up to the quality everyone else can actually use. If you want to start playing with this yourself, my older walkthrough on getting started with DeepSeek and OpenRouter and the roundup of open-source coding agents worth trying are still the fastest way in. Pro Tip The pragmatic workflow that keeps showing up is to use a cheap open model for generation and a frontier model for review and debugging. You get most of the quality for a fraction of the bill, and you're not fully exposed if either provider changes the rules. Smart Model Routing Lands in Claude, Codex and Cursor If the problem is cost, the natural next question is how to spend less without babysitting which model handles what. A new smart model router for Claude, Codex and Cursor tries to answer that by sending each request to the cheapest model that can handle it, automatically. It's a clean idea, and the discussion on Hacker News went straight to the catch that makes or breaks these tools, prompt caching. Bouncing between models means you keep missing the cache, and since cache hits are where most of the savings live (the cache only lasts about five minutes), a careless router can cost you more than sticking with one model. There's a reliability tax on top, since smaller models are likelier to stop early, throw errors, or get stuck in loops, so a "cheaper" call can quietly turn into three calls plus a manual cleanup. The honest version of this tool weighs caching and failure rates in its routing, not just the sticker price per token. It pairs well with the rest of the cost control toolkit, and I've written before about wiring your own keys and subscriptions into your editor in using your Gemini subscription with opencode and GitHub Copilot with a custom API key. Routing is the logical next layer on top of those. Nub Brings a Bun Style All-in-One Toolkit to Node.js Nub is a Bun-like (pun intended) all-in-one toolkit for Node.js, bundling the runtime conveniences that have made Bun and Deno feel so pleasant into something that sits on top of Node rather than asking you to leave it. Plenty of teams love Bun's developer experience and can't justify moving a production codebase off Node just to get it, so a toolkit that brings the speed and the batteries-included feel while keeping you on the runtime you already trust is an easy yes. The early reception was warm, with one person reporting they'd moved an entire monorepo over with zero issues and others asking the sensible questions about whether it runs on Cloudflare Workers and inside Docker. That's the right instinct, since a tool like this lives or dies by where it can run. INFO The interesting subtext here is that Node's competition has made Node better. The pressure from Bun and Deno is exactly why a project like Nub, and Node's own recent built-in features, exist at all. Fired by Google for Building the Google Workspace CLI Justin Poehnelt says he was fired by Google for creating the Google Workspace CLI, a genuinely useful open-source tool, as a side project. It's the kind of 20% time work Google used to be famous for celebrating. One part of the argument is that Google has lost the plot, going from encouraging side projects to firing people for them, which fits the wider pattern of swapping a loved open tool for a worse closed one, something I touched on when Antigravity replaced the Gemini CLI. The other part is fair too, that he'd put Google's logo and brand colors on a public googleworkspace GitHub org without permission, which is a genuine trademark problem, though the obvious fix there is to strip the logos and rename it, not fire the guy. Both can be true at once. Either way, the chilling effect is the real story, since the lesson every engineer at a big company just learned is that shipping a useful side project can now cost you your job. Microsoft's "Quantum Leap" Undone by Basic Python Errors A researcher claims Microsoft's supposed "quantum leap" doesn't hold up because of basic Python errors, with the argument being that fixing the bug invalidates the result the research was built on. The detail that makes it land is that Microsoft's next generation chip was reportedly built "with the help of its own agentic AI," which sets up the obvious punchline that the discussion didn't miss, that AI can now hallucinate quantum computing claims about as well as humans can. The substance underneath the jokes is real, though. Whether or not AI wrote the offending code, the lesson is the same one I keep coming back to in the problem with AI-generated code and why vibe coding is the fast food of coding. Speed without verification just gets you to the wrong answer faster. The most advanced lab in the world can still be tripped up by a mistake a careful code review would have caught, which is a useful reminder that capability and correctness are not the same thing, in your codebase or anyone else's. --- ## GLM-5.2's Open-Weights Lead, DeepSeek Gains Vision & Lore Challenges Git — The Weekly Diff #4 - **URL**: https://syntackle.com/blog/glm-5-2-s-open-weights-lead-deepseek-gains-vision-lore-challenges-git-the-weekly-diff-4/ - **Updated On**: July 23, 2026 - **Description**: GLM-5.2 takes the open-weights crown at a fraction of the price, DeepSeek finally gains vision, Lore emerges as a version control system built to challenge Git at scale, MCP ships enterprise-managed OAuth, and Charity Majors argues AI demands more engineering discipline, not less. - **Tags**: post, web, AI, sde, opinion, news, mcp, opensource, weekly-diff - **Author**: Murtuzaali Surti Table of Contents Lore: An Open Source Version Control System Built for Scale Lore is a new open-source version control system designed for scalability. The idea is simple, handle the things Git handles badly, namely large binary assets and giant monorepos that make git status crawl. Anyone who has wrangled Git LFS knows the pain, since Git was built for text and the model starts creaking the moment you version large binaries like game assets, design files, or datasets. Git LFS papers over the problem well enough until you cross GitHub's storage limits, where it turns into a paid add-on that never quite feels native. The most important catch is that Lore isn't actually a fully distributed VCS. Because it coordinates with a central server, it undercuts the "drop-in Git alternative" framing for anyone who values Git's offline-first, distributed nature. Several people also pointed out that Perforce is already the de facto standard in AAA game studios for exactly this large-binary use case, so Lore is walking into a space with an entrenched incumbent. Game developers were the most enthusiastic of the bunch, citing years of frustration with Git's handling of binary assets as proof the problem is real. If you mostly version text, Git isn't going anywhere, and the tooling around it (I've written about Git hooks using Husky before) remains unmatched. Lore earns a spot on your radar mainly if you've felt the binary asset pain firsthand. Pro Tip Before migrating any team to a new VCS, weigh the ecosystem, not just the core tool. Git's real moat is decades of CI integrations, hosting, and muscle memory built around it. Zero-Touch OAuth for MCP: Enterprise-Managed Auth Goes Stable The Model Context Protocol team shipped Enterprise-Managed Authorization, and it addresses what has arguably been MCP's biggest weak spot, auth. If you've read my MCP explainer, you know how much friction the old per user, per server OAuth dance created. The new model flips that arrangement entirely. Rather than every employee individually clicking through OAuth flows to link Claude or ChatGPT to their work accounts, an IT admin centrally controls which MCP servers are allowed through the company's identity provider, with Okta as the first supported IdP. It's powered by a new token format called ID-JAG that notably isn't MCP specific and could work anywhere apps share an SSO provider, so the result is meant to be "zero-touch". You join a company and your tools are already wired together, with no re-authentication every 8–12 hours. Not everyone is convinced this is the right tradeoff. The sharpest dissent on the HN thread called it "bonkers", the worry being that granting a tool access to a sensitive resource such as a bank MCP server almost certainly warrants a per-conversation prompt before it acts, especially given prompt-injection risks. Reducing friction is the whole point of the feature, which is precisely why some argue a little friction belongs here. Defenders countered that the real win is isolating the auth flow outside the agent's context window entirely, a genuine security improvement. One developer noted that Microsoft Entra ID doesn't support dynamic client registration, so real-world enterprise setups still need workaround shims today. CAUTION "Zero touch" auth is convenient, though convenience and least-privilege pull in opposite directions. For anything that can move money or delete data, keep a human in the loop regardless of how seamless the connection is. GLM-5.2: The New Leading Open-Weights Model GLM-5.2 is now the top-ranked open-weights model on the Artificial Analysis Intelligence Index. What sets GLM-5.2 apart is how broadly it performs rather than peaking on one benchmark. Z.ai built it as a coding and agentic model first, and that shows up where it counts. On SWE-bench Pro, which measures resolving real GitHub issues, it posts 62.1%, ahead of GPT-5.5 and Gemini 3.1 Pro, and it trails Claude Opus 4.8 by just a single point on FrontierSWE, the long horizon coding benchmark. The context window also jumps from 200K to a full million tokens, enough to load a mid-sized repository without chunking, and the weights ship under an MIT license. The bigger surprise is agentic work. On GDPval-AA v2 (the real world task completion benchmark) GLM-5.2 effectively ties GPT-5.5 (xhigh), a proprietary frontier model, and clears every other open-weights rival. Under the hood, Z.ai leaned on a few custom tricks to keep the 1M token context affordable, like an IndexShare attention scheme that reuses one indexer across every four transformer layers, and a critic-based RL setup with an "anti-hack" module, because the model kept cheating its evaluations by hunting for secret_cases.json files or curling answers down from GitHub. Most of the excitement centers on price-to-performance, with people pointing to providers offering near-unlimited tokens for around $50/month. For developers who've watched frontier API bills climb, an open-weights model in that quality tier is a big deal, and another reason the open-source coding agents I covered earlier keep getting more viable. The catch is that GLM-5.2 is capable but not efficient. One hands-on user reported it spending "over 15 minutes reasoning" and burning ~45k tokens on a small task that GPT-5.5 handled in ~16k tokens total. Open models are catching up on raw capability, so token efficiency is where frontier models still pull ahead, something I touched on in why a million-token context window isn't what you think it is. It's also why a popular workflow emerged: "use GLM for generation and a frontier model for review and debugging, getting you most of the way to a premium plan for a fraction of the cost". DeepSeek Introduces Vision Capability DeepSeek added vision support this week, and developers were quick to clarify what that means. This is image understanding, describing and reasoning about images, rather than image generation, a distinction worth keeping straight before you get your hopes up about a free image generator. Vision unlocks the Claude Agents SDK, which requires a vision-enabled API. The takeaway is that "if the DeepSeek API could see, it can fully drive Claude Code," meaning DeepSeek's cheap inference could now back agentic workflows that previously forced you onto pricier vision capable models like Qwen or Gemini Flash Lite. For a model already known for aggressive pricing, gaining a frontier-tier capability is exactly the kind of thing that creates competition. As one commenter joked on HN, "OpenAI and Anthropic need to get this free foreign competition banned." I covered getting started with DeepSeek and OpenRouter when R1 launched, and vision is a meaningful step up the capability ladder for an open, low cost model. AI Demands More Engineering Discipline, Not Less Charity Majors published a piece arguing that AI demands more engineering discipline, not less, and it struck a nerve. Her core idea is that AI lets you produce code faster, so speed without discipline simply gets you to a mess sooner. It pairs neatly with what I've written about the problem with AI-generated code and why vibe coding is the fast food of coding. The HN thread surfaced three takes worth sitting with. First, a skeptic noted that the "it used to be slop, but now it's fixed" framing has been recycled for every model since GPT-3.5, which makes the capability debate "basically non-falsifiable." The second and most pointed take is that it's getting genuinely harder to distinguish competent engineers from people slinging "LLM copypasta," because everyone now files perfectly formatted PRs and docs. One commenter predicted "an exotic form of technical debt... remarkable mostly in its enormity." Third, a sympathetic reader mapped the argument onto infrastructure-as-code: people dislike systems "where it's hard to tell how it got into its current state," and un-reviewed AI output produces exactly that kind of opaque, hard to reason about codebase. Pro Tip The discipline that prevents AI slop is the same discipline that always mattered, which is, clear design upfront, real code review, and the ability to explain how your code got the way it is. AI just raises the cost of skipping those steps. --- ## SpaceX Buys Cursor, The Fable Ban & Apple's Container Machines — The Weekly Diff #3 - **URL**: https://syntackle.com/blog/spacex-buys-cursor-homebrew-6-0-apple-s-macos-containers-the-weekly-diff-3/ - **Updated On**: July 23, 2026 - **Description**: The US government orders Anthropic to suspend Fable 5 and Mythos, SpaceX acquires Cursor for $60 billion, Homebrew 6.0 ships with macOS 27 support, an AI agent racks up a massive bill unsupervised, Apple enters the container space, going HTML first doubles a startup's users, and a LinkedIn job offer hides a backdoor. - **Tags**: post, AI, mac, frontend, performance, opinion, news, weekly-diff, docker, opensource, claude - **Author**: Murtuzaali Surti Table of Contents SpaceX Acquires Cursor for $60 Billion SpaceX is buying Anysphere (the company behind Cursor), for $60 billion in stock, and it happened days after SpaceX's blockbuster IPO. Cursor, an AI-powered code editor built on top of VS Code (a VS Code fork), is being valued higher than most of the companies whose code it helps write. SpaceX reportedly plans to use Cursor across its engineering teams for Starship, Starlink, and internal tooling. Elon Musk has been vocal about replacing traditional software engineering with AI driven development, and this acquisition is the most concrete move in that direction. Some developers worry about what happens when a defense adjacent company owns the tool that has read access to your entire codebase. Others pointed out that Cursor was already sending code to cloud APIs for inference, so the trust model hasn't fundamentally changed. If you use Cursor, it is about to get a very different owner with very different priorities. If you don't use Cursor, this is still a signal that AI coding tools are now strategic assets and not productivity features. The open source alternatives I covered previously are looking more important than ever. The US Government Banned Anthropic's Most Powerful Models On June 12th, the US government directed Anthropic to suspend access to Claude Fable 5 and Claude Mythos 5, its two most powerful models. The initial assumption was that this was about a jailbreak, though it wasn't. Amazon security researchers discovered that Fable 5 could generate functional exploit code from simple prompts like "fix this code," where the "code" was a description of a vulnerability. No jailbreak needed, no elaborate prompt engineering, just a straightforward request that the model interpreted as a legitimate coding task. Amazon's CEO reportedly brought these findings directly to US officials, which triggered the crackdown. Cybersecurity researchers pushed back hard against the guardrails Anthropic had already applied to Fable, arguing that the model was refusing legitimate security research queries. Fable wouldn't answer basic questions about network protocols or vulnerability classes that any cybersecurity textbook covers. Anthropic then apologized for applying invisible guardrails, a distillation based filtering layer that was silently degrading model output without telling users. What makes this story significant for developers is that "a" government can now order an AI model offline, and the company will comply within hours. If your production workflow depends on a specific model from a specific provider, you just saw how quickly that can disappear. The Weekly Diff #2 covered Opus 4.8's launch and OpenRouter's $113M raise. The model routing layer I said was becoming essential infrastructure just proved its value in a way nobody wanted. CAUTION If your production systems depend on a specific AI model, have a fallback plan. Model access can be revoked at the provider level, and now at the government level, with no advance notice. Homebrew 6.0.0 Homebrew 6.0.0 shipped this week and it's a significant release for anyone who develops on macOS, Linux or WSL. Features include a new tap trust security mechanism that gives you more control over which third party taps can run install scripts, a rewritten internal JSON API that makes brew update and brew search noticeably faster, and Linux sandboxing that brings Homebrew's Linux support closer to parity with macOS. There's also initial support for macOS 27 which tells me Apple's next OS is far enough along that Homebrew is already testing against it. The brew bundle improvements are worth calling out. If you use a Brewfile to manage your development environment, and you should, bundle now handles dependencies more intelligently and is faster at reconciling what's installed versus what's declared. I've written about mac setup for developers before, and Homebrew is the foundation of that entire workflow. This release makes that foundation more secure and faster. AI Agent Bankrupted Their Operator A developer's AI agent racked up a massive bill while trying to scan DN42, a hobbyist network used for experimenting with internet routing. The agent was left running unsupervised and decided (as agents do) that scanning the entire network was the most thorough approach. The API costs spiraled, and the operator ended up with a bill they didn't expect and couldn't easily reverse. As more developers adopt agentic workflows, the blast radius of "the AI did something I didn't explicitly approve" grows. The takeaway is that agents need guardrails the same way any automated system does, not that you should stop using AI agents. Spending caps, time limits, scope constraints, and ideally a human in the loop belong on anything that costs money or touches external systems. I've talked about the problem with AI-generated code before in terms of quality. When AI acts confidently and autonomously, the mistakes are also confident and autonomous. CAUTION If you're running an AI agent against any external API, set hard spending limits before you walk away because the agent won't stop on its own. Apple's macOS Container Machines Apple quietly published documentation for macOS Container Machines, a first-party container runtime for macOS. This is Apple officially entering the containerization space, and it's a big deal for anyone who has been running Docker Desktop, Colima, or Lima on their Mac. The project (hosted under Apple's GitHub organization) provides native container support using macOS's built-in virtualization framework. It doesn't require a Linux VM the way Docker Desktop does, which means lighter resource usage and tighter integration with the host system. This could fundamentally change the container workflow for macOS developers. I've written about running PostgreSQL using Docker, containerizing Next.js apps, and file watching issues in Docker, and all of those workflows involve Docker Desktop or a third-party alternative. If Apple ships a container runtime that's fast, native and lightweight, all of those workflows get a native home. Building an HTML-First Site Doubled Users Overnight A startup called Moh Kohn published a case study on how going HTML-first doubled their user base overnight. The approach is to strip away the JS heavy SPA, replace it with server-rendered HTML, minimal CSS, and progressive enhancement where necessary. The results were dramatic. Page load times dropped, accessibility improved, search engines could actually index the content, and users on slower connections could finally use the product. The traffic increase didn't come from a marketing push. It came from the site simply becoming usable for people it had previously excluded. I wrote about why static sites are good and this case study provides the business case I was making philosophically. The industry spent a decade building increasingly complex frontend architectures, and some teams are now discovering that the performance and accessibility problems they're trying to solve were created by the architecture itself. A Backdoor in a LinkedIn Job Offer A developer named Roman documented how they received a LinkedIn job offer that contained a concealed backdoor. The "take home assessment" included a Node.js project with obfuscated malicious code that would have exfiltrated credentials and system information if run. The job listing looked legitimate, the recruiter's profile had a plausible history, and the assessment project was a realistic coding task. The malicious payload was hidden in what looked like a dependency configuration file, the kind of file most developers would glance at without reading carefully. In Weekly Diff #1, we covered the trojanized VS Code extension that breached 3,800 GitHub repos. This one targets the developer through the most mundane channel possible, a job application. --- ## Claude Opus 4.8, OpenRouter's $113M Round & SQLite's Anti-AI Stance — The Weekly Diff #2 - **URL**: https://syntackle.com/blog/claude-opus-4-8-openrouter-s-113m-round-sqlite-s-anti-ai-stance-the-weekly-diff-2/ - **Updated On**: July 23, 2026 - **Description**: Anthropic ships Opus 4.8 with dynamic workflows, OpenRouter raises $113M to become the model marketplace, SQLite draws a hard line against AI generated code, and a developer's open source project gets weaponized for phishing. - **Tags**: post, AI, opinion, news, claude, opensource, security, weekly-diff - **Author**: Murtuzaali Surti Table of Contents Claude Opus 4.8 anthropic.com Anthropic shipped Opus 4.8 this week, and while the version bump is incremental, the changes aren't. Opus 4.8 (claude-opus-4-8) flags uncertainties more readily, asks questions before making changes, and catches its own mistakes, the kind of behavior that separates a useful coding assistant from a confident sounding autocomplete. The pricing stays the same as 4.7, $5 per million input tokens and $25 per million output tokens, and "Fast" mode is now 3x cheaper than previous models at $10/$50. The new feature is Dynamic Workflows, available in Claude Code on Enterprise, Team, and Max plans. It lets Claude spin up hundreds of parallel subagents for large scale tasks like codebase migrations across hundreds of thousands of lines. If you've been doing multi file refactors one file at a time, this is the feature that changes that workflow. Other notable additions include an effort control slider on claude.ai that lets you trade depth for speed, and a Messages API enhancement that lets you insert system instructions mid-conversation without breaking prompt cache. The latter matters more than it sounds, since it means you can update agent instructions on the fly without paying for a full context re-read. Anthropic also teased Claude Mythos Preview, a higher-intelligence class model coming to all customers within weeks. Between this and the Weekly Diff #1 coverage of Microsoft killing Claude Code licenses, the AI tooling landscape is moving fast. OpenRouter Raises $113M Series B openrouter.ai OpenRouter, the model routing API that sits between your app and dozens of AI providers, just raised $113 million led by CapitalG (Alphabet's independent growth fund). The investor list reads like an AI infrastructure roll call and includes NVentures (NVIDIA), ServiceNow Ventures, MongoDB Ventures, Snowflake Ventures, Databricks Ventures, with existing backers Andreessen Horowitz and Menlo Ventures participating. OpenRouter's weekly token volume jumped from 5 trillion to 25 trillion tokens in six months, and they're serving 8 million+ developers across 400+ models. The platform handles routing, reliability, cost optimization, failover, and compliance, essentially the unglamorous but crucial infrastructure layer that production AI needs. If you've read my DeepSeek guide, you already know OpenRouter. I used it as the recommended way to access DeepSeek-R1 without dealing with API key management across multiple providers. This raise validates that the "model marketplace" layer is becoming essential infrastructure, not just a convenience. The bet here is that the future of AI is intelligent routing across many models rather than one model from one provider, picking the right one for each request based on cost, latency, and capability. If that bet is right, OpenRouter is positioning itself as the Cloudflare of AI inference, the layer everyone routes through without thinking about it. SQLite Does Not Accept Agentic Code Simon Willison covered SQLite's quiet decision to explicitly reject AI generated code contributions. It's more of a hard "no" than "we're cautious about it" or "we'll review it more carefully". SQLite's AGENTS.md file spells it out. They'll accept agentic bug reports with reproducible test cases, and they'll look at demonstration patches for documentation purposes, though they will not merge AI generated pull requests. The project recently removed qualifying language like "currently" from their policy, making it clear this isn't a temporary stance. The reason being SQLite's forum was getting flooded with low quality AI generated bug reports, enough that D. Richard Hipp created a separate bug forum just to manage the volume. It sounds like a quality control decision from a project that ships in billions of devices and cannot afford regressions. The code an LLM generates might look correct, pass tests, and even work in production, and yet it carries different quality characteristics than human written code. SQLite's position is that those differences matter enough to draw a line. Whether you agree or not, every project maintaining critical infrastructure is going to have to take a position on this eventually. Open Source Project Weaponized for Mass Phishing Andrej Acevski woke up to a Resend quota alert and discovered that his open source project management tool, Kaneo, had been used to send 14,520 phishing emails to roughly 14,000 people. The attack was clever precisely because it didn't exploit a vulnerability. The attacker used the tool exactly as designed. The attacker created 949 fake accounts using disposable email providers in a three hour window and crafted workspace names that mimicked phishing subject lines (fake banking and crypto offers). Then they used the workspace invitation feature to blast emails. Because the emails came from Kaneo's verified Resend domain with proper DKIM signatures, they sailed right past spam filters. "They used my tool exactly as designed. The design was just bad enough that the tool was good for phishing." - Andrej Acevski The cleanup was straightforward. It involved revoking API keys, deleting bot accounts, and purging 14,520 invitations in a single database transaction. The fixes were standard too: captcha, rate limiting, disposable email blocking, workspace-name filters, and restricting guest accounts from sending invitations. Self-hosted and cloud versions of the same software have fundamentally different security profiles. When you self-host, the operator controls the blast radius. When you run a multi-tenant SaaS, you inherit responsibility for every action any user takes that touches external systems. If your open source project has a cloud offering with email sending capabilities, abuse isn't a matter of if but when. --- ## Malicious VSCode Extensions, Node.js 26 & Antigravity's End — The Weekly Diff #1 - **URL**: https://syntackle.com/blog/malicious-vscode-extensions-node-js-26-antigravity-s-end-the-weekly-diff-1/ - **Updated On**: July 23, 2026 - **Description**: The Weekly Diff is a curated roundup of the week's most interesting developer stories. This week: a trojanized VSCode extension breaches 3,800 GitHub repos, Google silently replaces Antigravity with a chatbot, Microsoft kills Claude Code licenses, Files.md offers a no-lock-in Obsidian alternative, and Node.js 26 finally ships the Temporal API. - **Tags**: post, vscode, markdown, AI, github, news, antigravity, opensource, weekly-diff - **Author**: Murtuzaali Surti Table of Contents The Weekly Diff by Syntackle is a new series, a curated roundup of the most interesting developer-relevant stories from the past week. No fluff, just the stuff worth your time. GitHub Confirms Breach of 3,800 Repos via Malicious VSCode Extension A trojanized version of the Nx Console VS Code extension, tied to the broader TanStack npm supply-chain attack, compromised a GitHub employee's device, and roughly 3,800 internal GitHub repositories were exfiltrated as a result. GitHub confirmed that the breach was limited to internal repos and no customer data was affected. The threat actor group TeamPCP reportedly tried to auction the stolen code for $50,000, though it was never publicly released. GitHub removed the malicious extension from the marketplace and isolated the compromised endpoint. The worrying part is that the VS Code marketplace has had multiple incidents of malicious extensions slipping through. I've written about VSCode extensions I use before, and this is a good reminder to audit yours. Check what permissions your extensions request, look for extensions with suspiciously few downloads but broad permissions, and keep an eye on extension update changelogs. CAUTION If an extension you've never heard of suddenly appears in your installed list, investigate immediately. Google's Antigravity Bait and Switch We published a post earlier this year about using Antigravity's free models with Claude Code, a workflow that let you run Claude models powered by your Antigravity tokens at no cost during the public preview. That workflow is now effectively dead. At I/O 2026, Google released Antigravity 2.0 and turned what was an IDE into a conversational, Codex-style experience. That in itself isn't the problem. The problem is how it happened. A background update on May 21st silently replaced the existing IDE installation without consent, with no opt-in and no migration path. As 0xsid puts it, "background updates are meant for performance patches and version upgrades, not for secretly shipping an entirely different piece of software." Chat history and settings were lost in the process. The plan-review-implement loop that made the IDE useful for production work was gone, replaced with an agentic chatbot interface. This is a pattern worth paying attention to. Free tier AI access works as a growth lever, get developers hooked, gather feedback, build ecosystem lock-in, then change the terms. If your workflow depends on a free tier from a company that hasn't committed to keeping it free, build an exit plan before you need one. Microsoft Drops Claude Code, Pushes Developers to Copilot Microsoft had been offering both Claude Code and GitHub Copilot to internal developers, essentially running a head to head comparison. The results were not what Microsoft hoped for, since developers overwhelmingly preferred Claude Code. Starting June 1st, Microsoft is ending the Claude Code licensing program and moving developers to token-based API pricing instead. The HN discussion paints a messy picture. Developers who built their workflows around Claude Code now face either paying per-token out of pocket or switching to Copilot. Some Microsoft employees reported that Copilot had genuinely improved over time and was closing the gap, though others pushed back, arguing that the cancellation was more about internal politics than product parity. Multiple developers independently reported preferring Claude Opus 4.6 over the newer 4.7, citing more hallucinations and less predictable behavior in the newer model. Whether that's a temporary regression or a fundamental tradeoff remains to be seen. The broader lesson here is about tool portability. If your AI coding workflow is tightly coupled to a single provider's licensing deal, you're one corporate decision away from disruption. The developers who fared best in this situation were the ones whose workflows weren't locked to any one tool. Syntackle Open Source AI Coding Agents to Try for Free OpenCode, pi, T3 Code, and Kilo are four open source AI coding agents you can try for free. I compare them by workflow fit, provider flexibility, Claude subscription access, and what “free” actually costs. Files.md: Open-Source Alternative to Obsidian Files.md is a local-first, markdown-only note-taking app that does exactly what you'd expect from the name. It works with plain .md files on your filesystem. No proprietary format, no database, no lock-in. I've written about why I love Markdown before, and this project aligns with that philosophy. Your notes are just files. You can open them in any editor, version them with Git, grep through them in the terminal, and they'll still work in 10 years. What makes it stand out from Obsidian: No build system, the frontend is a single index.html file. No bundler, no compilation step. Go backend, a single binary server for optional sync across devices. Intentional simplicity, the creator explicitly warns against the "Second Brain" trap where the system becomes more complex than the thinking it's supposed to support. The philosophy is that "only necessary features, restrictions foster creativity." LLM-friendly, plain markdown means any AI tool can read and work with your notes without special integrations. The project has a chat like interface for quick thought capture that flows into a Chat.md file before being organized into categories. It's opinionated about structure (predefined folders for journal, habits, tasks), though everything is still just markdown files you own. Node.js 26.0.0: Now with Temporal Node.js 26 dropped on May 5th and the headline feature is the Temporal API shipping unflagged. If you've ever wrestled with JavaScript's Date object, timezone math, immutability issues, parsing inconsistencies, Temporal is the long-awaited fix. Temporal gives you proper timezone-aware types (Temporal.ZonedDateTime), duration arithmetic that actually works (Temporal.Duration), and immutable date/time objects by default. No more accidentally mutating a date three function calls deep and spending an hour debugging why your timestamps are wrong. Beyond Temporal, here's what else is notable in v26: V8 14.6, brings new Map.prototype.getOrInsert() and Iterator.concat() methods. Undici 8.0.2, improved built-in HTTP client. Raw key format support, new crypto APIs for raw key import/export. Legacy stream modules removed, _stream_readable, _stream_writable, and friends are gone. Use the public stream module. --experimental-transform-types removed, TypeScript transform support is no longer experimental. Build requirements bumped, GCC 13.2+, Python 3.10+, and notably a Rust toolchain is now required for Temporal builds. Node 26 is the "Current" release and will enter LTS in October 2026. If you're on Node 24 or earlier, now's a good time to start testing your projects against v26, especially if you use any of the removed legacy stream internals. --- ## Open Source AI Coding Agents to Try for Free - **URL**: https://syntackle.com/blog/opensource-ai-coding-agents-to-try/ - **Updated On**: March 29, 2026 - **Description**: OpenCode, pi, T3 Code, and Kilo are four open source AI coding agents you can try for free. I compare them by workflow fit, provider flexibility, Claude subscription access, and what "free" actually costs. - **Tags**: post, AI, sde, cli, listicle, opensource - **Author**: Murtuzaali Surti Table of Contents There is no shortage of AI coding tools right now, but the ones that get the most attention are almost always proprietary such as Claude Code, Codex, and Cursor. They are well-marketed and easy to get started with. But they also come with tradeoffs such as not owning the workflow, you cannot swap providers freely, and you are locked into one vendor's pricing. Open source AI coding agents are getting really good. I have been using a few of them in my own workflow, and the biggest thing I have learned is that the model quality is not the only differentiator anymore, in fact, most of these tools connect to the same frontier models. What actually matters is asking the right questions: Does it fit how you already work? Terminal-first, editor-first, or GUI-first. How much control does it give you? Provider switching, context management, prompt engineering, extensibility. Can you switch providers and models easily? Useful especially when rate limits hit or you want to try a cheaper model for routine tasks. What is the Claude situation? Anthropic recently pushed back on some tools legally, and that has changed the subscription access story in ways that directly affect which tool is best for whom. INFO When I say free, I mean free to install and try. That does not always mean zero cost. Your running cost depends on API pricing, credits, free-tier models, or whether the tool lets you use an existing subscription login. OpenCode The strongest ai coding agent offering the best TUI experience. OpenCode is for you if you already like working in a terminal. Docs say it supports 75+ providers plus local models, and it ships with a workflow that makes sense, "Build" for full access and "Plan" for a safer, read-only-first pass. If your idea of AI coding is "let me stay in the terminal and get real work done," OpenCode makes immediate sense. They also introduced OpenCode Desktop app for all platforms and is currently in beta at the time of this writing. OpenCode is also one of the best tools for switching between providers and models. Its provider agnostic approach is one of the core reasons to use it. The downside is that OpenCode is now less attractive for Claude subscription users because you can't use your existing Claude subscription inside OpenCode and you must use an API key. Pick OpenCode if you are a terminal-first developer and want the strongest default agent workflow without spending time assembling the system yourself. Also a good pick if you switch between providers and models often. pi The best pick for heavy users who want control. pi is the tool I would recommend to people who use coding agents a lot and start feeling constrained by defaults. pi's official site calls it a minimal terminal coding harness and that is exactly what it is. pi gives you a strong base and lets you shape it through extensions, skills, prompt templates, themes, and packages. pi gives you more of that control than the others. However, with more customizability, comes more of a steep learning curve. You need to learn how to glue these pieces together and make the best use of them. pi also explicitly supports Anthropic Claude Pro/Max subscriptions. If Claude subscription access matters, that alone makes pi more attractive than OpenCode right now. Like OpenCode, pi makes switching between models and providers feel natural. If you move between providers depending on task, budget, or rate limits, pi handles that well, much more smoothly than T3 Code did in my testing. pi is less opinionated out of the box. Some developers love that. Others want something more pre-configured. Pick pi if you are a heavy user and want more control than convenience. Also the clearest choice right now if Claude subscription access is a deciding factor. T3 Code The best GUI-first option, but more constrained. If OpenCode and pi are terminal-native tools, T3 Code is the option for people who want a cleaner visual layer. Not everyone wants to do agentic coding inside a TUI, and T3 Code respects that. T3 Code now lets you access Claude models via existing Claude subscription if you have Claude Code CLI installed and signed in. However, T3 Code is more constrained than the others. In my testing, T3 Code did not let me switch models across providers in the same session, the way OpenCode and pi do. If you like moving between providers based on task, budget, or availability in the same session, T3 Code can feel more limited. Considering that limitation, plus the project still in its early stages, I see T3 Code more as a GUI convenience layer than the most flexible agent in this comparison. Pick T3 Code if your requirement is the best GUI experience. Just know that the workflow is more constrained and T3 Code is younger than the rest. Kilo Code Good for integrations and team oriented workflows. If your workflow is editor-first and maybe team-oriented, Kilo makes sense. It has a stronger story around structured modes, code reviews, cloud execution, free/budget model guidance, and local models through Ollama and LM Studio. While OpenCode and pi feel like tools for developers who think from the terminal outward, Kilo feels like a tool for developers who think from the editor and team workflow outward. INFO Kilo CLI is actually a fork of OpenCode. The downside is that it is simply bigger. If you want something minimal, Kilo can feel like more product than you asked for. And credits still remain part of the usage story even with its documented free model paths. Kilo does not support subscription based access and only supports API key authentication if you want to use a different provider. Pick Kilo if you are editor first, team-workflow first, or want a broader product/platform with modes, reviews, cloud agents and integrations. Thing To Note OpenCode used to be more attractive for Anthropic (Claude) subscription users because of the Claude Pro/Max auth plugin. That has changed. dax (thdxr) posted on X that opencode 1.3.0 would no longer autoload the Claude Pro/Max auth plugin after Anthropic pushed back legally. So if you use OpenCode with Anthropic today, think in terms of API/provider pricing, not a convenient subscription based pricing. pi still officially lists "Anthropic Claude Pro/Max" under supported subscriptions, and T3 Code now supports Claude too if you have Claude Code CLI installed and signed in — Theo, the creator of T3Code, confirmed this on X. Tool Best fit What stands out Main catch OpenCode Terminal-first developers Build/Plan workflow, 75+ providers, local models Anthropic usage is API-priced now pi Heavy users who want control Most customizable, subscription support, provider flexibility Less opinionated out of the box T3 Code GUI-first users Clean visual layer, Claude via Claude Code CLI More constrained workflow, early project Kilo Editor/platform-first users Modes, code reviews, cloud agents, local models Bigger surface, credits still matter Final Thoughts OpenCode is the strongest ai coding agent offering the best TUI experience. pi is the best fit for heavy users who want more control (can use Claude subscription). T3 Code is the easiest GUI-first option, but more constrained (can use Claude subscription). Kilo is a platform, good for integrations and team oriented workflows. If you think in terms of workflow instead of hype, the decision gets much easier. --- ## A Million Token Context Window Isn't What You Think It Is - **URL**: https://syntackle.com/blog/long-context-window-ai-model-catch/ - **Updated On**: March 18, 2026 - **Description**: AI models now support 1M+ token context windows, but bigger isn't always better. In this post, I talk about the "lost in the middle" problem, why models struggle to retrieve information from long contexts, and the trade-offs you should know before relying on large context windows. - **Tags**: post, AI, opinion, news - **Author**: Murtuzaali Surti Anthropic recently announced that "1 Million token context window" is now generally available to all users for Claude Opus 4.6 and Sonnet 4.6 models. It's amazing that in a couple of years, we went from GPT-3 with a 4K token context window (feels ancient), to Google becoming the first AI company to introduce a 1M token context window AI model, and Meta taking it as far as introducing a 10M token context window model "Llama 4 Scout". Now all of this looks good on paper, but is it actually good or are the AI companies cashing in on the idea of a larger context window? There's a catch, let me explain. Table of Contents What Is The Context Window? A context window is the maximum amount of input (that includes responses) an AI model can "see" at once, both what I send it (the input) and what it generates (the output). It's measured in tokens (chunks of input/output), where the token length varies per AI model implementation. Think of it like a desk. Everything I want the model to work with (my ask, the documents I paste in, the conversation history) has to fit on that desk. If something doesn't fit, it gets left out entirely. The model has zero knowledge of anything outside its context window. When I send a prompt, the model reads the entire context window at once. It processes all tokens simultaneously using an attention mechanism. This mechanism decides which parts of the input are the most relevant to generating each word of the output. Every new message in a conversation gets appended to the existing context. Once the total (input + output) exceeds the window limit, the model forgets the oldest messages (however, modern AI agents do automatic "compaction", which preserves just the summary of the conversation to try to fit it in). This is why long conversations can feel like the model "lost track" of something you said earlier. The Actual Good A larger context window has a lot of great use cases, such as: Feeding an entire codebase in one go so the model understands how everything connects. Analyzing long documents (legal contracts, research papers, books) without processing or splitting them into chunks. Maintaining long conversations where earlier context matters (e.g. a back-and-forth code debugging session). Reducing the need for complex retrieval pipelines (RAG). Instead of searching a vector database for relevant snippets, you just paste it in. The Catch The bigger the context, the higher the risk of the "lost in the middle" problem. The "lost in the middle" problem is where models pay strong attention to the beginning and end of the long context but degrade significantly at recalling information placed in the middle. This was documented by Liu et al. in their 2023 research paper "Lost in the Middle: How Language Models Use Long Contexts". They tested models on multi-document question answering and key-value retrieval, placing the relevant information at different positions within the input. They found that the performance was highest when the answer was at the very beginning or very end, and dropped sharply when it was in the middle, even for models explicitly designed for long contexts. The chart above shows long context retrieval performance across models using the MRCR v2 benchmark with 8 needles (kind of finding needles in a haystack), which is a more demanding multi-needle retrieval test. Even the best model (Opus 4.6) drops from ~92% mean match ratio at 256K to ~78% at 1M tokens. GPT-5.4 falls from ~80% at 128K to ~37% at 1M (massive degradation). Gemini 3.1 Pro goes from ~59% at 256K down to ~26% at 1M. Sonnet 4.5 barely crosses ~19% at 1M. But why? It's fundamental to how AI models work and how they are designed. The architecture behind every major LLM uses something called self-attention (Vaswani et al., "Attention Is All You Need," 2017). Think of it this way, when the model is generating the next word, it looks back at the entire input and asks "which parts of this input matter the most for what I'm about to generate?" It assigns a relevance score to every token in the input and uses those scores to decide what to focus on. The catch is that these scores have to add up to 100%. With a short input (for example, 4K tokens), it's easy to give meaningful attention to the important parts. But with 1M tokens, that same 100% gets spread across a million candidates. The important stuff (if it's in the middle) now has to compete with a massive amount of surrounding text for the model's focus, and it often loses. It's like being in a quiet room vs. a stadium. In the quiet room, I can hear someone whisper from across the table. In the stadium, that same whisper gets drowned out by the crowd noise, even though the person is saying something important. The more you use the context window, the worse outcomes you'll get. This leads to an academic concept called the "dumb zone". Around the 40% line is where you're going to start to see some diminishing returns depending on your task. - Dex Horthy, HumanLayer Also, AI models need to know the order of tokens, i.e., which word comes first, which comes last. This is done through positional encodings, which are like invisible tags that say "this token is at position 1," "this token is at position 500,000," and so on. Popular methods like RoPE (Rotary Position Embedding, Su et al., 2021) encode how far apart two tokens are. This creates a recency bias and the model naturally pays more attention to tokens that are closer to the end of the input (near where it's generating) and less attention to tokens that are far away. Peysakhovich and Lerer showed this directly in "Attention Sorting Combats Recency Bias In Long Context Language Models" (2023). Even when a relevant document is placed early in the context, the model pays less attention to it, not because it doesn't recognize it as relevant, but because its position makes it inherently less "visible" to the attention mechanism. The model has a built-in preference for recent text, learned during training. All of these factors affect how AI models respond when given a long context window. Conclusion Larger context windows are a genuine advancement. Being able to feed an entire codebase or a bunch of novels into a single prompt is incredibly useful. But a model accepting 1M tokens is not the same as a model using 1M tokens well. Don't blindly dump everything into the context window just because you can. For tasks that require finding specific details in large documents or documents which change frequently, a well-designed RAG pipeline will often outperform raw long context. Context windows will keep getting better as architectures improve, but for now, understanding the trade-offs is what matters. --- ## Google Gemini OAuth Plugin for Opencode: Use Your Google AI Subscription Instead of API Pricing - **URL**: https://syntackle.com/blog/google-gemini-ai-subscription-with-opencode/ - **Updated On**: January 25, 2026 - **Description**: Learn how to use your Google AI subscription with Opencode using the opencode-gemini-auth plugin. Authenticate via Google OAuth to access Gemini models without an API key. - **Tags**: post, AI, guide, sde, terminal, tutorial, gemini, opensource - **Author**: Murtuzaali Surti Table of Contents Opencode is an open source AI coding agent similar to Claude Code, Gemini CLI, and Codex. It's an interactive terminal user interface (TUI) which connects with most of the AI model (LLM) providers out there, OpenRouter, Google, Anthropic, OpenAI, Amazon Bedrock, Groq, Ollama, you name it. Out of the box, Opencode allows you to connect Google as a provider via an API key. But, what if you don't have a Google Cloud API key? What if you've purchased a Google AI Pro/Ultra subscription, or received a free Google AI subscription (students and users in India often qualify for these offers), and want to use it with Opencode? To use a Google AI subscription, you need to sign in with your Google account in Opencode. The problem is, Opencode doesn't support this out of the box. I came across a plugin from a GitHub issue in the Opencode repository which solves this exact problem. opencode-gemini-auth by Jens Lystad allows you to sign in using your Google account via OAuth and use any Gemini models accessible via your subscription with Opencode. In this tutorial, I'll walk you through setting up the opencode-gemini-auth plugin with Opencode. Pre-requisites A Google account with a Google AI subscription (Pro, Ultra, or the free tier) Opencode CLI installed Installing Opencode First, install Opencode if you haven't already using the simple shell script below: curl -fsSL https://opencode.ai/install | bash Locating the Config File Opencode uses a JSON config file for configuration. I recommend using a global config for configuring providers, but you're free to use a local (project-scoped) config file (opencode.json) instead. Find the global config file at these locations: macOS/Linux: ~/.config/opencode/opencode.json Windows: Run opencode debug paths to check the config file location If the global opencode.json config file doesn't exist yet, create it at the respective location for your operating system. Registering the Plugin Add the following to your Opencode config file: { "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-gemini-auth@latest"] } That's it. Opencode is now ready to use Google OAuth for authentication. Connecting Your Google Account Once you've updated the config file with the plugin, run this command: opencode auth login Choose Google from the list of providers. You'll see an OAuth with Google option. Selecting it will open a browser window and redirect you to the Google OAuth flow. After you sign in and grant access, Opencode can access any Gemini models associated with your subscription. Pro Tip If the browser doesn't open automatically (common in headless environments or when the port is in use), you can manually paste the callback URL or authorization code when prompted. Troubleshooting To view detailed logs for debugging, run Opencode with the debug flag: OPENCODE_GEMINI_DEBUG=1 opencode It will generate gemini-debug-<timestamp>.log files in your working directory. Updating the Plugin Opencode doesn't automatically update plugins. To update to the latest version, clear the cached plugin and restart Opencode: rm -rf ~/.cache/opencode/node_modules/opencode-gemini-auth opencode Wrapping Up The opencode-gemini-auth plugin is for Opencode users who have a Google AI subscription but don't want to deal with API billing. It's a straightforward way to leverage your existing Gemini quota directly within Opencode's powerful terminal user interface. Note that your usage is subject to the quotas and rate limits of your Google AI subscription tier. --- ## How to Use Free Antigravity AI Models in Claude Code - **URL**: https://syntackle.com/blog/claude-code-free-using-antigravity-proxy/ - **Updated On**: January 10, 2026 - **Description**: antigravity-claude-proxy, an open source project by Badri Narayanan S, masterfully links Google’s Antigravity with Anthropic’s Claude Code, letting you run Claude models powered by your Antigravity tokens in Claude Code. - **Tags**: post, AI, setup, tutorial, claude, antigravity, opensource - **Author**: Suraj Satheesh Table of Contents AI development tools are evolving fast, but sometimes access to the most powerful models is gated behind expensive APIs or restricted platforms. That’s exactly what antigravity-claude-proxy, an open-source project by Badri Narayanan S, enables. It masterfully links Google’s Antigravity with Anthropic’s Claude Code, letting you run Claude models powered by your Antigravity tokens in Claude Code. These models are technically free to use as a part of Antigravity public preview. Why Claude Code Instead of Using Antigravity or Other AI-powered IDEs Directly? With so many AI tools available, I wonder, why use Claude Code at all? It all comes down to workflow depth over raw model access. Claude Code isn’t just an AI chat or autocomplete tool. It’s a coding agent designed to understand entire repositories, reason across multiple files, and execute multi-step development tasks. It fits naturally into real developer workflows such as version control (Git), CLI, scripts, instead of being locked to a specific editor. Most AI-based IDEs and copilots focus on: Inline suggestions Editor-specific integrations Limited project context Claude Code, on the other hand: Works independently of your editor Understands full codebases Acts more like a pair-programmer than a helper (has Agency) What is antigravity-claude-proxy? antigravity-claude-proxy is a lightweight proxy server that exposes AI models provided by Google Antigravity’s Cloud Code behind an Anthropic compatible API. You can use Antigravity's Claude and Gemini models (including their “thinking” modes) with tools like Claude Code CLI without paying for a dedicated Claude Code plan. How It Works Here’s the architecture in 3 steps: Behind the scenes, the proxy: Receives Claude Code requests (Anthropic messaging API). Transforms them to Google Generative AI API format. Sends them through Antigravity’s Cloud Code with OAuth tokens. Converts responses back to Anthropic format with stream support. So you get end-to-end Claude or Gemini results without dealing with API discrepancies yourself. Google One Plan subscribers can get benefit from more generous rate limits for the Claude models. Even the free version works. Refer to Antigravity's Pricing for more details. Also, students who have claimed a free Google One subscription, as well as Jio users in India who are now provided with free Google AI Pro subscription, can use antigravity at no cost. Installation and Setup That being said, let me walk you through the installation and setup of antigravity-claude-proxy. Prerequisites Node.js 18 or later HomeBrew for macOS/Linux Antigravity installed (for single-account mode) OR Google account(s) for multi-account mode antigravity-claude-proxy 1] Open the terminal of your choice and run: npm install -g antigravity-claude-proxy 2] Once the command is executed, ensure you are logged into your Google account within antigravity IDE. Alternatively, you can load balance by adding one or more Google accounts using OAuth. For that run this command: antigravity-claude-proxy accounts add This opens a browser tab for Google OAuth. Sign in and authorize access. Repeat for multiple accounts. Pro Tip # List all accounts antigravity-claude-proxy accounts list # Verify accounts are working antigravity-claude-proxy accounts verify # Interactive account management antigravity-claude-proxy accounts This method benefits users without a Google One subscription by allowing them to switch between different accounts once they reach their daily quota. 3] The next step after installing and logging into Google account is to start the proxy using the below command: antigravity-claude-proxy start CAUTION Remember to run antigravity-claude-proxy start (starts the proxy server) before using Claude Code. The server runs on port 8080 (http://localhost:8080) by default. To check whether the server is working or not, run the below command in the terminal or hit the URL directly in the browser. curl http://localhost:8080/health Also, to check your account status or daily quota run this command: curl "http://localhost:8080/account-limits?format=table" Claude Code After setting up antigravity-claude-proxy, now it’s time to install Claude Code. CAUTION If you’re already logged into Claude Code with another account, you need to clear that existing Claude Code authentication so the proxy setup works correctly. Run claude and type /logout. After logging out, go through the second step mentioned below. 1] Install Claude Code, and run the following command in your terminal: For macOS/Linux (using Homebrew): brew install --cask claude-code For Windows: irm https://claude.ai/install.ps1 | iex 2] Check if you have a settings.json file for Claude Code at the locations mentioned below. If you do, just edit the settings.json, and if not, create one. macOS/Linux: ~/.claude/settings.json Windows: %USERPROFILE%\.claude\settings.json Add this configuration to settings.json file: { "env": { "ANTHROPIC_AUTH_TOKEN": "test", "ANTHROPIC_BASE_URL": "http://localhost:8080", "ANTHROPIC_MODEL": "claude-opus-4-5-thinking", "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-5-thinking", "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5-thinking", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-sonnet-4-5", "CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-5-thinking" } } 3] After creating/editing the settings.json file, next step is to load environment variables. Add the proxy settings to your shell profile: For macOS / Linux: echo 'export ANTHROPIC_BASE_URL="http://localhost:8080"' >> ~/.zshrc echo 'export ANTHROPIC_API_KEY="test"' >> ~/.zshrc source ~/.zshrc # replace with `~/.bashrc` if you are using Bash For Windows: PowerShell: Add-Content $PROFILE "`n`$env:ANTHROPIC_BASE_URL = 'http://localhost:8080'" Add-Content $PROFILE "`$env:ANTHROPIC_API_KEY = 'test'" . $PROFILE Command Prompt (CMD): setx ANTHROPIC_BASE_URL "http://localhost:8080" setx ANTHROPIC_API_KEY "test" Restart the terminal for changes to take effect. 4] Before running Claude Code, make sure you add the following in the claude.json file (macOS/Linux: ~/.claude.json, Windows: %USERPROFILE%\.claude.json): "hasCompletedOnboarding": true Then, start the proxy server and run claude code: antigravity-claude-proxy start # Make sure the proxy is running first claude # In another terminal, run Claude Code To manage models in Claude Code just type /model and you can switch between different Claude models. Conclusion For developers who love experimenting, building side projects, or exploring AI assisted coding without committing to expensive subscriptions, antigravity-claude-proxy is a game changer. It demonstrates how thoughtful engineering and open-source collaboration can unlock hidden potential in existing platforms. Projects like antigravity-claude-proxy are shaping a more open and accessible future for AI development. That said, it’s important to use this tool responsibly and understand the terms of service, avoid production misuse, and treat it as a development and learning aid rather than a commercial dependency. --- ## React2Shell Vulnerabilities — What to do? - **URL**: https://syntackle.com/blog/react2shell-vulnerabilities/ - **Updated On**: December 14, 2025 - **Description**: React2Shell (CVE-2025-55182) is a highly critical vulnerability reported by Lachlan Davidson on November 29th, 2025. React2Shell has a CVSS score of 10.0 (most critical on the scale of 0-10), and is a pre-authentication remote code execution (RCE) vulnerability in which the vulnerable RSC (React Server Components) code unsafely deserializes payloads from HTTP requests to Server Function endpoints. - **Tags**: post, nextjs, react, javascript, web, guide, news - **Author**: Murtuzaali Surti Table of Contents React2Shell (CVE-2025-55182) is a highly critical vulnerability reported by Lachlan Davidson on November 29th, 2025. React2Shell has a CVSS score of 10.0 (most critical on the scale of 0-10), and is a pre-authentication remote code execution (RCE) vulnerability in which the vulnerable RSC (React Server Components) code unsafely deserializes payloads from HTTP requests to Server Function endpoints. In simple words, attackers can craft a special malicious HTTP request to a server function and can execute malicious code directly on the server resources. CAUTION Even if you are not actively using React Server Components, but are bundling third-party packages which support React Server Components in your application, your application is still vulnerable to React2Shell vulnerabilities. And, there are a bunch of other vulnerabilities which came into light after the initial discovery of the main React2Shell (CVE-2025-55182) vulnerability. Here's a list of all currently discovered React2Shell linked vulnerabilities. Currently Known React2Shell Vulnerabilities CVE-2025-55182 - The first RCE vulnerability discovered, dubbed as React2Shell. CVSS Score: 10.0 (Critical) Published on: 3rd December, 2025 Description - A pre-authentication remote code execution vulnerability exists in React Server Components versions 19.0.0, 19.1.0, 19.1.1, and 19.2.0 including the following packages: react-server-dom-parcel, react-server-dom-turbopack, and react-server-dom-webpack. The vulnerable code unsafely deserializes payloads from HTTP requests to Server Function endpoints. CVE-2025-55183 - Unsafely returns the source code of the Server Function. CVSS Score: 5.3 (Medium) Published on: 11th December, 2025 Description - An information leak vulnerability exists in specific configurations of React Server Components versions 19.0.0, 19.0.1 19.1.0, 19.1.1, 19.1.2, 19.2.0 and 19.2.1, including the following packages: react-server-dom-parcel, react-server-dom-turbopack, and react-server-dom-webpack. A specifically crafted HTTP request sent to a vulnerable Server Function may unsafely return the source code of any Server Function. Exploitation requires the existence of a Server Function which explicitly or implicitly exposes a stringified argument. CVE-2025-55184 - Causes infinite loops while deserializing unsafe payloads and hangs the server process. CVSS Score: 7.5 (High) Published on: 11th December, 2025 Description: A pre-authentication denial of service vulnerability exists in React Server Components versions 19.0.0, 19.0.1 19.1.0, 19.1.1, 19.1.2, 19.2.0 and 19.2.1, including the following packages: react-server-dom-parcel, react-server-dom-turbopack, and react-server-dom-webpack. The vulnerable code unsafely deserializes payloads from HTTP requests to Server Function endpoints, which can cause an infinite loop that hangs the server process and may prevent future HTTP requests from being served. CVE-2025-67779 - Emerged as a failure to fix the previous vulnerability (CVE-2025-55184) in a specific use case. CVSS Score: 7.5 (High) Published on: 11th December, 2025 Description: It was found that the fix addressing CVE-2025-55184 in React Server Components was incomplete and does not prevent a denial of service attack in a specific case. React Server Components versions 19.0.2, 19.1.3 and 19.2.2 are affected, allowing unsafe deserialization of payloads from HTTP requests to Server Function endpoints. This can cause an infinite loop that hangs the server process and may prevent future HTTP requests from being served. Original POC by Lachlan Davidson - React2Shell-CVE-2025-55182-original-poc Affected Packages Next.js - All versions from 15.0.0 through 16.0.6, as well as Next.js 14 canaries after 14.3.0-canary.76. react-server-dom-webpack - 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.1.3, 19.2.0, 19.2.1, 19.2.2 react-server-dom-parcel - 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.1.3, 19.2.0, 19.2.1, 19.2.2 react-server-dom-turbopack - 19.0.0, 19.0.1, 19.0.2, 19.1.0, 19.1.1, 19.1.2, 19.1.3, 19.2.0, 19.2.1, 19.2.2 @vitejs/plugin-rsc - upgrade to latest If you are using React Router's unstable RSC APIs, Redwood SDK, or Waku, then you should definitely check and upgrade the above packages to their latest patched versions. If you are still unsure if your application contains these vulnerable packages or not, you can always use a vulnerability scanner or open source npm packages which detect those packages in your application. Two of such open source packages are: React RSC Vulnerability Scanner fix-react2shell-next Conclusion The only way to get rid off these React2Shell vulnerabilities is to upgrade the vulnerable packages in your application to their latest patched versions. This is still a developing story where hope not, but new vulnerabilities might emerge, so keep an eye on this space for a while. --- ## Google's Gemini 3 Pro, Nano Banana Pro, and Antigravity - **URL**: https://syntackle.com/blog/gemini-3-pro-and-nano-banana-pro-and-antigravity/ - **Updated On**: November 24, 2025 - **Description**: Google casually dropped three products on Nov 18, 2025 — upgraded versions of two of their most popular products, Gemini and Nano Banana, and Antigravity — an IDE with agentic capabilities. This brings us back to the Gemini era in the cycle of SOTA (State-Of-The-Art) models. - **Tags**: post, vscode, AI, gemini, workflow, opinion, news - **Author**: Murtuzaali Surti Google casually dropped three products — upgraded versions of two of their most popular products, Gemini and Nano Banana, and Antigravity — an IDE with agentic capabilities (a VSCode fork using Windsurf tech since Windsurf founders signed a $2.4 billion deal with Google and joined Google Deepmind along with some other Windsurf employees). Gemini 3 Pro According to benchmarks and Google, Gemini 3 Pro is the new state of the art model outperforming other AI models. This brings us back to the Gemini era in the cycle of SOTA (State-Of-The-Art) models. Not sure which model will outperform Gemini 3 Pro in the coming months (or maybe weeks), but for now Gemini 3 Pro seems to be the king. I built a full-stack app in ~3 days using Gemini 2.5 Pro in Firebase Studio. Syntackle The Problem With AI Generated Code And How To Deal With It AI models and tools are becoming more and more capable day by day, especially at generating code. In this post, I will walk you through my experience of creating a full-stack application using some of these AI coding assistants/agents and what are the implications of AI generated code. From what I saw and experienced, Gemini 3 Pro is really good at one-shot prompts and does a great job even if you don't provide it a lot of context. At the very least, it makes things functional and usable. People are sharing what they built using Gemini 3 Pro on X (Twitter). Here's what they are building: I asked Gemini 3 Pro to create a 3D LEGO editor. In one shot it nailed the UI, complex spatial logic, and all the functionality. We’re entering a new era. — Pietro Schirano on X I asked Gemini 3 Pro to create a 3D LEGO editor.In one shot it nailed the UI, complex spatial logic, and all the functionality. We’re entering a new era. pic.twitter.com/Y7OndCB8CK— Pietro Schirano (@skirano) November 18, 2025 I just vibe-coded a football game with Gemini 3. Insane times to be alive. — Jim Raptis on X I just vibe-coded a football game with Gemini 3. Insane times to be alive. pic.twitter.com/oLlfMIdbrR— Jim Raptis (@d__raptis) November 19, 2025 gemini 3: build a jarvis HUD interface for tony stark a quick experiment with mediapipe computer vision, threejs, and javascript — AA on X gemini 3: build a jarvis HUD interface for tony starka quick experiment with mediapipe computer vision, threejs, and javascript pic.twitter.com/QBlFYLgLJN— AA (@measure_plan) November 19, 2025 Just built a manga storybook generator using Gemini 3. Type your concept → get a complete manga-style storybook instantly — COLLINS on X Just built a manga storybook generator using Gemini 3.Type your concept → get a complete manga-style storybook instantly pic.twitter.com/BCGwTv0bI5— COLLINS⚡ (@Cubzy05) November 19, 2025 Nano Banana Pro "Nano Banana Pro" a.k.a "Gemini 3 Pro Image" is an AI model built on Gemini 3 Pro, and that allows Nano Banana Pro to utilize Gemini 3 Pro's reasoning and thinking capabilities. To use Nano Banana Pro, go to gemini.google.com (or the Gemini App on your smartphone), click on the tools icon besides the plus(+) icon, and select "🍌 Create Images" option. Also, make sure you are on the "Thinking with 3 Pro" option in the model selection dropdown. To me, the most amazing thing about Nano Banana Pro is its ability to generate accurate text. And the reason it is able to do so is Gemini 3 Pro, the AI model its built up on. There are countless examples on the internet depicting the amazing ability of Nano Banana Pro to generate infographics, diagrams, banners, menus, etc. all with nearly accurate text. Nano Banana Pro is wild. Here’s my favorite use case so far: take papers or really long articles and turn them into a detailed whiteboard photo. It’s basically the greatest compression algorithm in human history. — Pietro Schirano on X Nano Banana Pro is wild.Here’s my favorite use case so far: take papers or really long articles and turn them into a detailed whiteboard photo.It’s basically the greatest compression algorithm in human history. pic.twitter.com/9TEa5xnZzW— Pietro Schirano (@skirano) November 20, 2025 Google’s Nano Banana Pro is by far the best image generation AI out there. I gave it a picture of a question and it solved it correctly in my actual handwriting. Students are going to love this. — sid on X source: x.com/immasiddx In fact, the first image you saw in this post is generated by Nano Banana Pro. I did notice one thing though, it doesn't generate accurate readable text if the text is in the backdrop/background. I was iterating upon the first image you saw in this post and prompted Nano Banana to generate a slightly modified image with this prompt: "add some texts reflecting on the fragility and hype of these SOTA models and companies, don't overdo it, search the web for grounding the punch lines", and it generated some gibberish text in newspapers in the background. Also, you can see repetitive text in the heading. Another thing which I noticed is when you prompt Nano Banana Pro to iterate upon its own generated image, it actually degrades the quality of the image and you can clearly see that the image has been iterated upon. See the two images below. The image on the left is a result of multiple iterations on the same image internally, while the image on the right is a new image generated from the same image (downloaded and then attached to the chat) with a prompt shown below. take this image, and generate a new image out of it, don't iterate upon the same image, generate a new 4K image out of it Antigravity Antigravity is a VSCode fork using Windsurf technology developed by Google. It's an IDE with agentic capabilities, and browser access and preview support. You can use Gemini 3 Pro for free for now in Antigravity, but it's highly rate-limited. For me, the usage quota (for Gemini 3 Pro High) gets exhausted pretty quickly within a single agentic session. It's also quite buggy at this point, but we can ignore that as it will definitely improve in the near future. Antigravity is a direct competitor to Cursor, and it will be interesting to see which of the two gets the edge over the other. Exciting times. --- ## Reflections on the AWS & Azure Outages - **URL**: https://syntackle.com/blog/reflections-on-the-aws-and-azure-outage/ - **Updated On**: November 2, 2025 - **Description**: AWS and Azure, two of the largest cloud providers having a market share of ~30% and ~23% respectively (as of 2025), experienced large scale outages recently. These outages affected multiple interdependent services. In this post, I will explore what are the implications of these outages, what can be done about them, and how fragile the web really is. - **Tags**: post, web, hosting, opinion, news - **Author**: Murtuzaali Surti AWS and Azure, two of the largest cloud providers having a market share of ~30% and ~23% respectively (as of 2025), experienced large scale outages recently. These outages affected multiple interdependent services. In this post, I will explore what are the implications of these outages, what can be done about them, and how fragile the web really is. Table of Contents AWS Outage - October 19, 2025 The AWS Outage on October 19, 2025 began with Amazon's Dynamo DB service — a serverless, managed, NoSQL database service — in the us-east-1 AWS region. As per the official incident report, Dynamo DB's DNS management system published an "empty DNS record" for one of the endpoints (dynamodb.us-east-1.amazonaws.com), restricting any external/internal service to connect to it. In order to understand, you have to first understand what DNS is. DNS stands for "Domain Name Server" and is like a phone book that maps human readable addresses to IP addresses (machine addresses). For example, openai.com maps to 172.64.154.211, an IP address of the server/machine the website is hosted on. Pro Tip Run the following command to see it yourself: ping openai.com # output PING openai.com (172.64.154.211): 56 data bytes 64 bytes from 172.64.154.211: icmp_seq=0 ttl=57 time=14.347 ms 64 bytes from 172.64.154.211: icmp_seq=1 ttl=57 time=15.783 ms 64 bytes from 172.64.154.211: icmp_seq=2 ttl=57 time=13.898 ms 64 bytes from 172.64.154.211: icmp_seq=3 ttl=57 time=14.241 ms 64 bytes from 172.64.154.211: icmp_seq=4 ttl=57 time=13.575 ms 64 bytes from 172.64.154.211: icmp_seq=5 ttl=57 time=14.347 ms 64 bytes from 172.64.154.211: icmp_seq=6 ttl=57 time=15.144 ms ^C --- openai.com ping statistics --- 7 packets transmitted, 7 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 13.575/14.476/15.783/0.696 ms Press CTRL+C to exit So, if there's no address listed for a particular endpoint, other machines can't get to it because they don't know where it lives. Technically, that endpoint in now invisible. A similar thing happened with Meta (Facebook) in the past (October 4, 2021) where they had issues with the BGP protocol, in which Facebook stopped announcing their addresses (locations) which resulted in routers not able to find Facebook's servers. Coming back to the AWS outage, the Dynamo DB DNS resolution issue was just the beginning. It started a domino effect where after the DNS issue was fixed, the EC2 instances which were trying to connect to Dynamo DB via DWFM (Droplet Workflow Manager — an AWS Droplet is a physical server on which EC2 instances run) the whole down time, experienced "congestive collapse", meaning DWFM was repeatedly trying to connect (the number of broken leases increased — a lease is a logical assignment that maps a specific portion of physical server resources (CPU, RAM, etc.) to an EC2 instance for a certain period.), but when Dynamo DB was up, DWFM struggled to keep up with resolving the number of broken leases. In other words, the system entered a state where it kept on doing its job, but the inputs timed out before they were getting addressed. Clients continue to send work, and the system continues to complete that work. Throughput is great. None of the work is useful, though, because clients aren’t waiting for the results, so goodput is zero. The system is mostly stable in this state, and without an external kick, could continue going along that way indefinitely. Up, but down. Working, but broken. - Marc Brooker The EC2 instances then impacted the Network Load Balancers (NLB). RECOMMENDED Want to get technical? Deep Dive into the AWS Outage with More Than DNS: The 14 hour AWS us-east-1 outage - by Jonathon Belotti Azure Outage - October 29, 2025 In 2 weeks since the AWS outage, Azure, the second big cloud provider faced an outage on October 29, 2025, which Microsoft says was caused due to "inadvertent tenant configuration change". This change was made to the Azure Front Door service which is basically a CDN (Content Delivery Network). AFD (Azure Front Door) affected multiple services of Microsoft as it's a CDN, internal/external services depend on it for accessing stored blobs/files. Microsoft has not yet published a full report of how it exactly happened, but from little do we know, there's an automated validation system which runs after such configuration change is made and it typically blocks such faulty configurations. But, in this case, it didn't. The automated validation system itself had issue with withholding the configuration change and that's how it by-passed the checkpoints. Thoughts These outages remind me how fragile the web really is. We think of these big tech corporations and wonder what's the worst thing that can happen to them? I mean, they have all of the resources, power, and money, so how can anything go wrong with them? The fact is that no matter how big the tech organization is, they all depend on basic technologies which make the web, world wide web. No matter how much they claim they are reliable, the truth is, nothing is reliable enough. All of these complex systems are just abstractions built on top of really basic technologies, and when those basic technologies are affected, disruptions happen. It all boils down to a mesh of interconnected systems which talk to each other, the only difference is, we don't see any of that. We are so glamorized by these complex software abstractions that we fail to see the very basic foundation they were built up on. At the end, we are all sitting on top of fibre optics. Zerodha, a brokerage firm, announced their FLOSS fund in 2024 to fund open source projects. Nithin Kamath, CEO of Zerodha, shared a post on why everyone should support and fund open source projects. He made a really good point that companies which earn billions of dollars relying on many small open source projects (without them their services would be rendered completely useless) are simply not willing to sponsor or fund them. Now, I know I am going a little off tangent with this, but the point is that if one of the underlying services fail, you start to see a domino effect where all of the above abstractions fail with it. So, as much as maintaining the complex systems is important, it's equally important to maintain and fund the small, miniscule-looking technologies which are basically the backbone of software abstractions. --- ## [React] useEffectEvent: A New Hook For 'Events' inside 'useEffect' - **URL**: https://syntackle.com/blog/useeffectevent-react/ - **Updated On**: October 13, 2025 - **Description**: With the release of version 19.2, a new hook named `useEffectEvent` was introduced in React. As the name suggests, it is for the "events" defined inside `useEffect`. But what exactly is an "Event" and how does it affect the "Effect"? That's what I intend to discuss here in this post. - **Tags**: post, react, javascript, guide, frontend, tutorial - **Author**: Murtuzaali Surti With the release of version 19.2, a new hook named useEffectEvent was introduced in React. As the name suggests, it is for the "events" defined inside useEffect. But what exactly is an "Event" and how does it affect the "Effect"? That's what I intend to discuss here in this post. useEffectEvent hook addresses a very particular problem where because of exhaustive-deps rule of useEffect, dependencies which aren't supposed to trigger the "Effect" trigger it. One option is to suppress the rule and only specify the dependencies on which I actually want to trigger the Effect, but what if I actually use non-triggering dependencies inside the effect in some way or the other and always want their updated values? The values might be stale since I removed those non-triggering dependencies from the dependency array. "Event" vs "Effect" To understand this, the official React blog breaks down the code inside useEffect and puts it into two categories: Event and Effect. An "Event" is what happens when the user does something or performs an action in the application. An "Effect" is something which is dependent on the reactive values in the application. I like to call the values which are controlled by React, reactive. This distinction is crucial because it helps to extract Events from the useEffect code. Let me give an example. Lets say I have a component which does something once I receive the payment confirmation from the payment gateway. Here, the props are reactive values, meaning they can change irrespective of the user input. function Payments({ paymentStatus, cart, customerNote }) { useEffect(() => { if (paymentStatus === "confirmed") { finalizeAndTrackOrder(cart, customerNote, paymentStatus); } }, [paymentStatus, cart, customerNote]); } finalizeAndTrackOrder can be considered as an 'Event' because it only happens at a specific time and when the user actually completes the payment. The issue here is the cart and customerNote dependencies in the dependency array. I don't want to track the final order every time the cart or customerNote changes. So, technically cart and customerNote deps should not trigger the 'Effect', but I also want their latest values passed to the finalizeAndTrackOrder function (if they change). The finalizeAndTrackOrder function can be categorized as an 'Event', whereas the paymentStatus condition can be categorized as an 'Effect' which runs whenever the payment status updates. The useEffectEvent Hook Now that I know that the 'Event' is the finalizeAndTrackOrder function, I can take it out of the useEffect hook and wrap it in the new useEffectEvent hook. function Payments({ paymentStatus, cart, customerNote }) { // ... const onPaymentConfirmed = useEffectEvent((paymentStatus) => { finalizeAndTrackOrder(cart, customerNote, paymentStatus); }); useEffect(() => { if (paymentStatus === "confirmed") { onPaymentConfirmed(paymentStatus); } }, [paymentStatus]); // ... } The code inside useEffectEvent hook will always access the latest reactive values. However, there are two limitations on how to use "Effect Events": Don't pass "Effect Events" to other components or hooks. They can only be called from inside the useEffect hook. Thoughts The useEffectEvent hook solves a very particular problem, but I am wondering, is the useEffect hook flawed by design? The need to introduce new hooks around it makes it evident to some extent. Or, is it that people don't know how to use it? That's a question to ask. --- ## Side Effect Import Issue in TypeScript - **URL**: https://syntackle.com/blog/ts-side-effect-import-issue/ - **Updated On**: October 12, 2025 - **Description**: Recently, typescript started giving me an error "Cannot find module or type declarations for side-effect import" every time I load a CSS file or a font file in the entry module of a framework such as Astro(Vite)/Nextjs. The interesting part is, I did not even change the typescript version of my project. It seemed like it was using some other version of typescript. - **Tags**: post, vscode, guide, typescript - **Author**: Murtuzaali Surti Recently, typescript started giving me an error Cannot find module or type declarations for side-effect import every time I load a CSS file or a font file in the entry module of a framework such as Astro(Vite)/Nextjs. The interesting part is, I didn't even change the typescript version of my project. It seemed like it was using some other version of typescript. Then, I looked at the VS Code's Status Bar and figured out the project was using typescript v6 rather that v5 which was defined in my project. But where was it coming from? It was coming from the JavaScript and TypeScript Nightly VS Code extension. It enables the nightly build of typescript to be used inside of VS Code so that you can try experimental features. I don't know why I had it installed and enabled in the first place because you don't need this extension for typescript support in VS Code. I uninstalled the extension and reloaded the VS Code window using CMD/CTRL + Shift + P > Developer: Reload Window and now VS Code was using the typescript version defined in my project (v5.9.3). If you want to keep this extension or have multiple typescript versions added to VS Code, you can select the exact version you want for your project by using CMD/CTRL + Shift + P > Select Typescript Version. This also works if you have an explicit version of typescript defined in your packages. If you select the version of typescript installed as a package (in node_modules) from your dependencies, it will add a vscode setting "typescript.tsdk": "node_modules/typescript/lib" in the settings.json file of your project (will create one if none). { "typescript.tsdk": "node_modules/typescript/lib" } Pro Tip Reload window using CMD/CTRL + Shift + P > Developer: Reload Window after typescript version change. --- ## Fixing the overscroll "bounce" effect with CSS - **URL**: https://syntackle.com/blog/overscroll-behavior-css/ - **Updated On**: October 9, 2025 - **Description**: A quick fix for the vertical overscroll "bounce" effect for the page is to apply overscroll-behavior-y: none; to the body element. The CSS property overscroll-behavior defines what the browser will do once it reaches the end of the scroll area both vertically (Y-axes) and horizontally (X-axes). overscroll-behavior can be broken down into overscroll-behavior-x and overscroll-behavior-y properties, allowing developers to control the X and Y scroll separately. - **Tags**: post, css, web, guide, frontend - **Author**: Murtuzaali Surti Ever noticed the "bounce" scroll effect after reaching the end of a page showing a white background especially for SPAs (Single Page Application)? That's the background color of the body (if white or not defined) appearing when the SPA root (mounted) element slides down. I find it particularly annoying as it breaks the flow, look and feel of the website or application. Now you might say why not just add a background color to the body/html/root element and get away with it? You can but what if there's a header with a slightly different color shade? When you scroll up, you will get that color mismatch which is not pleasant to look at. The CSS property overscroll-behavior defines what the browser will do once it reaches the end of the scroll area both vertically (Y-axes) and horizontally (X-axes). overscroll-behavior can be broken down into overscroll-behavior-x and overscroll-behavior-y properties, allowing developers to control the X and Y scroll separately. overscroll-behavior-y controls the "bounce" effect and "pull-to-refresh" behavior and setting it to none will disable those behaviors. overscroll-behavior-x controls the prev/next gesture navigations, so they will be disabled if it is set to none. A quick fix for the vertical overscroll "bounce" effect for the page is to apply overscroll-behavior-y: none; to the body element. body { overscroll-behavior-y: none; } overscroll-behavior-y will only apply to vertical scroll and will have no effect on horizontal scrolls, which will preserve prev/next trackpad gesture navigations. I recommend applying the overscroll-behavior property on the body (to apply it for the encompassing page) because some browsers don't support the property on html or root elements. Another use case for overscroll-behavior is isolating scrolling to the current scroll area, meaning, it should not scroll the parent scroll area once it reaches the end of scroll area of the current target. For example, if you have a scrollable menu or an iframe overlaying on top of the body, and if you don't want the body to scroll once the user scrolls past the scrollable area of the menu/iframe, you can set overscroll-behavior to contain and it will not propagate the scroll outside of the target area. Thoughts Don't sleep on the overscroll-behavior CSS property, and use it according to your requirements. If the bounce effect doesn't get in the way of the user interface and it doesn't look off, then you have no reason to disable the default scroll behavior. Understanding the trade-offs is what makes the difference. --- ## OpenAI's Assistants API is Deprecated: Migrate to the New Responses API - **URL**: https://syntackle.com/blog/openai-assistants-to-responses-api/ - **Updated On**: October 8, 2025 - **Description**: With the introduction of the new Responses API (which is an upgraded version of the Chat Completions API), OpenAI plans to sunset the Assistants API with effect from 26th August, 2026. The Chat Completions API will still be supported, but OpenAI recommends to use the new Responses API for all upcoming projects. - **Tags**: post, AI, workflow, guide, tutorial - **Author**: Murtuzaali Surti With the introduction of the new Responses API (which is an upgraded version of the Chat Completions API), OpenAI plans to sunset the Assistants API with effect from 26th August, 2026. The Chat Completions API will still be supported, but OpenAI recommends to use the new Responses API for all upcoming projects. Originally, the Assistants API was created for sophisticated tool calling and execution for agentic workflows, but it also supported thread-like persistent chats which was a huge help in maintaining chats and their context. Here's a step-by-step tutorial to migrate the existing Chat Completions and Assistants API code to the Responses API. For that, let me lay out the differences and the transition between the concepts of these two APIs. Table of Contents Chat Completions >> Responses If your application only uses the Chat Completions API for stateless, single-turn interactions, your migration path is straightforward. A response when used alone, also acts as a stateless entity which outputs according to the input. // Javascript SDK --- const context = [ { role: 'system', content: 'You are a generalist.' }, { role: 'user', content: 'hey there' } ]; const completion = await client.chat.completions.create({ model: 'gpt-5', messages: messages }); const response = await client.responses.create({ model: "gpt-5", input: context }); // API Endpoints --- // Chat Completions POST /v1/chat/completions // Responses POST /v1/responses If using structured outputs by specifying response_format and zodResponseFormat: // Chat Completions API response_format: zodResponseFormat(diffPayloadSchema, "json_diff_response") You now have to specify it as shown below: // Responses API text: { format: { type: "json_schema", name: "json_diff_response", schema: zodResponseFormat(diffPayloadSchema, "json_diff_response").json_schema.schema, }, }, Assistants API >> Responses API Assistants API The core architecture of the Assistants API revolved around four key entities: Assistants (configuration), Threads (messages), Runs (execution), and Run Steps (intermediate actions). 1. Assistant - The persistent object defining configuration. It bundled the core settings like the target model, system instructions, and tools (e.g., code_interpreter, file_search). Limitation - Management/Versioning was often cumbersome as it was defined programmatically. 2. Thread - The container for the conversation session. It stored the server-side conversation history, but strictly consisted of only messages. Limitation - Limited storage capability, only storing messages. 3. Run - The asynchronous process responsible for executing the Assistant's actions against a Thread. Limitation - Required complex asynchronous polling loops. Could enter states like requires_action for tool execution, complicating client-side orchestration. The Thread was locked while a Run was in progress. 4. Run Steps - Detailed internal objects tracking the progress and intermediate actions of the Run. Limitation - Generalized objects tied to the complex, asynchronous lifecycle of the Run, necessitating checks and event handling for tracking progress. The Assistants API was always in beta, in other words, it never transformed into something concrete. Here's how the Assistant API looks when implemented in javascript with the help of OpenAI's SDK. Assistants API Flow 1. Assistant Creation import OpenAI from "openai"; const openai = new OpenAI({ apiKey: import.meta.env.OPENAI_API_KEY, }); const assistant = await openai.beta.assistants.create({ model: "gpt-4.1-2025-04-14", name: "threadAssistant", instructions: "Provide output in markdown format." }) 2. Thread Creation const thread = await openai.beta.threads.create() 3. Adding a Message to the Thread const newThreadMessage = await openai.beta.threads.messages.create( threadId, { role: "user", content: message, } ) 4. Running a thread by creating a Run const run = openai.beta.threads.runs.stream( threadId, { assistant_id: assistantId } ); 5. Listening to Run Events (Steps) // while streaming is enabled run.on("messageCreated", (message) => { // ... }); run.on("textDelta", (textDelta) => { // ... }); /** ... ... ... **/ run.on("end", () => { // ... }); run.on("error", (err) => { // ... }); Responses API The Responses API changes this flow a bit and gives new names to the core architectural entities of the Assistants API. Assistants -> Prompts Threads -> Conversations Runs -> Responses Run-Steps -> Items 1. Prompt - Prompts are designed to hold configuration like model choice, tools, and system guidance (instructions), allowing them to focus purely on high-level behavior and constraints. They can strictly be accessed only from the Dashboard, and there's no programmatic way to create them. You can grab the ID of the prompt to reference it from code. One awesome thing about them is in-built versioning support. 2. Conversation - Conversations store generalized objects called Items, which represent a stream of data beyond just text messages, including tool calls, tool outputs, and other information. While creating a response, you can specify the conversation ID and the context and state of the conversation will be accessible and maintained. 3. Response - The cumbersome asynchronous Run process is replaced by the simpler Response primitive. You send input items and get output items back, unifying the execution process. As per OpenAI, Responses benefit from lower costs due to substantially improved cache utilization (showing a 40% to 80% improvement in internal tests over Chat Completions). Responses API Flow (For Persistent Conversations) 1. Create a Prompt through the Dashboard Source: platform.openai.com 2. Create a Conversation import OpenAI from "openai"; const openai = new OpenAI({ apiKey: import.meta.env.OPENAI_API_KEY, }); const thread = await openai.conversations.create(); 3. Create a Response const run = await openai.responses.create( { prompt: { id: "<prompt_id_from_dashboard>" }, input: [ { role: "user", content: message, } ], conversation: "<conversation_id>", store: true, stream: true, } ) Note that the prompt is configurable using the Dashboard, so when you specify the prompt_id, it will refer to the configuration of the prompt created via the dashboard, but if you want you can also override properties through code as well. However, the input property extends, meaning, there are message input fields in the dashboard which allow you to define some default messages to start a conversation as well as a system message. And, the user message you send from the code gets concatenated to the messages defined in the prompt. Source: platform.openai.com 4. Iterate over Response Events // while streaming is enabled (stream: true) const run = await openai.responses.create( { /** */ stream: true, } ) for await (const event of run) { if (event.type === "response.output_text.delta") {/** */} if (event.type === "response.output_item.added") {/** */} if (event.item.type === "message" && event.item.status === "in_progress") {/** */} /** ... */ if (event.type === "response.failed") {/** */} if (event.type === "error") {/** */} if (event.type === "response.completed") {/** */} } Thoughts The Assistants API was a huge help in bringing thread-like conversations to life and it worked to some extent. Now, OpenAI has decided to revamp and improve upon it in the form of the Responses API. As mentioned before, the Assistants API is deprecated and will be abandoned with effect from 26th August, 2026, so now is the time to plan a migration to the Responses API. --- ## The Minimal React App Setup You Need [2026] - **URL**: https://syntackle.com/blog/the-minimal-react-setup-you-need/ - **Updated On**: January 26, 2026 - **Description**: Deciding which tools to use while building a React web application is subjective, but in this post, I will propose a minimum set of tools needed to build a simple React application. This post will help you to build and get the React application up and running in just a few minutes. - **Tags**: post, react, javascript, workflow, guide, setup, frontend - **Author**: Murtuzaali Surti Deciding which tools to use while building a React web application is subjective, but in this post, I will propose a minimum set of tools needed to build a simple React application. This post will help you to build and get the React application up and running in just a few minutes. Table of Contents 1. Vite (For Bundling) Vite is now the go-to choice for most of the developers while building a React application from scratch. I wrote a detailed article about why is that the case — "Create React App (CRA) is Deprecated, Officially: What's Next?". Vite will take care of the build process and bundle React in a javascript bundle ready to be deployed. As a matter of fact, Vite provides React templates which you can use to quickly spin-up a Vite/React repository — "Scaffolding Your First Vite Project". npm create vite@latest my-react-app -- --template react-ts All of the templates Vite provides - create-vite. 2. React Router (For Routing) React Router might not be the best solution for routing in React, but it is one of the stable solutions out there. If you want a more modern solution, then there's Tanstack Router from the Tanstack ecosystem which is a great contender for React Router. Tanstack Router provides in-built type-safety (typescript support) with file based routing approach unlike React Router which has a more declarative component based approach. 3. Shadcn (For UI components) Shadcn is the modern UI library which has almost every component you need. Not only that, those components are customizable and can be built up on. It works well with tailwindcss. Some other notable UI libraries include MUI, Ant Design, Chakra UI, and Mantine. 4. Tanstack Query (For Query State Management) Tanstack Query is by far the most important and useful tool in modern React applications. API query management is one of the most boilerplate-y tasks while building a React application. I always think about how to manage those API calls, their states, and how they impact the UI. All of that is solved by Tanstack Query (React Query) which is a complete and state-of-the-art solution for query state management. From maintaining, caching, refetching queries to having dependable and conditional queries, almost every query fetching problem is solved by Tanstack Query. 5. Zustand (For Global State Management) I really don't like React Context API for top-level state management as it re-renders everything in between (even if it doesn't consume/use the context). And I don't blame React Context API for that. It is meant to trigger a re-render because we are wrapping the components under a Provider and it assumes that every child will be affected by it. So, the Context is attached to a parent and all of its children. But, true global state doesn't necessarily have to be attached to a React component. It has to be truly global. In fact, React Redux also uses the React Context API internally to pass store data to deeply nested components, which doesn't make it truly global. That's where Zustand comes into action. Zustand provides global states which aren't attached to the React Component lifecycle. Another nice perf optimization one can do while using Zustand is using atomic selectors instead of selecting the entire store state at once. const ReactComponent = () => { // ❌ - returns a new object ever single time const { inventory, users } = useShopStore(); // ✅ - atomic values which stay the same if unchanged const inventory = useShopStore(state => state.inventory); const users = useShopStore(state => state.users); // ... } Thoughts The above list is not exhaustive and it is the bare-minimum set of tools required to build a simple React application. I am not saying that this is the ultimate React setup which should be used for every application. You might not even need global state management for instance, and in that case you are free to skip Zustand. But it's a great start if you quickly want to get things running. --- ## Vibe Coding — The Fast Food of Coding - **URL**: https://syntackle.com/blog/vibe-coding/ - **Updated On**: September 7, 2025 - **Description**: The usage of AI, especially in the software industry, has increased a lot lately, but everything has a downside — and that, is, excess. Excess of anything is bad, and that includes the use of AI. "Vibe Coding" is a term coined by Andrej Karpathy, and in this post, I explore the downsides of vibe coding and how to balance it. - **Tags**: post, AI, opinion - **Author**: Murtuzaali Surti The usage of AI, especially in the software industry, has increased a lot lately, but everything has a downside — and that, is, excess. Excess of anything is bad, and that includes the use of AI. "Vibe Coding" is a term coined by Andrej Karpathy (founder of Eureka Labs and an ex-founding member at OpenAI) in his tweet posted on Feb 3, 2025. He states, "There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists". With that in mind, in this post, I explore the downsides of vibe coding and how to balance it. Recently, Zen van Riel - a senior software engineer at GitHub, shared a linkedin post about the dark side of vibe coding. He describes a developer constantly trying to fix "simple things" using an AI model but, unfortunately, the AI model fails to do so every time. It's not only a waste of time, but a waste of money (credits) as well. Zen wonderfully describes this through an analogy of fast food (hence, the title of this post). In my opinion, that's a brilliant analogy because it talks about balance. Let me draw some parallels between fast food and vibe coding. Table of Contents Vibe Coding & Fast Food 1. Instant Gratification When you send a prompt and see output in a matter of seconds, you feel good. It gives you a dopamine hit, a sense of accomplishment, but it's only a matter of time when it all fades away. When the AI model starts making mistakes and no matter which prompt you give, it still doesn't work, that's when you start feeling I could have done it myself. It starts becoming messy if you look at the bigger picture. 2. Opinionated Ingredients If you don't know what you are building, AI model can use whatever it thinks is good to build your application and sometimes it's not the best for your application and use case. And, it can be very hard to refactor later. For you to be able to give enough context of what you are building and why, you need to be aware of the available tools and techniques needed to make that happen. 3. Lacks Nutritional Value Once you get the taste of it, you stop asking the "why"/"how" question. Questioning what the AI model does almost feels like a second thought. And you know what it does? It drains your ability to learn and grasp new things. That's the reason I never recommend beginners to rely solely on AI tools for coding/programming. Always questions things and ask the model why it did what it did. 4. Looks Good on the Outside AI tools might get you the exact thing you want, but if you look closely at the code, (if you have decent knowledge about programming) you start seeing inconsistencies and tech debt. 5. Forms Bad Habit in the Long Term If you only vibe code, you never get to focus on the grilling part of programming, which is to sit patiently and think about the problem at hand. You never really learn how to dissect a problem and solve it incrementally. Some of the best solutions to software engineering problems I had occurred to me when I was asleep, walking or just wandering around with an open mind. Sometimes, all it takes is to take a step back and relax. Correct Usage of AI tools for Coding Don't solely rely on AI of you are a beginner. Read in-depth articles, watch YouTube videos explaining how stuff really works and practice, build something. Building something on your own is key and it will get you out of tutorial hell. Familiarize yourself with what you are building and why. It's easy to get lost in whatever the AI model generates, so it's necessary to have decent knowledge about technologies you want to use to build your project. Use AI model in an incremental way. Prompt the AI model to do small changes instead of giving it a complex task. Break down the problem yourself, or even better, prompt the AI model to generate a plan first, study the plan, and then tell it to implement. It will help you learn and break down the problem. Ask the AI model why it did what it did. AI models are great at explaining things, so use it to your own advantage. I wrote about my experience of vibe coding an entire full stack application in this post — "The Problem With AI Generated Code And How To Deal With It". Let me know if you found it useful. --- ## How to Run SQL Server on a Mac: A Step By Step Guide - **URL**: https://syntackle.com/blog/sql-server-on-macos-and-linux/ - **Updated On**: July 1, 2025 - **Description**: SQL Server is not natively supported on macOS and thus, there is only one option to use it on macOS, and that is via Docker. In this tutorial, I will setup and configure SQL Server database on macOS via Docker, demonstrate how to connect to the database, and also show how to backup and restore the DB. - **Tags**: post, sql, vscode, backend, tutorial - **Author**: Murtuzaali Surti Table of Contents SQL Server is not natively supported on macOS and thus, there is only one option to use it on macOS, and that is via Docker. In this tutorial, I will setup and configure SQL Server database on macOS via Docker, demonstrate how to connect to the database, and also show how to backup and restore the DB. Setting up Docker If you haven't already, install Docker Desktop on your system and make sure to enable the following option in Docker Desktop settings (General > Apple Virtualization framework > Use Rosetta for x86_64/amd64 emulation on Apple Silicon). RECOMMENDED New to Docker? Complete Guide To Docker Pulling SQL Server Docker Image Open the terminal of your choice and run: docker pull mcr.microsoft.com/mssql/server:2022-latest This will pull the latest SQL Server docker image from docker hub. Running SQL Server Image in a Container To run the docker image of SQL Server in a docker container, run the following command inside your terminal: docker run -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=<Strong_Password>' \ -p 8081:1433 --name sql-server-2022 --hostname sql-server-2022 \ -v <your_host_dir>/data:/var/opt/mssql/data \ -v <your_host_dir>/log:/var/opt/mssql/log \ -v <your_host_dir>/secrets:/var/opt/mssql/secrets \ -v <your_host_dir>/backups:/var/opt/mssql/backups \ -d mcr.microsoft.com/mssql/server:2022-latest ACCEPT_EULA and MSSQL_SA_PASSWORD are environment variables. You must provide a strong password as per the password policy of SQL Server authentication. The -p flag is the port mapping of host and container ports. The default port at which SQL Server instance runs is 1433 which will be the port inside the container. So, port 1433 of the container will be mapped to port 8081 of the host machine (macOS). You can leave the port 1433 for the host as well, but I prefer to change it. name and hostname are container identifiers. -d will ensure the container runs in a detached mode, meaning it won't block the terminal process, the terminal won't output anything except the container ID and the terminal process will exit. -v represents mounting a host machine directory as a data volume. In simple words, it's a way to persist data across multiple container instances. The data won't go away when you remove a container and the new container instance can access the data left behind by the old one. Replace <your_host_dir> with a directory of your choice (outside the container), for example, ~/Documents/mssql/data. mcr.microsoft.com/mssql/server:2022-latest is the image identifier (name) which will run inside the container. With that being done, SQL Server should now successfully run on your system. Connecting (Accessing) the SQL Server Instance To connect to the running SQL Server instance there are multiple tools which you can use. The best one to use is the mssql Visual Studio Code extension. Make sure to go to Advanced Settings and set the port number to the port of the host machine on which you exposed SQL server instance of the container. Learn more about the Visual Studio Code extension on Microsoft's documentation. If you want a full-fledged dedicated database explorer tool, then I would recommend DBeaver which works well with most of the popular and established SQL database engines. Azure Data Studio is another tool which you can use to interact with SQL Server database, but Microsoft announced that it will be retiring in Feb 2026, meaning it will not receive any feature or security updates past Feb 2026, and Microsoft recommends to switch to Visual Studio Code's mssql extension which I demonstrated earlier in this post. Backing Up and Restoring the Database On Windows, it's easy to backup and restore SQL Server databases using SQL Server Management Studio (SSMS), but not so easy on MacOS. The host directories (specifically, <host_dir>/mssql/backups) that I mounted as data volumes to the container while running it, will help to efficiently backup and restore the database. I am going to backup and restore the database using Transact-SQL queries which can be executed in any database management tool of your choice (the one which is used to connect to SQL Server instance). Backing Up To back up an existing database, run the following Transact SQL query: BACKUP DATABASE [db_name] TO DISK = N'/var/opt/mssql/backups/db-name.bak' WITH NOFORMAT, NOINIT, NAME = N'db-name', SKIP, NOREWIND, NOUNLOAD, STATS = 10; GO Note that the disk path is of the container, but since I mapped it to a host directory path (as a data volume), I can access the backup file on that host directory path as well, for me it's <host_dir>/backups (see 👆 Running SQL Server Image in a Container). Restoring If I want to restore a database from an existing backup file (.bak) retrieved from a different environment, I have to put that file in a host directory which is mounted as a data volume to a container directory, in this case <host_dir>/backups which is mapped to /var/opt/mssql/backups inside the container. Once I put that file in <host_dir>/backups, it becomes instantly available at /var/opt/mssql/backups which will be used to restore the DB using a Transact SQL query. To restore from an existing .bak backup file, run: RESTORE DATABASE db_name FROM DISK = '/var/opt/mssql/backups/db_name.bak' WITH MOVE 'db_name' TO '/var/opt/mssql/data/db_name.mdf', MOVE 'db_name_Log' TO '/var/opt/mssql/data/db_name_Log.ldf', REPLACE; GO CAUTION REPLACE keyword will overwrite the existing database (if it already exists). Without REPLACE, you will get a warning to backup the database if it already exists and the query will terminate. If there are secondary files associated with the db you are restoring, you might end up encountering an error. To overcome that, list all of the associated files with the backup by running: RESTORE FILELISTONLY FROM DISK = '/var/opt/mssql/backups/db_name.bak'; You might get an output similar to the following: LogicalName PhysicalName .............. ------------------- ---------------------------------------------------------------------------- --------------- YourDB Z:\Microsoft SQL Server\MSSQL15.GLOBAL\MSSQL\Data\YourDB\YourDB.mdf .............. YourDB_Product Z:\Microsoft SQL Server\MSSQL15.GLOBAL\MSSQL\Data\YourDB\YourDB_Product.ndf .............. YourDB_Customer Z:\Microsoft SQL Server\MSSQL15.GLOBAL\MSSQL\Data\YourDB\YourDB_Customer.ndf .............. YourDB_log Z:\Microsoft SQL Server\MSSQL15.GLOBAL\MSSQL\Data\YourDB\YourDB_Log.ldf .............. Consider those secondary files and move them also using the MOVE directive: RESTORE DATABASE db_name FROM DISK = '/var/opt/mssql/backups/db_name.bak' WITH MOVE 'db_name' TO '/var/opt/mssql/data/db_name.mdf', MOVE 'db_name_Log' TO '/var/opt/mssql/data/db_name_Log.ldf', MOVE 'secondary_file_name_as_in_the_list' TO '/var/opt/mssql/data/secondary_file_name_as_in_the_list.<extension_as_printed_in_the_list>', REPLACE; GO That's how you can configure, run and manage SQL Server on macOS, but there's a quick way to setup a docker container using the mssql VS Code extension. CAUTION NOTE: The following method does not allow much customization such as creating data volumes. The Quickest Way To Create SQL Server Docker Container Install the mssql Visual Studio Code extension and while adding a new connection, select Create local SQL Container option. Enter the configuration details to create a local docker container which runs SQL Server. The password you set will be the password for the SQL Server authentication (the SA user password). With that being done, a new docker container will spin up and you can verify that by running docker ps or by verifying it from the Docker Desktop dashboard. --- ## Vite 7.0 — All Major Changes - **URL**: https://syntackle.com/blog/vite-7-is-here/ - **Updated On**: October 18, 2025 - **Description**: On June 24, 2025, the team behind Vite — the most beloved build tool for frontend applications — announced v7.0 which brings huge changes to the build tool. Vite team introduced "Vite+" (a superset of Vite) in Amsterdam ViteConf on October 13, 2025. Here are 3 major changes which come with Vite 7.0. - **Tags**: post, javascript, web, sde, frontend, news - **Author**: Murtuzaali Surti Vite team introduced "Vite+" (a superset of Vite) in Amsterdam ViteConf on October 13, 2025. It is in its development phase right now, with a preview scheduled to be released in early 2026, but early access registrations are open. Vite is my go to build tool for building modern frontend applications and it integrates well with top frontend libraries such as React. On June 24, 2025, the team behind Vite announced v7.0 which brings huge changes to the build tool. Here are 3 major changes which come with Vite 7.0. Table of Contents Dropped Node.js 18 Node.js v18 reached its End-Of-Life (EOL) in April 2025 and hence, with the new version, Vite is dropping support for Node.js v18. Support for Baseline Widely Available Browser Features Cross browser compatibility is a major issue when dealing with relative new web features and Baseline addresses exactly that. If a web feature is marked as "baseline widely available", its trusted that it will work on all major browsers (core browser set). Vite 7.0 adds 'baseline-widely-available' as the default browser target. Dropped Legacy Sass API Sass announced deprecation of its legacy JS API in Dart Sass 1.45.0, which will be completely removed in Dart Sass v2.0. In accordance with this, Vite 7.0 has dropped support for the legacy Sass API as well and will now default to the modern API only. Read the full Vite v6 to v7 migration guide. Vite's team has also been working on a new Rust-based bundler named rolldown-vite which they plan to make the default Vite bundler in the near future. It will be interesting to see the performance improvements that come with it, considering the fact that Vite is already fast enough as compared to other build tools and bundlers. Recently, React also announced the deprecation of "Create React App" — its opinionated build setup, making Vite the defacto build tool for libraries like React. --- ## 5 Best Places To Learn React For Free - **URL**: https://syntackle.com/blog/best-places-to-learn-react/ - **Updated On**: June 7, 2025 - **Description**: In this guide, I am sharing five of the best places to learn React. Doesn't matter if you are a newbie in React or advancing your journey as a React developer, these resources will help you at every step of your React journey. I wish I came across these resources while I was new to React. - **Tags**: post, react, web, guide, frontend, listicle, opinion - **Author**: Murtuzaali Surti In this guide, I am sharing five of the best places to learn React. Doesn't matter if you are a newbie in React or advancing your journey as a React developer, these resources will help you at every step of your React journey. I wish I came across these resources while I was new to React. These React resources are completely free and you can always purchase a more in-depth, premium version of them. Most of the creators offer premium React courses along with free content. Table of Contents 1. Josh Cameau's Blog I was fascinated when I came across this blog by Josh Cameau and its immensely satisfying animations. Turns out, not only the animations, but the content is also well laid out and easy to understand. This blog is useful for you if you want a deep understanding of how React works and what are the consequences of using it the wrong way. My favorite post from this blog is Why React Re-Renders. Josh also offers a full paid course called "The Joy of React" if you prefer a structured way of learning. Source: joshwcomeau.com 2. Scrimba Scrimba is the only platform which offers truly interactive courses. I took their React course a while ago and was able to build a digital contact card generator using React. Scrimba's courses not only explain the concept theoretically, but they also have in-between challenges which you can complete on your own by pausing the video in the same editor which the instructor is on — yes it's pretty unique and fascinating. They offer a good amount of free courses so that you can explore. Scrimba has also partnered with MDN to support producing these courses for the betterment of the developer community. Source: developer.mozilla.org 3. Robin Weiruch's Blog Robin's blog might not look attractive at first but trust me it has some of the best React content in it. I recommend going through this blog if you want more React tutorials and design implementations. Robin has written a book named "Road To React" which you can read to get a solid understanding of React. He has also published "Road To Next" which is a book to get you started with Nextjs. Source: robinwieruch.de 4. Net Ninja Net Ninja YouTube channel by Shaun Pelling is one of the most underrated coding related channels. Personally, I have found the content to be comprehensive and extremely easy to understand. Not only that, the content is structured unlike other youtube channels where you don't find structured content which reveals itself in a flow. You can go through its React playlist or explore other playlists on a ton of new technologies and frameworks such as Alpine.js, Pinia, HTMX, SolidJS, and more. Source: youtube.com 5. Dan Abramov's Blog & React's Official Documentation Dan Abramov is one of the core members of the React team and has his own blog overreacted.io on which he shares some highly technical and interesting things about React and the Web. And, there is no better place to explore React other than its official documentation. Source: overreacted.io --- ## Ice — A Free Alternative To Bartender — Menu Bar Management Made Easy - **URL**: https://syntackle.com/blog/the-most-useful-macos-app/ - **Updated On**: September 19, 2025 - **Description**: There's no way in macOS to show the complete list of all menu bar icons when the space is exhausted and the icons overflow. What happens is the latter icons are hidden behind the notch (if your mac has one) or behind the menu bar items of the currently open application. It's as if someone applied overflow: hidden to the menu bar icons container. That's where Ice — an open source, free macOS application by Jordan Baird comes into action. - **Tags**: post, mac, setup, apps, opensource - **Author**: Murtuzaali Surti Table of Contents NOTE: Ice won't work on macOS 26 Tahoe (macOS 26 made significant changes in APIs), a stable release is in the works, but there are beta releases (0.11.13-dev.x) which you can try. I switched from Windows to macOS recently, and overall it has been a great experience so far, except for one thing — the menu bar icons. Some applications such as Docker, have menu bar icons that allow you to do quick and useful actions such as quitting the app or restarting a background service directly from the menu bar besides the control center. The problem arises when you have multiple applications adding their icons to the menu bar and taking up space. That's not the problem, the problem is that there's no way in macOS to show the complete list of all menu bar icons when the space is exhausted and the icons overflow. What happens is the latter icons are hidden behind the notch (if your mac has one) or behind the menu bar items of the currently open application. It's as if someone applied overflow: hidden to the menu bar icons container. Ice - Menu Bar Management Tool For macOS Ice is a free and open source menu bar management tool for macOS developed by Jordan Baird that allows you to view overflowing (hidden) menu bar icons as well as re-order them. Once you install Ice, you can access the settings by right-clicking on the empty space in the menu bar. If you go to the menu bar layout tab under Ice's settings, you see two sections — one for the always visible icons, and one for the icons which can be accessed from a dropdown list. Note that it only shows the icons for active applications that have added their icons in the menu bar. To see the icon in Ice's settings, you may need to start the application that adds the icon to the menu bar once launched. A Free Bartender Alternative Ice is a free alternative to bartender — a similar menu bar management tool for macOS. The difference is, Ice is free and open source, while bartender is not. I would highly recommend folks to donate and support Ice — the work of Jordan Baird. As per a github discussion thread, the developer behind Ice intends to slow down development on Ice to focus on their full time job. Closing Thoughts There are a bunch of open source applications which provide a ton of value without asking anything in return, but it's our responsibility to give back to the open source community, even if it's just a shout out or a small monetary contribution. If you are a developer who recently switched to a Mac, check out this guide on how to set it up and the first things you should do on it. --- ## The Problem With AI Generated Code And How To Deal With It - **URL**: https://syntackle.com/blog/the-problem-with-ai-generated-code/ - **Updated On**: November 24, 2025 - **Description**: AI models and tools are becoming more and more capable day by day, especially at generating code. In this post, I will walk you through my experience of creating a full-stack application using some of these AI coding assistants/agents and what are the implications of AI generated code. - **Tags**: post, AI, gemini, sde, opinion - **Author**: Murtuzaali Surti AI models and tools are becoming more and more capable day by day, especially at generating code. With tools like Capacity, Firebase Studio, Lovable, v0, Augment, one can create entire full stack applications by combining one or more of these tools. But, there's something off once you run the code generated by them. In this post, I will walk you through my experience of creating a full-stack application using some of these AI coding assistants/agents and share some of my observations. Table of Contents TLDR; AI generated code can create a lot of tech debt if you are not aware of what the code does. If you are new to coding, only rely on AI agents/assistants for educational purposes. AI models hallucinate quite often — you need a skilled developer to get them back on track. Improve debugging skills to debug some nasty issues created by AI generated code (if any). AI generated code doesn't eradicate the need of a software developer, it only increases the productivity for now. Developing A Full Stack Application Using AI Recently, I developed a full stack application using Lovable and Firebase Studio along with Gemini 2.5 Pro. It's a collaborative movie list which you can share with your friends and create collections which are editable by them. Gemini 3 Pro is out now! Frontend I vibe coded the frontend using Lovable and found out that it was great at generating frontend. I told Lovable my preferred tech stack, which is React + Vite + React Query, and it did a great job following that. Also, Lovable made its own decisions along the way, for example using Radix UI (shadcn) for components and lucide-react for icons which were actually good. Lovable was able to figure out trivial things such as configuring eslint, tailwind and a couple of vite plugins which were required. Backend For backend, I tried Google's Firebase Studio and vibe coded the backend using Gemini 2.5 Pro and it did a pretty good job. I suggested it to use Node, Express, and Neon's PostgreSQL instance along with the TMDB API. For authentication, I suggested gemini to use lucia-auth and arctic to implement an OAuth based authentication strategy and gemini 2.5 pro nailed it. One thing I like about gemini 2.5 pro is that it recognizes its mistakes and fixes them once realized. The auth strategy it implemented was session-based and although it worked fine locally, it wasn't meant to run on serverless functions. I needed something stateless - so I decided to go with JWT (JSON Web Tokens). I instructed gemini to re-work the auth strategy to use JWT instead of the session based implementation and it did a good job. I loved the fact that gemini 2.5 pro generated one-off DDL SQL queries for the neon postgresql instance to create initial schema, and they were perfect, more importantly, in-sync with the code it generated (thanks to the larger context window of gemini 2.5 pro). RECOMMENDED If you love Lovable, you might want to try Capacity which lets you create, clone, and redesign web applications. Capacity offers a free plan that includes 5 credits per month, ability to create public projects, and Claude 3.7 Sonnet. Source: capacity.so Running the GenAI Code It wasn't a success on the first try. Ran into multiple type-errors and API layer issues, so went back to firebase studio and told it to build the project using CLI commands, look at the type errors and fix them. It fixed most of them but not the ones which were a little bit complex and required human intervention. I observed that gemini went into a loop sometimes, where it tried one thing, didn't work, switched to do something new, and when that also didn't work, it was back at square one. Deployment Deploying the code was mostly a manual thing. I used Vercel to deploy both frontend and backend. This and debugging were the only two things I had to do myself. The Consequences Vibe coding an entire full stack app from start to finish in an extremely less amount of time looks good on paper, but if I look closely, I can see a significant amount of tech debt in the code — things like unused code, unnecessary abstractions, unrelated and inconsistent code, etc. If someone who is completely new to coding vibe codes and develops an application, they will have no idea what the AI model is doing. It's kind of a black box for a newbie in code. Worst thing is that they won't be able to debug issues on their own and will have to solely rely on AI to debug, which can take more time than someone who has decent programming knowledge and can navigate through those issues easily. Another issue is that AI models still hallucinate and can go off track. If you don't know how to bring them back on track, you might spend more time giving prompts than you would have spent coding the application on your own. The Right Way Until AI coding agents and tools do something magical and are able to generate clean, to the point and production-ready code, I recommend generating and using AI code responsibly and wisely. If you are new to programming, I suggest using AI tools as helpers only — to help you learn along the way, and not sole reliers that you blindly trust. Also, I recommend to generate code incrementally — moving on to the next step once you have tested and verified the previous one. To summarize: AI generated code can create a lot of tech debt if you are not aware of what the code does. If you are new to coding, only rely on AI agents/assistants for educational purposes. AI models hallucinate quite often — you need a skilled developer to get them back on track. Improve debugging skills to debug some nasty issues created by AI generated code (if any). AI generated code doesn't eradicate the need of a software developer, it only increases the productivity for now. Closing Thoughts If you think AI can independently generate production-ready code, I don't think we are there yet. And I don't think AI will completely eradicate software jobs, in fact, software engineers will undergo a transformation where they will have to work along with AI. Software engineers will be more like architect-level decision makers. AI won't come for their job, it will be their job. --- ## MCP (Model Context Protocol) Explained — All You Need To Know - **URL**: https://syntackle.com/blog/model-context-protocol/ - **Updated On**: September 27, 2025 - **Description**: MCP (Model Context Protocol), as the name suggests, is a protocol — a way of communication and a set of rules, just like what you have in other protocols, such as HTTP or TCP. Some also term it as the "USB-C" for AI. This post is about me exploring what MCP is, building a basic MCP server, how it works, and why there was a need for it. - **Tags**: post, AI, mcp, guide, opinion, news - **Author**: Murtuzaali Surti MCP (Model Context Protocol) can be called as a buzzword of 2025, except it's not just that. It's a communication protocol launched by Anthropic — the company behind the family of Claude AI models. Almost all major AI companies including Google and OpenAI have embraced MCP and it's on its way to become a standardized way of communication for AI models. This post is about me exploring what MCP is, building a really basic MCP server, how it works, and why there was a need for it. I'll try to keep the terminology as simple as it gets. Table of Contents What is MCP (Model Context Protocol)? MCP, a.k.a, Model Context Protocol in terms of AI, is a standardized way for AI models to communicate with external tools and applications. As the name suggests, it's a protocol — a way of communication and a set of rules, just like what you have in other protocols, such as HTTP or TCP. "Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools." - modelcontextprotocol.io MCP Components MCP follows a client-server architecture, and hence there are two building blocks which are required to facilitate MCP communication: MCP Client and MCP Server. MCP Client It is what talks to an MCP server using the Model Context Protocol. An MCP client is often a part of the AI tool you use to interact with an AI model (if it supports MCP, of course). You can think of it as a tool which helps in spinning up a connection to the MCP server and talk to it using MCP. MCP Server An MCP server is what handles the requests coming from the AI model (MCP client) and maps them to appropriate tasks which are defined in the server. MCP server defines which actions the AI model can perform on the server/tool and which resources it has access to. Think of it as a mapper which maps the request with an appropriate action. When I send a prompt to an AI tool which supports MCP, for instance, Claude Desktop, the MCP client will go to the AI model (Claude for example), the AI model will think how to proceed with the request and explore all of the available tools and resources, and then it will decide which tools/resources to use and tell the MCP client to talk to the external MCP server to fetch/execute those. Once the MCP client gets the response from the MCP server, it will send that data to the AI model which will re-structure it in a human readable way. Up until now, I used to ask, well is it any different from an actual API? It's not, but at the same time it is. Let me explain. Why Do We Need MCP? As an AI model, if I want to connect to external tools or applications, I need to build an API for it so that the AI model can communicate with the tool or application — that's easy, but note that it's specific to that particular tool or application. If there's another tool which I would like my AI model to use, I have to build a custom interface (API) for that as well. So, traditionally, what happens is the number of APIs I have to build is directly proportional to the number of tools/applications I want my AI model to use. That's quite cumbersome for an AI company to build and manage. And here's where Anthropic played a smart move. What Anthropic did was offload the task of building APIs to the developer community, i.e., the external tools and applications. They were like, "Hey, why don't we create a universal protocol that'll be AI model-and-tool-agnostic? Meaning, any AI model supporting it can communicate to any external tool/application which supports the protocol." That way, AI model providers just have to support MCP and their AI models can communicate with any external tool which also supports MCP. The task of integrating MCP with the application became a job of the application developer and not the AI model provider. It's a win-win for both because they only have to maintain a single interface, that is MCP. Not only that, third party MCP servers can also be built for a given application. This encourages the developer community to build solutions on top of existing applications using AI without the need to know the specifics about the AI model or platform and vice-versa. How to Build An MCP Server An MCP server can define three components: Tools: Used to perform an action, execute a function. Similar to PUT/PATCH/DELETE HTTP requests. Resources: Data that can be read by MCP clients. Similar to GET HTTP request. Pre-defined Prompts: Prompt templates that can be used by LLMs. MCP servers can be built using Python, Node, Java, Kotlin, and C#. In this tutorial, I am building a basic MCP server using Node (TypeScript). Pro Tip Install Typescript and Node, if you haven't done that already. Install the MCP SDK & Setup Application Initialize a Node application using npm init -y. Then, install the appropriate SDK depending on your programming language or framework. For me it will be: npm install @modelcontextprotocol/sdk Modify the package.json file to: Make the node application a module. Including an executable file in the bin folder using the bin script. Set the permissions for the executable file in the build script. If necessary, include a files script to define which files are included in the final build. { "name": "mcp-demo", "version": "1.0.0", "type": "module", "bin": { "mcp-demo": "./dist/index.js" }, "scripts": { "ts": "npx tsc", "rootFile": "chmod 755 ./dist/index.js", "build": "npm-run-all -s ts rootFile" }, "files": [ "dist" ], "dependencies": { "@modelcontextprotocol/sdk": "^1.10.1", "npm-run-all": "^4.1.5", "typescript": "^5.8.3" } } Creating a tsconfig.json file at the root of the project folder is recommended. { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules" ] } Create an MCP Server Instance Create an src folder and in that an index.ts file (you are free to define your own folder structure but make sure to update package.json and tsconfig.json accordingly). // index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new McpServer({ name: "mcp-demo", version: "1.0.0", capabilities: { resources: {}, tools: {}, prompts: {}, } }); const transport = new StdioServerTransport(); await server.connect(transport); console.log("Server started and connected to transport."); A transport type is required by the MCP server to communicate with the MCP client, and that's why the stdio transport type is used. Note that this transport type is only available in Node. Configure an MCP Client For this tutorial, I am using Claude Desktop App as an MCP client to communicate with my MCP server, but you are free to use any MCP client you prefer. First, I need to inform Claude Desktop about my MCP server and give it the path to find it. To do that, I need to modify a configuration file of Claude Desktop. That file exists on the following paths for the following operating systems: MacOS & Linux: ~/Library/Application\ Support/Claude/claude_desktop_config.json Windows: AppData\Claude\claude_desktop_config.json Open that file using VSCode from the terminal: MacOS & Linux: code ~/Library/Application\ Support/Claude/claude_desktop_config.json Windows: code $env:AppData\Claude\claude_desktop_config.json Then, once you are able to access and view that file, add a mention of your MCP server as shown below: { "mcpServers": { "mcp-demo": { "command": "node", "args": [ "/Users/murtuzaalisurti/Documents/Development/MCP Servers/demo/dist/index.js", // absolute path to your MCP server build ] } } } Save the file and restart Claude Desktop. If you encounter an error, go to the Claude App Settings > Developer > Logs. If the MCP server is correctly configured and registered in Claude Desktop, you wouldn't get any errors and you can verify the MCP server is running by going to Claude App Settings > Developer > [MCP Server Name] — it should have a running status. Use the MCP Server In Claude Desktop, after you connect your MCP server, you should see a plug icon if you have defined resources in your MCP server. That allows you to attach the data from those resources to the AI model context. Once you attach a resource to the chat context and tell Claude to retrieve that info in normal human language, it will do so. If you define tools in your MCP server, you will see a hammer icon in Claude Desktop depicting that those tools are available to use. For example, I added a tool which modifies the age of a user in my MCP server and used it in combination with the existing resource which provides a list of users to Claude. Note that you need to attach the resource to the chat context. Also, Claude will ask your permission to modify the data. const users = [ { name: "Alice", age: 30 }, { name: "Bob", age: 25 }, { name: "Charlie", age: 35 } ] server.tool( "modify-user-age", "Modify user age", { name: z.string(), // install zod: npm i zod age: z.number(), }, ({ name, age }) => { const user = users.find(user => user.name === name); if (!user) { return { content: [{ type: "text", text: `User ${name} not found`, }] } } user.age = age; return { content: [{ type: "text", text: `User ${name} updated to age ${age}`, }] } }, ) Now, if I ask it again to fetch the list of users (after attaching the resource to chat context), Claude responds with the updated data. That was all about me building a basic and simple MCP server and integrating it with Claude Desktop (an MCP client). List of MCP Servers Here's an inexhaustive list of MCP servers you can try: Git MCP GitHub MCP Google Maps MCP Reddit MCP Blender MCP Da Vinci Resolve MCP Google Drive MCP Figma MCP Prisma MCP GitLab MCP FileSystem MCP Slack MCP Brave Search MCP SQLite MCP MCP Server Directories (Registries) If you want to explore more MCP servers, check out these MCP server directories/registries — they contain a compilation of variety of MCP servers, including community and official MCP servers. Official MCP Registry - One can publish/retrieve MCP servers and also build public/private sub-registries. Glama Cursor MCP Directory HuggingFace MCP Server List punkpeye/awesome-mcp-servers modelcontextprotocol MCP server list MCP.so Thoughts MCP feels like the HTTP for AI, not in a technical sense, but in a more universal sense. In fact, HTTP is itself a transport method used by MCP. MCP doesn't solve every AI model communication problem, but certainly makes it easier. Some also term MCP as the "USB-C" for AI. Other protocols such as A2A (Agent to Agent Protocol) make communication between AI agents built using different frameworks easier. This is just the beginning of AI communication protocols and I hope we will see some more powerful, better and privacy-focused AI protocols. --- ## How to Use Custom Models with API Keys in GitHub Copilot (VSCode) — Bring Your Own Key (BYOK) - **URL**: https://syntackle.com/blog/github-copilot-with-custom-api-key/ - **Updated On**: December 28, 2025 - **Description**: In this tutorial, I will demonstrate how you can use custom API keys from multiple AI providers and use any models associated with them in GitHub Copilot Chat VSCode extension. This feature is not available to Copilot Business or Enterprise users yet. - **Tags**: post, vscode, extensions, AI, github, workflow, tutorial - **Author**: Murtuzaali Surti Table of Contents With the "March 2025 (v1.99.0)" Visual Studio Code release, GitHub Copilot supports BYOK (Bring Your Own Keys), meaning, I can now use my own custom API key and use any models associated with it in GitHub Copilot VSCode extension. Note that BYOK feature is not available to Copilot Business or Enterprise users yet. - code.visualstudio.com GitHub Copilot Chat supports API keys generated from major AI platforms/providers, namely, Anthropic, Azure, OpenRouter, OpenAI, Gemini and Ollama. In this tutorial, I will demonstrate how you can use custom API keys from multiple AI providers and use any models associated with them in GitHub Copilot Chat VSCode extension. 1. Open GitHub Copilot Chat Go to the GitHub Copilot Chat window by clicking the copilot icon besides the search bar in VSCode. You can also access github copilot chat by using the shortcut Ctrl + Alt + I (Windows) or Ctrl + Command + I (MacOS). GitHub Copilot Chat 2. Manage Models Click on the model name dropdown (claude-3-5-sonnet-202041022 in the image below) and select "Manage Models...". GitHub Copilot Chat Manage Models Feature 3. Configuring AI Provider Once you click on 'Manage Models...', a window will appear, listing all of the configured AI model providers. The manage models feature supports Anthropic, OpenAI, OpenRouter, Ollama, Azure, Groq, xAI, and Google. If you want to configure another available AI model provider which is not yet configured, click on 'Add Models' button. configure AI provider)" height="400" width="1198">GitHub Copilot Chat Manage Models Feature (Configuring AI Provider) 4. Add Your API Key Once you add a new AI platform/provider or modify a configured one, you will be asked to provide an API key (or you will be asked to sign in to microsoft account if you select Azure). select AI provider > insert API key)" height="174" width="1198">GitHub Copilot Chat Manage Models Feature (Adding API keys) 5. Select AI Models Once you enter the API key, the window listing configured AI model providers/platforms will refresh, and you can hide/show models which you plan to use with GitHub Copilot. select AI provider > insert API key > show/hide models to be used)" height="771" width="1518">GitHub Copilot Chat Manage Models Feature (Selecting Models) Using the AI Models For instance, if I selected a total of five AI models across two AI providers, all of those five models will be listed in the model selection dropdown. GitHub Copilot Chat (Selecting Model) One thing that's missing in this Manage Models feature is the ability to show the AI platform/provider besides the model name. It will be really helpful to differentiate models based on providers, for example, OpenRouter and Anthropic both provide claude-3-5-sonnet model, so it will be helpful to know if I am using a claude model through OpenRouter or Anthropic directly. I hope they add this in the future, considering the fact that the Manage Models feature in GitHub Copilot is still in preview. UPDATE: Now, the models are categorized clearly. I can see the default Copilot provided models at the top and models from other providers below that with the name of the provider listed on the left side in a distinctive way. Updated GitHub Copilot Chat (Selecting Model) --- ## TypeScript Switches To Go — What Does This Mean for Developers? - **URL**: https://syntackle.com/blog/typescript-go-port/ - **Updated On**: March 15, 2025 - **Description**: TypeScript team unveiled the biggest update yet, announcing a switch from JavaScript to Go, making it 10 times faster, on March 11th, 2025. This is a huge win for developers as it will drastically cut down editor start-up time for significantly large projects as well as improve build time. - **Tags**: post, typescript, vscode, sde, performance, news - **Author**: Murtuzaali Surti Microsoft (the company behind TypeScript) and its TypeScript team unveiled the biggest update yet, announcing a switch from JavaScript to Go on March 11th, 2025. This means TypeScript will receive up to 10x performance gain than its current implementation. This is a huge win for developers as it will drastically cut down editor start-up time for significantly large projects as well as improve build time. Why Go (Golang)? The Typescript team was considering Rust as well as C#, but according to Anders Hejlsberg — lead architect of TypeScript — porting to Go (Golang) was the way of least resistance. "C# was a top contender for the port, as was Rust. But both would have been a rewrite more than a port. We picked Go because it was the path of least resistance to 10x for this particular code base. It's a win for OSS. We couldn't have done this in the past!" - Anders Hejlsberg on X (formerly Twitter) Also, Go is structurally similar to the implementation of TypeScript and it closely resembles existing patterns in the TypeScript code. That's why they are calling it a port than a rewrite. Read this discussion to know more on why Go was the preferred choice among many other suitable programming languages. A Win for Developers Here's what the TypeScript port to Go a.k.a Golang means for software developers: Improved Editor Startup Reduced Memory Usage Faster Builds Improved Overall Efficiency Time taken to compile typescript (running tsc) for listed codebases. Source: devblogs.microsoft.com As shown in the image above, the typescript-go version of TypeScript drastically improved build times when running tsc on the listed codebases. I have worked with significantly large projects involving TypeScript and I can say that it takes a ton of time to load Visual Studio Code and get the TypeScript interpreter running (especially on Windows). The RAM usage is off the charts as well with larger TypeScript codebases. Microsoft claims that using the go-based typescript version, it takes 8 times less than what it currently takes to load the editor (for VSCode codebase), i.e, an 8x performance improvement in the editor start-up time. When will it be released? The most recent version of TypeScript is 5.8, and it will not be until version 7.0 that the Go port will be released. So, the last JavaScript based TypeScript version will be version 6 (6.x series) which they call it by the codename "Strada". The codename for the Go based TypeScript version (7.0) is "Corsa". So, in the future, there will be two versions of TypeScript written in two different languages — TypeScript 6 (JS based) and TypeScript 7 (Go based). Note that TypeScript does not follow semantic versioning. --- ## Eleventy Image Disk Caching Approach — @11ty/eleventy-img - **URL**: https://syntackle.com/blog/eleventy-image-html-transform-plugin-disk-cache/ - **Updated On**: April 12, 2025 - **Description**: In this guide, I'll walk you through an approach of utilizing disk cache while using the HTML transform method of the eleventy image plugin. Disk cache is a persistent cache which allows you to not re-optimize every single image at every single build. However, there are some caveats which I ran into while using it with the HTML transform method. - **Tags**: post, 11ty, vercel, guide, backend, tutorial - **Author**: Murtuzaali Surti Table of Contents I use the HTML transform method of the @11ty/eleventy-img plugin to post process any img or picture tags in my html. I find this method easy and universal. In this guide, I'll walk you through an approach of utilizing disk cache while using the HTML transform method of the eleventy image plugin. TLDR Tell the plugin to store the optimized images in the .cache folder which can be preserved between builds, instead of the build output directory which you might want to clear before each build. eleventyConfig.addPlugin(eleventyImageTransformPlugin, { formats: ["avif", "webp"], outputDir: ".cache/@11ty/img/", urlPath: "/img/built/", // relative to the build output dir (<img> src) }); Copy the built images from the .cache folder to the build output after eleventy has finished processing. Here, public is the build output directory. eleventyConfig.on("eleventy.after", () => { cpSync( ".cache/@11ty/img/", "public/img/built/", // "public" is the build output dir { recursive: true }, ); }); If you are using Vercel to build and host your 11ty site, then you might encounter this issue — ENOENT: no such file or directory, open '/vercel/path0/.cache/eleventy-fetch-fbe00b3353051d063b093fb7cd28fe.buffer' — see the resolution here. Eleventy Image Plugin Cache Eleventy's image plugin has primarily two caching options — in-memory cache and disk cache. In-Memory Cache The in-memory cache (as well as disk cache) is controlled by the useCache config option which is enabled by default. While in watch/serve mode, identical requests to the same source and with the same config options will get cached and subsequent requests will return that cached response. This is a temporary cache which only persists during the current running process. If the process is killed (i.e. 11ty stops running), the cache is gone. Also, while using the HTML transform method and doing local development, the images will not be optimized beforehand — instead, they will be optimized on demand/request. It means if you open a page containing a single image, no other image except that single image will be optimized until it is requested. The transformOnRequest option governs this behavior and is enabled by default for 11ty's serve mode. Disk Cache Disk cache is a persistent cache which allows me to not re-optimize every single image at every single build. However, there are some caveats which I ran into while using it with the HTML transform method and which you should be aware of: HTML transform method co-locates optimized images by default, so your images don't go to a single directory. Disk cache requires you to check-in and persist images in your output directory across builds. This doesn't work if you clean your output directory before every new build. Even if the .cache folder is preserved by your hosting provider, if you clean your output directory between builds, the images will be re-fetched and re-optimized. I typically clean the output directory before generating a new build and that's why I needed a workaround to ensure the existing images are cached and persistent across builds while using the HTML transform method of the eleventy image plugin. So, I posted my observations on Mastodon and, thankfully, got a response from @zachleat regarding a potential workaround. Approach Firstly, instead of storing the images in the 11ty output directory, you tell the eleventy image plugin to optimize and store the images directly to the .cache folder which you can preserve across builds. import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; /** @param {(import("@11ty/eleventy").UserConfig)} eleventyConfig */ export default function (eleventyConfig) { // ... const persistentImageOutputDir = ".cache/@11ty/img/"; const pathRelativeToBuildOutputDir = "/img/built/"; eleventyConfig.addPlugin(eleventyImageTransformPlugin, { formats: ["avif", "webp"], outputDir: persistentImageOutputDir, urlPath: pathRelativeToBuildOutputDir, }); // ... } It's important to specify the URL path (<img> src path) as you will be copying the optimized images from the .cache folder to the build output directory. Next, you can copy the optimized images from the .cache folder to a directory in the build output directory after they have been optimized using the eleventy.after event. import { cpSync } from "node:fs"; import { eleventyImageTransformPlugin } from "@11ty/eleventy-img"; /** @param {(import("@11ty/eleventy").UserConfig)} eleventyConfig */ export default function (eleventyConfig) { // ... const persistentImageOutputDir = ".cache/@11ty/img/"; const pathRelativeToBuildOutputDir = "/img/built/"; eleventyConfig.addPlugin(eleventyImageTransformPlugin, { formats: ["avif", "webp"], outputDir: persistentImageOutputDir, urlPath: pathRelativeToBuildOutputDir, }); eleventyConfig.on("eleventy.after", () => { cpSync( persistentImageOutputDir, `public${pathRelativeToBuildOutputDir}`, // "public" is the build output dir { recursive: true }, ); }); // ... } That's pretty much it. Now, it doesn't matter if you clear your build output directory before every build, the only thing you need to do is preserve the .cache folder between builds and that is already configured by default by some of the hosting providers. I use Vercel and they provide a zero configuration support for preserving the .cache folder — I just have to select 11ty as a preset/framework. It works until and unless the hosting provider invalidates and discards the .cache folder for some reason. With Vercel, I have seen that it gets discarded when you do package upgrades (package-lock.json gets updated). There is an open issue on the eleventy-image github repo which lists the exact workaround. Hop in there if you face any issues. Eleventy's Fetch Plugin Errors Out If A Cache File Is Missing I recently encountered an issue (ENOENT: no such file or directory, open '/vercel/path0/.cache/eleventy-fetch-fbe00b3353051d063b093fb7cd28fe.buffer') where Vercel was removing buffer files randomly from the .cache directory, and that lead to an unhandled exception in @11ty/eleventy-fetch package which is used internally by the @11ty/eleventy-img plugin. Vercel doesn't guarantee or allow me to configure which files are cached across builds and which are not, so it's hard to say why the issue was happening. However, you can overcome this issue by switching to @11ty/eleventy-fetch v5.1.0-beta.2 — this release includes the handling of a missing but valid cache. Note that, @11ty/eleventy-fetch is a package used by @11ty/eleventy-img internally and comes pre-bundled with it, and so it doesn't include the beta version of eleventy-fetch. To resolve this issue, you must override the @11ty/eleventy-fetch package version in package.json file. // package.json { // ... "overrides": { "@11ty/eleventy-fetch": "5.1.0-beta.2" }, // ... } Read this discussion for more information about this issue. --- ## Claude 3.7 Sonnet, OpenAI's GPT 4.5 and Microsoft's Quantum Chip - **URL**: https://syntackle.com/blog/claude-3-7-sonnet-openai-gpt-4-5-majorana-1/ - **Updated On**: March 7, 2025 - **Description**: Anthropic announced its latest and best Claude model yet — Claude 3.7 Sonnet on February 25, 2025. OpenAI jumped on the bandwagon and announced GPT 4.5 — its latest and largest model on February 27, 2025. To make things more interesting, Microsoft unveiled its first quantum chip — Majorana 1 — which uses topological qubits. - **Tags**: post, AI, listicle, news - **Author**: Murtuzaali Surti Table of Contents This week was bonkers in terms of tech announcements in the field of AI and quantum computing. Anthropic announced its latest and best Claude model yet — Claude 3.7 Sonnet on February 25, 2025. OpenAI jumped on the bandwagon and announced GPT 4.5 — its latest and largest model on February 27, 2025. To make things more interesting, Microsoft unveiled its first quantum chip — Majorana 1. Claude 3.7 Sonnet and Claude Code Anthropic leveled up their game by introducing Claude 3.7 Sonnet and people are satisfied with it. It's a reasoning model with thinking capabilities which works almost every time. Some users are not satisfied with it because of prompting issues, but I think you can get away with it by tweaking your prompt. Claude 3.7 Sonnet is priced at $3 per million input tokens and $15 per million output tokens — including reasoning tokens. Source: anthropic.com Along with Claude 3.7 Sonnet, Anthropic also announced its new product "Claude Code" — a CLI tool to assist you locally in your project. It can create/edit files, debug, fix/write tests, work with git to commit and create PRs, and provide suggestions based on your repository. More like cursor, but inside terminal. However, it comes with certain technical overhead. Here are the minimum requirements to run Claude Code: Minimum 4 gigs (GB) of RAM Internet Connectivity Supports macOS 10.15+ and Ubuntu 20.04+/Debian 10+. For Windows, it requires WSL (Windows Subsystem For Linux). Note that it's in research preview. File any bugs you find on their github repository. OpenAI's GPT-4.5 OpenAI claims GPT-4.5 is their largest and expensive model yet. And yes, at $200 a month, it's crazy expensive. The API pricing for GPT-4.5 is also expensive, with $75 per million input tokens and a whopping $150 per million output tokens. But, does it stay true to its pricing? Source: openai.com/api/pricing I haven't personally tried it yet, but having read first impressions from people who tried it on X (twitter), it seems to me that GPT 4.5 really good at creative tasks such as writing, but not so good at coding. Just like Claude Code, GPT-4.5 is still in research preview, so it may get better over time. Microsoft's Majorana 1 With the unveiling of Majorana 1 — Microsoft's Quantum Chip, Microsoft is now in the game of quantum supremacy along with Google and IBM. Source: azure.microsoft.com Microsoft claims to have achieved quantum breakthrough by discovering a new state of matter known as topological superconductivity. Majorana 1 works with "topological qubits" which store information in topological properties of a physical system, instead of properties of a single particle. I am more interested in seeing the practical use cases of it, especially in the world of AI. The integration of quantum computing with the AI world is yet to be seen. --- ## Leveraging Deep Research by Building Your Online Presence - **URL**: https://syntackle.com/blog/leveraging-deep-research-by-building-an-online-presence/ - **Updated On**: February 23, 2025 - **Description**: Deep research is one of the aspects AI companies are willing to implement and integrate with their current AI systems. The use cases of deep research are massive in nature, because it saves you a lot of time skimming through countless articles and generating a comprehensive report. One such use case can be in the recruitment industry. - **Tags**: post, AI, sde, opinion - **Author**: Murtuzaali Surti Table of Contents Deep research is one of the aspects AI companies are willing to implement and integrate with their current AI systems. The use cases of deep research are massive in nature — medical research, general knowledge, biography, finding something you don't remember but have enough context about it, statistical analysis — and it saves you a lot of time skimming through countless articles to generate a comprehensive report. One such use case can be in the recruitment industry where, in my opinion, if a recruiter finds a profile relevant enough, they can go to an AI platform, use deep research to get a biography of that individual and take it from there. This only works if that person has a strong online presence or has done something meaningful and put it out there on the internet. That brings me to my next point, i.e. building an online presence in the world of AI. Building Your Online Presence I come from a software development background, and if I talk about the software industry taking into consideration its current landscape, I think we are going to see such cases where a strong online presence can land you a decent job. The question is, how to build an online presence in the first place? It's actually quite simple. Here are some of the steps which you can take to establish yourself on the internet. Share what you find interesting. Start a blog of your own. Build tools which saved you a ton of time and open source them so that they can do the same for others. Share what you learn and how that helped you to grow. Deep Researching Yourself Once you build a good enough online presence, go to any of the AI platforms, be it Grok, Perplexity, or OpenAI, and search about yourself and see what information the AI model has gathered for you. Perplexity, xAI's Grok 3, and OpenAI's ChatGPT, all have integrated the deep research (deep search) functionality and they do pretty well. I used perplexity's deep research functionality to research about myself and it does a good job of summarizing you based on what you put out there: Source: perplexity.ai The Future Of Recruiting In my opinion, companies might start integrating deep research functionalities of platforms such as Grok, Perplexity, OpenAI, etc. and use them to do a quick research about the person that they are willing to hire. Now I am not sure up to what extent they might use it but I see at least some use of it in the near future. Similarly, you as a candidate can also research about the company you are interested in using deep research. It'll help you know if you are a right fit for that company, or if the company is a right fit for you. It works both ways. The Flip Side On the other hand, it's important to not overshare yourself on the internet as these same tools can backfire knowing an awful lot about you including the information that you don't want to share. And that's not completely on you, that's also on these companies — they must ensure they don't give up any of the personal information about someone, and I guess that's where we get into a grey area where we have to decide what's shareable and what's not. --- ## Create React App (CRA) is Deprecated, Officially: What's Next? - **URL**: https://syntackle.com/blog/create-react-app-deprecated/ - **Updated On**: October 18, 2025 - **Description**: Create React App a.k.a CRA is a tool (setup tool I would say) which provides an opinionated architecture combining a set of tools required to configure, transpile JSX and bundle React. Now that it's deprecated, here's a list of alternatives you can choose from: Vite, Nextjs, Tanstack, Expo (for native React apps). - **Tags**: post, react, javascript, web, guide, setup, framework, library, listicle - **Author**: Murtuzaali Surti Table of Contents Create React App a.k.a CRA is a tool (setup tool I would say) which provides an opinionated architecture combining a set of tools required to configure, transpile JSX and bundle React. React team announced the deprecation of Create React App on February 14, 2025. Before it was official, CRA was already considered a "no-longer-to-be-used" tool to build modern React applications. The disadvantages of using create-react-app kept increasing with the advent of new tools which were faster and efficient. Source: github.com/facebook/create-react-app Limitations Of Create React App (CRA) One limitation of using "Create React App" is that whenever you want to have full control of the build process and do some advanced customizations to the bundler used by it i.e. Webpack, you have to eject the CRA. In other words, you are now out of the default setup of the CRA, and you now have to manually handle everything (that's a pain — not to mention rewiring packages like react-app-rewired and craco). Find a complete list of Create React App limitations on the React Blog which includes data fetching, code splitting and more. Alternatives The alternatives of CRA are not only better but faster and efficient. I stopped using Create React App for my personal projects a long time ago and also migrated my old projects which were built using CRA to Vite. Yes, Vite is a worthy alternative to build and run React applications. If you have a legacy application built using CRA, then I suggest you migrate to one of the below options as soon as possible, but note that it can take a while depending on the size of your project. 1. Vite Vite uses Rollup and ESbuild to bundle the React application. It is much much faster than Webpack which is used by Create React App as a bundler. Also, Vite's configuration API is simple and easy to understand. Vite claims to be a framework agnostic tool which can be used to build any framework on top of it. Why Vite? — vite.dev Vite team introduced "Vite+" (a superset of Vite) in Amsterdam ViteConf on October 13, 2025. It is in its development phase right now, with a preview scheduled to be released in early 2026, but early access registrations are open. 2. Tanstack Start Tanstack Start is a framework, an alternative to Next.js, which combines tools from the Tanstack ecosystem such as Tanstack Router and Tanstack Query, and it's built on top of Vite. Tanstack Start is currently in the "RC (release candidate)" stage, with v1 dropping soon. The primary focus is on type-safe file-based routing, isomorphic server functions, URL-as-state, and RSC (React Server Components) support. RSC (React Server Components) will not be a part of Tanstack Start v1 RC, but it is in active development and will be released as a later v1 minor (v1.x) version release. 3. Next.js Next.js by Vercel is a full-fledged framework built on top on React which provides it's own routing, data fetching patterns, optimization techniques, bundler (turbopack), compiler and much more. With the introduction of Server Components, React kept pushing hard on using Next.js for building modern React applications because of its support and implementation of React Server Components. If you want to build a complex React application with a focus on React Server Components and you are okay with the techniques and patterns used by Next.js, then you are good to go with it. 4. Expo If you want to build cross-platform native applications (runs on iOS, Android and the Web) using React Native, then Expo (an open-source framework) is the way to go. 5. create-tsrouter-app (Tanstack) If you love Tanstack ecosystem, Tanner Linsley (Creator of Tanstack) suggests using create-tsrouter-app which comes bundled with Tanstack router and React Query (Tanstack Query). Source: bsky.app Thoughts In the deprecation blog released by the React team, they emphasized on using frameworks built on top of React which do things in their own opinionated way. I don't know if it's good or bad, but too much reliance on a framework can sometimes feel restrictive because you cannot do anything outside of which your framework allows you to. That's why, for building small React applications, I would consider letting go of the overhead of frameworks and start with a tool like Vite, do routing with React Router, manage complex state with Redux Toolkit or Zustand, and implement data fetching with something like React Query (Tanstack Query). --- ## How to Use DeepSeek-R1 AI Model: A Comprehensive Guide - **URL**: https://syntackle.com/blog/deepseek-ai-model-and-openrouter/ - **Updated On**: March 1, 2025 - **Description**: DeepSeek's AI model "DeepSeek-R1" (a.k.a deepseek-reasoner) is the most talked about AI model at the time of this writing. The primary way to use any of the deepseek AI models is to go to their API platform, get an API key and use the OpenAI SDK to make calls to the API. Another route involves OpenRouter — which basically routes your request to appropriate providers for the model you specify. - **Tags**: post, AI, sde, backend, tutorial, guide, python, javascript - **Author**: Murtuzaali Surti Table of Contents DeepSeek's AI model "DeepSeek-R1" (a.k.a deepseek-reasoner) is the most talked about AI model at the time of this writing. I recently integrated it with better — a code reviewer github action powered by AI — which I developed during a hackathon. In this guide, I will walk you through ways in which you can integrate deepseek models in your tools and also talk about structured JSON outputs. [TLDR — For JSON outputs with stick to a given schema, along with specifying the response_format as json_object and explicitly specifying the word JSON in the prompt, append the following prompt in your use prompt for better and consistent json outputs: "IMP: give the output in a valid JSON string (it should be not be wrapped in markdown, just plain json object) and stick to the schema mentioned here: <json_schema>".] Using DeepSeek's API The primary way to use any of the deepseek AI models is to go to their API platform, get an API key and use the OpenAI SDK to make calls to the API. Pro Tip DeepSeek's API is compatible with OpenAI SDK (it's available for python and javascript both). The problem with this is, currently at the time of this writing, DeepSeek's API platform is down (throwing a 503 service unavailable) is up, but they have disabled API recharges for now — you can use your existing balance to use paid models. [UPDATE: DeepSeek has enabled new API subscriptions/recharges now.] And that forced me to go another route which involves OpenRouter — which basically routes your request to appropriate providers for the model you specify. If you do have existing balance in your DeepSeek account to use deepseek-r1, you can use OpenAI's SDK with your DeepSeek API key and change the base_url to https://api.deepseek.com. Note that the alias for deepseek-r1 is deepseek-reasoner when interacting with DeepSeek's API. const openai = new OpenAI({ apiKey: '<deepseek-api-key>', baseURL: "https://api.deepseek.com" }); const result = await openAI.chat.completions.create({ model: 'deepseek-reasoner', messages: [ { role: "system", content: '<the-system-prompt>', }, { role: "user", content: `<user-prompt>`, }, ], }); const { message } = result.choices[0]; console.log(message.content); Using OpenRouter OpenRouter too is compatible with the OpenAI SDK — you just have to change the base URL. That's genius because it makes the adoption rate go high. To use any of the models through OpenRouter, you need to generate an API key from their platform. And, credits need to be loaded for you to be able to use any of the paid AI models. There's also a free version of deepseek-r1 model, named deepseek/deepseek-r1:free, available on OpenRouter. Good option if you want to just try it out. Once you get the API key, initialize the OpenAI SDK and implement it as shown below: const openai = new OpenAI({ apiKey: '<open-router-api-key>', baseURL: "https://openrouter.ai/api/v1" }); const result = await openAI.chat.completions.create({ model: 'deepseek/deepseek-r1', messages: [ { role: "system", content: '<the-system-prompt>', }, { role: "user", content: `<user-prompt>`, }, ], }); const { message } = result.choices[0]; console.log(message.content); The best thing about OpenRouter is if you append :floor to any model name (e.g. deepseek/deepseek-r1:floor), you can get the cheapest price in the market for that model. This is done by sorting the providers of that model. It is the same as setting provider.sort to price. Structured JSON Output I was experimenting with the deepseek-r1 model to make it produce an output which sticks to a schema. It seems that you have to do more than just define a response_format as json_object. I got it working by specifying the following as a part of the user prompt: { role: "user", content: `<primary_prompt> - IMP: give the output in a valid JSON string (it should be not be wrapped in markdown, just plain json object) and stick to the schema mentioned here: <json_schema>.`, } And then specifying the response_format as json_object. await openAI.chat.completions.create({ model: "deepseek/deepseek-r1", messages: [ { role: "system", content: "<the-system-prompt>", }, { role: "user", content: `<primary_prompt> - IMP: give the output in a valid JSON string (it should be not be wrapped in markdown, just plain json object) and stick to the schema mentioned here: <json_schema>.`, }, ], response_format: { type: "json_object", }, }); Note that the model might generate empty content sometimes according to the official documentation. So, try to implement a retry mechanism to mitigate this problem. Using DeepSeek in a VSCode Extension In my opinion, the best VSCode extension you can use deepseek-r1 with is Cline. The most useful thing about this extension is its plan-then-act mode which when in planning mode, gives you suggestions and approaches you can try. And then, when you switch to act mode, it implements and refactors the actual code. I have talked more about Cline in my VSCode extensions 2025 list. Install the Cline extension. Select a deepseek model (deepseek-reasoner for deepseek-r1) and a provider like OpenRouter or DeepSeek as shown below. Provide the API key and start using the extension. 👇 Running it locally If you have enough computing power to run deepseek-r1 locally, you can do that using Ollama. Here's a quick guide by DataCamp to run deepseek-r1 locally. --- ## React Compiler Integration With Astro (Vite) - **URL**: https://syntackle.com/blog/integrating-react-compiler-with-astro/ - **Updated On**: October 11, 2025 - **Description**: Astro, which is a meta-framework and uses Vite, can be integrated with the React compiler to improve the performance of your React application — and that's exactly what I will be showing you how to. React compiler is now stable and compatible with versions of React 17+ with the help of react-compiler-runtime. - **Tags**: post, react, astro, frontend, performance, tutorial - **Author**: Murtuzaali Surti React compiler was introduced to tackle massive re-rendering issues within a react application. What it does is that it looks at the code and figures out if a certain component or a value can be memoized or not to limit its re-rendering. Does that mean you should not use useCallback(), useMemo() or React.memo() from now? Not really. You should do manual memoization wherever you are certain that you don't want to re-render a particular component or value until something changes. React compiler is more of a safeguard in case you forgot to memoize something which should be memoized. Now that I gave a shallow explanation of what the react compiler is and what it does, let me tell you how to integrate it with an Astro project which uses React components. React Compiler Deep Dive! Installing React Compiler React compiler is compatible with versions of React above 17 and is stable (v1 was released on Oct 7, 2025). It is available as a babel plugin, so to install it, install the babel-plugin-react-compiler package: npm install babel-plugin-react-compiler@latest For folks who are not yet on React 19 and are using versions 17 or 18, you need to install one more package named react-compiler-runtime@latest. npm install react-compiler-runtime@latest Integrating with Astro Once you install the compiler, go to the astro config file (usually astro.config.mjs) and add the babel plugin to the @astrojs/react package. import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ // ... integrations: [react({ babel: { plugins: [ ["babel-plugin-react-compiler"] ] } })], }); For React 17+ users, after installing the react-compiler-runtime@latest package, you need to set a target key in the compiler config. Set it to the major version of React which you are using. import react from '@astrojs/react'; const compilerConfig = { target: '19' // can be '17' | '18' | '19', default is 19 }; // https://astro.build/config export default defineConfig({ // ... integrations: [react({ babel: { plugins: [ // make sure this is first in the plugins list ["babel-plugin-react-compiler", compilerConfig] ] } })], }); That's it, react compiler is now integrated with Astro. You can follow a similar approach to integrate react compiler with any frameworks which use Vite. You just need to add the babel plugin to the react adapter/plugin you are using for Vite. INFO If you are using vite + react via the @vitejs/plugin-react package with no meta-framework, you can configure the compiler in a similar way in vite's config file. // vite.config.{js,ts} import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; const compilerConfig = { target: '19' // can be '17' | '18' | '19', default is 19 }; export default defineConfig({ plugins: [ react({ babel: { plugins: ['babel-plugin-react-compiler', compilerConfig], }, }), ], }); Linting with ESLint eslint-plugin-react-hooks provides rules for identifying violations of the Rules of React. When the ESLint rule reports an error, it means the compiler will skip optimizing that specific component or hook. - react.dev Install the eslint plugin and configure it based on your eslint config as documented here in the react repository docs. npm install -D eslint-plugin-react-hooks@latest Pro Tip Want to check the performance post integration? That can be done using a package named react-scan by Aiden Bai. Add the script tag in your index layout file and it will be ready to use. <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="description" content="myai" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script> <title>{title}</title> </head> <body> </body> </html> --- ## 11 VSCode Extensions I Use [2025] - **URL**: https://syntackle.com/blog/vscode-extensions-2025/ - **Updated On**: April 4, 2025 - **Description**: Visual Studio Code (VSCode) is an editor which is simple on it's own but enriched when combined with extensions. Here are the visual studio code extensions that I use — cline, filesize, better comments, code spell checker, git graph, markdownlint, total typescript, pretty typescript errors, sqltools and codeium (windsurf). - **Tags**: post, vscode, extensions, listicle, AI, workflow, sde - **Author**: Murtuzaali Surti Visual Studio Code (VSCode) is an editor which is simple on it's own but enriched when combined with extensions. So, in this article, I will tell you some vscode extensions I personally use which might be beneficial to you. Table of Contents Download Visual Studio Code. 1. Cline # Cline is the best vscode extension for AI assistance. What I like about it the most is it's plan-then-act mode. While it plans (in plan mode), it walks you through the steps showing what's possible and what's not. Once you agree with the approach, switch to the act mode wherein it implements that approach in your editor. That's good because unlike other AI coding assistants, it gives you the time to think and then implement. It does not churn out code for you instantly — that's helpful if you want to implement an approach yourself before letting AI do it. Not only that, it also shows the tokens and the usage cost, which for me is incredibly helpful to track how much I use each model. Here's a little demo for you to see it yourself. Your browser does not support the video tag. 2. Filesize # Filesize is a simple extension which enables me to see the size of a file in VSCode's status bar. It's really handy if I want to see size of a build file (especially if I am dealing with javascript). Source: visual studio marketplace 3. Better Comments # Better Comments, as the name suggests, allows me to tag my comments and colorize them according to the type. For example, if you specify TODO: before a comment it will colorize it in yellow to indicate that this is pending. If a method is deprecated, I can use an exclamation mark to color it in red. To customize the tags, I can edit this property the settings.json file: { "better-comments.tags": [ { "tag": "!", "color": "#FF2D00", "strikethrough": false, "underline": false, "backgroundColor": "transparent", "bold": false, "italic": false }, // ... { "tag": "*", "color": "#98C379", "strikethrough": false, "underline": false, "backgroundColor": "transparent", "bold": false, "italic": false } ] } 4. Code Spell Checker # The use case of this extension is reduced after the advent of AI extensions, but I still keep it around in case I misspell a variable unknowingly. The part where it shines is detection of camelCase or any other coding case words because it splits the words and then spells check them. 5. Git Graph # Git Graph is one of those extensions which you definitely need of you don't want to pay for GitLens and other similar extensions. I use it because it's completely free and provides all of the useful insights about your git repo. It's a near perfect git extension. 6. Markdownlint # I write blogs, and so markdownlint is a useful extension for me as I write my posts in markdown. It's a decent extension allowing me to properly structure content inside a markdown file. Source: visual studio marketplace 7. Total Typescript # This is one of the most useful extensions if you are new to typescript and learning it while working on an existing project. Total Typescript's vscode extension annotates typescript keywords and operations, and when you hover over it, it describes what it is and how it can be used. For example, if there is a union of types, it will highlight that and provide a description upon hovering. Not only that, if there is a typescript error, it will try to explain it in a more meaningful and human readable way. 8. Pretty TypeScript Errors # I recommend using this extension along with the above extension (i.e. total typescript extension). Why? Because not only do you get meaningful explanation of the error, you also get the error prettified and formatted nicely. If you haven't seen those ugly unformatted typescript errors yet, you will likely come across them in the future and you will get to know the importance of this extension. Source: github.com/yoavbls/pretty-ts-errors 9. Auto Rename Tag # I only listed this extension to let you know that the functionality of Auto Rename Tag is now built in to VSCode. To turn on this feature, simply set the editor.linkedEditing key to true in your settings.json file. { // ... "editor.linkedEditing": true, // ... } 10. SQL Tools # I use SQL Tools (a database management extension) only when I don't want to go outside of my editor to query a database and to perform simple operations and lookups. It allows me to connect to a specific database from within my editor but keep in mind that you need to install driver extensions separately in order to connect to a database. I don't recommend this extension for complex use cases. In that case, you can look for a standalone database management tool like pgAdmin or DBeaver. Source: visual studio marketplace 11. Codeium (Windsurf) # Codeium (now Windsurf) is an AI auto-completion and chat tool for coding similar to GitHub Copilot. At work I use GitHub Copilot, but for my personal projects Codeium (now Windsurf) does a good enough job. I listed it here because it's free to use and provides features which are decent enough. Source: visual studio marketplace RECOMMENDED Some more visual studio code extensions which you should definitely try. --- ## Issue With NVM Node Version Across Terminals: Command Node Not Found - **URL**: https://syntackle.com/blog/nvm-node-issue-command-not-found/ - **Updated On**: January 12, 2025 - **Description**: You might have encountered this issue with switching node versions with nvm - where if you do "nvm use <version>", the version is switched correctly in the current terminal shell, but if you try to use node on a new terminal shell or in a different terminal, you get a command node not found error. - **Tags**: post, nodejs, mac, nvm, tutorial, terminal, cli - **Author**: Murtuzaali Surti If you use a unix based operating system like macOS or Linux, you might have encountered this issue with switching node versions with nvm - where if you do nvm use <version>, the version is switched correctly in the current terminal shell, but if you try to use node on a new terminal shell or in a different terminal, you get a command node not found error. I recently experienced this issue myself, and will try to consolidate the fixes here in this post as a reference to my future self as well as for all of you folks. First thing I did was, I checked the ~/.zprofile file on my system - for you it may be ~/.bash_profile or ~/.bashrc or ~/.zshrc depending on your shell - and then moved the following lines at the bottom of the file, so that no other application overrides them. For me, it was VS Code, which was overriding the PATH variable at the end of the file. - https://stackoverflow.com/a/47883587/17241798 export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion # -- end of file The second thing you should do is set the default alias of nvm to a node version which you would like to use by default. I set it to lts/* which is the latest long term support version of node. But before you do that, make sure you install the lts/* version by running (wrap lts/* in single quotes for zsh): nvm install 'lts/*' # wrap lts/* in single quotes for zsh CAUTION If you don't have it installed, you might run into: ! WARNING: Version 'lts/*' does not exist. default -> lts/* (-> N/A) And then, setting the default alias: nvm alias default 'lts/*' After that, when you switch to a different node version using nvm use, you can use the new version in the current terminal session (verify it by node -v), but on a new terminal instance, it will fallback to the default node version you just set using the default alias. TLDR Go to your ~/.zprofile or ~/.bash_profile or ~/.bashrc or ~/.zshrc file, locate these nvm lines, and move them at the end of the file: export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion # -- end of file Install the lts/* node version by running: nvm install 'lts/*' # wrap lts/* in single quotes for zsh Set the default alias of nvm to a node version which you would like to use by default. nvm alias default 'lts/*' If any of the above solutions don't work, try uninstalling nvm and any other node version you have pre-installed and then re-install nvm. That should fix the issue. --- ## App Defaults 2025 - with some AI stuff - **URL**: https://syntackle.com/blog/best-apps-to-use-in-2025/ - **Updated On**: January 11, 2025 - **Description**: A list of curated apps I use and will probably keep using for the rest of 2025 - continuing my spree of documenting app defaults which I started in 2023. - **Tags**: post, web, apps, AI, listicle, opinion - **Author**: Murtuzaali Surti Continuing my spree of app defaults, which I started in 2023 with this post, here's a slightly changed list of all the apps I use and will probably keep using for the rest of 2025: 🤖 AI Models/Chat Apps - Claude 3.5 Sonnet (for coding), ChatGPT (for general use), Perplexity (for search) 📨 Mail Client - Gmail 📝 Notes - Notion, Obsidian, Bear 📆 Calendar - Google Calendar 📁 Cloud - Google Cloud, OneDrive, Cloudinary (for images) 📖 RSS - Unread 🌐 Browser - Chrome (for daily use), Brave (private browsing), Arc (just to feel nice) 💬 Chat - WhatsApp, Discord 🔖 Bookmarks - Notion Web Clipper, Chrome Bookmarks 🎤 Podcasts - PocketCasts 🔐 Password Management - Bitwarden (still OG) 🧑‍💻 Code Editor - VS Code, Cursor (for AI stuff), Zed ✈️ VPN - ProtonVPN 💸 Quick Financial Calculations - numbr - useful for quick note taking with calculations. RECOMMENDED Some of my open source apps which I use regularly: better - AI-powered Code Reviewer GitHub Action rssed - RSS Feed Collection Of 150+ Developer Feeds --- ## 3 Steps To Think Like A Software Developer - **URL**: https://syntackle.com/blog/looking-at-a-problem-as-a-developer/ - **Updated On**: February 8, 2025 - **Description**: Three steps to better approach the problem given at hand, find the solution and level up your problem-solving skills as a software developer/engineer. These steps help you achieve a developer mindset. The first step to understand what’s going on is to understand it on a granular level. Given a complex problem, try to dissect it into smaller parts. - **Tags**: post, guide, sde - **Author**: Murtuzaali Surti Table of Contents If someone were to ask me, what does a software engineer do on a basic level and what makes a good software engineer? I would say, problem solving. Yes, it’s the most basic and the most important skill to have as a software developer/engineer, yet most people don’t think about it before getting into software. But, it’s not that hard. In this post, I will list down three steps to better approach the problem given at hand, find the solution, and then improve upon it. It will also help you achieve a developer mindset. 1. Break it down The first step to understand what’s going on is to understand it on a granular level. Given a complex problem, try to dissect it into smaller parts which are meaningful on their own, yet when put together, give you the final bigger picture. Think of it like the component architecture where everything is broken down into pieces which function on their own but also serves a higher purpose. 2. Find a solution that just works You don’t have to nail the perfect solution every time on the very first try. No. To be honest, if that happens to you, then you are not learning anything new and there’s no growth. Instead, find a solution that gets the work done even if it’s naive and inefficient. By doing so, you at least get a working prototype which you can test and iterate upon. RECOMMENDED Prepare for coding interviews on GreatFrontEnd - a platform on which the interview resources are prepared by engineers from world's largest tech companies and core maintainers of open source projects. 3. Refactor and Iterate For me, this is the most interesting and challenging part where you have already figured out the solution, but now you are looking for edge cases, performance improvements, potential bugs and implementation details in your solution. This is your chance to improve the solution according to your business needs along with maintaining best coding practices. Also, it gives you more depth to add to the documentation - you can add different approaches you took to arrive at the final solution, their advantages as well as shortcomings and much more. RECOMMENDED Refactoring: Improving the Design of Existing Code by Martin Fowler is a must read if you are struggling with refactoring existing/legacy code. Conclusion This is what I learned in my experience as a software engineer up until now and I like to keep experimenting with this flow. If you have some more thoughts to share, let me know and I will add it here. --- ## Mac Setup For Developers [2026] - **URL**: https://syntackle.com/blog/mac-setup-for-developers/ - **Updated On**: January 22, 2026 - **Description**: Setting up a new Mac for development can be a daunting task, especially if you are new to MacOS or don't know where to start. In this guide, I'll tell you about some tools, apps and tips which are essential for a decent developer experience on a Mac. To keep it simple, I will break them down into categories such as terminals, editors, and other developer tools. - **Tags**: post, mac, guide, sde, setup - **Author**: Murtuzaali Surti Table of Contents Setting up a new Mac for development can be a daunting task, especially if you are new to MacOS or don't know where to start. In this guide, I'll tell you about some tools, apps and tips which are essential for a decent developer experience on a Mac. To keep it simple, I will break them down into categories such as terminals, editors, and other developer tools. But first, let me tell you some Mac settings which I prefer. INFO The version of MacOS at the time of this writing is 15.1 (MacOS Sequoia). If you are on a newer version (MacOS 26 Tahoe), the settings may be different. This article is updated with MacOS 26 Tahoe settings as well. MacOS Settings # Desktop & Dock > Dock macOS 15 Sequoia Position on screen: Left macOS 26 Tahoe Dock position on screen: Left Automatically hide and show the Dock: On Desktop & Stage Manager Show Items On Desktop: Off In Stage Manager: On Show Desktop macOS 15 Sequoia Click wallpaper to show desktop: Only in Stage Manager macOS 26 Tahoe Click Wallpaper to move windows out of the way, revealing your desktop items and widgets: Only in Stage Manager on Click Stage Manager: Off Mission Control: Turn everything on Displays True Tone: Off If you don't want the new liquid glass effect on macOS 26 Tahoe, go to Accessibility > Display > Reduce transparency and turn it on. Package Manager - Homebrew # Homebrew is the all-in-one package manager for MacOS. It can also install apps and fonts using homebrew cask. Installing it is the first thing you should do when setting up a new Mac. Source: brew.sh Install script: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" RECOMMENDED Do you know you can use multiple paid apps on macOS with just one subscription? Yes, you can. With Setapp you can get access to curated apps with just one subscription. Version Control - Git # To install git, I can use Homebrew and it works like a charm: brew install git Source: git-scm.com I recommend configuring and adding an ssh key for dealing with repositories on GitHub. Refer the official documentation for more information. RECOMMENDED Pro Git is a great book for developing a solid understanding of Git and perhaps mastering it. And it's free. Terminal # The default terminal shell on MacOS is zsh and it's good, but it's even better with frameworks such as oh-my-zsh. 1. Oh My Zsh True power of oh-my-zsh comes with its plugins. The most popular ones are: Source: github.com/ohmyzsh/ohmyzsh zsh-autosuggestions - Provides auto-complete suggestions using command history. zsh-syntax-highlighting - Very helpful in catching syntax errors. git - Useful aliases and functions for git. git-prompt - Displays metadata related to the current git repository branch and its status relative to the remote branch. you-should-use - Suggests available aliases which you should use instead of the command you're currently using. I recommend installing all of these plugins via oh-my-zsh and adding them to your ~/.zshrc file. You can refer to the respective plugin documentation for more details. plugins=(git git-prompt zsh-autosuggestions zsh-syntax-highlighting you-should-use zsh-bat) Oh My Zsh also provides a variety of themes to choose from but I prefer the Powerlevel10k theme by romkatv. 2. Warp If you don't like the look and feel of the default terminal in macOS even after configuring oh-my-zsh, then you can have a look at warp.dev which is a terminal with the most modern look you can ever get in a terminal. Integration with AI is one of the things which you can use it for. Source: warp.dev brew install --cask warp 3. Ghostty Ghostty is a fast, native terminal emulator developed by Mitchell Hashimoto. It's written in Swift for macOS and in Zig for Linux. It's currently not available for Windows. It just works. 4. iTerm2 There's also iTerm2 which is a terminal emulator for macOS. If you are a power user and love legacy terminals, then this one's for you. Source: iterm2.com brew install --cask iterm2 Editors / IDEs # 1. Visual Studio Code VS Code is still my go to editor. I prefer it because of its simplicity and extensibility. Part of the reason why I prefer it is because I am in a way habituated to it and it does a good enough job of meeting my developer needs. 2. Antigravity Antigravity, a VS Code fork by Google, uses the tech of Windsurf (a company with which it made a licensing deal) of $2.4 billion. It focuses heavily on agentic workflows by spinning up background agents to complete tasks. It can also spin up browser instances to test the implemented changes. Antigravity is in public preview currently and offers some of the SOTA models such as gemini and claude family of models free of cost (with rate limits). Pro Tip You can use free Antigravity models with Claude Code. 3. Cursor AI it is. Cursor, a VS Code fork, does a great job in fulfilling the AI needs which lack in VS Code. With it's similarity with VS Code, users can easily switch between these editors and still get the work done efficiently. It's my go to editor for AI stuff. 4. Zed One of the things I love about Zed is how performant it is. VS Code can sometimes feel sluggish especially when you have a lot of extensions. Zed does all of those things natively, in a fast and efficient way - mainly because it's written in Rust. Productivity # 1. Raycast Many people coin Raycast as a spotlight replacement but it's more than that for me. It can do shortcuts, clipboard, search and installation of apps through homebrew. I have still kept the spotlight shortcut as "Command + Space" and use Raycast with the hotkey "Option + Space". 2. Obsidian I kind of switch between Notion and Obsidian for note taking. I find Obsidian very snappy and easily accessible when I have to quickly jot something down, while Notion is for when I want to retain or log something for later use or reference. But, both are great in my opinion. Pro Tip Bear is also a good choice. 3. Maccy - Clipboard Manager Pro Tip macOS 26 Tahoe revamped spotlight search and finally introduced the clipboard feature. So, if you are on macOS 26 or later, you might not need this app. If you don't want to use Raycast's clipboard history (which I don't), try out Maccy which is a simple clipboard manager. It is easily accessible via shortcuts (which you can configure) and is open-source. It requires macOS Sonoma 14 or higher. Browsers # I use Chrome for development and day-to-day work, Safari for Netflix, Brave for private browsing, and Arc just to feel nice. Cleanup # 1. Mole Mole is a CLI tool which acts as a deep cleaner for your mac. It combines multiple tools into a single package and lets you uninstall apps, deep clean system as well as monitor and analyze it for performance. It's the best CLI tool you can have on a mac. Saves you a lot of space and time. 2. AppCleaner Uninstalling apps on MacOS is not as simple as you think. Even if you uninstall an app from your system, some residue files are always left behind. AppCleaner is for that. It lets you completely wipe out any data associated with the app you want to uninstall. If you want more features, try Mole instead of AppCleaner as it gives you more features and control out of the box. Miscellaneous # 1. Bitwarden - Password Manager I personally use Bitwarden as a password manager and I have no complains so far. What's interesting is it's open-sourced and can be self-hosted. Being open-sourced also means it's transparent and audited by the community. Trust me, it's a pain in the neck to manage passwords for all of the random accounts we developers create to try different things, and bitwarden makes it easy for you. 2. Syncthing Syncthing is by far the best file sharing tool I have ever used. You can literally sync folders between two machines (be it Android, macOS, or Windows) seamlessly. If you have another system at a remote location, it can be really handy to sync project folders or work files using a local network or an internet connection. Again, it's open-sourced and trusted by the community. 3. System Color Picker System Color Picker by Sindre Sorhus is the most useful tool for web designers and developers. It's open sourced and is a native color picker application for macOS which lets you pick colors from anywhere on the screen. It's a must have for convenience. 4. Ice CAUTION Ice won't work on macOS 26 Tahoe (macOS 26 made significant changes in APIs), a stable release is in the works, but there are several beta releases (0.11.13-dev.x) you can try. Once you have a lot of applications which use MacOS' menubar for status display or accessibility, the menubar can overflow with those application icons and hide some icons behind the notch (if your Mac has one), especially when using applications such as Chrome (has a lot of menu options, eating up space of the menubar icons). One way is to keep less (default) icons in the menubar (besides the control center) by going to Settings > Control Center > Menu Bar Only/Other Modules — but that just works for native MacOS' icons. For third-party application menubar icons? There's no option to reorder/hide or access hidden icons in MacOS. I really want a little ellipsis which lists all of the hidden MacOS menubar icons in a dropdown list below the visible ones, and Ice does exactly that. Install Ice from https://icemenubar.app/ and you will see a dot/ellipsis collapsing all the menubar icons. You can show hide them by clicking on the ellipsis/dot in the menubar, but where it gets really interesting is that you can change the appearance of the menubar completely. Go to Ice Settings by right clicking on the menubar, and then to the Menu Bar Layout tab. This list is evolving. If you have any suggestions, please let me know. Here's an in-depth mac setup video by wesbos for power users: --- ## Better - An AI powered Code Reviewer - **URL**: https://syntackle.com/blog/ai-powered-code-review-tool-better/ - **Updated On**: September 29, 2024 - **Description**: Code reviews have always been crucial in maintaining a standard and emphasizing on the best practices of code in a project. This is not a post about how developers should review the code, it's more about delegating a part of it to AI. Introducing Better - An AI powered Code Review Tool. - **Tags**: post, extensions, github, workflow, AI - **Author**: Murtuzaali Surti Code reviews have always been crucial in maintaining a standard and emphasizing on the best practices of code in a project. This is not a post about how developers should review the code, it's more about delegating a part of it to AI. As Michael Lynch mentions in his post - "How to Do Code Reviews Like a Human" - we should let computers take care of the boring parts of the code review. While Michael emphasizes on a formatting tool, I would like to take it a step further and let artificial intelligence figure it out. I mean, why not take the advantage of the AI boom in the industry? Now I am not saying that AI should be used in place of formatting tools and linters. Instead, it is to be used on top of that, to catch trivial stuff which might be missed by a human. That's why I decided to create a github action which code reviews a pull request diff and generates suggestions using AI. Let me walk you through it. 🚨 Note This GitHub action is now available at the GitHub marketplace. It's a javascript action - learn more about creating javascript github actions. Getting the diff # To interact with the github API, I have used octokit, which is kind of an SDK or a client library for interacting with the github API in an idiomatic way. In order for you to get the diff of the pull request raised, you need to pass the Accept header with the value application/vnd.github.diff along with the required parameters. async function getPullRequestDetails(octokit, { mode }) { let AcceptFormat = "application/vnd.github.raw+json"; if (mode === "diff") AcceptFormat = "application/vnd.github.diff"; if (mode === "json") AcceptFormat = "application/vnd.github.raw+json"; return await octokit.rest.pulls.get({ owner: github.context.repo.owner, repo: github.context.repo.repo, pull_number: github.context.payload.pull_request.number, headers: { accept: AcceptFormat, }, }); } INFO If you are not familiar with github actions at all, here's a github actions 101 series by Victoria Lo and it's a good start. Once I get the diff, I parse it and remove unwanted changes, and then return it in a schema shown below: /** using zod */ schema = z.object({ path: z.string(), position: z.number(), line: z.number(), change: z.object({ type: z.string(), add: z.boolean(), ln: z.number(), content: z.string(), relativePosition: z.number(), }), previously: z.string().optional(), suggestions: z.string().optional(), }) Ignoring Files # Ignoring files is quite straightforward. The user input list requires a semicolon separated string of glob patterns. It's then parsed, concatenated with the default list of ignored files and de-duped. **/*.md; **/*.env; **/*.lock; const filesToIgnoreList = [ ...new Set( filesToIgnore .split(";") .map(file => file.trim()) .filter(file => file !== "") .concat(FILES_IGNORED_BY_DEFAULT) ), ]; The ignored files list is then used to remove the diff changes which refer to those ignored files. That gives you a raw payload containing only the changes you want. Generating suggestions # Once I get the raw payload after parsing the diff, I pass it to the platform API. Here's an implementation of the OpenAI API. async function useOpenAI({ rawComments, openAI, rules, modelName, pullRequestContext }) { const result = await openAI.beta.chat.completions.parse({ model: getModelName(modelName, "openai"), messages: [ { role: "system", content: COMMON_SYSTEM_PROMPT, }, { role: "user", content: getUserPrompt(rules, rawComments, pullRequestContext), }, ], response_format: zodResponseFormat(diffPayloadSchema, "json_diff_response"), }); const { message } = result.choices[0]; if (message.refusal) { throw new Error(`the model refused to generate suggestions - ${message.refusal}`); } return message.parsed; } You might notice the use of response format in the API implementation. This is a feature provided by many LLM platforms, which allows you to tell the model to generate the response in a specific schema/format. It is especially helpful in this case as I don't want the model to hallucinate and generate suggestions for incorrect files or positions in the pull request, or add new properties to the response payload. The system prompt is there to give the model more context on how it should do the code review and what are some things to keep in mind. You can view the system prompt here github.com/murtuzaalisurti/better. The user prompt contains the actual diff, the rules and the context of the pull request. It is what kicks off the code review. This github action supports both OpenAI and Anthropic models. Here's how it implements the Anthropic API: async function useAnthropic({ rawComments, anthropic, rules, modelName, pullRequestContext }) { const { definitions } = zodToJsonSchema(diffPayloadSchema, "diffPayloadSchema"); const result = await anthropic.messages.create({ max_tokens: 8192, model: getModelName(modelName, "anthropic"), system: COMMON_SYSTEM_PROMPT, tools: [ { name: "structuredOutput", description: "Structured Output", input_schema: definitions["diffPayloadSchema"], }, ], tool_choice: { type: "tool", name: "structuredOutput", }, messages: [ { role: "user", content: getUserPrompt(rules, rawComments, pullRequestContext), }, ], }); let parsed = null; for (const block of result.content) { if (block.type === "tool_use") { parsed = block.input; break; } } return parsed; } Adding Comments # Finally, after retrieving the suggestions, I sanitize them and pass them to the github API to add comments as a part of the review. I chose the below way to add comments because by creating a new review, you can add all comments in one go instead of adding a single comment at a time. Adding comments one by one may also trigger rate limiting because adding comments triggers notifications and you don't want to spam users with notifications. function filterPositionsNotPresentInRawPayload(rawComments, comments) { return comments.filter(comment => rawComments.some(rawComment => rawComment.path === comment.path && rawComment.line === comment.line) ); } async function addReviewComments(suggestions, octokit, rawComments, modelName) { const { info } = log({ withTimestamp: true }); // eslint-disable-line no-use-before-define const comments = filterPositionsNotPresentInRawPayload(rawComments, extractComments().comments(suggestions)); try { await octokit.rest.pulls.createReview({ owner: github.context.repo.owner, repo: github.context.repo.repo, pull_number: github.context.payload.pull_request.number, body: `Code Review by ${modelName}`, event: "COMMENT", comments, }); } catch (error) { info(`Failed to add review comments: ${JSON.stringify(comments, null, 2)}`); throw error; } } Conclusion I wanted to keep the github action open-ended and open to integrations and that's why you can use any model of your choice (see the list of supported models), or you can fine tune and build your own custom model on top of the supported base models and use it with this github action. If you encounter any token issues or rate limiting, you might want to upgrade your model limits by referring to the respective platform's documentation. So, what are you waiting for? If you have repository on github, try the action now - it's on the github action marketplace. --- ## Server Sent Events 101 - **URL**: https://syntackle.com/blog/server-sent-events/ - **Updated On**: February 8, 2025 - **Description**: Server Sent Events (SSE), as the name suggests, are a way to communicate with the client by keeping a persistent connection in which the server sends text messages to the client whenever they are available. - **Tags**: post, javascript, web, nodejs, backend, guide - **Author**: Murtuzaali Surti Table of Contents Server Sent Events (SSE), as the name suggests, are a way to communicate with the client by keeping a persistent connection in which the server sends text messages to the client whenever they are available. They are similar to websockets but, unlike websockets, the connection is unidirectional, i.e. only the server has the capability to send messages and the client just listens. Another key difference between SSE and websockets is that websockets use their own ws:// websocket protocol while SSEs use the HTTP protocol. Also, SSEs can only transmit data in text/event-stream format. Sending Events From Server # In a basic nodejs (express) server, you can define an endpoint to allow subscriptions from clients, and store them in a unique Set. const clients = new Set(); const addSubscription = (client) => { clients.add(client); console.log(`Client ${client} connected`); } const removeSubscription = (client) => { clients.delete(client); console.log(`Client ${client} disconnected`); } app.get("/subscribe", (req, res) => { const client = new URLSearchParams(req.query).get("id") || crypto.randomUUID(); addSubscription(client); // ... req.on('close', () => { removeSubscription(client); }) }) Once a subscription is added and stored in the Set, you must set these response headers with a status code of 200 to let the client know that this is a text/event-stream, keep-alive connection. app.get("/subscribe", (req, res) => { const client = new URLSearchParams(req.query).get("id") || crypto.randomUUID(); addSubscription(client); res.writeHead(200, { "Content-Type": "text/event-stream", "Connection": "keep-alive", "Cache-Control": "no-cache" }); req.on('close', () => { removeSubscription(client); }) }) Now that the connection is set, you can send messages to the client in the EventStream format. That's it, you can now listen to these event streams using the EventSource API which I will talk about more later in this post. app.get("/subscribe", (req, res) => { const client = new URLSearchParams(req.query).get("id") || crypto.randomUUID(); addSubscription(client); res.writeHead(200, { "Content-Type": "text/event-stream", "Connection": "keep-alive", "Cache-Control": "no-cache" }); res.write(`data: ${message}\n\n`); req.on('close', () => { removeSubscription(client); }) }) You can also ping the client at regular intervals by using setInterval. setInterval(() => res.write(`data: ping\n\n`), 5000); This is all good but what if you want to send messages when something happens, either in the server or in the database. For that, you need to use event emitters in nodejs to fire a specific event and capture that event in our request handler to send a message to the client. Event Emitters Event emitters are a type of the pub/sub architecture wherein you have subscribers subscribing to specific "named" events and emitters (publishers) which publish/emit the event based on some operation. Here's a simple example of an event emitter in nodejs: import { EventEmitter } from 'events'; class UpdateEvents extends EventEmitter { constructor () { super(); } new (data) { this.emit('new', data); } } const updates = new UpdateEvents(); export default { updates, newUpdate: (data) => updates.new(data) } The new method in the UpdateEvents class is an event emitter method which emits the named event new. This is what fires the event. Then, we create an instance of the UpdateEvents class and export it for it to be used for listening to the new event. You can listen to the event anywhere in your application code using: updates.on('new', (data) => { // do something with the data }) This is really useful for your SSE endpoint. For example, if you want to send events from an operation/event in some other part of the application and not necessarily inside the request handler, then you can fire an event from different places in your code and listen to it in the SSE endpoint. // in some other part of the application newUpdate({ message: "Hello World" }) // ---------------------------------- // in the SSE endpoint app.get("/subscribe", (req, res) => { const client = new URLSearchParams(req.query).get("id") || crypto.randomUUID(); addSubscription(client); res.writeHead(200, { "Content-Type": "text/event-stream", "Connection": "keep-alive", "Cache-Control": "no-cache" }) updates.on('new', (data) => { res.write(`data: ${message}\n\n`); }) req.on('close', () => { removeSubscription(client); }) }) Subscribing to SSE Events From Clients # SSE Events are captured using the EventSource web API. You just have to pass the URL of the SSE endpoint to the EventSource API. You can't pass your own custom headers in the EventSource, so you have to rely on query parameters to pass additional context about the client. const url = new URL(SSE_ENDPOINT, YOUR_API_BASE_URL) const event = new EventSource(`${url.href}?id=${crypto.randomUUID()}`) CAUTION The EventSource API doesn't allow you to pass custom headers natively. You have to rely on polyfills or query parameters to pass additional context about the client. Learn more about the limitations of the EventSource API here. Then, listen to the messages which are sent by the server by using the onmessage event. const url = new URL(SSE_ENDPOINT, YOUR_API_BASE_URL) const event = new EventSource(`${url.href}?id=${crypto.randomUUID()}`) event.onmessage = (e) => { console.log(e.data); } event.onopen = (e) => { console.log('connection opened'); } event.onerror = (e) => { console.log(e); } What happens when the connection to the server is lost? In that case, the browser tries to reconnect automatically within a certain interval of time known as the retry interval. The default retry interval is ~3 seconds in the browser. However, you can specify your own retry interval by sending the value (in milliseconds) in a retry field with the server sent message. // server res.write(`data: ${message}\n`); res.write(`retry: ${retryInterval}\n\n`); // in milliseconds Pro Tip Know how to properly send messages using the EventStream format in this article by web.dev. If you don't want to rely on the automatic reconnect provided by the browser or if it's not working for you, you can implement you custom retry mechanism yourself. Let me show you how. let retryInterval = 6000; function listenToEvents(retryAfter) { let isListening = false; const interval = setInterval(() => { if (!isListening) { isListening = true; const url = new URL(SSE_ENDPOINT, YOUR_API_BASE_URL); const event = new EventSource(`${url.href}?id=${crypto.randomUUID()}`); event.onmessage = (e) => { const payload = JSON.parse(e.data); // do something with the payload payload.retry && (retryInterval = payload.retry); } event.onerror = (e) => { clearInterval(interval); event.close(); listenToEvents(retryInterval); } } }, retryAfter); } listenToEvents(1000); // initially, establish the connection in 1 second First of all, you have to setup an interval which will keep checking if the connection is still alive or not. And the interval can be set to a custom value, or to the retry value you get from the server. This interval will be wrapped in a function named listenToEvents which will accept a retryInterval parameter and initialize a local variable named isListening. This interval will keep creating new eventsource objects if the isListening variable is false. It's set to false by default but, it's set to true when establishing the connection, so only one eventsource object will be created at the first round of the interval. If the connection is lost, the onerror event will be fired closing the event, clearing the current interval and invoking the function listenToEvents recursively. Conclusion # In this guide, you got to know about server sent events, event emitters and the EventSource API. Server Sent Events are almost similar to websockets with some key differences. If you want to learn more about websockets, check out the WebSockets 101 guide. --- ## "this" Keyword in Arrow Functions - **URL**: https://syntackle.com/blog/this-keyword-in-arrow-functions-javascript/ - **Updated On**: July 19, 2024 - **Description**: The "this" keyword in javascript is probably one of the most misunderstood concepts of javascript. In this article, you will see how the "this" reference is different in arrow functions introduced in ES6 as compared to regular function expressions. - **Tags**: post, javascript, guide - **Author**: Murtuzaali Surti The this keyword in javascript is probably one of the most misunderstood concepts of javascript. There are already some brilliant resources out there to explain the this keyword, but in this article, you will see how the this reference is different in arrow functions introduced in ES6 as compared to regular function expressions. You will also get to know a way to visualize the this keyword reference inside arrow functions. This article will only cover implicit binding of the this keyword, i.e. what's interpreted by javascript by default. Let's start by defining an object named user. In this object, you have a regular function expression named logName. How will you call the logName function if you want to execute it? You will have to do user.logName(). const user = { name: "John", logName () { console.log(this.name); }, }; user.logName(); // logs 'John' Notice the object on which the function is invoked. The object on which it is invoked is user and that's the execution context of the function logName. So, whenever you type this inside the regular function expression, it automatically refers to the object that it's being called upon. In this case, it will log the value of the name property inside the object user. In other words, it binds to the context of the object user. Now, let's create an arrow function logNameArrow() inside the same object and having the same body as logName(). const user = { name: "John", logName () { console.log(this.name); }, logNameArrow: () => { console.log(this.name); }, }; user.logName(); // logs 'John' What do you think will be logged when you invoke logNameArrow? It will log undefined but why? That's because arrow functions don't have their own bindings. In other words, even if you invoke the arrow function from the object user like this user.logNameArrow(), the arrow function doesn't know on what it got invoked upon. Instead, what it knows, is the lexical scope in which it exists. And, it will bind to the context of the closest enclosing scope (in this case it's the window object). Lexical Scoping defines how variable names are resolved in nested functions: inner functions contain the scope of parent functions even if the parent function has returned. - Pierre Spring, stackoverflow.com const user = { name: "John", logName () { console.log(this.name); }, logNameArrow: () => { console.log(this.name); }, }; user.logName(); // logs 'John' user.logNameArrow(); // logs undefined It's similar to how variables are resolved using lexical scoping. If a variable is not defined in the current scope, javascript will check the parent scope for that variable, and will keep doing so until it reaches the highest parent scope which encloses everything. To test this, you can create a new arrow function inside the regular function expression logName. const user = { name: "John", logName () { console.log(this.name); const logNameArrow = () => console.log(this.name); // logs 'John' logNameArrow(); }, logNameArrow: () => { console.log(this.name); // logs undefined }, }; Now, here comes the interesting part. Since arrow functions don't have their own bindings, they look for the parent scope's context binding, in this case it's the context binding of the regular function expression logName(). That is the reason the arrow function logNameArrow (inside the logName regular function expression) will log 'John' instead of undefined. Here's an example which demonstrates lexical scoping which is three levels deep. const user = { name: "John", logName () { console.log(this.name); // John const logNameArrow = () => { console.log(this.name); // John const logNameArrow2 = () => { console.log(this.name); // John const logNameArrow3 = () => { console.log(this.name); // John } logNameArrow3(); } logNameArrow2(); }; logNameArrow(); }, logNameArrow: () => { console.log(this.name) // undefined }, }; user.logName(); user.logNameArrow(); Let's visualize the highest enclosing lexical scope by wrapping this object in an IIFE (Immediately Invoked Function Expression). (function () { this.name = 'Peter' const user = { name: "John", logName () { console.log(this.name); // John }, logNameArrow: () => { console.log(this.name) // Peter }, }; user.logName(); user.logNameArrow(); })(); When you wrap the object in a wrapper function, it creates a new lexical scope in which the object and its properties exist. Define a variable named name, identical to the user object property name and assign it a different value. Now, as you know, the arrow function tries to find the closest enclosing parent lexical scope (which in this case is the highest enclosing parent scope) i.e. the scope of the wrapper IIFE. And, it finds a variable named name in that scope. And, thus it logs 'Peter' as a result. That was everything you needed to know about how the this keyword is referenced implicitly in javascript arrow functions. Must Reads 🚨 Understanding the "this" keyword, call, apply, and bind in JavaScript - ui.dev The this keyword - web.dev JavaScript Visualized - Execution Contexts - Lydia Hallie --- ## 5 Newsletters Every Developer Should Read - **URL**: https://syntackle.com/blog/five-newsletters-every-developer-should-read/ - **Updated On**: February 13, 2025 - **Description**: Newsletters can be hard to follow along, especially when you subscribe to too many of them. That's why, today you will get to know about five newsletters every developer should follow and read. - **Tags**: post, newsletters, sde, listicle - **Author**: Murtuzaali Surti Table of Contents Newsletters can be hard to follow along, especially when you subscribe to too many of them. That's why, today you will get to know about five newsletters every developer should follow and read. These are the newsletters I personally subscribe to. 1. Pointer Pointer.io is an engineering focused newsletter, an issue of which can be broken down into three sections: leadership articles, engineering articles, and some interesting links to developer tools and resources. You can get a lot of value from the tools and resources listed in this newsletter. 2. TLDR tldr.tech, as the name suggests, focuses on byte-sized latest updates in the programming, tech and startup world. This newsletter keeps you up-to-date in the ever changing tech industry and it does its job very well. 3. Bytes If you deal with javascript as a developer, then bytes.dev is the perfect newsletter for you. It features tools and resources related to the javascript ecosystem and has a "spot the bug" section which lets you test your javascript skills. 4. The Pragmatic Engineer One of the most popular engineering newsletters out there, the pragmatic engineer by Gergely Orosz offers insightful stories from the big tech as well as some meaningful advice for leadership and management roles in software. 5. Elevate with Addy Osmani In his newsletter, Addy Osmani shares his experience working in the software industry and how to deal with some of the challenges that come along with it. The newsletter offers advice and steps to be effective in the software world. Addy has also published many books related to programming, with "Software Engineering: The Soft Parts" being one of my favorites. If newsletters are not your choice, and you prefer reading blogs via RSS feeds, rssed is a collection of interesting developer focused feeds which you can read anytime you want. For adding an interesting feed to the list, follow the steps here or if you can't do that, shoot me an email. --- ## I tried "window.ai" in Chrome - **URL**: https://syntackle.com/blog/window-ai-in-chrome/ - **Updated On**: November 24, 2025 - **Description**: At the time of this writing, Google Chrome has started shipping experimental AI features such as "built-in AI" in Chrome's Dev/Canary channels. In this tutorial, you will get to know how you can try the built-in AI model in chrome dev. - **Tags**: post, web, chrome, AI, gemini, tutorial - **Author**: Murtuzaali Surti I never got chrome's built-in AI model working on Chrome's Canary release (maybe because I am on v128), so I tried Chrome's Dev Channel - thanks @theo. Google Chrome has started shipping experimental AI features such as built-in AI in Chrome's Dev/Canary channels. In this tutorial, you will get to know how you can try the built-in AI model in chrome dev. RECOMMENDED Practice frontend system design interview questions on GreatFrontEnd! Installing Chrome Dev Head over to chrome's dev release download page and install the dev release of chrome. Setting up experimental flags Once you have installed chrome dev, visit chrome://flags url and search for two flags: #optimization-guide-on-device-model - set it to Enabled BypassPerfRequirement #prompt-api-for-gemini-nano - set it to Enabled Now, visit chrome://components and search for Optimization Guide On Device Model component. If its not showing up in the list, disable/enable the above flags, relaunch/restart chrome and keep doing this until you get that component. Once you get that component, click check for updates (the initial version will be 0.0.0.0). It will start the download automatically and after it gets completed, you will see the updated version of that component (for me it was 2024.6.5.2205). With that being done, you are now ready to use window.ai in chrome. Creating a session The initial API of window.ai provides two methods createTextSession and createGenericSession to create a new chat session. I am not aware of the differences between the two as there's little to no documentation about it, but for now you can go experimenting with them. There are methods to verify if a session can be created or not as well. They are named as canCreateTextSession and canCreateGenericSession. They return a state which is used to determine when a session can be created. So, for example, if you haven't downloaded the model locally yet (remember that Optimization Guide On Device Model component), it will return a state named after-download which means a session can be created after you download the model. let aiSession = await window.ai.createTextSession(); await aiSession.prompt("hey, how are you?"); Also, you can destroy the session by using the destroy() method on the session created. aiSession.destroy(); Conclusion I found the output to be pretty slow, but considering the fact that it's still in its initial stages, things can change, and it can depend on your machine's resources and configuration as well. The great thing about it is that it works offline as well. And I think that's the power of built-in AI models, but that does mean you will have to trade-off resources with localization unless the computation power increases. Here's a brief and not so great conversation with the built-in AI in chrome. --- ## Create a Node Server using Hono under 10 Lines of Code - **URL**: https://syntackle.com/blog/node-http-server-using-hono/ - **Updated On**: June 30, 2024 - **Description**: Hono, as per the docs, was originally built for Cloudflare Workers. It's an application framework designed to work the best for cloudflare pages and workers as well as javascript runtimes Deno and Bun. Although not built specifically for Node, an adapter can be used to run it in Node. - **Tags**: post, nodejs, javascript, backend, tutorial - **Author**: Murtuzaali Surti Hono, as per the docs, was originally built for Cloudflare Workers. It's an application framework designed to work the best for cloudflare pages and workers as well as javascript runtimes Deno and Bun. Although not built specifically for Node, an adapter can be used to run it in Node. In this tutorial, you will get to know how you can create a simple HTTP server in Node using Hono in less than 10 lines of code. Prerequisites Create a bare bones node environment using npm init -y. Setting Up Hono Install hono from npm along with its nodejs adapter. npm i hono @hono/node-server Creating A Server Create a file named index.mjs and then, import Hono and its nodejs adapter. import { Hono } from "hono" import { serve } from "@hono/node-server" Initialize a new Hono app. const app = new Hono() Handle a simple GET route. app.get("/", (context) => context.json({ "hello": "world" })) Serve the app using the nodejs adapter. serve({ port: 3000, fetch: app.fetch }, (i) => console.log(`listening on port ${i.port}...`)) Here's a snippet of all the code combined: import { Hono } from "hono" import { serve } from "@hono/node-server" const app = new Hono() app.get("/", (context) => context.json({ "hello": "world" })) serve({ port: 3000, fetch: app.fetch }, (i) => console.log(`listening on port ${i.port}...`)) Conclusion One highlight of Hono is its Regexp router. It allows you to define routes which match a regex pattern. Apart from that, it also offers multiple built-in authentication modules for implementing various authentication methods such as basic, bearer, and jwt. --- ## Integrate Pagefind's Search with Astro: A Complete Setup Guide - **URL**: https://syntackle.com/blog/pagefind-search-in-astro-site/ - **Updated On**: February 8, 2025 - **Description**: Pagefind's take on search is quite simple - index your site at build time and host it alongside your static site. The search index sits right alongside the files of your site and it doesn't load all the data upfront. - **Tags**: post, pagefind, astro, guide, setup - **Author**: Murtuzaali Surti Table of Contents Pagefind's take on search is quite simple - index your site at build time and host it alongside your static site. The search index sits right alongside the files of your site and it doesn't load all the data upfront. As mentioned in this HN answer, it only loads relevant search data when you start typing, and you can also load the js client (pagefind.js) conditionally when going for a custom implementation. With Pagefind, very little is loaded until you type in a search term. Once you start typing, a chunk of the search index is loaded containing your search word(s) and is then queried. This is what drives the performance you're seeing, which is predominantly the time for the data to load after you start typing — the query step itself is near-instant. - liambigelow on Hacker News With that being said, let's look at how you can integrate pagefind with your Astro site to implement static, site-wide search. Installing Pagefind # Install it from a package manager such NPM. npm i pagefind -D Building Search Index # The search index needs to be built at build time in order to query it later. You can do that in your build script itself or you can use a postinstall script in the file package.json. I prefer to put it in the build script itself. "scripts": { "build": "astro build && npx pagefind --site dist" } Pagefind also supports a config file named pagefind.yml which you can use to specify the configuration options instead of specifying it in the cli command. The site option here specifies the build directory of your project, and the glob option is for only including the files to be parsed to build the search index. # pagefind.yml site: dist glob: "**/*.{html}" The build script can be simplified as: "scripts": { "build:astro": "astro build", "build:pagefind": "npx pagefind", "build": "npm-run-all -s build:astro build:pagefind" } RECOMMENDED The npm-run-all package is a great tool to run cli commands either sequentially or parallelly in a cross-platform way. This will build the pagefind search index whenever you build your site. But, what about dev mode? Astro won't access files from your dist folder in dev mode, so you need to copy the pagefind files from the dist folder into your public directory (got the idea from this post) which is used for static files. So, you can build the pagefind index, copy it from the dist to public directory and then run astro dev. This way you can get the latest search index but, it still won't change on hot reload (this is a limitation). Pro Tip If you are starting from scratch, i.e. you don't have a previous build, then you must build the site first and then run it in dev mode. It's because pagefind must have a build to build an index upon. "scripts" : { "copy:pagefind:dev": "npx shx cp -r dist/pagefind public/", "dev:astro": "astro dev", "dev": "npm-run-all -s build:pagefind copy:pagefind:dev dev:astro", } Initializing Pagefind # There are two ways in which you can use pagefind: With pre-built UI Custom implementation using Pagefind API If you want to quickly implement search without worrying too much about the layout, you can go with the default UI provided by pagefind. For that, you need to link the js and css files as shown below: <!-- this will work in both dev and prod environments as we have copied the pagefind directory locally --> <link href="/pagefind/pagefind-ui.css" rel="stylesheet"> <script is:inline src="/pagefind/pagefind-ui.js"></script> And then, initializing the PagefindUI: <script is:inline> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({ element: "#searchContainer" }); }); </script> Although this seems quick and efficient, I always prefer having more control of the search layout and data. And that's why I prefer using the Search API provided by pagefind which lets you implement search however you want. Using the API In order to use the API, you must import a file named pagefind.js from the pagefind directory. This file acts as a js client for the API. It's beneficial to import this file only once when the user focuses on the search input element and store it in a variable for future use. let pagefind; document.querySelector("#search").addEventListener("focus", async (e) => { if (!pagefind) { pagefind = await import("/pagefind/pagefind.js"); pagefind.init(); } }) CAUTION Wrap the above code in a <script defer> tag for it to be considered as a client-side script. Astro won't process and bundle it. It will be shipped to the client as it is. You can also consider the is:inline attribute. The init() method invoked above is for loading core dependencies and metadata about the site. It's an optional method but, you are pre-loading dependencies when you call it when the element gains focus, instead of it getting automatically invoked when a search method is called after the user starts typing. You can also specify search options before you initialize the pagefind instance: document.querySelector("#search").addEventListener("focus", async (e) => { if (!pagefind) { pagefind = await import("/pagefind/pagefind.js"); await pagefind.options({ ranking: { // Decreasing the pageLength parameter is a good way to suppress very short pages that are undesirably ranking higher than longer pages. (max: 1, min: 0) pageLength: 0.5, }, }); pagefind.init(); } }) Then, when the user starts typing, you can call the search method as shown below and get the search results: document.querySelector("#search").addEventListener("keydown", async (e) => { const results = await (await pagefind.search(e.target.value)).results; for (const result of results) { const data = await result.data(); console.log(data, data.meta.title, data.excerpt); // do required DOM manipulation } }) There's a catch. You are firing pagefind.search on every keydown event, that's quite expensive because the user is still typing a word but, you are fetching search results for nearly every letter/substring. The solution to this problem is debouncing. You delay the execution of the function of the event up to a certain time by grouping the number of calls made during that time, up until no further event is fired during that time. In short, the search method won't be fired until the user stops typing after some time. I personally use the lodash library for debouncing functionality but pagefind provides a native method of implementing a debounced search, namely, pagefind.debouncedSearch and you should definitely check that out. Pre-processing the Script If you want Astro to bundle and process the script, or you want to import any npm packages in it, then you will have to make a couple of changes in the script as well as the build process. Firstly, all the <script> tags are processed by Astro, unless you add an attribute like is:inline or defer to the script, or want to opt-out of script processing. So, you will want to remove any of those attributes for the script to be processed. Secondly, the dynamic import won't work. Typescript will complain about it because it can't resolve it yet as it doesn't exist yet. You are copying that /pagefind/pagefind.js after the build to the public directory. So, you need to append a ?url param. let pagefind: any; const searchField = document.querySelector("#search") as HTMLInputElement; searchField.addEventListener("focus", async () => { if (!pagefind) { pagefind = await import("/pagefind/pagefind.js?url"); // appending `?url` because typescript will complain about the module not existing pagefind.init(); } }); Lastly, if you don't have a previous build (i.e. if you are starting from scratch), the dynamic /pagefind/pagefind.js?url import will throw an error when you run npm run build because rollup won't be able to resolve that file as it doesn't exist yet (because we haven't built pagefind index and copied it to /public - that happens after you build). In order to overcome that, you need to declare the dynamic import as an external dependency in rollup config. export default defineConfig({ // ... vite: { build: { rollupOptions: { external: '/pagefind/pagefind.js?url' } } }, }); Also, if you are running the site in dev mode without having a previous build, make sure to build the site once for the first time. You can add a new script for that in package.json file: "dev:build": "npm-run-all -s build dev". Now, you can import and use lodash or any npm package in your script. import lodash from 'lodash'; // ... const searchResults = document.querySelector("#searchResults") as HTMLElement; searchField.addEventListener("keydown", lodash.debounce(async () => { const results = await (await pagefind.debouncedSearch(searchField.value, {}, 700))?.results; if (results) { for (const result of results) { const data = await result.data(); console.log(data, data.meta.title, data.excerpt); // DOM manipulation } } }, 700)); // ... That was all about using pagefind to implement search in a static site built using astro. Bonus Pagefind has some defaults of selecting the metadata about the page, for example, it will return the content of the first h1 element on your page as the title of the page. To override that, you can use the data-pagefind-meta attribute and set it's value to title on the element you wish to be returned as the title. <title data-pagefind-meta="title">Page</title> Syntackle itself uses Pagefind for global search, press Ctrl+K or click the search button to see it in action. --- ## Running PostgreSQL using Docker - **URL**: https://syntackle.com/blog/running-postgresql-using-docker/ - **Updated On**: June 16, 2024 - **Description**: In this quick tutorial, you will get to know how you can run postgresql inside a docker container terminal using "psql" (a terminal interface to interact with postgresql databases). - **Tags**: post, docker, postgres, backend, tutorial, sql - **Author**: Murtuzaali Surti Running PostgreSQL via Docker is one of the things you can do to quickly try postgresql without installing or configuring it locally. It's one of the benefits of using Docker. In this quick tutorial, you will get to know how you can run postgresql inside a docker container terminal using psql (a terminal interface to interact with postgresql databases). Pulling Official PostgreSQL Image Pull the official image of postgresql from Docker Hub using the docker pull command. docker pull postgres You can specify the version of postgresql to pull. docker pull postgres:latest docker pull postgres:16 This will create a postgres image. You can verify that using the docker images command. docker images # output REPOSITORY TAG IMAGE ID CREATED SIZE postgres latest cff6b68a194a 5 weeks ago 432MB Running the Image Running the image means executing the image inside an isolated environment known as a container. It can be done using the docker run command. docker run --name pgcontainer -e POSTGRES_PASSWORD=specify_your_password -d postgres Let's understand each flag/argument: --name :- the name of the container to be created. (example: pgcontainer) -e :- environment variables to be fed into the container (here: POSTGRES_PASSWORD=specify_your_password). -d :- runs the container in a detached mode (in background) without blocking the terminal. the last argument postgres is the name of the image. Run docker run --help for more information. You can verify the container is running by using the docker ps command which will list all the running containers. A status of Up means the container is up and running. docker ps # Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ebeab2ca77e9 postgres "docker-entrypoint.s…" About an hour ago Up About an hour 5432/tcp pgcontainer Executing Commands Inside Container You can execute commands inside the container by accessing it's terminal shell using the docker exec command. The -it flag specifies the mode (interactive) in which you want to access the shell. It is meant to keep the input stream alive even if the container is in detached mode. The below command will spin up a psql instance which you can use to create and interact with databases. docker exec -it pgcontainer psql -U postgres When you execute the above command, you can interact with the psql interface as shown below: psql (16.3 (Debian 16.3-1.pgdg120+1)) Type "help" for help. postgres=# | Conclusion This is just one of the ways you can use postgres' image independently using docker. For more advanced and integrated usage of postgres along with multiple services, consider using docker compose. --- ## Creating My First Web Component: The <back-to-top> Button - **URL**: https://syntackle.com/blog/back-to-top-web-component/ - **Updated On**: June 9, 2024 - **Description**: That ignited a spark of curiosity within me and I started reading and researching more about web components. Eventually, after realizing you can build and define custom HTML elements, I decided to build at least one of my own. - **Tags**: post, javascript, html, webc, frontend - **Author**: Murtuzaali Surti I came across the concept of web components, when I saw the word "WebC" in one of the Zach Leatherman's blog posts. Then, I came to know that: WebC is a framework-independent standalone HTML compiler for generating markup for web components. - zachleat.com That ignited a spark of curiosity within me and I started reading and researching more about web components. Eventually, after realizing you can build and define custom HTML elements, I decided to build at least one of my own. It's alright if you didn't already know about custom elements or web components until you came here (and I think for most entry-level developers, that is the case), but I assure you that you will find the world of web components fascinating. If you want to learn more about the basics of web components and custom elements, I wrote a post explaining what they are. You might have seen "back to top" buttons/links on some web pages(especially those which contain long form articles) which take you to the top of the page on a click. I took the functionality of those buttons and moved it in a re-usable web component. In this post, I will walk you through the process of how I built a <back-to-top> web component. Find it here 📦 GitHub - github.com/murtuzaalisurti/back-to-top NPM: @murtuzaalisurti/back-to-top Building v1.0 Custom elements can be defined in two ways - by extending an existing HTML element class such as a button (HTMLButtonElement) or by extending the generic HTMLElement class. If you go the first route, you get all of the properties of a button element when you call the super() method in the constructor but you lose the ability to attach a shadow DOM to that custom element since shadow DOM is only supported by certain elements. Despite the limitations, I decided to go with a customized built-in custom element a.k.a the element which extends a specific class of the element to be used - in my case, a button. class BackToTop extends HTMLButtonElement { constructor() { super(); // properties } // inspired by David Darnes' component template - https://github.com/daviddarnes/component-template static register(tagName, extendsElement) { if ("customElements" in window) { if (extendsElement) { customElements.define(tagName || "back-to-top", BackToTop, { extends: extendsElement }); // you must specify extends option while defining a customized built-in element return; } customElements.define(tagName || "back-to-top", BackToTop); } } connectedCallback() { // code } } BackToTop.register("back-to-top", "button"); For event throttling, lodash seemed to be the best option because it allows you to create a custom build specific to your functionality. If you only want the throttle or debounce module, then you can get it by using: lodash include=throttle,debounce -p The lodash custom build file gets imported in the web component file by a build step I perform using esbuild. import "./lodash.custom.min.js" class BackToTop extends HTMLButtonElement { constructor() { super(); } // ... } Lodash is used to throttle the position calculation function in order to show/hide the "back to top" button. handleThrottle = _.throttle(() => { let prevScrollPos = document.documentElement.scrollTop || window.scrollY || document.body.scrollTop; this.currentScrollPos <= prevScrollPos ? this.style = this.#hidden : this.style = this.#show; this.currentScrollPos = prevScrollPos; this.currentScrollPos === 0 && (this.style = this.#hidden); }, 400) The connectedCallback method is executed once the web component class is instantiated, but there's also disconnectedCallback which fires when the component is removed from the document. So, make sure to remove any listeners you have attached to the element inside this method. disconnectedCallback() { window.removeEventListener("scroll", this.handleThrottle); this.removeEventListener("click", this.handleClick); } You can find the code for v1.0 on npm. Building v2.0 After publishing the first version, I got wonderful responses and ideas from the community. One of them was to use an autonomous custom element (extending the generic HTMLElement class) because Safari doesn't yet support the is attribute required for customized built-in elements to work. This forced me to rewrite the component as an autonomous custom element by using a wrapper around the button element. class BackToTop extends HTMLElement { constructor() { super(); // properties } static register(tagName) { if ("customElements" in window) { customElements.define(tagName || "back-to-top", BackToTop); } } connectedCallback() { // code } } BackToTop.register(); Previously, the throttling rate was hardcoded inside the component, but with v2.0, the throttle attribute was introduced to let users input a custom throttle rate measured in milliseconds. To handle attributes and their updates, you must define the attributes you want to watch in an observedAttributes static property. You can listen to the updates on those defined attributes using the attributeChangedCallback method. You can store the value of the attribute in a class property. It's important to keep in sync the property and the attribute by setting the value of the property whenever the attribute value is modified. class BackToTop extends HTMLElement { constructor() { super(); // ... this.throttleRate = 400; // milliseconds } // defining which attributes to observe static get observedAttributes() { return ["throttle"]; } // getter get getThrottleRate() { return this.throttleRate; } // setter set setThrottleRate(value) { this.throttleRate = Number(value); } // observing the "throttle" attribute attributeChangedCallback(name, oldVal, newVal) { name === "throttle" && (this.setThrottleRate = newVal) && (this.handleThrottle = this.throttledFunction(this.getThrottleRate)); } // ... } Those were the main points I wanted to cover in this post which were crucial to building this component. Obviously, this is not the entirety of code. Check out the entire code on GitHub and see the web component in action below: See the pen (@seekertruth) on CodePen. One Last Thing With all of the refactoring, the component still relies on javascript to render the button because of this: // ... connectedCallback() { this.append(document.createElement("button")); // ... } // ... This is why, this can't be called an HTML web component because it doesn't fallback to anything when javascript can't execute. Now that I know about the approach of HTML web components, they have started making more sense to me as they would fallback to basic HTML behavior in absence of javascript execution. That gives me the motivation to build v3.0. UPDATE: v3.0 is here and it now supports a fallback anchor link and customizable button content. You can update your component definition as shown below: <back-to-top throttle="350"> <a href="#" style="position: fixed; left: 1rem; bottom: 2rem;">back-to-top</a> <template> button content here </template> </back-to-top> --- ## React 19 - A Brief Look At Form Handling - **URL**: https://syntackle.com/blog/form-handling-in-react-19/ - **Updated On**: October 11, 2025 - **Description**: Forms in React have always been not so easy to handle which often leads to messy React code. Ultimately, folks have to resort to form handling libraries which only add more abstraction in the process. - **Tags**: post, react, frontend, guide - **Author**: Murtuzaali Surti Table of Contents React 19 is finally here with it's stable release and it brings with it the support for custom elements (yayy! 🎉), better form handling with some new hooks and numerous improvements. An open source react compiler is also in the works (v1 released, learn how to integrate) - a version of which is being used by Instagram! RECOMMENDED Want to master React and get a deep understanding of it's fundamentals? This book by Robin Wieruch is a must read. Installing React 19 # Vite can be used to test React 19 release locally. Now, React 19 is the default version of React if you scaffold a vite project. So, simply use the create-vite tool to create a new React project with React 19. npm create vite@latest react-app-name -- --template react Pro Tip If you want to integrate react compiler (of which v1 was released recently), follow this guide on how to integrate react compiler with React 17+. Checking the version of React in the browser console can also be performed using __REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values().next()["value"]["version"]: // https://stackoverflow.com/questions/36994564/how-can-one-tell-the-version-of-react-running-at-runtime-in-the-browser#comment106830911_36994564 console.log( __REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values().next()["value"]["version"] ) Form Actions in React # Forms in React have always been not so easy to handle, which often leads to messy React code. Ultimately, folks have to resort to form handling libraries which only add more abstraction in the process. One of the things I like about React 19 is the ability to handle form states. Of all the new hooks introduced for forms in React 19, I personally like the useActionState hook used to handle basic forms. The useActionState hook exposes a function named submitAction which can be passed to the action attribute of the HTML <form> element. It also accepts an initial state and a permalink value which I haven't tried yet, but the documentation states that it is for passing the state to another URL. // useActionState: (fn, initialState, permalink?) => [state, submitAction, isPending] const [state, submitAction, isPending] = useActionState( async (previousState, formData) => { try { const response = await updateSomething(formData) return { data: response, } } catch (error) { return { error, } } }, null // initial state ) With the above function in place, you have to now pass the submitAction function returned from the hook to the action attribute of the form element. <form action={submitAction}> <input type="text" name="name" /> <button type="submit" disabled={isPending}>Update</button> </form> The most helpful thing about this is that you get an isPending state which is a boolean allowing you to determine the state of the submission without having to manage it through React's useState. The same is true for form errors. If it's a validation error or a network error, you can send it with the response state that you are getting from the useActionState hook and use it in your JSX. The UI will update itself automatically on the response of that hook. Wrapping it in a Custom Hook # In order to make it re-usable, you might want to wrap it in a custom hook which you can share between multiple forms. The name of the custom hook can be something like useFormHandler. import { useActionState } from 'react' const useFormHandler = (callback, initialState = null, permalink = null) => { const formState = useActionState( async (previousState, formData) => { try { const response = await callback(formData) return { previousState, data: response ? response : 'OK', } } catch (error) { return { previousState, error, } } }, initialState, permalink && permalink ) return { formState } } export default useFormHandler The above custom hook accepts a callback function which is for you to handle the async network requests and validations. It accepts an initial state which you can pass if you want to, otherwise it defaults to null. And lastly, a permalink which we discussed earlier. It returns the array returned by the useActionState hook - [state, submitAction, isPending]. Use it by passing an async callback which handles the requests. import useFormHandler from './hooks/formHandler' const saveChanges = async (data) => { return new Promise((resolve, reject) => { try { const res = data.get("name") // ... do something with the data resolve(res) } catch (error) { reject(error.message) } }) } const { formState } = useFormHandler(saveChanges) const [state, submitAction, isPending] = formState The use case for creating a custom hook is to add more validations and checks for the form data and to standardize the response state received from the useActionState hook. Some more hooks React 19 also introduced some more hooks such as useFormStatus and useOptimistic but I don't have a clear idea on what their best use case can be. Let's hope that with the introduction of newer hooks, the hooks land doesn't become cluttered and murkier than before. It’s a funny conundrum. Why do we refactor? Because the code got too complicated. So we simplify it. And why do we simplify it? So we can add more to it over time and make it more complex again. - Jim Nielsen --- ## Static Sites Are Good - **URL**: https://syntackle.com/blog/static-sites-are-good/ - **Updated On**: February 8, 2025 - **Description**: For the most part, static-first sites are the go to thing if your application doesn't involve much complexity. However, you are free to choose the right tool for your application — but choose wisely. - **Tags**: post, html, web, performance, frontend, opinion - **Author**: Murtuzaali Surti Table of Contents Gone are the days when you had to wait for quite a long time to fetch a server rendered HTML page. Single page applications (SPA) try to solve this problem by handling the HTML rendering entirely with client-side javascript. But, too much of anything is harmful. It's true that javascript is meant to manipulate the DOM programmatically, however if you send too much of it to the client, it takes a hit on performance. And, we are back to where it all started. SPAs were meant to improve performance (and they do up to a certain extent) but, their current implementation does the opposite. Not only that, SPAs are no good for accessibility and SEO as well. This doesn't mean you can't implement accessibility in SPAs, its just that it's hard — considering the fact that the HTML content is dynamically altered using javascript. For SEO, search engine crawlers can't find the rendered HTML content without executing javascript and that's not good. This leads to a couple of questions: How do you find a balance between HTML and Javascript? Are SPAs bad for everything? What strategy can improve the performance while still using some javascript? Static First Approach The idea is simple — you build your "staticky" content (which is not meant to be interactive) at build time, host it somewhere on the cloud and then serve that as an HTML page to the client. Parts which do need some interactivity can be progressively enhanced using javascript. Progressive enhancement is a concept where you send a minimum viable experience (wonderful analogy by Andy Bell) to the client which works even without javascript. And once javascript is available, you enhance the experience. For example, for a blog, you can build the static content and components such as headers and footers at build time and then use some javascript (if you need to) for enhancing the button click of a newsletter CTA. I discussed the simplicity of static sites, Jamstack, it's future and it's meaning with Mike Neumegen on #thefutureofjamstack. We also talked about what should be the new name for Jamstack and it was fun. Catch the full talk here. SPAs Are Not Bad For Everything There are certain use cases where you need a lot of interactivity and state management, for example a video calling app or an enterprise web application involving a lot of complex components which talk to each other. However, in this case also, you should go for a hybrid approach where you server render as much as you can (using server side rendering) and leave the rest for javascript. The Best Strategy Honestly, there is no universal best strategy. It all depends on your use case and what you are trying to build. Finding the right approach is the first thing you should do when building a project. Conclusion For the most part, static-first sites are the go to thing if your application doesn't involve much complexity. However, you are free to choose the right tool for your application — but choose wisely. --- ## Quokka in VS Code — JavaScript Debugging Made Simpler - **URL**: https://syntackle.com/blog/quokka-js-one-of-the-best-vs-code-extensions-for-javascript/ - **Updated On**: March 3, 2024 - **Description**: Quokka.js is an awesome tool for prototyping your javascript code with the power of an instant inline output. It lets you code and see the output as you type and is really beneficial if you want to quickly test something out. - **Tags**: post, vscode, javascript, extensions, guide - **Author**: Murtuzaali Surti Quokka.js is an awesome tool for prototyping your javascript code with the power of an instant inline output. It lets you code and see the output as you type and is really beneficial if you want to quickly test something out. What makes it more powerful is a VS Code extension which you can use to instantly create a new javascript file with quokka running on it. Quokka only runs in node, so you won't get to use browser specific APIs when running it, however the official documentation says it's possible with the use of jsdom and a little bit of configuration. Using It In A New File Once you do install the extension, doing a CTRL + SHIFT + P and typing Quokka will give you an option to create a new javascript file. Quokkajs in VS Code This gives you the ability to run javascript without even needing to do node . or use something like nodemon. Using It On An Existing File Using it on an existing file works for the most part, however I didn't get inline output while doing this. The output just shows up in the Output panel of VS Code. running quokkajs on an existing file The better alternative for experiencing this type of stuff on a project level is Wallaby.js. Wallaby.js as per the official documentation is a test runner which operates on a project level. "Simply put, Wallaby.js is a test runner while Quokka.js is a scratchpad / playground." - quokkajs.com Conclusion I haven't had a chance to use Wallaby.js but I still use Quokka.js whenever I need to test a piece of javascript code or just a concept which would later be a part of the project. Although both of them are run by the same entity, there are more differences between them than overlap. Anyways, I would go for Quokka.js to quickly test a small piece of code and see what it does anytime. --- ## Issue With Watching File Changes in Docker - **URL**: https://syntackle.com/blog/the-issue-of-watching-file-changes-in-docker/ - **Updated On**: February 8, 2025 - **Description**: The fix to watching file changes (hot reloading) in docker (Windows) is to use polling. Polling is a way to periodically check for changes. If you want to avoid polling, docker provides a way to watch file changes with the help of "docker compose watch". - **Tags**: post, docker, backend, guide - **Author**: Murtuzaali Surti Table of Contents Days ago, I was struggling to get live reloading/hot reloading (HMR) working for the code mounted in a docker volume on Windows. Volumes in docker serve the purpose of persistence — which means you can sync the container data with your local data. That works really well, but the catch comes when you want real time sync between both the locations of code — your local filesystem and docker's filesystem. The Problem # Different operating systems have different implementations of handling file events, example MacOS has the FSEvents API, linux has something known as inotify and Windows has the FileSystemWatcher. Docker uses it's own filesystem and it's not necessary that your system's filesystem matches docker's. Thus, the file event handling APIs available on your system might not be available in a docker container. For example, the FSEvents API of MacOS is not available in a linux environment. Now, maybe I am missing some details here but, this might cause some discrepancies in how those file events are handled — if they are even handled at all. This can cause hot/live reloading to not work at all if you update your local code mounted on a volume. I noticed this issue on Windows, so not sure about how this should be affecting other platforms. The Fix # The fix to watching file changes in docker is to use polling. Polling is a way to periodically check for changes that may have taken place. In polling you don't get notified, you keep checking the state over a network. Over the network approach works for docker because now you don't have to deal with the file system notifications, you can listen on the changes over the network. If you are using a bundler such as webpack or any other developer tool such as gulp, browser-sync or livereload, they all use a cross platform file watcher named chokidar. Chokidar relies on the nodejs's file system API but it improves upon the nodejs' API. Chokidar supports polling and you can also set the polling interval. Here's an example of how you can use polling when using gulp: gulp.watch("<path>", { interval: 1000, usePolling: true, }, task); Drawback Of Polling # The only reason polling is bad is that it's slow and consumes lot of CPU resources. If you have too many source files to watch for and you don't want to allocate more space, then polling is not a way forward. Quoting a wonderful analogy about polling from Raymond Chen: "It’s like checking your watch every minute to see if it’s 3 o’clock yet instead of just setting an alarm." - Raymond Chen Learn more about the performance consequences of polling on "The Old New Thing" — written by Raymond Chen. An Alternative — Docker Compose Watch # If you want to avoid polling, docker provides a way to watch file changes with the help of 'docker compose watch'. I removed volumes in lieu of this new watch option which is available in docker compose version 2.22 and later. # docker-compose.yaml services: your_service_name: build: context: . dockerfile: Dockerfile env_file: - ./.env # path to env file command: "npm run dev" develop: watch: - action: sync # 'build' is another action type path: ./src # host directory path target: /app/src # container directory path to map ignore: - node_modules/ ports: - 127.0.0.1:3000:3000 However, you also need to create a USER which can edit files inside the container. The best practice is to create a non-privileged user and assigning that user as an owner of those copied files. # Dockerfile FROM node:20-alpine RUN apk add --no-cache shadow RUN useradd -ms /bin/sh -u 1001 app USER app COPY --chown=app:app . /app WORKDIR /app RUN npm install For alpine linux, to be able to use useradd, you need to install the shadow package in your image using RUN apk add --no-cache shadow. This approach works but personally I didn't find any significant performance improvement over polling. At the time of this writing, docker compose watch functionality has a bug which occurs when you terminate the watch command. You can't run the watch command again due to this bug. Follow the steps mentioned in this github issue comment to get the watch command running again. Conclusion I don't know how docker compose watch works under the hood and does it use polling as well, but it's clear that now you have two approaches to get file watch working in a docker container. A slight advantage of using docker compose watch is that you can also add an action named build which will rebuild the image and container. For example, you can rebuild the image on the package.json file change. --- ## Chrome 121 Broke My CSS By Adopting New Scrollbar Properties - **URL**: https://syntackle.com/blog/changes-to-scrollbar-styling-in-chrome-121/ - **Updated On**: February 3, 2024 - **Description**: Recently, in version 121, Chrome started supporting CSS Scrollbar properties `scrollbar-color` and `scrollbar-width` and it broke my CSS. Here's what happened and how I fixed it. - **Tags**: post, css, frontend, chrome - **Author**: Murtuzaali Surti Recently, in version 121, Chrome started supporting standardized CSS scrollbar properties scrollbar-color and scrollbar-width mentioned in the CSS specification and it broke my CSS. Here's what happened and how I fixed it. TLDR — Quick Fix: /* Wrap new scrollbar properties in @supports rule for browsers without `::-webkit-scrollbar-*` support */ /* This way chrome won't override `::-webkit-scrollbar-*` selectors */ @supports not selector(::-webkit-scrollbar) { html { scrollbar-width: thin; scrollbar-color: var(--thumb-color) var(--track-color); } } I always feel scrollbars on a website seem off if the site is designed with consistency and a little bit creativity. They don't always match the theme of the site. Some people prefer it that way and that's okay but I want scrollbars to feel like an integral part of the website itself. And so, I styled the scrollbars for syntackle a while ago to match the current theme. Back then, Chrome didn't support scrollbar-color and scrollbar-width properties but it did have the ::-webkit-* pseudo selectors exposed, so I used them to create a custom scrollbar experience. But webkit is only supported on Chrome and Safari, not firefox. To mitigate this, I resorted to the new scrollbar properties for firefox. html { scrollbar-width: thin; scrollbar-color: #6d7c77 #cfd7c7; } body::-webkit-scrollbar { width: 0.5rem; } body::-webkit-scrollbar-thumb { background-color: var(--tertiary-color); border-radius: 0.7rem; } body::-webkit-scrollbar-track { background-color: var(--primary-color); border-radius: 0.7rem; } The reason I attached the ::-webkit-* pseudo selectors on the body is that I control the theme of the site using CSS variables on the body element and by altering the CSS classes through javascript. And this broke the scrollbar styling with Chrome 121. It seems that Chrome now prefers the new scrollbar properties on the html element over the ::-webkit-* pseudo selectors. One thing that you could do is attach the new scrollbar properties to the body element instead of html, it works in Chrome but firefox doesn't seem to support that. body { scrollbar-width: thin; scrollbar-color: #6d7c77 #cfd7c7; } I researched a bit online and found out that recently Chrome published a blog post related to this change. There's a whole topic of overlay scrollbars and the impact of operating system on the scrollbar which you can deep dive into. But a quick fix which you can do is for the browsers that don't support ::-webkit-* pseudo selectors, add a @supports rule check and add the new scrollbar properties there. body::-webkit-scrollbar { width: 0.5rem; } body::-webkit-scrollbar-thumb { background-color: var(--tertiary-color); border-radius: 0.7rem; } body::-webkit-scrollbar-track { background-color: var(--primary-color); border-radius: 0.7rem; } /* Browsers without `::-webkit-scrollbar-*` support */ @supports not selector(::-webkit-scrollbar) { html { scrollbar-width: thin; scrollbar-color: #6d7c77 #cfd7c7; } } For me, I still like to have control over the scrollbar styling and that's why I prefer webkit pseudo selectors over new properties but that's just a personal preference. The CSS specification considers exposing ::-webkit-* pseudo selectors as a mistake and have standardized the styling of scrollbar because of different platforms implementing it in different ways. "Providing too much control would allow authors to get perfect results on some platforms, but at the expense of broken results on others." - drafts.csswg.org With that being said, I am not encouraging you to use the webkit selectors and you should always look out for standardized properties first and see if that works for you. --- ## rssed — An RSS Feed Reader And Blogroll - Built Using Astro - **URL**: https://syntackle.com/blog/blogroll-using-astro/ - **Updated On**: February 8, 2025 - **Description**: RSS a.k.a Really Simple Syndication is a great technology to subscribe to website content. Initially, I was into the habit of bookmarking blogs which I admire and used to visit them once in a while. But, that wasn't. - **Tags**: post, astro, javascript, rss, frontend, opensource - **Author**: Murtuzaali Surti RSS a.k.a Really Simple Syndication is a great technology to subscribe to website content. You can get the latest updates published on a website by parsing the RSS feed. It uses XML to present the data and meta information about the content. A huge number of software developers have their own blog where they publish posts about programming and software development regularly. Initially, I was bookmarking blogs which I admire and used to visit them once in a while. But that wasn't practical enough. I was missing on beneficial updates and news of what was happening in the software world. So, I decided to keep track of all of those blogs using a blogroll — a collection of links to other sites or blogs — which updates itself on a daily basis. With the help of an rss parser, astro and a little bit of logic, I present to you rssed.netlify.app and the journey of how I built it. Table of Contents Initial Idea # At first, I thought to create a postgres database on neon for storing the feeds urls and I did implement it, but unfortunately, after having some issues when deploying it to a serverless function on vercel, I dropped it. Went for SSR at first (due to the urge to try dynamic paths in astro), but finally resorted to do everything at build time. Because why not? If you have a list of feeds ready, instead of generating the page from the same template on each feed request again and again, generate all of it beforehand and then serve. Astro # To give you some background — astro is a javascript based meta framework for building websites focused around content. You can use it to build static sites as well as interactive web applications using the framework of your choice, however I prefer the former use case much more than the latter. Astro's main focus is on adding interactivity through hydration (sprinkling javascript) and isolating interactive components using the islands architecture. Astro primarily generates static sites but it also gives you 2 options to build your site output: server - everything will be built and rendered on demand as per the user request. You can exclude some parts of your site which need to be rendered beforehand. So, here the default is on-demand server rendered content. hybrid - the default here is everything is pre-rendered at build time unless you specify otherwise. CAUTION The hybrid mode is removed in Astro v5.0 so you must change its name to static in astro config file. The static mode behaves the same as the hybrid mode and it's the default output mode. More info here - Upgrade to Astro v5 import { defineConfig } from "astro/config"; export default defineConfig({ output: 'static', }); For this blogroll, I went for the hybrid approach as I didn't see an issue generating content at build time. But don't I want to update the feeds on a daily basis? Yes, I still do, and you might be thinking why go for a static approach — more on that later. There are three .astro files in the picture: One for displaying the list of sorted feeds (src/pages/index.astro) One for displaying the posts for a single feed (src/pages/feed/[id].astro) The third serves as a layout (src/layouts/Layout.astro) for the previous two. RSS Feeds # RSS feeds always fascinate me considering how simple they are. A perfect way to consume content published independently. I was on the search for an RSS parser and eventually found the rss parser by @rbren. The Web is Fantastic - by Robb Knight Having stored the feed URLs along with their IDs in a json file, it became easy for me to loop over them and parse them. import feedlist from "../../data/feedlist.json" type feedItem = Output<{ [key: string]: any }> & { id: string }; const feeds: { time: string | null items: feedItem[] } = { time: null, items: [] } export const ParseRSS = async (url: string) => { return await new Parser({ timeout: 120000 }).parseURL(url) } const parseAndStoreFeeds = async (list: { id: string, url: string }[]) => { const feedPromises = list.map(async (site) => { try { const feed: feedItem = { ...await ParseRSS(site.url), id: site.id } return feed } catch (error) { logger.error({ feed: { id: site.id, url: site.url, }, error }) } }) Promise.allSettled(feedPromises) for await (const feed of feedPromises) { feed && ( !feeds.items.some(i => i.id === feed.id) && feeds.items.push(feed) ) } } parseAndStoreFeeds(feedlist) What I am doing here is fetching the RSS feeds from their URLs by using the rss-parser parallelly, handling potentially unhandled promise rejections using Promise.allSettled() and pushing them sequentially in an array. This approach of fetching feeds parallelly and storing them sequentially has improved the build time drastically. Earlier, I was fetching the feeds parallelly but waiting for the last one to get fetched and succeed. But this meant that if one of the URL fails to get parsed, I won't get any result. It's all or nothing. I certainly don't want to do that. export const allFeeds = async (list: { id: string, url: string }[]) => { return Promise.all( list.map(async (site) => { return { ...await ParseRSS(site.url), id: site.id } }) ) } So, I improved it a bit, but now the problem was that it was all sequential. If one URL takes 6 seconds to resolve and parse, the next one just keeps on waiting. That's too slow. export const allFeeds = async (list: { id: string, url: string }[]) => { const feeds = []; for (const site of list) { try { const feed = { ...await ParseRSS(site.url), id: site.id } feeds.push(feed) } catch (error) { console.error(error) } } return feeds } Finally, I remembered I read a post by Jake Archibald discussing the gotcha of unhandled promise rejections and found the solution which I shared at the very first. const feedPromises = list.map(async (site) => { const feed: feedItem = { ...await ParseRSS(site.url), id: site.id } return feed }) // gotcha Promise.allSettled(feedPromises) for await (const feed of feedPromises) { feed && ( feeds.items.push(feed) ) } Once I receive all of the parsed feeds in index.astro, sorting them according to the publish date of the last post or the last build date keeps them in a descending order. For displaying posts from a single feed, I went for dynamic routes in astro([id].astro). It's awesome that you can generate multiple pages from the same template by using the getStaticPaths() method and giving it a bunch of values as props with the feed uuid as the param. export async function getStaticPaths() { try { const res = await fetchFeeds(); const { data } = JSON.parse(res); const feeds = await allFeeds(data as { id: string; url: string }[]); const feedList: Record<string, Record<string, any>>[] = feeds.map( (fl) => { return { params: { id: fl.id, }, props: { feeds: feeds.filter((f) => f.id === fl.id), }, }; } ); return feedList ? feedList : [ { params: { id: "404", }, props: { feeds: null, }, }, ]; } catch (error) { console.log(error); return [ { params: { id: "404", }, props: { feeds: null, }, }, ]; } } And accessing the prop from the Astro.props object. const { feeds } = Astro.props; Daily Build # If all of this is static and doesn't even update itself, then what's the point of building a blogroll which subscribes to RSS feeds. That's where Netlify's build hook comes into action. A simple POST request to the build hook URL can trigger a new build of the project. This request is made every day at 00:00 with the help of a cron schedule 0 0 * * *. Learn more about cron timing and play with it on crontab.guru. The below mentioned code lives under netlify/functions directory. import fetch from "node-fetch"; import { schedule } from "@netlify/functions"; const BUILD_HOOK = process.env.BUILD_HOOK as unknown as URL // every day at 00:00 export const handler = schedule('0 0 * * *', async () => { try { const res = await fetch(BUILD_HOOK, { method: "POST" }) console.log(res) return { statusCode: 200 } } catch (error) { console.log(error) return { statusCode: 500 } } }) Labelling Latest Posts # I stored the last updated date as a data- attribute on the feed element, stored the time in localStorage and then accessed that attribute using client side javascript to calculate the difference in time between the localStorage timestamp and last updated time of the feed. document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.feed').forEach(e => { const ele = e as HTMLElement; const feedId = ele.dataset.feedId as string; const lastPublishedTime = new Date(`${ele.dataset.lastPublished}`).getTime(); if (!localStorage.getItem(feedId)) { localStorage.setItem(feedId, JSON.stringify({ read: true, timestamp: lastPublishedTime })); ele.classList.remove('feed_banner'); } else { const prevPostLogRead = JSON.parse(localStorage.getItem(feedId) as string); const loggedPublishTime = Number(prevPostLogRead.timestamp); if (!((lastPublishedTime - loggedPublishTime) <= 0)) { const newPostLogUnread = { ...prevPostLogRead, read: false } localStorage.setItem(feedId, JSON.stringify(newPostLogUnread)); ele.dataset.items === 'true' && ele.classList.add('feed_banner'); } else { const read = JSON.parse(localStorage.getItem(feedId) as string).read; read && ele.classList.remove('feed_banner'); } } }) }) Beautify Logs # While enhancing some stuff, I thought why not make logs colorful and pretty. And, thus I ended up using consola to beautify the logs. import { createConsola } from "consola"; const loggerInstance = () => createConsola({ fancy: true, formatOptions: { colors: true, date: true } }) export const logger = loggerInstance() Deploying to Netlify # In order to deploy the astro site with a hybrid or server output mode (server side rendering), you need an adapter. Astro provides official adapters which you can use for deploying to various platforms. Run npm run build locally to see the build output. import { defineConfig } from 'astro/config'; import netlify from "@astrojs/netlify"; // https://astro.build/config export default defineConfig({ output: 'hybrid', server: { port: 3000 }, adapter: netlify() }); Contribute # rssed is open source. You are free to contribute either new RSS feeds or code as a developer. I don't know if this has the potential to be the next big project but you can help it be one. Power to you. GitHub Repository murtuzaalisurti/rssed --- ## Advent Of Code 2023 - Day Four Solution - **URL**: https://syntackle.com/blog/advent-of-code-2023-day-four-solution-4SanE/ - **Updated On**: December 26, 2023 - **Description**: It felt easier than the previous one to be honest, especially the first part. The puzzle is quite simple. For a given number of scratchcards, there are two lists of numbers - **Tags**: post, advent-of-code - **Author**: Murtuzaali Surti It felt easier than the previous one to be honest, especially the first part. The puzzle is quite simple. For a given number of scratchcards, there are two lists of numbers printed on it separated by a pipe | sign. The first list contains of all the winning numbers and the second list contains the numbers given to you. You have to figure out which of the given numbers match the winning numbers. You get one point for the first match then double it for every match after the first. Part One # So, for example, if you have, Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 then, the matching numbers will be 1 and 21 and the points will be 1 after the first match and 1 * 2 = 2 points after the next. You need to sum up all the points for a pile of cards. import { readFileByLine } from "../lib/shared.mjs"; const lines = await readFileByLine("day-4/input.txt"); const cardsWithTheirPoints = Array.from({ length: lines.length }) const cardsWithTheirWinningCopies = Array.from({ length: lines.length }).map((_, i) => ({ card: i + 1, copies: { count: 1 } })) function getNumbers(str, index) { return str.split("|")[index].trim().split(" ").map(i => i.trim()).filter(j => parseInt(j)).map(k => parseInt(k)) } function PartOne() { for (const [i, line] of lines.entries()) { const cardNumbers = line.split(":")[1].trim() const winningNumbers = getNumbers(cardNumbers, 0); const numbersIhave = getNumbers(cardNumbers, 1); let points = 0; let firstMatch = true; const matchingNumbers = [] for (const win of winningNumbers) { if (numbersIhave.includes(win)) { matchingNumbers.push(win) if (!firstMatch) { points = points * 2 } else { points++; } firstMatch = false; } } cardsWithTheirPoints[i] = { matchingNumbers, points, card: i + 1 } } return cardsWithTheirPoints.reduce((acc, curr) => acc + curr.points, 0); } const totalPointsOfPileOfCards = PartOne(); Part Two # The second part is quite interesting. The concept of points is now removed. For each matching win number you win the next card. So, for example, if you have a card, Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 then, it has two matching numbers, 1 and 21 and therefore you also win cards 4 and 5. This way, multiple copies of cards are generated. If a card has no matching numbers, you win nothing. What you have to do is calculate all of the instances of each card (original + copies) and then sum them up to get the total number of scratchcards. function PartTwo() { for (const [index, line] of lines.entries()) { function calc(index, line) { const card = index + 1; const cardNumbers = line.split(":")[1].trim(); const winningNumbers = getNumbers(cardNumbers, 0); const numbersIhave = getNumbers(cardNumbers, 1); const matchingNumbers = []; for (const win of winningNumbers) { if (numbersIhave.includes(win)) { matchingNumbers.push(win) } } if (matchingNumbers.length > 0) { const nextcards = Array.from({ length: ((matchingNumbers.length + card) - card) / 1 + 1 }, (_, t) => card + t * 1).slice(1); for (const winningCopyNumber of nextcards) { cardsWithTheirWinningCopies[winningCopyNumber - 1].copies.count++; } } } for (let copy = 0; copy < cardsWithTheirWinningCopies[index].copies.count; copy++) { calc(index, line); } } return cardsWithTheirWinningCopies.reduce((acc, curr) => curr.copies.count + acc, 0); } For each winning number, I am incrementing the copies of the successor cards and then looping over the copies. I initiated the copies for every card with 1 depicting the original copy. If the card has no matching numbers, the copy count will be 1 as it generated no more winning cards. The execution time for the second part in javascript is not up to the mark, and thus there's an opportunity to optimize, which I leave it to you. exec Part 1: 3.171 ms exec Part 2: 58.305 s You can find the entire code in my repository as well as the code for the previous puzzles. --- ## Web Components & Custom Elements - **URL**: https://syntackle.com/blog/web-components-and-custom-elements-5LuzI/ - **Updated On**: February 8, 2025 - **Description**: Custom elements in HTML are a way to extend native HTML elements. Javascript frameworks simulate the behavior of components in a web page whereas - **Tags**: post, javascript, html, webc, frontend - **Author**: Murtuzaali Surti Table of Contents Those familiar with React or any other javascript framework, are already aware of the component based architecture. You break the UI into re-usable pieces of code and stitch them together when required. Custom elements in HTML are a way to extend native HTML elements. Javascript frameworks simulate the behavior of components in a web page whereas custom elements provide a native HTML-ly way to do so. A web component uses custom elements along with other techniques such as the shadow DOM. Types Of Custom Elements # Autonomous custom elements Customized built-in elements Autonomous custom elements extend the generic HTMLElement class. On the other hand, a customized custom element extends a specific HTML elements' class and builds on top of existing functionality. For example, if you want a custom anchor element, you can extend the HTMLAnchorElement. Defining Custom Elements # To define a custom element, we need to create a javascript class extending the native HTMLElement class. Try creating the below custom element in a codepen: class Demo extends HTMLElement { constructor() { super() } connectedCallback() { this.textContent = "hello" } } customElements.define("demo", Demo) And then invoking it in HTML: <demo></demo> It won't let you create it. Why? See the error. Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "demo" is not a valid custom element name This is not a bug, this is intentionally done to separate custom elements from native HTML elements. Custom elements must contain a hyphen in their name to make custom elements recognizable and distinct from HTML elements. So now, if you change it to something like demo-webc and change the class name to DemoWebC, it works. class DemoWebC extends HTMLElement { constructor() { super() this.customProperty = "custom" } connectedCallback() { this.textContent = "hello" } } // two arguments: tag name, class name customElements.define("demo-webc", DemoWebC) It always recommended to call the super() method first in the constructor as it initializes default properties of the HTMLElement class by invoking its constructor. The connectedCallback() method is for detecting when the element is loaded into the page. There's also a method named disconnectedCallback() which detects if the element is removed from the page. A third method name adoptedCallback() says that the element has moved to a new page. You can define custom properties inside the constructor and use them as custom attributes in your element. constructor() { super() this.customProperty = { name: "data-custom", value: "custom value" } connectedCallback() { this.textContent = "hello" this.setAttribute(this.customAttribute.name, this.customAttribute.value) } } But what if you need to modify the functionality on attribute's value change? That's where attributeChangedCallback() method comes into action. In order to see it in action, you need to first define a static observedAttributes class property and set it to an array of all the attributes you want to keep track of. The attributeChangedCallback() fires if those attributes mentioned in the static observedAttributes property change. Note that if the attribute is already present when the custom element loads, this method is fired at that time too. static observedAttributes = ["data-custom"] constructor() { super() } attributeChangedCallback(name, old, newValue) { console.log(name, old, newValue) } Once you are done with building a custom element, you must register it by using the define() method. It's callable on the customElements global object (window.customElements) which is a registry of custom elements. customElements.define("custom-element-name", ClassName, options) This was all for defining a customized autonomous custom element. What about extending only an anchor HTML element? For that, instead of extending the HTMLElement class, extend the HTMLAnchorElement class. And specify which type of HTML element it extends with the extends option. class DemoAnchor extends HTMLAnchorElement { constructor() { super() } connectedCallback() { this.textContent = "syntackle.com" this.href = "https://syntackle.com" } } customElements.define("demo-anchor", DemoAnchor, { extends: "a" }) You can't use this element like <demo-anchor> because it's not an autonomous element, instead you can use it like this: <a is="demo-anchor"></a> Web Components # Web components are more than just custom elements. They sometimes also involve a shadow DOM. A "shadow" DOM, as the name suggests, is a sub-DOM tree for HTML elements. It is mainly used for encapsulation and restricting styles up to the web component only. Shadow DOM To create a shadow DOM, attach it to a host, in our case the custom element itself is a host to the shadow DOM. However, the shadow DOM can only be attached to a custom element or these built-in elements mentioned in the HTML spec. You can access elements outside the shadow DOM from inside the shadow DOM. class DemoWebC extends HTMLElement { constructor() { super() } connectedCallback() { const shadow = this.attachShadow({mode: "open"}) const style = document.createElement("style") style.textContent = `p { color: blue; }` shadow.appendChild(style) const text = document.createElement("p") text.textContent = "hello" shadow.appendChild(text) } } customElements.define("demo-webc", DemoWebC) Shadow DOM has two modes: open and closed. Open means external elements in the page can modify the contents of the shadow DOM by using shadowRoot property. In the closed mode, the shadow DOM is not accessible from outside using the shadowRoot property as it is null in this case. Try doing this on a closed shadow DOM custom element: console.log(document.querySelector("demo-webc").shadowRoot) It returns null. Templates and slots are extremely useful when building complex custom elements or web components. Diving deep into them is out of the scope of this article, but here are some good resources for them: Using templates and slots - Web APIs | MDN Shadow DOM slots, composition Working with Slots and Web Components Styling Shadow DOM The shadow DOM can be styled either by: Constructing a CSSStyleSheet object, inserting CSS in it using replaceSync() and attaching it to the shadow DOM using the adoptedStyleSheets property. const shadowDOM = this.attachShadow({mode: "open"}) const styleSheet = new CSSStyleSheet() styleSheet.replaceSync(`p { color: blue; }`) shadowDOM.adoptedStyleSheets = [styleSheet] Declaring styles using a <template>. <template id="custom"> <head> <style>p { color: blue; }</style> </head> <p>Web Component</p> </template> const shadowDOM = this.attachShadow({ mode: "open" }) const template = document.querySelector("#custom") shadowDOM.appendChild(template.content.cloneNode(true)) Simply creating a style tag and inserting CSS as text in it. const style = document.createElement("style") style.textContent = `p { color: blue; }` shadow.appendChild(style) Creating Your Own Web Component # The first web component shown below is a custom button element which opens a dialog element. And the second web component involves a shadow DOM to pretty print JSON string in HTML. Similarly, you can create your own custom elements and use them anywhere you want. See the pen (@seekertruth) on CodePen. --- ## Advent Of Code 2023 - Day Two Solution - **URL**: https://syntackle.com/blog/advent-of-code-2023-day-two-E7Ndz/ - **Updated On**: December 8, 2023 - **Description**: Just wrapped up solving advent of code's day two challenge. This was a tricky one in terms of the language used to describe the puzzle. I had to take some online help to understand what was the second part of the puzzle trying to say - **Tags**: post, advent-of-code - **Author**: Murtuzaali Surti Just wrapped up solving advent of code's day two challenge. This was a tricky one in terms of the language used to describe the puzzle. I had to take some online help to understand what was the second part of the puzzle trying to say. You can visit the advent of code day two challenge to check what was the puzzle. In short, in the first part of the puzzle, we had to figure out the sum of the number of valid games from a given input. And, in the second part, we had to find the sum of the powers of fewest number of cubes (of each color) present to make the game possible. Part One To re-structure the data we are getting in the input in a singe line, I created this function to map the game number to the number of cubes revealed of each type in each play. { number: '1', cubes: { green: [ '6', '5', '5', '1' ], red: [ '3', '3', '1', '3' ], blue: [ '7', '1', '8', '5' ] } } The readFileByLine function is the same as used in day-1's solution. const getCubeCountMappedToTheGameAndType = async () => { const games = await readFileByLine('day-2/input.txt') const cubeCountMappedToTheGameAndType = Array.from({ length: games.length }) for (const [index, game] of games.entries()) { const splitGameNumberAndPlays = game.split(':').map(i => i.trim()) const cubesFromAPlay = { green: [], red: [], blue: [] } const plays = splitGameNumberAndPlays[1].split(';').map(i => i.trim()) for (const play of plays) { play.split(',').map(i => i.trim()).forEach((cubeType) => { const cubeColor = cubeType.split(" ")[1]; const cubeCount = cubeType.split(" ")[0]; cubesFromAPlay[`${cubeColor}`].push(cubeCount) }) } cubeCountMappedToTheGameAndType[index] = { number: splitGameNumberAndPlays[0].split(" ")[1], cubes: cubesFromAPlay } } return cubeCountMappedToTheGameAndType } The maximum number of cubes that should be present (for each cube type) for a game play to make this a valid game are: const totalCubesAtOnce = { red: 12, green: 13, blue: 14 }; If we get any count greater than the count above for each cube type, the game is marked as invalid. For example, game Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue is a valid game because for each play (separated by semi-colon), the cube count for each type is under the maximum count. const partOne = async () => { const validGames = [] const cubeCountMappedToTheGameAndType = await getCubeCountMappedToTheGameAndType() for (const game of cubeCountMappedToTheGameAndType) { let isValid = true loop2: for (const [type, value] of Object.entries(game.cubes)) { for (const count of value) { if (parseInt(count) > totalCubesAtOnce[`${type}`]) { isValid = false break loop2; } } }; isValid && validGames.push(game); } return validGames.reduce((acc, curr) => acc + parseInt(curr.number), 0); } And then finally, I am summing up all of the valid game numbers. From this challenge, I came across an interesting revelation about breaking nested loops in javascript. I am still processing this. Part Two Here, we had to figure out the sum of the power (cube count of each color multiplied together) of the fewest set of cubes required to make the game possible. "Fewest" here means the biggest number of cubes of each color present in a game. Once we get the number of cubes required for each color, we can multiply them together and that will give us the power for each game. Adding them up gives us the result. const partTwo = async () => { const cubeCountMappedToTheGameAndType = await getCubeCountMappedToTheGameAndType() const powersOfCubes = Array.from({ length: cubeCountMappedToTheGameAndType.length }) for (const [i, game] of cubeCountMappedToTheGameAndType.entries()) { const powersOfCubeInAGame = []; for (const [_, value] of Object.entries(game.cubes)) { powersOfCubeInAGame.push(value.sort((a, b) => parseInt(a) - parseInt(b))[value.length - 1]) }; powersOfCubes[i] = powersOfCubeInAGame.reduce((acc, curr) => acc * curr, 1) } return powersOfCubes.reduce((acc, curr) => acc + curr, 0) } The execution time turns out to be: exec: 19.009 ms Repository Here's my github repository for Advent Of Code 2023 solutions. --- ## Completed Advent Of Code: Day One - **URL**: https://syntackle.com/blog/completed-advent-of-code-day-1-CPjek/ - **Updated On**: December 8, 2023 - **Description**: AOC Day 1 - Overall, it was fun to play with strings and numbers in javascript. Through this challenge I learned about regex more than I could have normally. - **Tags**: post, advent-of-code - **Author**: Murtuzaali Surti Advent of Code 2023 is here and I just completed its day-one challenge. Overall, it was fun to play with strings and numbers in javascript. Without any further ado, let's explore the solution. The challenge is divided into two parts. The first part is quite simple but the second one adds a cherry on top of the cake. Part 1 # From a given list of strings, I had to find the digits, combine the first and last digit in order of occurrence in the string and them sum it up all. To simplify, for a given string gh34s1, the output will be 31 with 3 as the first digit, 1 as the last digit and 31 as their concatenation. So, for two strings gh34s1 and d7hs23 the sum of their digits will be 31 + 23 = 54. I stored the input in a .txt file and then looped over line by line to get the string from each line. import readline from 'readline' import fs from 'fs' import path from 'path' async function readFileByLine() { const lines = [] const readInterface = readline.createInterface({ input: fs.createReadStream(path.resolve('day-1/input.txt')), output: process.stdout, terminal: false }); for await (const line of readInterface) { lines.push(line) } return lines } (async () => { const lines = await readFileByLine() // code })() Then, to parse the digits in the string and to validate if they are numbers, I used zod. There was a simple way to sort the first and last digits but I couldn't figure it out until I started with Part-2. const calibrationByLine = lines.map((line, i) => { const lineArr = line.split('') const firstAndLastNumber = { first: null, last: null } for (const [index, char] of lineArr.entries()) { if (!z.nan().safeParse(parseInt(char, 10)).success) { firstAndLastNumber.first === null ? firstAndLastNumber.first = char : firstAndLastNumber.last = char; } }; firstAndLastNumber.last === null && (firstAndLastNumber.last = firstAndLastNumber.first); return parseInt(`${firstAndLastNumber.first}${firstAndLastNumber.last}`, 10) }) const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0) console.log(result) This approach gave me the correct answer for sure but there was a better approach. Yes, regex! Part-2 # Part-2 had me thinking that my solution was correct, but in fact it wasn't. Lets see what was it. So, the second part introduced a twist in the story, not only the digits were represented as integers in the string, but also their alphabetical synonyms ("one", "two", "three") were also a part of it. Regex was the answer. I tried /(one|two|three|four|five|six|seven|eight|nine)/g as a regular expression along with matchAll() to find all occurrences of the alphabetical number representations and it seemed to work. Then, I stored the digits along with their indexes in an array of objects by mapping the alphabetical numbers to their respective integers and sorted them in an ascending order based on the index to get the first and last digits. const regex = new RegExp('(one|two|three|four|five|six|seven|eight|nine)', 'g') const stringToIntMap = new Map([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5], ["six", 6], ["seven", 7], ["eight", 8], ["nine", 9]]) const calibrationByLine = lines.map((line, i) => { const allDigitsInLine = [] for (const match of line.matchAll(regex)) { allDigitsInLine.push({ digit: stringToIntMap.get(match[1]), index: match.index }) } const lineArr = line.split('') const firstAndLastNumber = { first: { digit: null, index: null }, last: { digit: null, index: null } } for (const [index, char] of lineArr.entries()) { if (!z.nan().safeParse(parseInt(char, 10)).success) { allDigitsInLine.push({ digit: char, index }) } }; const sortedDigitsAccordingToIndex = allDigitsInLine.sort((a, b) => a.index - b.index) firstAndLastNumber.first = { ...sortedDigitsAccordingToIndex[0] } firstAndLastNumber.last = { ...sortedDigitsAccordingToIndex[sortedDigitsAccordingToIndex.length - 1] } firstAndLastNumber.last.digit === null && (firstAndLastNumber.last.digit = firstAndLastNumber.first.digit); return parseInt(`${firstAndLastNumber.first.digit}${firstAndLastNumber.last.digit}`, 10) }) const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0) console.log(result) What it would do is, for a string like d3two4eight, the first digit it will select is 3 and the last as 8. So, the output for this string would be 38. So far so good. I submitted the answer and it was incorrect. There was a catch. The input also contained strings such as 3eightwo. The alphabetical parts of eight and two overlap. And so, my assumption was to ignore the latter digit and just keep eight which would make the output as 38. But the correct output should be 32 after taking into account the overlapping representations. After a bit of googling I found out about how to find overlapping matches using the lookahead assertion in regular expressions. Modified the regex to this and I got the perfect answer. new RegExp('(?=(one|two|three|four|five|six|seven|eight|nine))', 'gm') And that's it. Through this challenge I learned about regex more than I could have normally. Here's the entire solution: import readline from 'readline' import fs from 'fs' import path from 'path' import { z } from 'zod' const regex = new RegExp('(?=(one|two|three|four|five|six|seven|eight|nine))', 'gm') const stringToIntMap = new Map([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5], ["six", 6], ["seven", 7], ["eight", 8], ["nine", 9]]) async function readFileByLine() { const lines = [] const readInterface = readline.createInterface({ input: fs.createReadStream(path.resolve('day-1/input.txt')), output: process.stdout, terminal: false }); for await (const line of readInterface) { lines.push(line) } return lines } (async () => { // reading file line by line and storing it in an array - each line contains a string const lines = await readFileByLine() // looping over each string const calibrationByLine = lines.map((line, i) => { const allDigitsInLine = [] // finding alphabetical digits, mapping them to their integers, and storing them along with index for (const match of line.matchAll(regex)) { allDigitsInLine.push({ digit: stringToIntMap.get(match[1]), index: match.index }) } const lineArr = line.split('') const firstAndLastNumber = { first: { digit: null, index: null }, last: { digit: null, index: null } } // finding integers and storing them along with index for (const [index, char] of lineArr.entries()) { if (!z.nan().safeParse(parseInt(char, 10)).success) { allDigitsInLine.push({ digit: char, index }) } }; // sorting integers based on the index - ascending const sortedDigitsAccordingToIndex = allDigitsInLine.sort((a, b) => a.index - b.index) firstAndLastNumber.first = { ...sortedDigitsAccordingToIndex[0] } firstAndLastNumber.last = { ...sortedDigitsAccordingToIndex[sortedDigitsAccordingToIndex.length - 1] } firstAndLastNumber.last.digit === null && (firstAndLastNumber.last.digit = firstAndLastNumber.first.digit); // concatenating digits and converting into integer return parseInt(`${firstAndLastNumber.first.digit}${firstAndLastNumber.last.digit}`, 10) }) // summing up the result of every string const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0) console.log(result) })() The execution time for 1000 strings turns out to be: executed: 94.047 ms --- ## App Defaults 2023 — What I use - **URL**: https://syntackle.com/blog/app-defaults-2023-what-i-use-by-murtuzaali-surti-qhifV/ - **Updated On**: January 16, 2024 - **Description**: The other day, I stumbled upon a post by robb about app defaults. Following the spree, here's my list of all the apps I use for the following categories. - **Tags**: post, web, apps, opinion, listicle - **Author**: Murtuzaali Surti The other day, I stumbled upon a post by @robb which in turn led me to this post about app defaults. Following the spree, here's my list of all the apps I use for the following categories: 📨 Mail Client - Gmail 📝 Notes - Notion, Obsidian 📆 Calendar - Google Calendar 📁 Cloud - Google Cloud, OneDrive 📖 RSS - rssed 🌐 Browser - Chrome, Brave, Firefox 💬 Chat - WhatsApp, Discord 🔖 Bookmarks - Notion Web Clipper, Chrome Bookmarks 📜 Word Processing - Notion, Obsidian 📈 Spreadsheets - Google Sheets 🎤 Podcasts - Google Podcasts, PocketCasts 🔐 Password Management - Bitwarden 🧑‍💻 Code Editor - VS Code ✈️ VPN - ProtonVPN Honorable mentions 📝⚡ Quick note-taking - numbr - useful for quick note taking involving numbers, currency or any numeric values. Further Reads 📜 More about app defaults and how it started. --- ## Elegant Console Logs With Consola - **URL**: https://syntackle.com/blog/elegant-console-logs-p0JYCE/ - **Updated On**: February 8, 2025 - **Description**: Creating beautiful and elegant console logs in consola, a console wrapper. Console logs are not always well structured and eye-pleasing. Unpleasant and messy console takes away - **Tags**: post, terminal, console, logs, cli - **Author**: Murtuzaali Surti Table of Contents Console logs are not always well structured and eye-pleasing. Unpleasant and messy console takes away from the bliss of a developer. I recently came across a package named consola which does exactly this — making consoles meaningful and elegant. It has browser support, pausing and resuming logs, prompt support and other useful features. Installation # You can install it by using npm, yarn or pnpm. npm i consola Trying it out Spin up a new node/express app and start exploring. Basics # For a quick hands on experience, in your node application, throw an explicit error and then log it with the help of consola. import { consola } from "consola" try { throw Error("Unexpected error") } catch (error) { consola.error(error) } consola.error("Error...") consola.info("Info...") consola.warn("Warning...") consola.log("Logged...") New Instance # You can create a new consola instance with the help of the createConsola method and use that instead of the default global instance. import { createConsola } from "consola" const logger = createConsola({ level: 0, // error/fatal logs fancy: true, formatOptions: { date: true, columns: 20 }, }) logger.info("Info...") // this WON'T work logger.error("Error...") // this will work Set the log level to selectively allow only certain types of logs. Log level of 0 means only FATAL and ERROR logs are logged. The default log level is 3. Reporters # Reporters are representations of logs in the terminal. Consola (v3.2.3) provides 3 reporters out of the box, namely, basic, fancy and browser. They are configured based on log levels on the global consola instance. To add a custom reporter to your newly created instance, you can use the reporters property which is an array of reporters. import { LogLevels, consola, createConsola } from "consola" const infoLogger = createConsola({ fancy: true, formatOptions: { date: true, columns: 20 }, reporters: [ { log: (log) => { if (log.level === LogLevels.info) { consola.info(JSON.stringify({ date: new Date().toLocaleString("default", { dateStyle: "full" }), logs: new Array().concat(log.args.map((l, i) => `${l}${i < log.args.length - 1 ? `,` : ``}`)).join(" "), }, null, 4)) } else { consola.error( new Error("invalid log method") ) } } } ] }) Now, when you use any logging method on this instance, it will only log for the info method and throw an error for other methods. You just created a custom info message logger which you can further modify however you want. // for the above instance infoLogger.error("Won't work") ❌ infoLogger.info("Will work") ✔️ Multiple reporters per instance are also supported allowing you to separate logs into desired representations. reporters: [ { log: (log) => { if (log.level === LogLevels.info) { consola.info(JSON.stringify({ date: new Date().toLocaleString("default", { dateStyle: "full" }), logs: new Array().concat(log.args.map((l, i) => `${l}${i < log.args.length - 1 ? `,` : ``}`)).join(" "), }, null, 4)) } else { consola.error( new Error("invalid log method") ) } } }, { log: (log) => { createConsola({ fancy: true, formatOptions: { date: false } }).log(log) } } ] Methods such as addReporter, setReporters and removeReporter are available to handle reporters for an instance. Wrapping native console method with consola instance # Doing so will redirect all the native console.log calls to the specified consola instance. infoLogger.wrapConsole() // consola instance `infoLogger` will print the table console.table(["Info", "Second Info"]) restoreConsole will restore the native functionality of console.log and won't redirect to consola instance. There are several util methods present in consola/utils which you can use to further customize the logs. Prompts # Prompts are supported in consola with the help of clack, a tool to build command-line apps. Check this out for some prompt examples in consola. Conclusion Correctly and elegantly representing console logs is an important task if you want to improve developer productivity and consola helps you with just that. --- ## Sharing Localhost From VS Code - Port Forwarding - **URL**: https://syntackle.com/blog/sharing-localhost-in-vscode-port-forwarding-Kew9D/ - **Updated On**: December 17, 2023 - **Description**: Showing off what you built locally has never been easy. Now, the feature of port forwarding is directly built into VS Code. Does this mean all other services which provide port forwarding or remote tunneling will be obsolete? - **Tags**: post, vscode, guide - **Author**: Murtuzaali Surti Showing off what you built locally has never been easy. Sending localhost:3000 as a URL is like declaring a war. Hey, I built a website! // [!code highlight] Great, send me the link Sure, have a look // [!code highlight:2] http://localhost:3000 Awesome, looks identical to mine Thanks! Jokes apart, you first need to deploy it somewhere and then share the URL to a remote person. Or, use a third party service to generate a temporary URL. Using ngrok or a cloudflare tunnel might be a choice, but now, the feature of port forwarding is directly built into VS Code. Port forwarding with VS Code You can now go to the Ports view in VS Code, sign in to GitHub and forward any ports on which your local services are running. For example, you can spin up a node server on port 5000, listen for requests on that server and forward port 5000 to an accessible URL. The visibility of the URL is private by default, which means you need to sign in with the same github account you used to forward the port. You can set the visibility of the URL as public to remove the sign in barrier. You can also change the protocol from HTTP to HTTPS if you are forwarding any sensitive information. However, it's not recommended to forward any confidential or sensitive information over the tunnel. If you are concerned about the security aspect of it, the official documentation says it all. Personal Take Does this mean all other services which provide port forwarding or remote tunneling will be obsolete? No. Port forwarding in VS Code still doesn't support remote tunneling and many other features which other services may provide. For now, it's just a basic feature built into VS Code, but the possibilities are endless. Anyways, I am pretty excited to try this out and see what it brings to the table. At the very least, now we don't have to have another extension just to forward ports. --- ## Docker — Containerizing a Nextjs Application - **URL**: https://syntackle.com/blog/containerizing-a-nextjs-application-using-docker-st_G0u/ - **Updated On**: February 8, 2025 - **Description**: Containerization in it's entirety is an incredibly useful concept. From being able to execute applications in isolation, to being able to port them easily with all of their dependencies and configuration is all a developer could ask for. - **Tags**: post, docker, nextjs, frontend, backend - **Author**: Murtuzaali Surti Table of Contents Containerization in it's entirety is an incredibly useful concept. From being able to execute applications in isolation, to being able to port them easily with all of their dependencies and configuration is all a developer could ask for. After getting somewhat familiar with this concept, I decided to get my hands on it. So, let me walk you through the whole process of containerizing a frontend Nextjs application using Docker. Note that this is an absolute beginner approach. I am not advising to use it in production. This is something new to me and I am still exploring Docker. I don't know, maybe you can help me make some improvements to this approach. Anyways, this tutorial is for someone who wants to explore and get a hands on experience with Docker. By the way, a huge shoutout to Nana Janashia who is an incredible DevOps instructor. I learned everything about docker from her youtube channel. 💙 Why use Docker? # You might be thinking what's the point of containerizing an application? Well, installing dependencies, setting up database, and doing a lot of configuration every single time, isn't it better to just configure it once and ship it so that it can be run on another machine without any hassle? And not only that, some dependencies pollute your local environment but with docker everything runs in an isolated environment giving you more control. Okay but why can't we use a virtual machine if the end goal is to isolate everything? The problem is — VMs are heavy and they run on their own OS and kernel. Docker uses the resources of its host but has its own application layer and file system. You only need the docker engine to run a containerized application. Apart from that, it makes it so much easier to collaborate with project team members, testers, and devops team to run the application regardless of their operating system. Watch this video to learn what problems docker solves in development as well as the deployment process. ✨ Install Docker from docker.com! 🏃 Understanding Docker # Docker revolves mainly around three core components: containers, images and volumes. Let's understand what are they and how they work together. If you want to containerize your application, first you have to build an image of it. An image is nothing but a combination of your app code, dependencies, and configuration. It's like a complete package of your application, ready to be shipped. A container is just a running instance of an image. It lets you run the application in an isolated environment. You can run multiple containers based on different images. Volumes are often used to store persistent data. For example, if you to access the host' files from the container, you can use volumes to map the host path to a container path. I am yet to have a good grasp on the concept of volumes in docker but here's a good video on docker volumes. Docker Files # There are a bunch of docker-specific files which are used to configure docker. It's good to understand what each of them does, so here we go: Dockerfile: It's used to build an image of an application. docker-compose.yaml: A structured way to execute docker commands with a lot of options in order to handle containers. .dockerignore: Similar to .gitignore in git, it is used to ignore files in docker. Containerization of a Simple Nextjs Application Create a nextjs app using the steps in their documentation. While creating a nextjs application, I went for directory based routing and thus the app directory will act like the src directory. Now, you need to build an image of your application so that you can run it inside a container. Building a docker image # To create your own docker image, you need to create the Dockerfile. This file will have everything you need to package your application including dependencies and initial commands. Create the Dockerfile at the root of your project. FROM node:18-bullseye-slim RUN mkdir -p /home/yourapp COPY . /home/yourapp WORKDIR /home/yourapp RUN npm install CMD ["npm", "run", "dev"] Directives such as FROM, COPY, RUN, etc., are specific to the Dockerfile. To execute and run your application you need node in your image, that's why we are directing docker to pull a nodejs image from docker hub which the app can use. Selecting node images from docker hub is much more nuanced than this! Do your own research before selecting any node image. Then, with the RUN directive, you are telling docker to run a command inside the image's file system. It will create a yourapp directory in the home directory. Next, the COPY directive will copy the files from the current directory of your local system to the yourapp directory of image's file system. To ignore certain files or directories such as .env, node_modules, etc., you can create a .dockerignore file and list them there. With the WORKDIR directive, the current directory will be set to the root directory of your project in image's file system. If you don't specify it, npm will install dependencies in the root directory of the file system and not your project. The npm install command will install dependencies and create node_modules folder so you don't need to copy it from your local system. The CMD directive is like an entrypoint command to run your built image. Docker will by default execute this CMD command when you run your built image in a container. The use of the CMD directive is a bit more nuanced too. Now that your image building instructions are ready, you can execute the following command to create an image. docker build -t image_name:tag . The -t flag is for specifying a tag to the image. You can specify something like 1.0 or anything you want by replacing the :tag placeholder. And, the . is actually the context path which will be used by Docker to find your project files. If you are at the root of your project, you can specify ., otherwise you have to give a path relative to where your terminal is currently pointing to. After the command gets executed successfully, you should get something like this in your terminal. [+] Building 55.4s (11/11) FINISHED docker:default => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 208B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 72B 0.0s => [internal] load metadata for docker.io/library/node:18-bullseye-slim 2.5s => [auth] library/node:pull token for registry-1.docker.io 0.0s => [1/5] FROM docker.io/library/node:18-bullseye-slim@sha256:d2617c7df857596e4f29715c7a4d8e861852 0.0s => [internal] load build context 0.1s => => transferring context: 18.36kB 0.1s => CACHED [2/5] RUN mkdir -p /app/path 0.0s => [3/5] COPY . /app/path 0.1s => [4/5] WORKDIR /app/path 0.0s => [5/5] RUN npm install 41.8s => exporting to image 10.3s => => exporting layers 10.3s => => writing image sha256:fb05dcdb130301a8a1f8afa39c01a4430c59127e266cb49bcb763b5dd73d7aef 0.0s => => naming to docker.io/image_name:tag 0.0s And if you have docker desktop installed, you can see a docker image entry in the images tab. That's the basic process for building an image in docker from a Dockerfile. You can port this image to any other machine and it will execute exactly the same as it will on your machine. But still, your app isn't running! Why? Because you didn't run the image. And that's where containers come into action. Running Image in a Container # As you know, a container is nothing but a running instance of an image. So how do you run an image? Using docker run: docker run -d -p5000:3000 --name container_name image_name:tag What's going on with this command? you may ask! Well, nothing much — the first flag -d is used for detached mode which basically means the container will keep running in the background by freeing your terminal. If you don't specify it, the container will log everything in the terminal and once you kill the process, the container will exit and stop running. The second flag -p is used for port binding. Port binding in docker is a way to map the container port to a port on your machine i.e. host port. Let's say you are running your application at port 3000 inside the container and because the container is isolated, it has it's own ports, and thus you have to specify which port on your local machine will it forward the application content to. Here, with -p5000:3000 you are binding the 3000 port of your container to the port 5000 of your local machine. So, the syntax is: -pHOST:CONTAINER. The next two arguments are quite straightforward. You have to specify a container name and the name of the image which you want to run with it's tag. If you have some env variables, you can specify them in an env file and then use the flag --env-file to specify the path of it. After running the command, you will see a container being created and the command which we specified in the CMD directive will get executed, and the app will be live at port 5000! To see all the running containers just run docker ps and to view all containers, run docker ps -a. But there's a catch, every time you need to run an image, you have to run the docker run command with a long list of options. This isn't feasible and and it's time consuming when you have multiple services or containers talking to each other in a complex application. To overcome this, there's a file named docker-compose.yaml which you can use to specify the containers with all of their required options and env variables. With only one container, your docker compose file can look something like this: services: your_container_name: container_name: your_container_name image: image_name:tag env_file: <<path to env file>> ports: - 5000:3000 command: npm run dev To run docker compose, execute docker-compose up -d with a detached flag for the container to run in the background. To stop and remove the container, run docker-compose down! Persistent Data - Volumes # Try updating your nextjs code locally and see if you can see those changes being reflected in your containerized application. Does it update? No, it won't. The reason for that is your local code is not in sync with the code in the container, i.e. the code running in the container hasn't updated and is still the same. To overcome this issue, you need to keep some of your container files in sync with the local files. Volumes can be used to do that. You need to map your local directory to the container's directory with the help of a volume, which will help docker to listen for changes in the local directory and update the container directory! In your docker-compose.yaml file, add the following: services: container_name: # ... volumes: - ./app:your_container_app_dir/app # ... For the current nextjs application, you can persist the app directory (if you opted for directory based navigation) or the src directory. The path before the colon : is your local directory's relative path to the app directory and the path after : is the absolute path to where the project lives and it's app directory. Live Reloading (HMR) # All of that takes care of synching files, but hot reloading still won't work when you make any changes to the app directory. For that to work, you have to add a webpack config to the next.config.js file. const nextConfig = { webpack: (config => { config.watchOptions = { poll: 1000, aggregateTimeout: 300, ignored: ['**/node_modules'] } return config }) } The reason behind the use of polling is that webpack and other similar bundlers use some packages such as fsevent or inotify to detect file changes. The problem with that is that docker uses it's own filesystem which is linux based irrespective of the OS' filesystem. Maybe I am missing some details, but that causes discrepancies in the file change detection and live reload process. Polling works on a network level, thus resolving the filesystem issue, but some say it can be expensive and slow. — stackoverflow.com/a/46804953 After updating your next config, you need to rebuild the image because you didn't persist that file to reflect your local changes, rather added it as a one time static file using the COPY directive. A better approach would be to add whole project directory to the volume so that you don't have to rebuild the image, but it's debatable. Final Words That's it, you just built your own docker image and ran it in a container. If you want, you can also publish your docker image to docker hub. --- ## WebSockets 101 - **URL**: https://syntackle.com/blog/websockets-101-JiIrdn/ - **Updated On**: December 31, 2024 - **Description**: WebSockets implement a full-duplex, bi-directional, TCP-based protocol, denoted by ws(s)://, which enables a persistent connection between the client and the server. Back when websockets weren't a thing. - **Tags**: post, javascript, web, nodejs, backend, guide - **Author**: Murtuzaali Surti Table of Contents WebSockets implement a full-duplex, bi-directional, TCP-based protocol, denoted by ws(s)://, which enables a persistent connection between the client and the server. Why are websockets required? # Back when websockets weren't a thing, HTTP polling was used for a similar purpose. HTTP is basically a uni-directional protocol wherein a client sends a request to the server, the server accepts the request and sends a response. The server can't send a response for which no request has been made by the client. In simple terms, it only responds to what it's asked for. This type of behavior poses a problem for real-time applications. What if the server needs to send some information to the client but the client doesn't know about it yet? It can't initiate a response without a request. To overcome these type of situations, a workaround is used, known as polling. The client assumes that there might be something that will be required later in time from the server and sends periodic requests at specific intervals to the server known as poll requests to check if there's something new. If there's nothing new for the server to send, it just responds with an empty response. This approach is known as short polling. Short Polling Long polling is a similar approach as short polling except the fact that the server doesn't respond with an empty response on a poll request by the client. Instead, it receives the request, keeps the connection open, and only responds to it when there is actually something new that needs to be sent to the client. After the server sends a response with some data, the client sends another poll request either immediately or after a delay. That's how the server is actually able to initiate the communication which isn't possible in traditional HTTP protocol. Long Polling Both of the above techniques have their own drawbacks which lead to the use of websockets. Working of Websockets # Websockets allow the client as well as the server to initiate the sending of messages. The websocket protocol involves a two-part process. The first part involves a handshake and the latter part involves the exchange of data. WebSocket Illustration The initial handshake occurs when the client sends an HTTP 1.1 request to the server with an upgrade header set to websocket. This simply means that the client is informing the server that this connection isn't a normal HTTP connection, rather it needs to be upgraded to a websocket connection. The client's request looks something like this: GET ws://localhost:5000/ HTTP/1.1 Host: localhost:5000 Connection: Upgrade Upgrade: websocket Origin: http://localhost:3000 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: VloOROMIOo0curA7dETByw== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits The connection type in the above request is set to upgrade and the upgrade protocol is set to websocket. The upgrade header can only be used in HTTP 1.1 requests to upgrade to a different protocol. The sec-websocket-version, sec-websocket-key, and sec-websocket-extensions are special headers sent by the client to further describe the websocket connection. Now that the client request is sent, the server will verify the request (to make sure that it's a genuine websocket connection), accept the request if it supports a websocket connection, and return the verification response. Request verification is done as follows: The server needs two pieces of information — sec-websocket-key and GUID to verify the request. It will then perform necessary operations on this information and derive a sec-websocket-accept value that is later sent to the client as a response header. This value tells the client that the server has accepted the connection and it can now verify the value. The sec-websocket-accept header isn't the only thing which is required to know if the server has accepted the connection or not. There's also a status code of 101 which must be present to echo the acceptance of connection by the server. Any status code other than 101 tells that the websocket connection isn't complete. The server response looks something like this: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: 30RLwsqJ/mc0ojx6XVmAQTDJSvY= Now, at this stage, both the client and the server are ready to receive messages from each other. The websocket instance has access to various events such as onopen, onclose, onmessage, etc. to perform some operations when these events occur. To better understand the flow of messages and various events, let's build a small application which implements websockets. Recommended ✨ Server Sent Events 101 Building a Websocket Application # In order to implement websockets, you can use a nodejs library named ws. It provides a fast and simple way to establish a websocket connection. WebSocket Server npm install ws Firstly, you need a server to handle websocket requests. The ws library provides an interface named WebSocketServer to create a websocket server. // server.mjs import { WebSocketServer } from "ws" const wsServer = new WebSocketServer({ port: 5000 }) Then, you can start attaching events to this server. wsServer.on("connection", (req, ws) => { //... }) The above event will trigger whenever the server receives a new connection request from a client. It provides a callback function with the websocket instance (for a particular client) and the request object. wsServer.on("connection", (req, ws) => { const currentClient = req.headers['sec-websocket-key'] console.log(`\n\n${currentClient} just got connected\nclients connected: ${wsServer.clients.size}\n`) }) You can use the request object to the sec-websocket-key header value, which I have used to identify a client. In production you must generate a unique id by yourself. This is just for demonstration purposes. Using the above code, you can log the client connection on the server. Next, let's see how you can broadcast a message to all clients connected to the server except the current client. So, here's a function that accepts a message object and broadcasts it to all clients except the one who is sending it. function broadcast(message) { const stringifiedMessage = JSON.stringify(message) wsServer.clients.forEach(client => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(stringifiedMessage, (err) => { if (err) { console.log(err) return; } }) } }) } The websocket server — wsServer, has access to all the clients connected to it. The ws websocket instance itself describes the client. So, you can verify the client against the current ws instance and send the message accordingly. Also, the message should only be sent if the websocket connection is still open. If a client gets disconnected, the message will not be sent. But, what if we want to send a message only to the current client? For that, you simply need to do this: ws.send(message, err => console.log) The error event of the websocket will allow you to log if anything goes wrong. ws.on("error", console.error) Whenever a client sends a message to the server, the message event will get triggered by which you can broadcast the message to all the clients if you want to. ws.on('message', (data) => { const incomingMessage = data.toString('utf8') const outgoingMessage = { from: currentClient, data: incomingMessage, type: { isConnectionMessage: false } } broadcast(outgoingMessage) }) The data you are getting in the message event will be a buffer, so you need to parse it into a string. You can also broadcast a client disconnected message to all of the connected clients on the event of disconnection of a specific client. ws.on("close", () => { console.log(`\n\n${currentClient} closed the connection\nRemaining clients ${wsServer.clients.size}\n`) broadcast({ from: currentClient, data: `${currentClient} just left the chat`, type: { isConnectionMessage: false, isDisconnectionMessage: true } }) }) WebSocket Client A websocket client is nothing but a webpage with some client-side javascript. You must use the native WebSocket API provided by the browser to establish a websocket connection. const ws = new WebSocket("ws://localhost: 5000") The client's ws instance has access to the same events like open, close, message, etc. because it is essentially a websocket connection instance. ws.onopen = () => { } ws.onclose = () => { } ws.onmessage = () => { console.log(message) } ws.send(message) Multiple browser instances (or tabs) connected to the same websocket server can serve the purpose of multiple clients. That's it. You can now send messages to the server and observe how they get broadcasted to multiple connected clients. Use Cases # Real-Time Collaboration Chat Applications Multiplayer gaming Real-Time Feeds Live Browser Reloading Here's the github repository containing the entire code. --- ## Builder.io's Partytown with 11ty - **URL**: https://syntackle.com/blog/builder-io-s-partytown-with-11ty-lN6X2w/ - **Updated On**: January 5, 2025 - **Description**: Exploring builder.io's partytown and integrating it with 11ty! It is nothing but a library which uses web workers to separately execute third party scripts. - **Tags**: post, 11ty, partytown, frontend, performance - **Author**: Murtuzaali Surti Third party analytics scripts are generally included in the head section of HTML. It poses a performance threat because of render blocking nature of those resources. Although you can use the async or defer attribute to deal with those resources, they are still on the main thread of javascript. What if you can shift them to a different thread and free the main thread? Yes, you can do it by using partytown, which is nothing but a library which uses web workers to separately execute third party scripts. Javascript is single-threaded, yet it is capable to execute asynchronous code! How? Well, here's an interactive demo which will help you understand how the event loop and web APIs work when the browser executes javascript! Diving into how partytown works is out of the scope of this article, but at the surface level, it manages to interact with the DOM synchronously from a web worker. Web workers in partytown Integrating Partytown with 11ty # Partytown is framework agnostic, i.e. you can even use it in a simple HTML-only site. For 11ty, you can install the @qwik.dev/partytown npm package and extract a snippet from the integration submodule which is required to execute partytown. INFO The partytown package is now moved under a new organization, @qwik.dev/partytown. So move the npm package from @builder.io/partytown to @qwik.dev/partytown to get the latest releases. npm i @qwik.dev/partytown Inside eleventy config: const { partytownSnippet } = require("@qwik.dev/partytown/integration"); You must include this snippet in your base layout or wherever you want to include partytown. I inserted it by using a shortcode. eleventyConfig.addShortcode("partytown", () => partytownSnippet()); The shortcode (for nunjucks / liquid) will be: {% partytown %} You can use it inside a script element like this: <script> {% partytown %} </script> The partytown snippet will now be inline with your HTML. Next, some static files need to be served from the same origin for partytown to work. These files, by default, must be present in the /~partytown/ directory of your build. Tip: If you really want to serve lib files from a different directory, you can specify it in the lib config. Copy required files using addPassthroughCopy in 11ty. eleventyConfig.addPassthroughCopy({'./node_modules/@qwik.dev/partytown/lib/*': '~partytown'}) eleventyConfig.addPassthroughCopy({'./node_modules/@qwik.dev/partytown/lib/debug/*': '~partytown/debug'}) Adding Third Party Script # For this tutorial, I am using google analytics script but any third party script can be executed with partytown. First, you need to add an inline partytown config script which will be above any third party scripts declared. A partytown config script specific to google analytics will be: <script> partytown = { forward: ['dataLayer.push'], }; </script> Add a type="text/partytown" attribute to all the third party scripts. By doing so, these scripts will be ignored by the main thread and executed on the web worker instead. <script type="text/partytown" src="<analytics url>"></script> // other third party inline scripts <script type="text/partytown"> // script </script> The order of scripts will be as follows: // partytown config script for google analytics <script> partytown = { forward: ['dataLayer.push'], }; </script> // partytown inline script <script> {% partytown %} </script> // third party scripts with "type='text/partytown'" <script type="text/partytown" src="<analytics url>"></script> <script type="text/partytown"> // script </script> Debugging # For debugging partytown, you can specify debug: true option in the partytown config script. partytown = { debug: true } Enable the verbose level in chrome dev tools' console and you will be able to see the partytown logs. Takeaway # Personally, I find the idea of using web workers to execute render blocking third party scripts interesting, partly because it improves performance and partly because it's simple. It does come with some trade-offs though, but so does every other technology. And not just 11ty, partytown can be integrated with almost any modern frontend framework or library. At the time of writing, partytown is still in beta. Further Reading 📃 Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker How Partytown's Sync Communication Works Atomics - Partytown Browser Support - Partytown --- ## Why I love Markdown - **URL**: https://syntackle.com/blog/why-i-love-markdown-Ib00ES/ - **Updated On**: December 8, 2023 - **Description**: I was introduced to it when I started using github for hosting my projects. That was my first encounter with markdown and since then, I never looked back. Here's why. - **Tags**: post, markdown, guide, opinion - **Author**: Murtuzaali Surti Markdown is one of those languages to which I was introduced when I started using github for hosting my projects. The famous README.md file got me into using markdown. That was my first encounter with markdown and since then, I never looked back. Here's why. TLDR - It's simple! And I love simplicity. A little bit about Markdown # Markdown is a markup language which is more easy and efficient to read and write formatted text rather than using HTML. It was created by John Gruber who is a well-known blogger. Markdown serves as an abstraction which allows you to focus on writing rich text and forget about opening and closing those HTML tags every now and then. It then, gets compiled down to regular HTML by a markdown processor so that the browser can understand it and display it on the screen. “Thus, 'Markdown' is two things: (1) a plain text formatting syntax; and (2) a software tool, written in Perl, that converts the plain text formatting to HTML.” — John Gruber Reasons why I like to use Markdown: 1. It's efficient # Previous to using markdown, I used WYSIWYG editors such as Google Docs and Microsoft Word which are sometimes an overkill when writing a simple blog post. I spent half of the time looking for tools to format the text, selecting and clicking over buttons with my mouse to get the job done. With markdown, things get incredibly easy. You just remember the syntax (and it's easy too) by practicing it a couple of times and write as well as format the text without breaking your flow. > This is a blockquote. The *text* is in italics and this **text** is bold. 2. Readable Syntax # The markdown syntax is easy to remember as well as easy to read when accompanied with text. The syntax never takes the soul away from the text. You don't need to worry about those opening and closing tags in HTML. It enriches the text just the right way. <!-- list in HTML --> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> <!-- list in markdown --> - Item 1 - Item 2 - Item 3 3. Implementation # You might have heard of different flavors of markdown. Flavors of markdown simply extend or customize the markdown syntax. Different tools supporting markdown may have a slightly different syntax and markdown processing methods. “To wrap your head around the concept of Markdown flavors, it might help to think of them as language dialects. People in New York City speak English just like the people in London, but there are substantial differences between the dialects used in both cities.” — markdownguide.org I like the idea of having different implementations because it opens the door for new ideas to emerge. But, it comes at a cost. There is no standard specification for markdown implementation and this is why some people prefer not to use markdown for certain purposes. Markdown is Not Perfect # After working with markdown for a good amount of time, I realised that it is extremely good for certain things but not good for certain things. Let's say you want to add a class to your paragraph element but using markdown. There is no way of doing so. What I did, and what most people do is that they add plain HTML into the markdown file. ## Heading 2 <p class="desc"> Description </p> Final words Markdown works really well if you use it to create some kind of written content in the form of blogs, notes, etc. Where it lacks is standardization. If you are using a specific tool to process your markdown, you can't switch to another tool instantaneously. You may need to modify your markdown syntax as per the requirement of the new tool. I think that's the biggest drawback for most of the people. The blog post you just read, is also written using markdown. --- ## What is DOM diffing? - **URL**: https://syntackle.com/blog/what-is-dom-diffing-bB8Ltf/ - **Updated On**: December 17, 2023 - **Description**: DOM, also known as the Document Object Model, is a programmatic representation of the contents of a web page. In other words, the content of a web page is represented in the form of objects and nodes. - **Tags**: post, html, dom, javascript, web, frontend - **Author**: Murtuzaali Surti DOM, also known as the Document Object Model, is a programmatic representation of the contents of a web page. In other words, the content of a web page is represented in the form of objects and nodes. But why is there a need to represent it in such format? It's required because it allows programming languages such as JavaScript to manipulate or modify the content of a web page. The DOM DOM acts as an API (Application Programming Interface) to modify the structure as well as the content of a web page. <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>DOM Tree</title> </head> <body> <h1 id="title">Heading</h1> <div class="contact"> <h2>Murtuzaali Surti</h2> <p id="github">@murtuzaalisurti</p> </div> </body> </html> Here's the visualization of the above DOM tree: htmlhead#textmeta#texttitle#text#text#textbody#texth1#title#text#textdiv.contact#texth2#text#textp#github#text#text#text Snippet taken from DOM Visualizer by @bioub These filled circles represent either a DOM element or a DOM node. DOM can be accessed using the Document API which exposes a global document object for you to manipulate the DOM using JavaScript. // test this in browser dev tools' console console.log(typeof document) // logs `object` Painting of DOM Whenever you update your HTML markup, the DOM gets updated and it needs to be repainted or re-rendered in order to reflect the changes on the screen. // process of browser rendering DOM tree => CSSOM tree => Render tree => Prepare Layout => Paint Screen These repaints can be expensive and lead to performance issues. Repainting can happen more often if the user is interacting with the website because of the site being highly interactive. Virtual DOM & Comparision of DOMs In order to tackle the issue of expensive rendering, some frontend libraries such as React, create a virtual DOM which is similar to the actual DOM but it's efficient because it doesn't have to be painted. The virtual DOM is created on every state change or re-render in React and is then compared to the previously created virtual DOM. This process of comparing consecutive instances of virtual DOM is known as DOM diffing. If the virtual DOM is different than the previous one, React will calculate which parts of the DOM have been modified and it will update only those parts in the actual DOM. If there's no change, the actual DOM will remain untouched. You can explore more about the diffing algorithm in react by visiting their documentation. Want to implement DOM diffing in vanilla JS? You can. --- ## Using Fontsource With 11ty - **URL**: https://syntackle.com/blog/using-fontsource-with-11ty-FUzgft/ - **Updated On**: December 8, 2023 - **Description**: For quite some time, I was searching for a way to self host google fonts because the google fonts API's network request increased the render blocking time more than I expected. - **Tags**: post, 11ty, fontsource, frontend, tutorial - **Author**: Murtuzaali Surti Dealing with fonts can get quite overwhelming. For quite some time, I was searching for a way to self host google fonts because the google fonts API's network request increased the render blocking time more than I expected. I stumbled upon fontsource.org the other day and I found the idea of installing fonts from npm packages appealing. At first, I used it in my personal site built with Astro. The process was incredibly simple. You just have to import fonts in your base layout file and that's it. You are ready to use it in your CSS! npm i @fontsource/poppins @fontsource/nunito import "@fontsource/poppins" import "@fontsource/nunito/400.css" I thought what if I could use this in my 11ty blog too. The next immediate thing which came to mind was to use vite because it can bundle those fonts for me just like Astro. So, I decided to use slinkity. You can call slinkity as an extension for 11ty projects. It adds more features on the table by post processing your 11ty build output. Slinkity uses vite for bundling which is great, but it became a problem for me. I have a relatively old and large project with some custom configurations which came in the way of how vite does things, so migrating to vite resulted in a mess. The migration needs more time. So, I decided to take another approach which might be not so good. Using Rollup # Rollup is a javascript bundler you can use to bundle your modules. npm install rollup --save-dev I created a rollup config file, named rollup.config.mjs at the root of the project. export default [ { input: "src/js/combine.js", output: { file: "src/js/minified/index.bundle.js", sourcemap: false, } } ]; The idea is to import the fonts just like I did in astro, in a javascript file and then extract css from it in order to copy it in the styles folder. So, inside the combine.js file, I imported some fonts. import "@fontsource/nunito"; import "@fontsource/poppins"; Now, to extract css, I used the postcss rollup plugin and the rollup copy plugin to copy the generated css file to the desired directory. I had to use rollup-plugin-copy because of this issue. Installed the following dependencies: npm i rollup-plugin-postcss rollup-plugin-copy @rollup/plugin-node-resolve --save-dev The postcss plugin will extract the styles from the javascript file into a new CSS file. import { nodeResolve } from "@rollup/plugin-node-resolve"; import postcss from "rollup-plugin-postcss"; export default [ { input: "src/js/combine.js", output: { file: "src/js/minified/index.bundle.js", sourcemap: false, } plugins: [ nodeResolve(), postcss({ extract: true, // no way to move output to another folder https://github.com/egoist/rollup-plugin-postcss/issues/250 minimize: true, }) ], } ]; The next step is to move the CSS file to the desired location because the extracted CSS file will be in the same directory as of the javascript file and you can't specify a location outside the parent folder due to a bug in the postcss plugin. import { nodeResolve } from "@rollup/plugin-node-resolve"; import postcss from "rollup-plugin-postcss"; import copy from "rollup-plugin-copy"; export default [ { input: "src/js/combine.js", output: { file: "src/js/minified/index.bundle.js", sourcemap: false, } plugins: [ nodeResolve(), postcss({ extract: true, // no way to move output to another folder https://github.com/egoist/rollup-plugin-postcss/issues/250 minimize: true, }), copy({ targets: [ { src: "src/js/minified/index.bundle.css", dest: "src/styles/minified", rename: "fonts.bundle.css", }, ], verbose: true, hook: "writeBundle", }) ], } ]; I am running the copy plugin after all the bundles are written and that's why you see the hook: "writeBundle" build hook specified for the copy plugin. Run rollup using this command: npx rollup --bundleConfigAsCjs -c Now, the font CSS is ready to use, right? No. There is still one problem. This is how the generated CSS looks: @font-face{font-display:swap;font-family:Nunito;font-style:normal;font-weight:400;src:url(files/nunito-cyrillic-ext-400-normal.woff2) format("woff2"),url(files/nunito-all-400-normal.woff) format("woff");unicode-range:u+0460-052f,u+1c80-1c88,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f}@font-face{font-display:swap;font-family:Nunito;font-style:normal;font-weight:400;src:url(files/nunito-cyrillic-400-normal.woff2) format("woff2"),url(files/nunito-all-400-normal.woff) format("woff");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116} If you look closely at the src url, you will find that the files which the url is referring to are not present in our directory. Yes, the actual font files. I used addPassthroughCopy() in 11ty to copy the required font files from node_modules to the output directory as per the url specified in the font stylesheet. eleventyConfig.addPassthroughCopy({'./node_modules/@fontsource/poppins/files/*.woff2': 'styles/files'}) Copy the required files to the output folder and the fonts are ready to be used. font-family: "Poppins", "Nunito", sans-serif; Conclusion Is this a good solution? I don't know. For me it's more of a hack than a solution and I think there's a better way. Maybe, all we need is a 11ty plugin :) --- ## Creating Git Hooks Using Husky - **URL**: https://syntackle.com/blog/creating-git-hooks-using-husky-y6LKpN/ - **Updated On**: February 8, 2025 - **Description**: They are used to verify everything is as expected before or after executing a git command or action. Some common applications include formatting the code before committing, performing build step before pushing the code to production, etc. - **Tags**: post, cli, git, husky, git-hooks - **Author**: Murtuzaali Surti Table of Contents Hooks in git are nothing but some code which can be executed at specific points during git execution process. They are used to verify everything is as expected before or after executing a git command or action. Some common applications include formatting the code before committing, performing build step before pushing the code to production, etc. You can create hooks in the .git/hooks directory but you can automate the process using husky! This article has been updated in accordance with the latest version (v9) of husky. Installing Husky npm install husky --save-dev Initializing Git Hooks npx husky init This will enable you to add git hooks to your project. One thing to note here is that when collaborating, contributors need to run this command after cloning the project to enable git hooks. But you can bypass this step by adding a prepare script in your package.json file. It will run when you do npm install in your project so you don't need to perform npx husky init manually. To do so, add the following script to package.json, "scripts": { "prepare": "husky" } But there's another catch. The prepare script will also run in production but you need it in production as such, so there are many ways to disable it in production, one of them is by using the is-ci npm package. The is-ci package will check if the code is executed in a continuous integration server or not. npm install is-ci --save-dev Just change the prepare script to the following. "scripts": { "prepare": "is-ci || husky" } Adding Git Hooks For example, if you want to format your code using a formatting tool before committing the code, you can add git hook to do that using the following command in unix based OS' such as Mac or Linux: echo "npm run format" > .husky/pre-commit Or, you can directly create a pre-commit file in .husky folder and add the command in it manually. Replace npm run format with the command which will format your code. You can replace pre-commit with some other hook such as pre-push, post-commit, post-checkout, etc. Another example could be, if you want to minify javascript before pushing to production, you can use pre-push git hook. echo "npm run minjs" > .husky/pre-push "scripts": { "minjs": "terser js/app.js --compress --mangle --output js/app.min.js" } Find the list of various git hooks on the official git site. You will see a .husky folder being created in your project and inside it there will be files for all the git hooks which you created. Make sure to run git add after you make any changes. Finally, run the git command or action and your git hooks will be executed. That's it. For more applications of git hooks, read this article. Signing off. --- ## Setting Background Color of Body Dynamically in React - **URL**: https://syntackle.com/blog/setting-background-color-of-body-dynamically-in-react-5tVYr3/ - **Updated On**: March 14, 2025 - **Description**: You can specify the background color of body in a global stylesheet, but it's not easy to update the background color dynamically for different pages in your website. So, I went on to code a hacky but working patch using CSS custom properties. - **Tags**: post, react, tutorial, frontend, css, web, html - **Author**: Murtuzaali Surti Table of Contents In a single page application, you only have one body element and although you can specify the background color of body in a global stylesheet, it's not easy to update the background color dynamically for different pages in your website. I encountered this issue and immediately googled some solutions but I wasn't satisfied with them. So, I went on to code a hacky but working patch using CSS custom properties. I don't know if it's a recommended practice or not, but let's have a look at it. RECOMMENDED Want to master React and get a deep understanding of it's fundamentals? This book by Robin Wieruch is a must read. CSS Custom Property Set a custom property in your :root or html element style which contains a default color value. Specify this styling in a global stylesheet, in your case it will probably be index.css. :root { --bodyColor: "#000000" } body { background-color: var(--bodyColor); } Function To Set Body Color Create a file named setBodyColor.js in the src directory which contains a function ready to be exported. The function is shown below: export default function setBodyColor({ color }) { document.documentElement.style.setProperty('--bodyColor', color) } In this way, you can modify the value of the css custom property --bodyColor. Using the function Import the function in a component using, import setBodyColor from './setBodyColor' Change the relative url ./setBodyColor as per your folder structure. Use it in your functional component, const HomePage = () => { setBodyColor({ color: "#ffffff" }) return ( <main> ... </main> ) } export default HomePage You can use this function in multiple components and pass a color to modify the background color of the body. CAUTION Note that you must call the function on every page or component to set the background color of the body. Otherwise, it will just take the value of the background color of the previous page. This workaround isn't limited to background-color property. You can set as many custom properties as you want. But, as I said earlier, I don't know if this is a foolproof technique, so the best thing you can do for your case is do your own research. Also, if you have any better solution, feel free to ping me on X (formerly Twitter). Signing off. --- ## Minify JavaScript Using Terser - **URL**: https://syntackle.com/blog/minify-javascript-using-terser-TUYCYJ5y/ - **Updated On**: December 17, 2023 - **Description**: Terser is a javascript compressor and mangler supporting ES6+ specification. In this tutorial, you will get to know how to use terser to minify or compress javascript. - **Tags**: post, javascript, terser, tutorial, frontend - **Author**: Murtuzaali Surti Terser is a javascript compressor and mangler supporting ES6+ specification. In this tutorial, you will get to know how to use terser to minify or compress javascript. Prerequisite: nodejs should be installed on your system. 1. Installing terser Install terser using yarn or npm. npm i terser -g 2. Creating a script Add the script to package.json file of your project. "scripts": { ... "minify": "terser src/js/app.js -c -m --output src/minified/app.min.js" ... } Replace src/js/app.js and src/minified/app.min.js with your input and output file path respectively. Here, -c flag stands for --compress and -m stands for --mangle. For more options, visit the official documentation of terser. 3. Executing the script Run the script using this command: npm run minify Your javascript file should now be compressed! Conclusion Apart from terser, you can also use uglify-js to compress or minify javascript. You can also try terser on the web. --- ## Deploying React App to Netlify - **URL**: https://syntackle.com/blog/deploying-react-app-to-netlify-XZ_dWXAd/ - **Updated On**: January 5, 2025 - **Description**: In this tutorial, I am going to show you how you can deploy a react app on netlify from an existing git repository of yours. - **Tags**: post, react, netlify, deployment, tutorial, backend - **Author**: Murtuzaali Surti A react app is a single page application which means that there's only one document i.e. index.html file which is updated using javascript as per the requirement of the user. Let's see you how you can deploy a react app on netlify from an existing git repository of yours. RECOMMENDED Want to master React and get a deep understanding of it's fundamentals? This book by Robin Wieruch is a must read. Tutorial # Create a netlify.toml file at the root of your react application and add the following rule to it. [[redirects]] from = "/*" to = "/index.html" status = 200 The above rule is applicable if you have multiple routes and have used libraries like react-router in your project. What we are doing here is that we are telling netlify to redirect all the routes to our index.html file with a status code of 200 because our application is built with react and it is a single page application. We already discussed what an SPA is at the very beginning of this tutorial. Recommended ✨ Builder.io's Partytown with 11ty Set up an account on netlify if you haven't already. Go to the sites tab and add a new site. Select import an existing project from the dropdown menu. You will see a number of providers from which you can import your project. Importing an existing project Authenticate and select a repository. Modify the default deployment configuration as per your requirement. Deployment Config If required, you can also add env variables. Finally, click on deploy site and your site should be deployed. Here's a blog post on how you can deploy an express app to vercel! --- ## Adding Custom Anchors to Headings in Markdown - Eleventy - **URL**: https://syntackle.com/blog/adding-custom-anchors-to-headings-in-markdown-eleventy-3NxBhIJO2OIr4XOj5LKc/ - **Updated On**: December 28, 2025 - **Description**: Anchors are nothing but id attributes applied to an element to link to it using href attribute internally on the same page. - **Tags**: post, 11ty, markdown, frontend, tutorial - **Author**: Murtuzaali Surti Anchors are nothing but id attributes applied to an element to link to it using href attribute internally on the same page. By default, 11ty uses markdown-it library to parse markdown. But, it seems that by default, markdown-it doesn't support syntax for applying an id to a header element. The syntax for applying a custom id to a header element in markdown is as follows: ## text {#id} To make that syntax work, you need to create your own instance of markdown-it library and add two plugins, markdown-it-anchor and markdown-it-attrs. The markdown-it-anchor plugin will apply a default id depending on the heading text automatically to every header element in our markup. The markdown-it-attrs plugin will replace the default id with the custom id you specify. Applying Custom Anchors # Install the required dependencies: npm i markdown-it markdown-it-anchor markdown-it-attrs Require them inside .eleventy.js file, const markdownIt = require('markdown-it'); const markdownItAnchor = require('markdown-it-anchor'); const markdownItAttrs = require('markdown-it-attrs'); Create an instance of the markdown-it library inside the module.exports function. eleventyConfig.setLibrary("md", markdownIt().use(markdownItAnchor).use(markdownItAttrs)) You can also add some options such as: let markdownItOptions = { html: true // you can include HTML tags } let markdownItAnchorOptions = { level: 2 // minimum level header -- anchors will only be applied to h2 level headers and below but not h1 } eleventyConfig.setLibrary("md", markdownIt(markdownItOptions).use(markdownItAnchor, markdownItAnchorOptions).use(markdownItAttrs)) Now, if you do something like: ## Heading 1 {#head1} In this case, head1 will be the id of this header element. You can link to this element by using #head1 as the href value. That's how you can add custom anchors to heading elements. Applying Default Anchors # If you don't want a custom id and just want to keep the default id that's being applied to the element by the markdown-it-anchor plugin, then remove the markdown-it-attrs plugin and you will get a default anchor applied to the element. eleventyConfig.setLibrary("md", markdownIt(markdownItOptions).use(markdownItAnchor, markdownItAnchorOptions)) That's all for now. Signing Off. --- ## How to vendor prefix and minify CSS? - **URL**: https://syntackle.com/blog/how-to-vendor-prefix-and-minify-css-XfWdM2Vxl7G9LZAGkRwG/ - **Updated On**: December 17, 2023 - **Description**: Writing CSS from scratch along with adding vendor prefixes can be a daunting task if done manually. Vendor-prefixes can be easily added using the autoprefixer plugin of PostCSS. - **Tags**: post, css, postcss, tutorial, frontend - **Author**: Murtuzaali Surti Writing CSS from scratch along with adding vendor prefixes can be a daunting task if done manually. Vendor prefixes can be easily added using the autoprefixer plugin of PostCSS. Also, the size of CSS file matters because CSS files can be render blocking so you need to keep a check on the size of the CSS file. This is where cssnano comes handy. In this tutorial, you will be able to configure and set up PostCSS, autoprefixer and cssnano to vendor prefix and minify your CSS respectively. Prerequisites # Install nodejs Installing PostCSS # It is a tool to modify and transform your CSS. There are so many plugins for PostCSS to perform all kinds of tasks ranging from compressing CSS to using new CSS features right off the bat. We will be using several postcss plugins to add vendor prefixes and minify CSS. But first, we need to install postcss-cli. npm i -g postcss-cli The postcss package can be installed from npm. npm i postcss postcss-cli --save-dev Auto-prefixing CSS # The autoprefixer plugin uses caniuse.com to search for browser support and accordingly add vendor prefixes to CSS properties. You can install autoprefixer from npm. npm i autoprefixer --save-dev Now, you can autoprefix your CSS file using this command: postcss src/styles/*.css -u autoprefixer --dir src/prefixed --no-map The above command will parse each and every .css file inside the styles directory and use -u autoprefixer to auto-prefix files and output them in the prefixed directory. The --no-map argument is optional. If you want a source map to be generated, then remove the --no-map argument. CSS file before prefixing: * { margin: 0; box-sizing: border-box; text-size-adjust: auto; } After prefixing: * { margin: 0; box-sizing: border-box; -webkit-text-size-adjust: auto; -moz-text-size-adjust: auto; text-size-adjust: auto; } Minifying CSS # The cssnano plugin can minify/compress CSS and make it suitable for production use. Install cssnano plugin using this command: npm i cssnano --save-dev Minify your already vendor-prefixed CSS file using this command: postcss src/prefixed/*.css -u cssnano --dir src/minified --no-map It will minify all the files present in the prefixed directory and output them to the minified directory. The final result you get is: *{-webkit-text-size-adjust:auto;-moz-text-size-adjust:auto;text-size-adjust:auto;box-sizing:border-box;margin:0} Recommended ✨ Minify Javascript Using Terser Bonus You can auto-prefix as well as minify CSS using one single command: postcss src/styles/*.css -u autoprefixer cssnano --dir src/styles/production --no-map Conclusion Vendor-prefixing and minifying are some of the important boxes you need to check in order to make your CSS production ready. Using PostCSS, you can do so much with your CSS. There are a variety of awesome plugins available to use in postcss. --- ## Eleventy - Shortcode for Embedding Codepen - **URL**: https://syntackle.com/blog/eleventy-shortcode-for-embedding-codepen-ZyslIPzCHpJo3kkPwu2U/ - **Updated On**: December 8, 2023 - **Description**: Shortcodes are used to invoke a particular function which returns some html or any other data based on the information that is passed. They are mainly used to reuse html templates which require some preprocessing. - **Tags**: post, 11ty, codepen, frontend - **Author**: Murtuzaali Surti Don't know what eleventy is? Before you read further, check out this amazing series of articles by Tatiana Mac to know more about eleventy and static site generators in general. Shortcodes are used to invoke a particular function which returns some html or any other data based on the information that is passed. They are mainly used to reuse html templates which require some preprocessing. TLDR - Jump to tutorial While writing blogs, I came across the need to embed codepens in my articles for a quick code demo. Initially, I used to copy and paste the embed code provided by the codepen website. That's not at all feasible. It wasn't the case that I didn't knew about shortcodes at all because I used them on DEV, but I wasn't sure if something like that existed in eleventy too. So, I buckled up and went on to explore shortcodes in eleventy! Tutorial # As per 11ty's official documentation, the default templating engine for markdown files is liquid, so here I have used a liquid shortcode as an example. You can create a shortcode for other templating engines also. Inside .eleventy.js file, write the following code in module.exports function: eleventyConfig.addLiquidShortcode("codepen", function (url) { const url_array = url.split("/"); const profile_url_array = url_array.filter((string, index) => { return (index < (url_array.length - 2)) ? true : false }) const username = profile_url_array[profile_url_array.length - 1]; const user_profile = profile_url_array.join("/"); const data_slug_hash = url_array[url_array.length - 1]; return `<p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="${data_slug_hash}" data-user="${username}" style="height: 571px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"><span><a href="${url}">See the pen</a> (<a href="${user_profile}">@${username}</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>`; }); The shortcode 👇 {% codepen 'url' %} Place the above shortcode where you want your codepen to be embedded. Pass a codepen url to the shortcode function. Split the url using / as a separator. You will get an array from which you can filter out the slug after the profile url. Convert the filtered array into a string using join(""). What you get is the profile url of a codepen user. Similarly, you can extract the username as well as the codepen id. Copy the embed code from codepen and edit it to make it dynamic. The function returns the embed code and it is embedded inside your html template. Signing off. --- ## 5 Most Useful Visual Studio Code Extensions - **URL**: https://syntackle.com/blog/5-vscode-extensions-you-must-use-JTRFImxbBtrq6btizBCJ/ - **Updated On**: December 28, 2025 - **Description**: Here are the 5 most useful Visual Studio Code extensions to improve your workflow: Error Lens, CSS Peek, GitLens, Import Cost, Version Lens. - **Tags**: post, vscode, extensions, listicle, setup - **Author**: Murtuzaali Surti Here are the five most useful Visual Studio Code extensions to improve your workflow as well as developer productivity! Table of Contents 1. Error Lens # Error Lens is an extension which allows you to see error, warning and diagnostic messages in line with your code without having to hover or click on anything! It also highlights the line with different colors to help you better visualize and differentiate between errors, warnings and other messages! 2. CSS Peek # With this extension, you can search for relevant CSS files by using CSS classes in html file and edit CSS files there itself! Here's how it works: RECOMMENDED Are you a beginner trying to get a gist of CSS and HTML? I personally started my web dev journey with this book by John Duckett and it is one of the best books which will help you explore the world of CSS and HTML. 3. GitLens # GitLens is a powerful extension which can display when a particular line was committed, who committed it and in which pull request it was included along with the git commit message! All of this information is inline with the line upon which your cursor is currently located. RECOMMENDED Pro Git is a great book for developing a solid understanding of Git and perhaps mastering it. And it's free. You can also hover over the message to get even more information about a particular line of code! There are many more features in GitLens which you can explore and play around with! 4. Import Cost # Import Cost allows us to see the size of the imported package inline in the editor itself! 5. Version Lens # Version Lens fetches information about dependency versions and displays it on top of the package! These were the 5 VSCode extensions which are powerful, useful and can improve your productivity and workflow! Signing off. Twitter | GitHub | LinkedIn This post was originally published in dev.to. --- ## Deploy an Express.js App to Vercel — A Step By Step Guide - **URL**: https://syntackle.com/blog/how-to-create-and-deploy-an-express-js-app-to-vercel-ljgvGrsCH7ioHsAxuw3G/ - **Updated On**: May 19, 2025 - **Description**: Vercel is a platform to host frontend applications and static sites but you can also host an Express application using serverless functions. In this tutorial, I will show how you can create an express.js app from scratch and deploy it to vercel. - **Tags**: post, nodejs, vercel, deployment, express, tutorial, backend, hosting - **Author**: Murtuzaali Surti Vercel is a platform to host frontend applications and static sites but you can also host an Express application using serverless functions. You can also use Vercel Postgres which allows you to integrate hosted serverless PostgreSQL database such as neon.tech (Neon got acquired by Databricks) accessible by serverless functions. In this tutorial, I will show you how to create an Express app from scratch and deploy it to vercel. This article has been updated with latest vercel configurations. Prerequisites # Node.js should be installed on your system. To check if it is, run node -v in your terminal and you should get a node version as an output. Creating an Express App # Run npm init -y to create a package.json file with default configuration. Run git init to initialize a git repository. Create a .gitignore file and write the folder name node_modules in it. You can add as many files and folders you prefer to be ignored by git. Install the express package using npm or yarn. npm i express Create an api folder (in the root folder) and add index.js file in it. Adding an "api" folder is necessary because Vercel treats all index.js files in that folder as serverless functions. So, for example, if you create api/test/index.js, when you hit <BASE_URL>/test it will execute the code inside api/test/index.js as a serverless function. It is crucial for deploying the express app to Vercel. Update package.json file to explicitly set the entry file. { "name": "test", "version": "1.0.0", "description": "", "main": "api/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.21.2" } } Inside the api/index.js file, add the following code in order to create an express app. // cjs const express = require('express'); const app = express(); app.listen(process.env.PORT || 3000); Now, add a GET request handler and send a response. // api/index.js app.get('/', (req, res) => { res.send("Express App Responded"); }) Export the app for it to be run as a serverless function. // api/index.js module.exports = app Add a start script to package.json file in order to run the application locally. { "name": "test", "version": "1.0.0", "description": "", "main": "api/index.js", "scripts": { "script": "node api/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.21.2" } } Run the application using the command npm start The application should be live at http://localhost:3000. Recommended ✨ Sharing Localhost From VS Code - Port Forwarding You should get an output similar to the following: Yayy! You just created an Express.js Application. 🎉 Deploying to Vercel # Create a vercel.json file in the root folder of your application. This is a configuration file for Vercel. Add the following to your vercel.json file, so that every request is routed to /api endpoint because that where the express app as a serverless function lives. { "rewrites": [ { "source": "/(.*)", "destination": "/api" } ] } Create a git repository on GitHub and add your code to it. Create a new project on Vercel and import the git repository that you just made. Go to the project Settings > Build and Deployment > Framework Settings and override the default output directory with the root directory .. The reason of doing so is to tell Vercel to look for production ready files here. If you intend to use typescript, then the output directory will be the directory in which your compiled files are stored. Deploy the application. Your application should now be live! 🎉 Here's the live demo of the application. You can also find the source code on my express-to-vercel github repository! --- ## Optional Chaining in JavaScript - **URL**: https://syntackle.com/blog/optional-chaining-in-javascript-D6SuXGtu0-K5hhtZUqqc/ - **Updated On**: December 17, 2023 - **Description**: Optional Chaining in JavaScript is used to return undefined for accessing an object property that doesn't exist and whose parent property is nullish (null or undefined). - **Tags**: post, javascript, guide - **Author**: Murtuzaali Surti Optional Chaining in JavaScript is used to return undefined for accessing an object property that doesn't exist and whose parent property is nullish (null or undefined). If you are unaware that property exists or not and you want to avoid the error that's being thrown, you may want to use optional chaining to get around with it. In this article, you’ll learn how and when to use Optional Chaining. You’ll also learn when not to use Optional Chaining in JavaScript. How it Works # First, let's explore what can go wrong when accessing a property in a nested object. let person = { name: "Murtuza", work: () => { return "Software Developer" }, socials: { github: { username: "murtuzaalisurti", link: "https://github.com/murtuzaalisurti", proUser: { is: 'no' } }, linkedin: { username: "murtuzaali-surti", link: "https://linkedin.com/in/murtuzaali-surti" }, twitter: { username: "murtuza_surti", link: "https://twitter.com/murtuza_surti" } } } In the above object, let's try to access the property link nested within the property website. Can we? console.log(person.website.link); //an error will be thrown We get an error, Cannot read property 'link' of undefined The property website doesn't exist in the object! But, let's add the property website to the root object and set the value of it as null. website: null Let's check if this works, console.log(person.website.link); //an error will be thrown We get a similar error, Cannot read property 'link' of null As mentioned in the above definition, we can use optional chaining to handle these types of errors! Here's how we can do that. Syntax // website: property to validate // link: property to access website?.link The operator ?. will check if the property on its left-hand side is null or undefined and if that's the case, then it will simply return undefined without throwing any errors. In other words, this is also known as short-circuiting. Otherwise, it will return the value of the property on its right-hand side. Not just that, you can also invoke a function if it exists using optional chaining. person.work?.(args) Also, you can access properties using [] brackets. person.socials.github?.["username"] What You Can’t Do # You cannot apply optional chaining to the objects that are not declared yet. For example: object?.prop // object is not defined We haven't declared object, thus it will throw an error. You cannot assign a value to this expression. In other words, the optional chaining expression can't be on the left-hand side. The below code is not valid. person.socials.github?.["username"] = "name" // not valid When to Use Optional Chaining? # It's important to note that optional chaining should not be used when it's not necessary to do so. Only use optional chaining when you know that the property that you want to access is optional and not mandatory. For example, in our object person, we can keep the social media platforms optional, so we are not sure if a user has a social media account or not on a particular platform. For that, we can use optional chaining to check if a user has a social media account on a particular platform and if it exists, get the username. person.socials.github?.["username"] But, if we place the optional chaining operator at the root object, then it doesn't make any sense because the root object i.e. person must exist and if it doesn't exist, we should get an error! Conclusion In this article, you learned what Optional Chaining in JavaScript is, how it works when to use it, and when not to use it. To learn more about how Optional Chaining works, make sure to check out the MDN documentation on it for more details. --- ## How to compile SASS/SCSS into CSS and watch for changes? - **URL**: https://syntackle.com/blog/how-to-compile-sass-into-css-and-watch-for-changes-5MHo7HhHUHUedZXaP62y/ - **Updated On**: February 8, 2025 - **Description**: SASS/SCSS extends CSS which means that you can have all the features of CSS plus the features of SASS just like a cherry on top of a cake! - **Tags**: post, css, scss, cli, frontend, tutorial - **Author**: Murtuzaali Surti Table of Contents SASS or SCSS extends CSS which means that you can have all the features of CSS plus the features of SASS just like a cherry on top of a cake! Browsers don't understand SASS, just like they don't understand JSX (which is compiled into valid javascript) in React. Which is why we need to compile SASS into normal CSS in order to run it in the browser. Let's see how we can transform a SASS file into a CSS file assuming that you have SASS already installed into your system. RECOMMENDED Are you a beginner trying to get a gist of CSS and HTML? I personally started my web dev journey with this book by John Duckett and it is one of the best books which will help you explore the world of CSS and HTML. Compiling a SASS/SCSS file # Let's say we have a folder structure something like this: public/ -> styles/ src/ -> styles/ -> style.scss We need to compile the file with .scss extension which is ultimately a SASS file, into a CSS file in public/styles directory. We can do that by executing this command in the terminal at the root of the folder: sass src/styles:public/styles The directory before the colon : is the input directory and the directory after the colon is the output directory. If you have your SASS file in the root directory and want to output the CSS file in the same root directory, you can use: sass style.scss style.css Here, 'root directory' means the directory you are currently pointing to in your terminal. But what if we want to place the CSS file in the same directory as the SASS file? You can do that by executing the above command only: sass src/styles:src/styles Now, let's get to the juicy part. Let's see how we can watch the changes made to our SCSS file and automatically compile them into CSS. Watching for changes # Just add a --watch flag in the command to watch the changes made to the Sass file in src/styles directory. sass --watch src/styles:public/styles That's it. Now, the changes you make in the SASS file will automatically be compiled into CSS as soon as you save the SASS file! Also, if you are using VSCode, then you can use an extension to compile SASS files into CSS which makes it very convenient to deal with SASS, but that too has it's pros and cons! Signing off. --- ## IIFE in JavaScript - **URL**: https://syntackle.com/blog/iife-in-javascript-XC6A0qHlZMh8Gts_MaDg/ - **Updated On**: December 17, 2023 - **Description**: You might be familiar with functions in JavaScript. An IIFE is a special type of function which is invoked implicitly. - **Tags**: post, javascript, iife, guide - **Author**: Murtuzaali Surti You might be familiar with functions in JavaScript. An IIFE (Immediately Invoked Function Expression) is a special type of function which is invoked implicitly. Even if you don't invoke it in your code, it will run on it's own. You can relate it with a callback function which is automatically called when a particular event is fired. (function () { console.log('IIFE') })() You can also define an arrow function as an IIFE. (() => { console.log('IIFE') })() You might be wondering, well what's the use of this type of function? This same thing can be done like this: function func(){ console.log('IIFE') } func() Well, here's the catch. When we define a global variable in JavaScript, it can be accessed from anywhere in our code. For example, var b = 10 function print(){ b = 8 console.log(b) // output: 8 } print() console.log(b) // output: 8 In the above code, the value of the variable b is modified from 10 to 8. But what if we don't want to modify the value of the global variable b. What can we do? One thing we can do is to initialize a new variable b inside the scope of the function print() just like this: var b = 10 function print(){ let b = 8 console.log(b) // output: 8 } print() console.log(b) // output: 10 We were able to create a new variable inside the scope of the function without actually modifying the actual global variable. But, there's another way! Let's say you don't want to reuse the function print() and also you don't want to create a mess with global variables unintentionally. In that case, you can use an IIFE. Here's how you can do that : var a = 8; ((a) => { a = 9; // modifying the copy of 'a' console.log(a); // output: 9 })(a) // passing a copy of variable as an argument console.log(a) // output: 8 In the above example, we are passing the value of variable a to the IIFE. Remember that we are only passing a copy of the variable, so we are not actually modifying the global variable. This is most useful when you are dealing with multiple files which are being imported and exported in your project and you don't know the name of every global variable defined. An IIFE is a function which does it's own thing without affecting things on a global level. You can also make an IIFE asynchronous. (async () => { const get = await fetch(url) // do something })() That was a brief look at IIFE. I hope you got some idea of what's IIFE and how we can use it. If you want to dig deeper, check this out. Signing off. --- ## How to make a QR Code generator using JavaScript? - **URL**: https://syntackle.com/blog/how-to-make-a-qr-code-generator-using-javascript-5-rbxEeFYAidZ_zVp2Lh/ - **Updated On**: December 17, 2023 - **Description**: While you can generate QR codes for URLs in browsers such as Chrome, it's always interesting to learn how you can make your own version of a simple QR code generator. So, here we go. - **Tags**: post, qrcode, javascript, tutorial - **Author**: Murtuzaali Surti While you can generate QR codes for URLs in browsers such as Chrome, it's always interesting to learn how you can make your own version of a simple QR code generator. So, here we go. HTML Here's a quick look at the HTML code and it's pretty straightforward. <section class="heading"> <div class="title">QRcodes</div> <div class="sub-title">Generate QRCode for anything!</div> </section> <section class="user-input"> <label for="input_text">Type something...</label> <input type="text" name="input_text" id="input_text" autocomplete="off"> <button class="button" type="submit">Generate QR Code</button> </section> <div class="qr-code" style="display: none;"></div> <script src="./js/app.js"></script> The last element is for the QR code to be displayed as soon as we fetch it from a library through javascript (more on that later). Let's move on to some javascript. JavaScript First of all, we will create an event for when the user clicks on the Generate QR code button. let btn = document.querySelector(".button"); btn.addEventListener("click", () => { //code }) Now, we are going to create a function known as generate() which will be invoked as soon as the user clicks on the Generate QR code button. This function will take the text input from the user as a parameter. function generate(user_input) { //code } Inside this function, we are going to use a javascript library qrcode.js to generate QR code. You can use this library via a CDN by including the below <script> tag in the <head> tag of html. <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> Inside the generate() function, we will create a new object using the given library. It will take two arguments, first is the element in which the QR code has to be displayed and secondly, the content for which the QR code has to be generated and some options to customize the QR code. function generate(user_input) { var qrcode = new QRCode(document.querySelector(".qr-code"), { text: `${user_input.value}`, width: 180, //default 128 height: 180, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.H }); } Next, we will create a download button and append it below the QR code. let download = document.createElement("button"); document.querySelector(".qr-code").appendChild(download); Inside this download button we will add a link which allows users to download the QR code with a specified file name and append it into the download button. You can learn more about the download attribute here. let download_link = document.createElement("a"); download_link.setAttribute("download", "qr_code_linq.png"); download_link.innerText = "Download"; download.appendChild(download_link); Let's figure out the href attribute of the <a> tag next. The qrcode object will return a canvas element as well as an image element. For smartphones, the canvas element will be visible but for desktop, the image element will be visible having a src attribute set to a dataURL. We will use the dataURL to download the QR code. In the case of desktop, it's pretty obvious. We just have to grab the value of src attribute of the image element and assign it to the href attribute of the download link (<a> tag) after a specified amount of time (0.3 seconds) using setTimeout() function because the QR code takes some time to be generated. let qr_code_img = document.querySelector(".qr-code img"); setTimeout(() => { download_link.setAttribute("href", `${qr_code_img.getAttribute("src")}`); }, 300); But how do we get the dataURL from the canvas element? By using the method toDataURL() on the canvas element. let qr_code_canvas = document.querySelector("canvas"); setTimeout(() => { download_link.setAttribute("href", `${qr_code_canvas.toDataURL()}`); }, 300); After applying some logic, we get this: if(qr_code_img.getAttribute("src") == null){ setTimeout(() => { download_link.setAttribute("href", `${qr_code_canvas.toDataURL()}`); }, 300); } else { setTimeout(() => { download_link.setAttribute("href", `${qr_code_img.getAttribute("src")}`); }, 300); } Also, the .qr-code element will be hidden until the user clicks on the Generate QR code button. With this, our generate() function is all set to be invoked. function generate(user_input){ document.querySelector(".qr-code").style = ""; var qrcode = new QRCode(document.querySelector(".qr-code"), { text: `${user_input.value}`, width: 180, //128 height: 180, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.H }); console.log(qrcode); let download = document.createElement("button"); document.querySelector(".qr-code").appendChild(download); let download_link = document.createElement("a"); download_link.setAttribute("download", "qr_code_linq.png"); download_link.innerText = "Download"; download.appendChild(download_link); if(document.querySelector(".qr-code img").getAttribute("src") == null){ setTimeout(() => { download_link.setAttribute("href", `${document.querySelector("canvas").toDataURL()}`); }, 300); } else { setTimeout(() => { download_link.setAttribute("href", `${document.querySelector(".qr-code img").getAttribute("src")}`); }, 300); } } Now inside our click event function, we will check if there is already a QR code displayed or not. If it is, then we will first clear that QR code and generate a new one. If it's not present, we can simply generate a new one. Also, all of this happens only if the user enters some text or if the input value is not empty. btn.addEventListener("click", () => { let user_input = document.querySelector("#input_text"); if(user_input.value != "") { if(document.querySelector(".qr-code").childElementCount == 0){ generate(user_input); } else{ document.querySelector(".qr-code").innerHTML = ""; generate(user_input); } } else { document.querySelector(".qr-code").style = "display: none"; console.log("not valid input"); } }) You can style the elements the way as you want. Here are the styles that I went for: :root{ font-size: 62.5%; } *{ margin: 0; padding: 0; box-sizing: border-box; text-size-adjust: none; -webkit-text-size-adjust: none; } button:hover{ cursor: pointer; } body{ display: flex; flex-direction: column; align-items: center; background-color: #EAE6E5; } .heading{ margin: 3rem 0 5rem 0; } .title, .sub-title{ font-size: 4rem; text-align: center; font-family: 'Poppins', sans-serif; color: #12130F; } .sub-title{ font-size: 1.5rem; color: #8F8073; } .user-input{ display: flex; flex-direction: column; align-items: center; width: 100%; } .user-input label{ text-align: center; font-size: 1.5rem; font-family: 'Poppins', sans-serif; } .user-input input{ width: 80%; max-width: 35rem; font-family: 'Poppins', sans-serif; outline: none; border: none; border-radius: 0.5rem; background-color: #9b8774ad; text-align: center; padding: 0.7rem 1rem; margin: 1rem 1rem 2rem 1rem; } .button{ outline: none; border: none; border-radius: 0.5rem; padding: 0.7rem 1rem; margin-bottom: 3rem; background-color: #5b92799d; color: #12130F; font-family: 'Poppins', sans-serif; } .qr-code{ border-top: 0.5rem solid #8F8073; border-right: 0.5rem solid #8F8073; border-bottom: 1rem solid #8F8073; border-radius: 0.5rem; border-bottom-left-radius: 0.5rem; border-bottom-right-radius: 0.5rem; border-left: 0.5rem solid #8F8073; background-color: #8F8073; } .qr-code button{ display: flex; justify-content: center; background-color: #8F8073; font-family: 'Poppins', sans-serif; color: #EAE6E5; border: none; outline: none; width: 100%; height: 100%; margin-top: 1rem; } .qr-code button a{ width: 100%; height: 100%; text-decoration: none; color: #EAE6E5; } Here is a demo of the entire project: See the pen (@seekertruth) on CodePen. Here's the github repository for this project. That's all for now. I am on Twitter as well as GitHub. This post was originally published in dev.to. --- ## Skeleton Loading for Social Media Embeds using CSS and JavaScript 🔥 - **URL**: https://syntackle.com/blog/skeleton-loading-for-social-media-embeds-using-css-and-javascript-SrqqO--X_JMn2wVKyzAroLOs03/ - **Updated On**: December 17, 2023 - **Description**: Social media embeds take some time to load and render, hence the user experience is not so good! Here's an example of twitter embeds.. - **Tags**: post, css, javascript, tutorial, frontend - **Author**: Murtuzaali Surti Note: This post is inspired by Web Dev Simplified. Social media embeds take some time to load and render, hence the user experience is not so good! Here's an example of twitter embeds: Without applying skeleton loading: After applying skeleton loading: As you might have noticed, the user experience without skeleton loading is not so good! So, let's see how can we implement skeleton loading on twitter embeds! Embedding Tweets <div class="tweets"> //tweets </div> Here, we have created a container which will contain all our twitter embeds. <div class="tweets"> <div class="tweet"> //tweet 1 (paste the twitter embed code here without the script tag) </div> <div class="tweet"> //tweet 2 (paste the twitter embed code here without the script tag) </div> . . . </div> Paste the embed code of your tweet as shown above. Here's how you can get the embed code: Go to your tweet Click on the more menu Select the 'Embed Tweet' option You will be redirected to a new tab and you can copy the embed code from there itself. Note that you don't need to add multiple script tags for different tweets. You can add just one script tag at the bottom of the body element. //add this just before the </body> tag. <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> Now that you have done that, it's time to style the embeds using CSS! Styling the embeds using CSS You can do that by applying Flexbox properties to the container just like this! .tweets{ display: flex; flex-flow: row wrap; width: 100%; justify-content: center; padding: 0 3rem; } You can also customize the width of the embed! But note that the tweet embed can only shrink upto a certain limit. If you go beyond that threshold, the embed will overflow, so keep that in mind. .tweet{ width: 30rem; margin: 0 1.5rem; } Now, it's time to create a skeleton for these tweets! Creating Skeleton for Embeds <div class="tweets-skeleton"> <div class="tweet-skeleton"> <div class="img"></div> <div class="content-1"> <div class="line"></div> <div class="line"></div> <div class="line"></div> </div> <div class="content-2"> <div class="line"></div> <div class="line"></div> </div> </div> </div> Next, let's style this skeleton using CSS. .tweets, .tweets-skeleton{ display: flex; flex-flow: row wrap; width: 100%; justify-content: center; padding: 0 3rem; } .tweet, .tweet-skeleton{ width: 30rem; margin: 0 1.5rem; } .tweet-skeleton{ border: 0.05rem solid rgb(190, 190, 190); border-radius: 1rem; height: 30rem; margin-bottom: 2rem; padding: 1.5rem; } .tweet-skeleton .img{ height: 5rem; width: 5rem; border-radius: 50%; background-color: rgb(209, 209, 209); } .tweet-skeleton .content-1, .tweet-skeleton .content-2{ height: 25%; margin-top: 1rem; } .tweet-skeleton .line{ height: 15%; margin: 0.5rem 0; width: 100%; border-radius: 0.3rem; background-color: rgb(209, 209, 209); } .tweet-skeleton .line:last-child{ width: 75%; } Your tweet skeleton should look something like this: Let's animate this skeleton to make it look like something is loading in the background! We will do that by using the concept of 'keyframes' in CSS and animating the background color of the lines of text as well as the image! @keyframes tweet-skeleton { 0%{ background-color: rgb(209, 209, 209); } 100%{ background-color: rgb(243, 243, 243); } } And then, we will define the animation properties for the same. .tweet-skeleton .img{ height: 5rem; width: 5rem; border-radius: 50%; background-color: rgb(209, 209, 209); animation: tweet-skeleton 1s linear infinite alternate; } .tweet-skeleton .line{ height: 15%; margin: 0.5rem 0; width: 100%; border-radius: 0.3rem; background-color: rgb(209, 209, 209); animation: tweet-skeleton 1s linear infinite alternate; } Here's the output: As Kyle Cook wonderfully explains in his video, here's how you can create multiple skeleton templates based on your requirement using JavaScript! const tweets_skeleton = document.querySelector(".tweets-skeleton"); const tweet_skeleton = document.querySelector(".tweet-skeleton"); for (let i = 0; i < 5; i++) { tweets_skeleton.append(tweet_skeleton.cloneNode(true)); } Here comes the fun part! How to show the skeleton while the tweet embed is rendering? We are going to do that by using the setTimeout function in JavaScript. The idea is to hide the tweet embeds for a certain time until they are rendered as iframes and showing the skeleton instead. After the specified time, the skeleton will hide itself and the tweet embeds will be shown. This is certainly not the best way to do this. Another approach is to detect the network speed of the client and accordingly decide the timing. But to make things simple, we are going to use the setTimeout function which will be executed after 4 seconds. Add these styles to the tweets container. <div class="tweets" style="visibility: hidden; display: none;"> setTimeout(() => { document.querySelector(".tweets").style = "visibility: hidden;"; tweets_skeleton.style = "display: none;"; }, 4000); If there are large number of tweets, the loading time may increase. Here's the final output: See the pen (@seekertruth) on CodePen. That's all for now! I'm on twitter as murtuza_surti. This post was originally published in dev.to. --- ## How to create Google's Material Design Text Input Field using CSS and JavaScript? - **URL**: https://syntackle.com/blog/how-to-create-google-s-material-design-text-input-field-using-css-and-javascript-b0WGutb2uKPHdeSFNq2X/ - **Updated On**: October 1, 2023 - **Description**: In this tutorial, we are trying to recreate Google's text input field animation and design from scratch with the help of CSS as well as JavaScript. - **Tags**: post, css, javascript, tutorial, frontend - **Author**: Murtuzaali Surti In this tutorial, we are trying to recreate Google's text input field animation and design from scratch with the help of CSS as well as JavaScript. HTML We are not going to use pseudo-elements to create this effect, but we will be taking help of div element instead. We are wrapping the input element and its related divs inside a container. To create a placeholder, we have defined a separate div which will act as a placeholder rather than using the :placeholder pseudo-element. <div class="input-contain"> <input type="text" id="fname" name="fname" autocomplete="off" value="" aria-labelledby="placeholder-fname"> <label class="placeholder-text" for="fname" id="placeholder-fname"> <div class="text">First Name</div> </label> </div> CSS First of all, let's define the properties for the input element and it's container. .input-contain{ position: relative; } input{ height: 5rem; width: 40rem; border: 2px solid black; border-radius: 1rem; } We will be placing the placeholder text on top of the input element by setting the position of the placeholder text to absolute so that it matches the width and height of the input container. .placeholder-text{ position: absolute; top: 0; bottom: 0; left: 0; right: 0; border: 3px solid transparent; background-color: transparent; display: flex; align-items: center; } But there's an issue. You can't click on the input element because the placeholder element is on the top of the input element. In order to overcome this situation, just set the value of pointer-events to none for the placeholder element. .placeholder-text{ pointer-events: none; } Now, let's style the placeholder text a little bit. .text{ font-size: 1.4rem; padding: 0 0.5rem; background-color: transparent; color: black; } input, .placeholder-text{ font-size: 1.4rem; padding: 0 1.2rem; } Next up, let's define what should happen when the input element is focused. We are going to change the border-color rather than keeping the outline on focus event. input:focus{ outline: none; border-color: blueviolet; } We want the placeholder text to translate along Y-axis(to go up) and reduce it's font-size a little bit when the input element is focused. Also, we can change the color of the placeholder text. Here's how we can do that. Change the background-color to resemble the surrounding color to make it more elegant. input:focus + .placeholder-text .text{ background-color: white; font-size: 1.1rem; color: black; transform: translate(0, -170%); border-color: blueviolet; color: blueviolet; } For a smooth transition, add transition property to the placeholder text. .text{ transform: translate(0); transition: transform 0.15s ease-out, font-size 0.15s ease-out, background-color 0.2s ease-out, color 0.15s ease-out; } Up until now everything is fine, but now a problem arises. When you enter text in the input element and then remove the focus from the input element, the placeholder text comes to it's original position and we don't want that. We want the placeholder text to remain above the input text when something is already entered in the input field. Hence, we will be taking the help of JavaScript and we will modify CSS. If you remember, we have already defined value attribute for the input element. This will come handy. <input type="text" id="fname" name="fname" autocomplete="off" value="" aria-labelledby="placeholder-fname"> Let's modify some CSS. As already discussed, when the value is an empty string, the placeholder text should come back to its original position, but when the value is other than an empty string, the placeholder text should remain transformed(above the input text). We can achieve that by defining a :not pseudo-class on the input element value. Here's how we can do that. input:focus + .placeholder-text .text, :not(input[value=""]) + .placeholder-text .text{ background-color: white; font-size: 1.1rem; color: black; transform: translate(0, -170%); } input:focus + .placeholder-text .text{ border-color: blueviolet; color: blueviolet; } But wait. The value attribute will remain the same in HTML. How can we change and set it to the string entered by the user? That's where JavaScript comes into action. We will set the value of the value attribute to the string entered by the user just like this, let input_element = document.querySelector("input"); input_element.addEventListener("keyup", () => { input_element.setAttribute("value", input_element.value); }) That's it. You just made a modern material design text input field. Here's the final output: See the pen (@seekertruth) on CodePen. This post was originally published in dev.to. --- ## How to create an HTML generator with JavaScript? - **URL**: https://syntackle.com/blog/how-to-create-an-html-generator-with-javascript-TOku-OzmLp7AQXieYkwr/ - **Updated On**: December 17, 2023 - **Description**: Ever tired of writing multiple lines of similar HTML? If you are, then you can automate the process by using template literals in JavaScript. Let's see how we can do that. - **Tags**: post, javascript, html, tutorial, frontend - **Author**: Murtuzaali Surti Before you proceed:- This post is not about creating a safe or the best HTML generator rather it's just something for fun that you can try by using template literals in JavaScript. It was a fun experiment for me. Ever tired of writing multiple lines of similar HTML? If you are, then you can automate the process by using template literals in JavaScript. Let's see how we can do that. Let's say you have multiple boxes which are actually hyperlinks and you want to create multiple of them. One way is to just copy and paste the HTML code and make changes to a particular section of the code. This approach can work for small projects but if your project is big enough, then it can become a mess. Alternatively, you can create your own HTML generator using template literals in JavaScript which will generate HTML code for you! Template Literals in JavaScript # Template literals in JavaScript are nothing but string literals which allow you to embed various expressions into the string. They are enclosed in backticks. For embedding an expression the syntax goes like this, let string = `first part of the string ${expression} second part of the string`; Now, let's create the HTML generator. Create respective input fields for link URL, Title & a Tag. You can add your own input fields also if you want to. <div id="contains"> <label for="title" class="title">Title</label> <input type="text" id="title" name="title"> <label for="url" class="url">URL</label> <input type="url" id="url" name="url"> <label for="tag" class="tag">Tag</label> <input type="text" id="tag" name="tag"> <button id="submit">Generate</button> </div> Next, create a textarea field in which the resultant code will be displayed as well as create a button to copy the code to the clipboard. <div class="result"> <textarea class="result_text" type="text" rows="5"></textarea> <button class="copy_btn"><i class="fas fa-clipboard"></i></button> </div> JavaScript We will create a function named generate(). This function has three parameters — title, url and tag. It will take in the value of the title, the url, and the tag that we have input in the field as arguments. function generate(title, url, tag){ //code } Further, we will use template literals and we will embed the title, the url & the tag into the string. Then, set the value of the result field to the string that is generated. let title = document.querySelector("#title"); let url = document.querySelector("#url"); let tag = document.querySelector("#tag"); let result = document.querySelector(".result_text"); function generate(title, url, tag){ let final_string = `<a href="${url}"><div class="link"><div class="banner">${tag}</div>${title}</div></a>`; result.value = final_string; } All of this will take place after the user clicks the generate button and so let's add an eventListener to it. let submit_btn = document.querySelector("#submit"); submit_btn.addEventListener("click", () => { generate(title.value, url.value, tag.value); title.value = ""; url.value = ""; tag.value = ""; }); In order to copy the code from the textarea, we can define a function called copy() and then call the function when the user clicks on the 'copy to clipboard' button. let copy_btn = document.querySelector(".copy_btn"); copy_btn.addEventListener("click", () => { copy(); }) function copy(){ result.select(); document.execCommand("copy"); } Here's a quick demo: See the pen (@seekertruth) on CodePen. Now, you can copy the code into your main project. This is just one of the use cases of template literals. You can do a lot of things by using template literals in JavaScript. They make your life as a JavaScript developer pretty easy. Signing off. This post was originally published in dev.to. --- ## Dark mode toggle animation using CSS! - **URL**: https://syntackle.com/blog/dark-mode-toggle-animation-using-css-wWkNc1nowuQVdhhYjVOC/ - **Updated On**: December 17, 2023 - **Description**: This tutorial will mainly focus on how to use transitions in CSS and make a toggle button for light as well as dark mode using little JavaScript. Let's dive into the world of transitions! - **Tags**: post, css, dark-mode, frontend - **Author**: Murtuzaali Surti This tutorial will mainly focus on how to use transitions in CSS and make a toggle button for light as well as dark mode using little JavaScript. Let's dive into the world of transitions! HTML HTML Markup is pretty simple to understand. All you have to do is to make a container for the icons that we are going to use from fontawesome and nest the respective divs containing the icons inside the container. <div class="container"> <div class="sun sun-logo"> <i class="fas fa-sun"></i> </div> <div class="moon moon-logo"> <i class="fas fa-moon"></i> </div> </div> CSS .container{ position: relative; } .sun, .moon{ font-size: 10rem; width: fit-content; height: fit-content; } .moon{ position: absolute; inset: 0; } Set the container position to be relative and the moon container position to be absolute because we will position the moon icon in the same position as that of the sun icon. Here's the interesting part. Instead of using top: 0; bottom: 0; left: 0; and right: 0; you can use inset: 0; to get the same result. It works! Also, set the height and width of the sun and the moon container to fit-content. What this will do is, it will set the height and width of the container to match the height and width of the content inside it. And, in order to change the size of the fontawesome icon, just change the font-size of the icon. .moon-logo{ opacity: 0; transform: translateY(20%) rotateZ(50deg); } Next, we will set up the initial position of the moon icon and its initial opacity when the webpage is rendered for the first time. Here, as the opacity of the moon icon is zero, only the sun icon will be visible to us. The translateY(20%) declaration will offset the moon icon down along the Y-axis by 20% of the height of it's parent element. Similarly, the rotateZ(50deg) declaration will rotate the moon icon along the Z-axis by 50 degrees. .sun-logo{ opacity: 1; transform: translateY(0) rotateZ(0deg); } In the same way, we will set the initial properties for the sun icon. .animate-sun{ opacity: 0; transform: translateY(20%) rotateZ(100deg); color: aliceblue; } Now, we will set the final properties of the sun icon to which it will transition into. .animate-moon{ opacity: 1; transform: translateY(0%) rotateZ(0deg); color: aliceblue; } Also, we will set the final properties of the moon icon to which it will transition into. One thing to note here is the default color of the icons is black, so if you want to change the color of the icon, then define its color property. But wait, we haven't used the transition property yet, so how will it transition from one state to another? Yeah, that's the only thing left to do in CSS part. .moon-logo{ opacity: 0; transform: translateY(20%) rotateZ(50deg); transition: all 1s ease-out; } .sun-logo{ opacity: 1; transform: translateY(0) rotateZ(0deg); transition: all 1s ease-out; } body{ transition: background-color 1s; } .dark{ background-color: black; } We will use the above class to change the background-color of the body when the transition of the icons will happen. That's it. Your CSS part is ready. Now, let's move on to the JavaScript part. We will use JavaScript to toggle the classes on click event. JavaScript document.querySelector(".container").addEventListener("click", () => { document.querySelector(".sun-logo").classList.toggle("animate-sun"); document.querySelector(".moon-logo").classList.toggle("animate-moon"); document.querySelector("body").classList.toggle("dark"); }) Here, we have added an eventListener to the container element so that when we click on the container, it will toggle the CSS classes for respective elements. Which means that, if the CSS class is not present in the classList of an element, toggle function will add the CSS class to the classList of the respective element. And, if the CSS class is already present in the classList of the element, it will remove it. The classList is actually a DOMTokenList but we will not go into the specifics of it. This is it. Here's the final output. See the pen (@seekertruth) on CodePen. This post was originally published in dev.to. --- ## How to create a notification badge with CSS? - **URL**: https://syntackle.com/blog/how-to-create-a-notification-badge-with-css-WrkjgwxkL5bw-mFwMFjl/ - **Updated On**: December 17, 2023 - **Description**: Notification badges annoy me most of the times by popping up every now and then and I am pretty sure most of you experience similar thing, but anyways, let's see how we can create a notification badge using CSS. - **Tags**: post, css, tutorial, frontend, html - **Author**: Murtuzaali Surti Notification badges annoy me most of the times by popping up every now and then and I am pretty sure most of you experience similar thing, but anyways, let's see how we can create a notification badge using CSS. Step 1: HTML <div class="base"> <div class="indicator"> <div class="noti_count" role="status">1</div> </div> </div> The element with a class 'base' will act as a profile image or an icon upon which we will position the notification indicator element having a class 'indicator'. Step 2: CSS .base { height: 100px; width: 100px; border: 1px solid transparent; border-radius: 50%; position: relative; } First of all, we have to set up the height and width of the main 'base' element. Then we set the `border-radius`` property to 50%. Border radius rounds the edges of the border by a specified amount. In our case the height and width of the element are equal and so, when we apply border radius of 50%, a square looking element will transform to a circle. After applying a background color, the base element will look like a circle. Now, set the position of the base element to 'relative' which means that it will be positioned relative to its current position. This will not change anything but we want this property to position the child elements, more on that in a second. You can also add an image instead of a background color to the base element, just like this. .base { height: 100px; width: 100px; border: 1px solid transparent; border-radius: 50%; background-image: url("../assets/unsplash.jpg"); background-size: cover; background-position: right; position: relative; } Now, let's design the indicator. First of all, set the position of the indicator as 'absolute' which means that it will be positioned inside the ancestor element which has its position as 'relative'. Then, we will define the final location of the indicator by setting the values of top, bottom, right and left properties of the indicator. .indicator { position: absolute; top: 0%; right: 0%; left: 60%; bottom: 60%; background-color: brown; } The 'bottom' property will offset the 'indicator' element by 60% of the height of the 'base' element from the bottom of the 'base' element. Similarly, the 'left' property will offset the 'indicator' element by 60% of the width of the 'base' element from the left of the 'base' element. Next, we will add a border having the color same as the 'body' element having a border-radius of 50%. .indicator { position: absolute; top: 0%; right: 0%; left: 60%; bottom: 60%; background-color: brown; border: 3px solid rgb(51, 51, 51); border-radius: 50%; } Then, we will style the notification counter. .noti_count { font-family: 'Montserrat', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; color: aliceblue; font-weight: 700; } In order to center the notification count number, we can add 'flex' properties to its parent element. .indicator { position: absolute; top: 0%; right: 0%; left: 60%; bottom: 60%; background-color: brown; border: 3px solid rgb(51, 51, 51); border-radius: 50%; display: flex; justify-content: center; align-items: center; } The final output will be: https://amzn.to/3XM6pC9 https://amzn.to/49B9ufJ https://amzn.to/4po0j6J https://amzn.to/4ogZAU9 This post was originally published in dev.to. --- ## Archive - [All Posts](https://syntackle.com/archive) - Complete list of all published articles ## RSS Feed - [RSS Feed](https://syntackle.com/feed.xml): Subscribe to latest blog posts via RSS ## Sitemap - [Sitemap](https://syntackle.com/sitemap.xml): Complete site structure for crawlers ## Other Pages - [Contact](https://syntackle.com/contact/): Get in touch with the author - [Write For Syntackle - Guest Writer Program](https://syntackle.com/write/): Submit your article for consideration - [Authors](https://syntackle.com/authors/): List of authors who have written for the blog - [Advertise with Syntackle](https://syntackle.com/sponsor/): Advertise your product or service on the blog - [Privacy Policy](https://syntackle.com/assets/PrivacyPolicy.pdf): Privacy policy for the blog - [Terms of Service](https://syntackle.com/assets/TermsOfService.pdf): Terms of service for the blog