# Lin Hugo — Full Site Content Source: https://1chooo.com --- ## About URL: https://1chooo.com # Lin Hugo I [build software and write](/bio) about it. Interning at [Hayden AI](https://www.hayden.ai) and studying Computer Science at [USC Viterbi](https://viterbischool.usc.edu/). By day, I design and architect scalable software systems, focusing on high-quality, large-scale software systems. On weekend, I'm a [photographer](/episodes) capturing the human touch in fleeting moments, using light and lines to construct my perspective. You can gain further insights into my background and interests through my [thoughts](/thoughts), [projects](/projects), or [code](https://github.com/1chooo). Let's [connect](https://www.linkedin.com/in/1chooo) if you'd love to collaborate or chat. {/* */} --- ## Bio URL: https://1chooo.com/bio # My Life in Five Minutes The philosophy I live by: > You are too focused on the future without realizing that today is exactly what you prayed for years ago. --- I'm Lin Hugo — a software builder currently pursuing a Master of Science in Computer Science at the [USC Viterbi School of Engineering](https://viterbischool.usc.edu/). My interests sit at the intersection of web development and large-scale software systems, where I care deeply about quality, reliability, and efficiency. I grew up studying Atmospheric Sciences at [National Central University](https://www.ncu.edu.tw) in Taiwan, where I picked up a minor specialty in Programming Design along the way. The detour from weather systems to software turned out to be less of a detour and more of a natural path — both fields are fundamentally about modeling complex systems and making sense of them. I graduated in June 2024. During those four years I kept one foot in the classroom and one foot in industry. I served as a teaching assistant for five courses — from Freshman English to Programming Python and Weather & AI — supporting over 250 students. Outside coursework, I managed a year-long project to build an orientation website for incoming freshmen; our team shipped a Blog feature that drew 2,000+ visitors per page. ## Work My most recent experience is as a Software Engineer Intern at [Hayden AI](https://www.hayden.ai) in the San Francisco Bay Area for Summer 2026. Excited to network with you all! Prior to that, in early 2025, I joined [Futurenest](https://futurenest.io) as an R&D Engineer Intern. There, I shipped Django REST APIs with 100% test coverage, containerized services through AWS ECR, and integrated a Next.js chat interface with a custom backend. In early 2024, I interned at [eCloudvalley](https://www.ecloudvalley.com) as a Cloud Engineer, leading a five-person agile team to deliver a serverless AWS Bedrock solution. We built a multi-modal ticket classification system that achieved 95% accuracy and improved operational efficiency by 80% through automation. The project was ultimately recognized as the top-performing internship project across all cohorts. My industry journey began in the summer of 2023 at [PEGATRON Corporation](https://www.pegatron.com), where I worked as an AI Engineer Intern on the Smart Manufacturing team. I developed a Large Language Model proof-of-concept that I presented directly to the CTO, and the team received a Silver Award for the project. During the same summer, I also served as an [AWS Campus Ambassador](https://aws.amazon.com/education/awseducate/campus-ambassadors/), organizing cloud computing workshops that reached more than 700 developers and achieved a 96% satisfaction rate. ## Beyond code I shoot film and digital photography whenever I can — mostly street and travel, capturing the human touch in fleeting moments through light and lines. The discipline of a single frame taught me to pause: golden light in a quiet café, the space between strangers on a sidewalk. Beauty is neither rare nor hidden; it surrounds us constantly. What is missing is our willingness to notice and feel it — an idea I explored in [The Seen](/thoughts/the-seen), shaped by a Rodin quote I first encountered in high school. More of my photography lives in [episodes](/episodes). On random weekends I visit art museums — MoMA, local galleries, wherever I can slow down and be present with the work. I love the silence they bring: the blank space around a painting creates room to think, feel, and respond on my own terms, without rushing to decode every label. Art is not what you see; it's what you feel. I also make pictures with code — procedural textures, noise fields, and visual experiments where a seed number becomes something that looks like it has been sitting in the rain for thirty years. No brushes, no cameras, just math. I keep those experiments in [Art](/thoughts/art). In a world that moves fast, beauty is how I slow down — and I carry that same patience and attention to variation within constraint into writing and engineering. If any of this resonates, feel free to [reach out](https://www.linkedin.com/in/1chooo). --- ## Projects URL: https://1chooo.com/projects # Projects ## [](/ '1chooo.com') I built [1chooo.com](/ '1chooo.com') as my own brand—a personal site crafted with Next.js v16 (App Router), Tailwind CSS v4, and Motion for a seamless, minimal experience. It leverages MDX (Rust), Cloudflare R2, and Supabase for high-performance content delivery. ## [](https://justhold.1chooo.com 'JustHold') I provide [JustHold](https://justhold.1chooo.com) as a platform for investors to think in decades, not days—independent market research with curated macro, earnings, and strategy coverage, wrapped in a clean, minimal interface for long-term thinkers. ## [](https://tawny.1chooo.com 'Tawny') I've crafted a lot of websites and components—that's why I built [Tawny](https://tawny.1chooo.com) as a curated collection of handcrafted web designs and reusable UI components—premium templates ready to ship, free to explore. ## [](https://github.com/powlaw 'PowLaw') I created [PowLaw](https://github.com/powlaw) in 2025, a service to help human beings discovering the power law of self-growth. Small, consistent actions compound into extraordinary results over time. ## [](https://github.com/1chooo/portfolio 'VCard') I developed [VCard](https://github.com/1chooo/portfolio), a popular Open Source portfolio template (300+ stars) built with Next.js and Supabase, optimized for 99+ Lighthouse performance and developer experience. --- My other mini projects (find all of them on [GitHub](https://github.com/1chooo)): - [](https://github.com/1chooo/art 'Art') - [](https://github.com/1chooo/Link 'Hugo Lin Link') - [](https://github.com/1chooo/tools 'Lin Hugo Tools') - [](https://github.com/1chooo/vibe-networking 'NetTrace') - [](/notes/traceroute 'Traceroute') - [](https://github.com/1chooo/image-compression-exif 'Image Compression EXIF') - [](https://github.com/1chooo/anon-chat 'AnonChat') --- ## Art URL: https://1chooo.com/thoughts/art Date: 2026.06.07 Description: Code-generated art pieces — procedural textures, noise fields, and visual experiments made with algorithms instead of brushes. # Art I write code that makes pictures. No brushes, no cameras — just math that turns a seed number into something that looks like it has been sitting in the rain for thirty years. This is where I keep those experiments. ## Concrete Texture Concrete is one of the few building materials that refuses to be uniform. Every pour is different: the aggregate settles unevenly, air pockets leave pores, formwork grain prints itself into the surface, and weathering finds its own path. The result is a material that is strictly industrial but visually rich — all variation within a narrow palette of greys. The generator below recreates that texture procedurally, using seeded Perlin noise and fractal Brownian motion to build up the same layers a real concrete pour would leave behind: base tone, pores, aggregate stones, cracks, and weathering. Five variants, each with a different character: - **brutalist** — coarse, heavy, lots of visible porosity and deep staining; the kind of surface you see on a brutalist housing block - **polished** — compressed and smooth, aggregate barely visible, the grey almost luminous - **weathered** — streaked with long stains and erosion, as though it has been outside for decades - **board formed** — horizontal grain impressed from timber formwork, the defining mark of mid-century concrete construction - **exposed** — dense with visible stones and pebbles set into the surface, every piece a different size Hit **↻ randomize** to change the seed and watch the same character express itself in a new configuration. The algorithm is deterministic: the same seed always produces the same image. --- ## AI URL: https://1chooo.com/thoughts/ai Date: 2026.05.23 Description: A reflection on how AI, while boosting productivity, quietly erodes deep learning and independent thinking—and why the solution isn't to reject it, but to engage with it more intentionally and grow into higher-level thinking. # AI Right now, I feel like the whole world is surrounded by AI. Everyone is talking about AI—whether it's influencers, founders, investors, or even normal people. There's this story that with AI, we can make money, startups can get funding, companies can gain market share, and governments can enhance their efficiency. It seems like with AI, we can access information faster and more easily than before. We can even earn more money. However, I don't think so. It seems like we can get information more efficiently with AI — something that has already summarized the context and hands us complete notes. The appeal is obvious: I can dump all my course materials into a chat window and walk away with a ready-made lecturer — one that already knows the key points, simplifies the hard parts, and tells me exactly what to study before an exam. The worse: there are even many influencers who feel like cheaters—they use AI to summarize information, reduce the cost of creating posts on social media, and present it as "fully organized notes" or something similar, without even processing the information through their own brains. With so much AI-generated information in my daily life, I start to question what is actually being learned. But why do I feel like I never actually learn anything new? I still have so many things needed to learn. A lot of the information feels vague—it's like I've heard it before, but I can't explain it clearly. I still have a lot of homework before the deadline. With AI, it feels like I can finish things faster than before, but why does it still take me so long—and I end up gaining less knowledge than before? I am not learning anything new. AI has deprived me of my ability to truly learn new things and to process information deeply. Take a look at one of my recent commits: Who else on Earth can map out an entirely new product requirement and implement it flawlessly **in under ten minutes?** Nobody used to be able to do that. Yet today, that is the standard we are told to simply accept. Am I really learning anything new here? No. I am just using AI to generate the thoughts through code for me. Previously, if I wanted to build a large-scale feature like this, I would have had to spend a significant amount of time reading documentation, understanding the codebase, searching for related resources in the open-source community, and then writing the code by hand. Now, I can just let AI generate it for me. With AI, I seemingly save a lot of time building large-scale features, but in reality, I am not learning anything new. I am merely using AI to generate information. Worse yet, I am losing my sense of oversight because I find myself blindly clicking the "accept" button without even reading the code, turning AI's output into an addictive shortcut. This concern isn't unique to me. [Lee Robinson](https://leerob.com)—a developer educator, formerly at Vercel and now at Cursor—recently warned that tons of AI-generated code is becoming a liability, and that engineers still need to be responsible for the code that gets deployed, regardless of who—or what—wrote it. What stood out to me was the idea that AI can dramatically increase output without guaranteeing understanding. The bottleneck is no longer producing code or content—it is developing the judgment necessary to evaluate, direct, and improve what AI generates. If we outsource all of our thinking to AI, we may become more productive while simultaneously becoming less capable. This has become a normal daily occurrence for many of us. Faced with this disappointment, I started to question whether this must be the working style of the future. As Viktor Frankl once wrote, *"When we are no longer able to change a situation, we are challenged to change ourselves."* This era of AI is exactly that kind of situation—how we grow within it is the only thing left in our control. Therefore, what I do now is after AI delivers a result, I try to ask it more questions, treating it like a real lecturer. I ask *why* it chose a specific approach. Fortunately, the silver lining is that I have a lot of raw, hands-on experience from the past. Since this code uses Next.js and React, I am incredibly familiar with how it works. My extensive hands-on coding in 2024 gave me the technical foundation I need to actually supervise and guide these AI tools today. This foundational experience means I can push myself to a more senior level. I now have a fleet of junior developers (AI agents) helping me build code, which allows me to inject my own critical thoughts and practical experience to make the system more efficient and scalable. This is exactly what I want to achieve in this era. It is no longer just about gaining hands-on programming experience, but about developing soft skills and leadership. Because more and more manual development will be replaced by AI agents, we must shift our focus to higher-level architecture and strategy. To summarize, AI can absolutely deprive us of our ability to learn new things and process information deeply. However, it also presents a rare opportunity to overhaul our workflow, elevate our skills to a higher level, and consciously decide what we truly want to build. --- ## AI Agent URL: https://1chooo.com/thoughts/ai-agent Date: 2026.03.07 Description: I delivered software with an estimated $1M engineering cost in just two days, and it only cost me $7.14 for the entire GitHub Copilot usage! # AI Agent > AI agents today are incredible!!! > > I delivered software with an estimated $1M engineering cost in just two days, and it only cost me $7.14 for the entire GitHub Copilot usage! Sloc, Cloc and Code (scc) is a fast code counter with complexity analysis and COCOMO cost estimation written in Go. I used scc to measure the size of the generated codebase and estimate the equivalent engineering cost, which is where the ~$1M software value figure comes from. } /> I built this website—[1chooo.com](/)—from scratch in just two days (though I did borrow the design from [shud.in](https://shud.in)). Not long ago, I spent over a year teaching myself the nuances of the React ecosystem just to make it as a large scale software builder. This week, using GitHub Copilot and the Claude Sonnet 4.6 model, I built a more advanced version of my vision in just two days. Guess how much it cost? **ONLY $7.14 for the entire GitHub Copilot usage!** My 300+ stars GitHub repo where I crafted a personal portfolio—vCard with React, TypeScript, and Tailwind CSS took me over a year to build and refine. } /> Building websites has become much more fun. Ideas that used to take days or weeks to deliver can now quickly turn into production-ready features. At this point, I'd say more than 80% of the code was generated by the AI agent. The remaining 20% was written by me—mostly code review, bug fixes, and small refinements. It honestly feels like a new era of building. ## What amazes me In the past, integrating libraries and tools into a project often required a large amount of manual effort. I had to carefully read documentation, resolve compatibility issues, and gradually stitch everything together. It was a slow and sometimes tedious process. Today, AI agents can handle much of this work automatically. Instead of integrating each dependency step by step, I can simply provide high-level instructions (prompts) and let the agent orchestrate the implementation. Tasks that once required hours of manual setup can now be completed in minutes. This becomes particularly valuable when working with UI component libraries such as **Shadcn**. When starting a new project with a standardized component library, things are usually straightforward. However, integrating such libraries into an existing codebase is often difficult. Legacy code tends to accumulate different patterns over time—components written in different styles, inconsistent structures, and duplicated implementations of similar functionality. Historically, cleaning this up required extensive refactoring and decoupling. Developers would need to manually update components, align patterns, and gradually migrate the codebase to a consistent design system. > With AI, the process becomes significantly easier. Instead of manually rewriting everything, I can provide the AI with a **reference component** that represents the desired style or architecture. The AI can then apply that pattern across the existing implementation, automatically adapting old components to match the new standard. Even better, these guidelines can be formalized in files such as `.github/copilot-instructions.md`. By embedding style conventions and architectural rules there, the AI can reference them whenever it generates code. This ensures that newly generated components follow the same design patterns and styling rules as the rest of the project. The result is a much more consistent interface with far less manual adjustment. For example, on my blog, this approach allows me to maintain a consistent visual style across different pages while quickly integrating small interactive **toy examples** alongside written explanations. This not only saves development time but also creates richer and more engaging experiences for readers. Another advantage is iteration speed. When the AI generates styles that are slightly off due to prompt differences, I can quickly provide a reference component and ask it to align with that design. Instead of manually tweaking dozens of CSS classes, the AI can regenerate the implementation in seconds. This fundamentally changes how frontend refactoring works. ## Pegatron This shift reminds me of the AI agent I built during my internship as AI Engineer at **Pegatron in the summer of 2023**. At that time, ChatGPT had just been released, and the industry was still exploring how to unlock its potential. I was already experimenting with building AI agents to integrate into existing workflows. Back then, the limitations were obvious. A single API call could take around **three seconds**, and once the agent began reasoning about which tools to use, even a simple prompt could take **over a minute** to produce a response. Under those constraints, building a practical AI-assisted workflow felt slow and sometimes frustrating. It was difficult to imagine how seamless the experience could eventually become. Looking at today's tooling, the difference is remarkable. Latency has decreased dramatically, model capabilities have improved, and the ecosystem around AI-assisted development has matured. What once felt experimental now feels like a natural part of the development process. ## Next Steps After this practical experience, I'm excited to explore how AI agents can become an integral part of my development workflow. For example, I've been integrating many of my previous toy example projects directly into my current website (see [`/notes`](/notes)). This process allows me to experiment with creating a more interactive reading experience for users, where explanations are paired with small runnable examples. At the same time, it gives me valuable experience integrating experimental prototypes into a real production codebase. > I think this distinction is important. If we only rely on "vibe coding" and keep building isolated toy examples, it may feel productive in the short term. But for someone like me who wants to go further in software development, that approach alone isn't enough. Real-world software requires much more than simply making something work. There are many important considerations in software engineering: **code quality, maintainability, scalability, security, performance, and proper decoupling**. These principles have always been central to how I approach my projects, and I don't want them to be overlooked in the era of AI-assisted development. Instead, what I'm interested in is something deeper: exploring how to collaborate with AI agents **while still preserving these core engineering principles**. Rather than replacing thoughtful engineering practices, I want AI agents to become tools that help reinforce them—assisting with refactoring, enforcing conventions, improving consistency, and accelerating development without sacrificing long-term quality. > In other words, the goal isn't just to build faster. > > It's to **build better systems, with AI as a collaborator rather than a shortcut**. If you want to see exactly how I structured the AI agent conventions for this project, I've published them as [Cursor Agent Skills](/skills) — browse, read, and copy any skill directly into your own project. --- ## The Seen URL: https://1chooo.com/thoughts/the-seen Date: 2026.01.02 Description: Art Is Not What You See—It's What You Feel! # The Seen > In short, Beauty is everywhere. It is not that she is lacking to our eye, but our eyes which fail to perceive her. > > — Auguste Rodin (1840 - 1917) Auguste Rodin on Life, Love, and the Soul of Art } /> I first encountered this quote from the French sculptor Auguste Rodin during my junior year of high school. At the time, the idea struck me deeply, though I didn't truly understand it. Its meaning only began to unfold later, as I became more attentive to everyday life. I started seeing it everywhere: the golden light pouring into a quiet café, the visible devotion between a pet and its owner, or the eyes of parents looking at their child with love. In these simple, ordinary moments, I finally began to feel what Rodin meant. Beauty is neither rare nor hidden; it surrounds us constantly. What is missing is not beauty itself, but our willingness to pause, to notice, and to feel. Beauty is everywhere—we just have to open our eyes and our hearts to see it. ## Beauty When I noticed how much beauty exists in my daily life, I became eager to **dive into the art world more deeply.** I wanted to immerse myself more in the atmosphere of Art, so I started visiting art museums on random weekends. I actually **love the silence** that Art brings to me. I can feel the beauty of the pieces and the atmosphere of the museum even if I don't quite understand the full meaning or history of the work. But what I know is this: with Art, I feel more like myself, and more aware of the world around me. I feel more alive and more connected to the world. ## Art Museum The art museum is a special place for me. It is a sanctuary where I can slow down and be present with the artwork. The quiet halls and the careful curation create an atmosphere that encourages contemplation and connection. The **blank space** around the paintings allows me to focus more fully on the artwork itself, without distraction. It creates a sense of openness that invites me to engage with the piece on a deeper level. The emptiness doesn't feel empty at all. Instead, it creates space within me — space to think, to feel, and to respond in my own way. The Starry Night, Vincent van Gogh }> When I look at the descriptions next to the artworks, I realize that they are designed with aesthetics in mind. The titles are larger, and there is ample margin around the text, creating a sense of space and allowing the reader to focus on the content. The paragraphs are well-organized, making it easier for the reader to follow along and understand the artist's intent. It's interesting how the layout of the text itself can enhance the reading experience, much like how code is formatted to improve readability. Both are designed with the reader in mind, aiming to facilitate understanding and connection with the content. 523 Portraits in Wartime }> ## AI World In this fast-paced world, where AI is rapidly advancing and everything seems to be accelerating, beauty is a medium that allows me to slow down. It requires time and effort to truly feel it, and I need to create a flow state where I won't be interrupted in order to fully experience it. That's why I want to surround myself with beautiful things in my life, and even create more beauty, because that gives me more opportunities to feel it. Just like with my website 1chooo.com, I want to create a space where people can slow down and feel beauty, a place where beauty can be experienced rather than just seen. This is, I think, what Rodin was pointing to. Beauty doesn't announce itself. It requires proximity — the willingness to stop, to be still, to look long enough for the painting to begin looking back at you. > Art Is Not What You See — It's What You Feel. --- ## Power Law URL: https://1chooo.com/thoughts/power-law Date: 2025.08.25 Description: How exponential compounding turns small, consistent gains into extraordinary wealth — and why starting early is the only real secret. # Power Law Most people think about money the way they think about arithmetic — linearly. Save $100 a month for 10 years and you have $12,000. But money does not follow arithmetic. Left to compound, it follows an exponential curve, and exponential curves are almost unimaginably steep once enough time passes. The underlying principle is a *power law*: repeated multiplication by a constant factor. The concept appears everywhere in nature — earthquake magnitudes, city populations, the frequency of words in a language. In finance, it takes a form you can personally exploit: **compound interest**. ## 1 % a Day, Every Day Start with the simplest possible question: *what happens if you improve by 1 % each day for a full year?* If growth were linear, 1 % a day over 365 days would give you a 365 % return — you would end up with times what you started with. Decent, but nothing special. Compounding works differently. Each day you multiply, not add: Starting from , you end with nearly $38 — almost ten times what the linear version promises. Now run the arithmetic in reverse: what if you slip back by 1 % every day? You are left with less than 3 cents on the dollar. The gap between 1 % better and 1 % worse is not 2 percentage points — it is the difference between **37.78×** and **0.026×**, a ratio of nearly 1 500 to 1. Drag the slider or click a preset to change the daily rate. The solid blue line is compounded growth; the dashed grey line is the equivalent simple (linear) growth. They start the same but diverge sharply — this divergence is the entire story. }> Self-improvement and portfolio returns are not literally 1 % a day. But the intuition transfers directly: **small, consistent gains, reinvested, compound into a curve that logic alone cannot picture**. ## The Formula The engine behind all of this is one equation: | Symbol | Meaning | |---|---| | | Final amount | | | Principal (money you put in at the start) | | | Annual interest rate (as a decimal, e.g. 0.07 for 7 %) | | | Compounding periods per year (12 for monthly) | | | Time in years | At 7 % annual return compounded monthly, $10,000 grows to roughly $20,097 in 10 years — without adding a single cent. The **doubling time** for any rate can be estimated with the Rule of 72: divide 72 by the annual rate to get the approximate years to double. At 7 % that is years. }> ```ts const A = P * Math.pow(1 + r / n, n * t) // P = 10_000, r = 0.07, n = 12, t = 10 // A ≈ 20_097 ``` The exponent is what makes this equation violent. It grows *with* time — meaning the longer you wait, the faster the curve steepens. At year 1 the difference between investing and not investing is small. At year 30 it is a completely different life. ## Regular Contributions: The Multiplier The formula above assumes a single lump sum. In reality, most people invest a fixed amount each month. A regular contribution is modelled as a *future value of an annuity*: where is the monthly rate and is the monthly contribution. Monthly contributions matter more than the initial lump sum over a long horizon. At 7 % annual return, $500 a month for 30 years accumulates to roughly $567,000 — even if you start with $0. That same $0 starting principal cannot compete: consistency beats magnitude. }> ```ts const rm = r / 12 // future value of each monthly contribution const fvContribs = C * ((Math.pow(1 + rm, n) - 1) / rm) const total = P * Math.pow(1 + rm, n) + fvContribs ``` Two insights follow from this formula: 1. **Start now, not when you earn more.** Because of the exponent , years at the beginning of your timeline are worth more than years at the end. Every year you delay is a year lost at the *steepest* part of someone else's curve. 2. **Rate matters more than you think.** Moving from 6 % to 8 % annual return does not grow your terminal balance by 33 %. Across 30 years it nearly doubles the gap between your final total and your contributions — the extra 2 % is compounded 360 times. ## Interest on Interest The reason compounding feels counterintuitive is that you earn interest on your interest. In the first year, $10,000 at 7 % earns $700. In the second year, you earn 7 % on $10,700 — an extra $749. By year 20 you are earning almost $2,500 *per year* on a $10,000 original investment, without touching the principal. The shaded region above the contributed band is pure interest — money your money made. After long enough, the interest dwarfs the contributions. This is the point at which compounding becomes genuinely self-sustaining. }> ## Time, Not Timing A common mistake is waiting for the "right moment" to invest. But power law curves do not reward timing — they reward *duration*. Consider two investors: - **Early Ellie** invests $5,000 a year from age 22 to 32 (10 years, $50,000 total) at 8 %, then stops. - **Late Louis** invests $5,000 a year from age 32 to 62 (30 years, $150,000 total) at the same rate. At 62, Ellie has roughly **$602,000**. Louis, who contributed three times as much for three times as long, has roughly **$566,000** — still less. Ellie's decade of head start, compounded over 40 years, overpowers Louis's extra $100,000 and 20 extra years of contributions. Ellie contributes for 10 years then lets it ride for 30 more. }> ```ts function ellieWealth() { let a = 0 for (let y = 0; y < 10; y++) a = (a + 5000) * 1.08 for (let y = 0; y < 30; y++) a = a * 1.08 return a // ≈ 602,000 } ``` Louis contributes for 30 years starting a decade later. }> ```ts function louisWealth() { let a = 0 for (let y = 0; y < 30; y++) a = (a + 5000) * 1.08 return a // ≈ 566,000 } ``` The mathematics are unambiguous: **the best time to invest was yesterday. The second-best time is today.** ## The Three Levers Every compounding model has exactly three inputs you can control: | Lever | Effect | |---|---| | **Principal** | Raises the baseline — a one-time lift | | **Rate** | Reshapes the exponent — high leverage over long periods | | **Time** | Sits *in* the exponent — the most powerful lever of all | Principal is the least powerful. A $100,000 windfall is wonderful, but it is a single multiplication. Rate matters more because it is applied repeatedly. Time dominates everything because it is the *exponent's exponent* — each additional year does not add a fixed increment; it multiplies everything that came before. --- Compound interest is often described as the eighth wonder of the world. What it actually is, more precisely, is a power law applied to money — patient, indifferent, and brutally consistent. The curve does not care how smart you are or how hard you work in any given year. It cares only about one thing: how long you let it run. --- ## Software Compound Interest URL: https://1chooo.com/thoughts/software-compound-interest Date: 2025.08.01 # Software Compound Interest **Takeaway:** Hit a milestone! My portfolio [1chooo.com](https://1chooo.com) recently reached [300+ stars](https://github.com/1chooo/portfolio/stargazers) on GitHub! [1chooo.com](https://1chooo.com) taught me a powerful lesson: building great software is a lot like **compound interest**. At first, you put in tons of effort, and no one really notices. But if you consistently contribute valuable work to the community, the impact eventually compounds—and so does the admiration you earn. So, I think it's a great time to share how I've experienced this **"software compound interest"** through my open-source journey. ## Star History Here is the star history chart of [1chooo.com](https://1chooo.com), a project I started developing on [January 6, 2024](https://github.com/1chooo/portfolio/commit/992313a3ed36cd427b4cf4c557b1b53492e68e57). After more than a year of development, the project had fewer than 50 stars. Honestly, for a personal project without any promotion, that's still pretty good—but not quite satisfying, especially considering the amount of hard work put into it. Then, one small change turned everything around: I started promoting it. That changed the game completely. ## Game Changer Through open-source, I've made a bunch of friends on GitHub. One day, while chatting with my friend [@m4xshen](https://github.com/m4xshen), he shared how his success in the open-source community came from actively engaging with others. He strongly encouraged me to start posting and promoting my work. So, I recorded a one-minute demo using [Screen Studio](https://screen.studio/) and posted it on Reddit: Then—**boom**! That's when I truly started to feel the **compound interest** of developing software. I gained over 100 stars in less than a week. All the effort I had quietly invested over the past year suddenly began to pay off. It was incredible to see my work finally being appreciated! On Reddit, I also received a lot of valuable feedback—people suggested ways to improve the design and offered encouragement. That feedback was not only helpful, but also motivating. The positive momentum, both from GitHub stars and community comments, really meant a lot. What's more, after posting on Reddit, my project was reposted on Twitter (X), which led to the second **exponential boost** in stars—another wave of that software compound interest kicking in. ## Iteration So far, we've talked about the value I've gained from GitHub—but just as important is what I've learned from evolving the **tech stack** behind the project. When I started, I only knew HTML, CSS, and JavaScript. But as the project evolved, I learned React, adopted Next.js for routing and server-side rendering, and eventually used Turborepo to manage a scalable monorepo. This portfolio became my sandbox where I built a Markdown blog system, pushed Lighthouse scores near 100, refined SEO, and learned to manage internal npm packages without version conflicts. Each step taught me something new and pushed me from frontend basics toward system thinking—how to build software that scales, maintains, and lasts. ## Quiet Moments In 2024 alone, I made over 5,000 contributions on GitHub. Of course, not all of them were groundbreaking—some were just minor tweaks, refactors, or chores. And sure, some people joked that I should "touch some grass." I usually reply, "**nuh, this is really my grass.**" But honestly, even if many of those commits were invisible to the whole community and the outside world, they weren't meaningless. A few fixed critical bugs. Others unlocked new features or helped me better understand the tech stack I was working with. Some days were just about showing up and pushing forward. Looking back, it's clear that those quiet, persistent contributions laid the foundation for everything that came later. ## Shining Quiet effort is important, but it's not always enough. It's easy to finish a project and hope someone notices, but I realized we have to speak up. Sharing the story behind your work isn't **showing off**—it's an invitation for others to connect with your journey. Sharing what you've built can open unexpected doors. It gives your project a chance to be discovered and gives people a chance to connect with your values and vision. For me, that shift happened when I posted on Reddit. That one post didn't just showcase the project—it **broke the silence** I had been sitting in for a year. And in that moment, the work I'd quietly built finally started to shine. ## Compound This is exactly what I mean by **Software Compound Interest**: when you consistently invest time, passion, and care into a project, the value accumulates quietly. Each small improvement compounds over time, building up until your steady effort surpasses its **maximum static friction**. At that point, what felt like slow growth suddenly turns into exponential momentum, and the results accelerate beyond what you imagined. So if you're building something, keep going. Even when no one's watching. **Especially when no one's watching.** Ultimately, this is the power of software compound interest: consistent effort, even in the quiet moments, always compounds into unexpected and exponential rewards. --- ## Git Multi-Account URL: https://1chooo.com/notes/git-multi-account Date: 2026.08.11 Description: Separate personal and work Git identities on one machine — includeIf, SSH host aliases, insteadOf URL rewrite, and a path-aware gh config switcher. # Git Multi-Account A brand-new machine is the best time to split personal and work Git. Once you start pushing, the cost of mixing identities is a commit stamped with the wrong email — or worse, a push authenticated as the wrong GitHub user. On this laptop the split is physical: personal repos live under `~/Developer/1chooo/`, work under `~/Developer/works/`. The goal is that every layer — commit author, SSH key, and `gh` — follows the folder automatically. ## Three Layers, One Mistake Git identity is not one setting. It is three independent layers that people often treat as one: | Layer | What it controls | Wrong symptom | |---|---|---| | `user.email` / `user.name` | Who the commit is *attributed* to | Wrong author on `git log` | | SSH key | Who GitHub *authenticates* as | `Permission denied to …` | | `gh` config | Which account the CLI talks to | PRs / issues under the wrong user | I learned this the hard way. `includeIf` correctly set my personal email inside `1chooo/dotfiles`, but the remote was still `git@github.com:1chooo/dotfiles.git`. SSH used the default work key. GitHub saw `your-work-user`, who has no write access to `1chooo/dotfiles`: ``` ERROR: Permission to 1chooo/dotfiles.git denied to your-work-user. fatal: Could not read from remote repository. ``` Email was personal. Auth was work. Toggle the path below to see how each layer should resolve. ## Folder-Scoped Git Config Put the work profile in the global config as the fallback, then load a personal file only when the repo lives under `1chooo/`. Create `~/Developer/1chooo/.gitconfig-personal`: ```bash # ~/Developer/1chooo/.gitconfig-personal [user] name = Your Personal Name email = you@personal.dev ``` Then wire it from `~/.gitconfig`. The trailing slash on `gitdir:` matters — without it, nested repos do not match: ```bash # ~/.gitconfig # Default fallback (work) [user] name = Your Work Name email = you@company.com [includeIf "gitdir:~/Developer/1chooo/"] path = ~/Developer/1chooo/.gitconfig-personal ``` With work as the default, you do not need a separate `includeIf` for `works/`. Anything outside `1chooo/` inherits the company identity. Verify from inside a repo: ```bash git config user.email git config --list --show-origin | grep user.email ``` Under `1chooo/` the second command should point at `.gitconfig-personal`. ## SSH: Work as Default, Personal as Alias Commit author and SSH key are separate. If your existing `~/.ssh/id_ed25519` is already registered on the work GitHub account, generate a second key for personal use: ```bash ssh-keygen -t ed25519 -C "you@personal.dev" -f ~/.ssh/id_ed25519_personal eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519_personal pbcopy < ~/.ssh/id_ed25519_personal.pub ``` Add the public key under the personal GitHub account (**Settings → SSH and GPG keys**). Then make work the seamless default in `~/.ssh/config`: ``` # Work account (default) Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519 # Personal account Host github.com-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal ``` Normal work clones keep the URL GitHub shows you: ```bash git clone git@github.com:company/awesome-project.git ``` Personal remotes must use the host alias so SSH picks the personal key: ```bash git remote set-url origin git@github.com-personal:1chooo/dotfiles.git ``` Test both: ```bash ssh -T git@github.com # Hi your-work-user! You've successfully authenticated... ssh -T git@github.com-personal # Hi 1chooo! You've successfully authenticated... ``` ## Auto-Rewrite Remotes with `insteadOf` Remembering to type `github.com-personal` on every personal clone defeats the folder setup. Git can rewrite the URL for you — but only when the personal config is active. Add this to `~/Developer/1chooo/.gitconfig-personal`: ```bash # ~/Developer/1chooo/.gitconfig-personal [url "git@github.com-personal:"] insteadOf = git@github.com: ``` Now, from inside `~/Developer/1chooo/`: ```bash git clone git@github.com:1chooo/another-repo.git cd another-repo git remote -v # origin git@github.com-personal:1chooo/another-repo.git ``` Work repos under `works/` never load that rule, so their remotes stay on plain `github.com`. ## Extending the Split to `gh` `brew install gh` is fine. The problem is that `gh auth login` stores one active account under `~/.config/gh/`. Logging into work overwrites personal credentials and vice versa. Give each account its own config directory, then swap `GH_CONFIG_DIR` based on the current path. ```bash mkdir -p ~/.config/gh-personal ~/.config/gh-work GH_CONFIG_DIR=~/.config/gh-work gh auth login # choose SSH → work account GH_CONFIG_DIR=~/.config/gh-personal gh auth login # choose SSH → personal account (1chooo) ``` Add a `chpwd` hook to `~/.zshrc` so the directory change picks the right folder automatically (work remains the fallback outside `1chooo/`): ```bash function chpwd_gh_switch() { if [[ "$PWD" == "$HOME/Developer/1chooo"* ]]; then export GH_CONFIG_DIR="$HOME/.config/gh-personal" elif [[ "$PWD" == "$HOME/Developer/works"* ]]; then export GH_CONFIG_DIR="$HOME/.config/gh-work" else export GH_CONFIG_DIR="$HOME/.config/gh-work" fi } autoload -U add-zsh-hook add-zsh-hook chpwd chpwd_gh_switch chpwd_gh_switch ``` ```bash source ~/.zshrc cd ~/Developer/1chooo/ && gh auth status # Logged in to github.com as 1chooo cd ~/Developer/works/ && gh auth status # Logged in to github.com as your-work-user ``` ## Config Reference The four files that make the whole stack work. Copy what you need and replace the placeholder emails. ## Daily Cheat Sheet **Work** (`~/Developer/works/`) — copy the normal SSH URL from GitHub. No host alias, no special flags. | Check | Expect | |---|---| | `git config user.email` | `you@company.com` | | `git remote -v` | `git@github.com:…` | | `ssh -T git@github.com` | work username | | `gh auth status` | work account | **Personal** (`~/Developer/1chooo/`) — clone with the normal URL; `insteadOf` rewrites it. Existing remotes may still need a one-time `git remote set-url`. | Check | Expect | |---|---| | `git config user.email` | `you@personal.dev` | | `git remote -v` | `git@github.com-personal:…` | | `ssh -T git@github.com-personal` | `1chooo` | | `gh auth status` | personal account | Before the first push on a new machine, run those four checks in both trees. If email, remote host, SSH greeting, and `gh` all agree, you are safe to push. --- ## The Bell Curve URL: https://1chooo.com/notes/bell-curve Date: 2026.05.04 Description: A bottom-up exploration of the normal distribution — from a single coin flip to Galton's bean machine to the Central Limit Theorem — with fully interactive simulations. # The Bell Curve The bell curve is everywhere. Human heights cluster around an average with most people near the middle and fewer at the extremes. IQ scores, measurement errors, the return on a stock portfolio over many small days, the number of heads in a thousand coin flips — they all approximate the same shape: high in the middle, tapering symmetrically toward the tails. Why does this shape appear so reliably? The answer is not magic. It emerges mechanically whenever you add up many small, independent random steps. To see why, it helps to start as small as possible: a single peg, a single ball, a single coin flip. ## One Peg, One Coin Flip Drop a ball onto a triangular peg. It will bounce either left (probability ) or right (probability ). That single decision is a **Bernoulli trial** — the most primitive random experiment. Its outcome is binary: 0 or 1, left or right, heads or tails. Click **flip** to drop a ball onto the peg. Adjust the bias slider to change how likely the ball is to go right. Watch how the long-run tally tracks the true probability as flips accumulate. When the bias is 0.5 the ball goes left and right with equal probability — a fair coin. Push the slider toward 1 and the ball almost always goes right; the tally converges to the bias you set. This is the **law of large numbers** at its simplest: the empirical fraction converges to the true probability as the number of trials grows. ## Stacking Flips: the Binomial Now imagine not one peg but a whole triangular lattice — *n* rows deep. At each peg the ball makes an independent left-or-right choice. After *n* pegs the ball lands in one of *n + 1* bins, indexed 0 (fell left every time) through *n* (fell right every time). The number of right-turns follows a **binomial distribution**, . The probability of landing in bin *k* is: where counts the number of distinct paths through *k* right-turns out of *n* total pegs. ```ts function nCr(n: number, k: number): number { if (k < 0 || k > n) return 0 if (k === 0 || k === n) return 1 let result = 1 for (let i = 0; i < Math.min(k, n - k); i++) { result = result * (n - i) / (i + 1) } return result } function binomialPMF(n: number, p: number, k: number): number { return nCr(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k) } ``` With a fair coin () most paths end near the centre — there are simply far more ways to get right-turns than to get 0 or . The middle bins collect the most balls; the outer bins collect almost none. ## Galton's Bean Machine Francis Galton built a physical version of this in the 1880s to demonstrate the binomial distribution. A machine drops beans through a lattice of pegs; the beans collect in columns at the bottom, forming a histogram. He called it a **quincunx**; today it is usually called a **bean machine** or **Galton board**. Drop balls using the **+1 / +10 / +100 / +1k** buttons or hit **auto** to stream them continuously. The chart on the right shows the observed histogram (blue bars), the theoretical binomial (orange dashed), and the normal approximation (pink curve). Adjust **rows** and **bias** to reshape the distribution in real time. Focus the panel and press **1–4**, **Space**, **A**, or **R** for keyboard control. Two things become clear as you drop more balls. First, the histogram converges toward the dashed orange binomial curve — the law of large numbers at work. Second, that binomial curve is itself starting to look like a smooth, symmetric arch. With enough rows that arch becomes indistinguishable from the normal (Gaussian) distribution. ## From Binomial to Normal: the Central Limit Theorem The normal distribution is the **limiting shape** of the binomial as *n* grows. More precisely, if , then for large : This is a special case of the **Central Limit Theorem (CLT)**: the sum of many independent, identically distributed random variables — regardless of their individual shape — converges to a normal distribution. The bean machine makes this theorem visible. Each ball's final position is the *sum* of *n* independent Bernoulli(p) steps. The CLT guarantees the sum converges to a bell curve. The normal distribution is parameterised by two numbers: - **Mean ** — the centre of symmetry. Shifting slides the entire curve left or right without changing its shape. - **Standard deviation ** — the spread. Larger flattens the peak and widens the tails; smaller sharpens it. Drag the **μ** slider to shift the curve. Drag **σ** to widen or narrow it. The shaded regions mark the ±1σ (68%) and ±2σ (95%) intervals. ### The 68 – 95 – 99.7 Rule For any normal distribution, the fraction of the population within each band is fixed: | Band | Coverage | |---|---| | | 68.3% | | | 95.4% | | | 99.7% | This is the **empirical rule**. In practice it means: if adult male heights are cm, roughly 95% of men are between 164 cm and 192 cm; finding a man taller than 199 cm (> 3σ above) is genuinely rare (~0.15% of the population). ## Mean, Variance, and Skew Three numbers summarise the shape of any distribution. **Mean **: the centre of mass, or the expected value. **Variance **: average squared deviation from the mean. **Skewness**: the normalised third central moment, measuring asymmetry. For a fair Galton board () the skewness converges to zero — the curve is symmetric. Push the bias away from 0.5 and the board's distribution skews in the direction of the bias. For the theoretical binomial: Notice the denominator grows with . As you add more rows the skewness shrinks toward zero — one more way to see that the binomial converges to the symmetric normal. ## Implementation Notes The Galton board is rendered on a `` element driven by `requestAnimationFrame`. Two design choices mirror those made in the [Snake Game](/notes/snake-game) note: **Mutable simulation state lives in a `useRef`.** The animation loop fires every frame; React's state scheduler is asynchronous. If the ball array and bin counts were held in `useState`, the rAF callback would read stale values after each render. A ref holds a plain object that is mutated synchronously every tick; `useState` is used only for the values displayed in the UI (histogram counts, stats). ```ts const ballsRef = useRef([]) const binCountsRef = useRef(Array(rows + 1).fill(0)) ``` **The component exposes an imperative handle via `useImperativeHandle`.** Parent controls call `boardRef.current?.drop(n)` and `boardRef.current?.reset()` without going through props or state — keeping the drop-rate tight. ```ts useImperativeHandle(ref, () => ({ drop, reset, getActiveCount: () => ballsRef.current.length, }), [drop, reset]) ``` **Canvas is sized by a `ResizeObserver`.** The container's `contentRect` drives both the logical dimensions (used for geometry calculations) and the `canvas.width`/`height` attributes scaled by `devicePixelRatio`, preventing blur on high-DPI screens. The ball's trajectory is split into two phases — falling toward the next peg (`easeInQuad` vertically, `easeOutQuad` horizontally) and settling into a bin — each implemented as a simple lerp with its own `duration`. A spawn queue staggers multiple simultaneous balls so they do not perfectly overlap. --- ## REST API URL: https://1chooo.com/notes/rest-api Date: 2026.03.08 Description: A deep dive into REST — resources, HTTP methods, request and response structure, status codes, authentication, and the constraints that make an API RESTful. # REST API Every modern app calls APIs. You type a URL into your browser, tap a button in a mobile app, or write `fetch('/api/users')` — and somewhere a server receives a structured request and sends back a structured response. Most of those exchanges happen over **REST**, yet the term is thrown around loosely enough that it deserves a careful look. REST answers a design question: *how should clients and servers communicate over HTTP in a way that is predictable, scalable, and easy to reason about?* The answer is a set of architectural constraints — not a protocol, not a specification — that emerged from Roy Fielding's 2000 doctoral dissertation. This article walks through what those constraints mean in practice. ## Where REST Fits REST is an **architectural style** for distributed hypermedia systems. It is not a standard or a wire format. In practice it means designing your API around HTTP verbs, status codes, and resource-oriented URLs. Step through the demo above to see the full lifecycle of a single REST exchange: the client sends an HTTP request, the server processes it, and returns a response. Click **next** to advance one step at a time. ## The HTTP Message REST rides on top of HTTP. Every exchange consists of exactly two messages: a **request** from the client and a **response** from the server. Each message has a fixed structure. Toggle between request and response to explore each field. Click a field in the diagram to see what it does and when to use it. ### The Request Line The first line of any HTTP request contains three things: the **method**, the **request target** (path + optional query string), and the **HTTP version**. Together they tell the server exactly what action to take on which resource. Request line anatomy }> ``` GET /articles/17?format=summary HTTP/1.1 │ │ └── protocol version │ └── request target (path + query) └── method (verb) ``` ### Headers Headers carry metadata. On a request: the `Host` (required in HTTP/1.1), `Accept` (what formats the client can handle), `Authorization` (credentials), `Content-Type` (body format). On a response: `Content-Type`, `Cache-Control`, `ETag`, rate-limit headers. Headers are case-insensitive plain text. ### The Body Only present on requests that carry data — `POST`, `PUT`, `PATCH`. The body is an uninterpreted stream of bytes; `Content-Type` tells both sides how to parse it. For REST APIs the body is almost always UTF-8 encoded JSON. ## HTTP Methods REST maps each CRUD operation to an HTTP **verb**. The choice of method carries semantic meaning that clients, proxies, and caches all depend on. | Method | Semantic | Safe? | Idempotent? | |--------|----------|-------|-------------| | `GET` | Read a resource or collection | Yes | Yes | | `POST` | Create a new resource | No | No | | `PUT` | Replace a resource entirely | No | Yes | | `PATCH` | Apply a partial update | No | No (by convention) | | `DELETE` | Remove a resource | No | Yes | | `HEAD` | Same as GET, headers only | Yes | Yes | | `OPTIONS` | Describe allowed methods (CORS preflight) | Yes | Yes | **Safe** means the method must not change server state — GET, HEAD, OPTIONS are safe. **Idempotent** means calling the method multiple times produces the same result as calling it once — DELETE and PUT are idempotent (deleting a resource that is already gone is still "gone"). POST vs PUT vs PATCH }> ``` POST /articles → create a new article (server assigns ID) PUT /articles/17 → replace article 17 entirely (client sends full document) PATCH /articles/17 → update only the fields provided DELETE /articles/17 → delete article 17 ``` ## Resource-Oriented URLs The central REST idea is that **every thing is a resource** and every resource has a URL. Design your URLs around nouns, not verbs. The verb is already in the HTTP method. ``` # Good (resource-oriented) GET /users → list users POST /users → create user GET /users/42 → get user 42 PUT /users/42 → replace user 42 DELETE /users/42 → delete user 42 GET /users/42/posts → get posts by user 42 # Bad (RPC-style, verb in path) POST /getUser POST /createUser POST /deleteUser ``` Nesting beyond one level (`/users/42/posts/17/comments`) becomes unwieldy. A common pattern at depth: expose the nested resource at its own top-level endpoint (`/comments/99`) and cross-link via ID fields. ## HTTP Status Codes Every response begins with a **status code** — a three-digit number that tells the client whether the request succeeded, and if not, why. The leading digit defines the class. Select a category, then click individual codes to see when to use them and what they mean. ### Picking the Right Code Getting status codes right matters more than most developers realise. A `200` response with `{"error": "User not found"}` in the body breaks every HTTP client, proxy, cache, and monitoring tool that inspects the code. A few principles: - **Do not use `200` for errors.** Return `4xx` when the client did something wrong, `5xx` when the server failed. - **Use `201 Created` + `Location` header** when a `POST` creates a resource. - **Use `204 No Content`** for successful deletes (no body needed). - **Return `404` instead of `403`** when you want to hide whether a resource exists. - **Use `422 Unprocessable Entity`** for business-rule validation failures (as opposed to `400` for malformed syntax). ## The Six REST Constraints Fielding's dissertation describes REST through six constraints. An API that satisfies all of them is properly RESTful; most real APIs satisfy some subset. **1. Client–Server** — Separate the UI concerns from the data storage concerns. The client does not need to know how the server stores data; the server does not need to know how the client renders it. This allows each to evolve independently. **2. Stateless** — Every request from client to server must contain all information needed to understand it. The server stores no client session state between requests. Authentication credentials are re-sent on every request. This makes servers horizontally scalable — any server in the pool can handle any request. Stateless auth — credentials travel with every request }> ``` # Every request carries its own credentials GET /orders HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJSUzI1NiJ9… # Server reads the JWT, verifies signature, no session lookup needed ``` **3. Cacheable** — Responses must label themselves as cacheable or not (`Cache-Control`, `ETag`, `Expires`). Caching at the client, proxy, or CDN layer reduces load and latency. **4. Uniform Interface** — All resources are identified by URIs; representations are transferred to clients; messages are self-descriptive; hypermedia drives application state (HATEOAS). In practice most "REST" APIs implement only the first two. **5. Layered System** — The client need not know whether it is talking to the origin server, a load balancer, a CDN edge node, or a caching proxy. Each layer can only see the adjacent layer. **6. Code on Demand** (optional) — Servers can transfer executable code (JavaScript) to clients. The only optional constraint; rarely cited. ## Authentication REST is stateless, so credentials travel with every request. Three common mechanisms: **API Keys** — A long opaque string issued to an application. Simple but coarse-grained — every request from that application shares the same permissions. Rotate keys regularly and scope them to the minimum required permissions. ``` GET /v1/data HTTP/1.1 X-API-Key: sk_live_abc123… ``` **Bearer Tokens (JWT)** — A signed token containing claims (user ID, roles, expiry). The server validates the signature without a database round-trip. Short-lived access tokens (minutes to hours) paired with longer-lived refresh tokens. ``` Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9… ``` **OAuth 2.0** — A delegation protocol. A third-party app obtains a scoped access token on behalf of a user without receiving the user's password. Used by "Log in with Google/GitHub" flows and machine-to-machine service accounts. Never send credentials over plain HTTP. Always require TLS (HTTPS). Never log Authorization headers. ## Versioning APIs change. A versioning strategy lets you evolve the API without breaking existing clients. **URL versioning** — Simple and visible. Widely used. ``` GET /v1/users/42 GET /v2/users/42 ← new response shape ``` **Accept-header versioning** — Cleaner URLs but harder to inspect in browser address bars. ``` Accept: application/vnd.example.v2+json ``` **No versioning** — works for internal APIs that control both client and server, or when you commit to backwards-compatible changes only (additive changes: new fields, new endpoints). ## Pagination, Filtering, and Sorting Collections can be large. Expose controls through query parameters. ``` # Cursor-based pagination (preferred for large, live datasets) GET /posts?cursor=eyJpZCI6MTAwfQ&limit=20 # Offset-based pagination (simple, watch out for drift) GET /posts?page=3&per_page=20 # Filtering GET /users?role=admin&active=true # Sorting GET /products?sort=price&order=asc ``` Cursor pagination avoids the "missing/duplicate items when rows are inserted during pagination" problem that offset pagination suffers from. ## Error Responses A predictable error shape is as important as a predictable success shape. A widely adopted convention (RFC 9457 — Problem Details): ```json { "type": "https://api.example.com/errors/validation-failed", "title": "Validation Failed", "status": 422, "detail": "The 'email' field must be a valid email address.", "errors": [ { "field": "email", "code": "invalid_format" } ] } ``` Always return errors from the same `Content-Type`, always include the status code in the body too, and never expose raw stack traces. ## Putting It All Together A `POST /articles` to create a new article: ``` POST /v1/articles HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGc… Content-Type: application/json Accept: application/json {"title": "REST API", "body": "…", "tags": ["networking"]} ``` A successful response: ``` HTTP/1.1 201 Created Content-Type: application/json; charset=utf-8 Location: /v1/articles/17 Cache-Control: no-store {"id": 17, "title": "REST API", "createdAt": "2026-03-08T09:00:00Z"} ``` Every field is deliberate: `201` signals creation; `Location` tells the client where the resource lives; `Cache-Control: no-store` prevents stale data from being served; the body echoes the created record back. REST is not a protocol — it is a set of habits. The habits are: pick the right HTTP method, pick the right URL, use status codes honestly, carry credentials on every request, label what can be cached, and version deliberately. Follow them and your API will work well with every HTTP client, proxy, CDN, and monitoring tool on the internet. --- ## DOM URL: https://1chooo.com/notes/dom Date: 2026.03.06 Description: A complete guide to the Document Object Model — from the node tree and traversal through querying, manipulation, events, and performance. # DOM The DOM is one of those things you use every day as a frontend developer without ever quite stopping to think about what it actually is. You call `document.querySelector`, you add event listeners, you update `innerHTML` — and it all just works. But when something breaks, or you need to reason about performance, or a `z-index` mysteriously stops responding to an event, you discover that the DOM has a lot of rules running underneath everything you do. This note walks through the DOM from first principles: what it is, how to navigate it, how to manipulate it, how events move through it, and how to work with it without tanking performance. ## What Is the DOM? When a browser parses an HTML document it does not store it as a string. Instead it builds a **live tree of objects** in memory — the **Document Object Model**. Every element, every text node, every comment becomes a node in that tree, and JavaScript can read and modify that tree at any time. The critical word is *live*. Change the tree from JavaScript and the browser re-renders the affected parts of the page immediately. The HTML source file never changes; the DOM is a separate, mutable representation of it. The tree starts at the `document` node (nodeType 9). Below it sits ``, which branches into `` and ``, and so on down to individual text nodes. Click any node in the explorer above to inspect its core properties. ### Node Types Not everything in the DOM is an element. Every node has a `nodeType` integer that describes what kind of node it is: | `nodeType` | Constant | What it is | |---|---|---| | `1` | `ELEMENT_NODE` | An HTML element (`

`, `

`, …) | | `3` | `TEXT_NODE` | A text run between tags | | `8` | `COMMENT_NODE` | An HTML comment | | `9` | `DOCUMENT_NODE` | The root `document` object | ```js document.nodeType // 9 — the document itself document.body.nodeType // 1 — an element document.body.firstChild // could be 3 — a text node (whitespace) ``` children gives you only element nodes (nodeType 1).
childNodes gives you everything — elements, text, comments.
Most of the time children is what you want. }> ```js const ul = document.querySelector('ul') ul.children // HTMLCollection — only
  • elements ul.childNodes // NodeList —
  • s plus any text/whitespace nodes ul.children.length // 3 (three
  • s) ul.childNodes.length // 7 (3 li + 4 text nodes — newlines/tabs) ``` ## Selecting Elements Before you can do anything with an element you have to find it. The modern API gives you two methods that cover nearly every case: ```js // Returns the first matching element, or null const el = document.querySelector('.card') // Returns a static NodeList of all matches const all = document.querySelectorAll('article h2') ``` Both accept any valid CSS selector, which makes them enormously flexible. The classic `getElementById` and `getElementsByClassName` still exist and are marginally faster for simple ID/class lookups, but `querySelector` is almost always the right choice. Toggle the selector presets above to see which elements in the demo document match. Note that unmatched elements dim out — this mirrors what `querySelectorAll` returns: only the highlighted subset. querySelectorAll returns a static NodeList — a snapshot taken at call time. It does not update as the DOM changes. getElementsByClassName and getElementsByTagName return live HTMLCollections that do update, which can cause confusing bugs when iterating. }> ```js // Static — safe to iterate while mutating the DOM const items = document.querySelectorAll('.item') // Live — length changes if you add/remove .item inside the loop const live = document.getElementsByClassName('item') // Convert live collection to static array when needed const safe = Array.from(live) ``` ## Traversal Once you have a reference to a node, you can navigate the tree relative to it: ```js const el = document.querySelector('main') el.parentElement // immediate parent element el.children // live HTMLCollection of child elements el.firstElementChild // first child element el.lastElementChild // last child element el.nextElementSibling // sibling after this one el.previousElementSibling // sibling before this one ``` Prefer the Element-suffixed properties (parentElement, firstElementChild) over the Node equivalents (parentNode, firstChild). The Node versions include text and comment nodes; the Element versions skip them and return only element nodes. }> ```js const li = document.querySelector('li') li.parentNode // could be a DocumentFragment li.parentElement // always an Element — usually what you want li.firstChild // might be a text node (whitespace) li.firstElementChild // always the first child Element ``` ## Manipulating the DOM Reading the DOM is half the job. The other half is changing it. ### Creating and Adding Nodes ```js // Create a new element const div = document.createElement('div') div.className = 'card' div.textContent = 'Hello' // Attach it — these are equivalent for appending parent.appendChild(div) parent.append(div) // also accepts strings parent.prepend(div) // add at the start sibling.after(div) // insert after sibling sibling.before(div) // insert before sibling ``` ### Removing and Replacing ```js el.remove() // remove from DOM parent.removeChild(el) // older API, same result parent.replaceChild(newEl, oldEl) // swap one node for another el.replaceWith(newEl) // modern shorthand ``` ### Reading and Writing Content ```js el.textContent = 'Safe text' // sets text; any HTML is escaped el.innerHTML = 'Bold' // parsed as HTML — use with care el.outerHTML // the element itself + its content as HTML string ``` Never set innerHTML from untrusted input. It parses and executes embedded scripts, making it the most common XSS vector in JavaScript. For user-supplied content always use textContent, which treats everything as plain text. }> ```js // ✗ XSS risk — never do this with user input el.innerHTML = userInput // ✓ Always use textContent for untrusted strings el.textContent = userInput // ✓ If you must build HTML, use DOMParser or a template const safe = document.createElement('span') safe.textContent = userInput el.appendChild(safe) ``` ### Attributes and Classes ```js el.getAttribute('href') // read attribute el.setAttribute('data-id', '42') // write attribute el.removeAttribute('disabled') // remove attribute el.hasAttribute('hidden') // check attribute // classList is the right way to manage CSS classes el.classList.add('active') el.classList.remove('active') el.classList.toggle('active') el.classList.contains('active') // → boolean ``` ## Events The DOM exposes a rich event system. Every interaction — clicks, keyboard input, form submission, scroll, resize — is represented as an event that you can listen for on any node. ```js const btn = document.querySelector('button') btn.addEventListener('click', (event) => { console.log(event.target) // the element that was clicked console.log(event.type) // 'click' }) ``` ### The Event Object Every listener receives an `Event` object. Its most important properties: | Property | What it is | |---|---| | `event.target` | The element that originally triggered the event | | `event.currentTarget` | The element whose listener is currently running | | `event.type` | The event name: `'click'`, `'keydown'`, … | | `event.preventDefault()` | Cancel the browser's default action | | `event.stopPropagation()` | Stop the event from travelling further | `target` and `currentTarget` differ when event bubbling is involved — `target` is always the origin element, while `currentTarget` changes at each step of propagation. ## Event Propagation When you click a button inside a `
    `, the browser does not just fire an event on the button. It sends the event on a journey through the entire tree. Understanding this journey — **propagation** — explains a huge class of bugs and makes event delegation possible. There are two phases: 1. **Capture** — the event travels *down* from the document root to the target element. Listeners registered with `{ capture: true }` fire during this phase. 2. **Bubble** — after reaching the target, the event travels back *up* through ancestors. This is the default phase; most listeners run here. In the demo, switch to capturing and click the inner button. The outer handler fires first because the event hasn't reached the target yet. Enable stopPropagation to see how it cuts the chain at the first handler. }> ```js // Bubbling listener (default) el.addEventListener('click', handler) // Capture listener — fires before bubbling listeners el.addEventListener('click', handler, { capture: true }) // Stop the event from reaching any further listeners el.addEventListener('click', (e) => { e.stopPropagation() // ← chain ends here }) ``` ### Event Delegation Because events bubble, you do not need a separate listener on every child. You can attach one listener to a parent and check `event.target` to know which child was acted on: ```js // ✗ One listener per item — scales poorly document.querySelectorAll('li').forEach(li => { li.addEventListener('click', handleClick) }) // ✓ One listener on the parent document.querySelector('ul').addEventListener('click', (e) => { if (e.target.matches('li')) { handleClick(e) } }) ``` This pattern — **event delegation** — is especially valuable for large lists or dynamically added elements, since the parent listener covers items that don't exist yet. ## DOM Performance The DOM is far slower than plain JavaScript. Reading a property like `offsetHeight` forces the browser to calculate layout; writing `innerHTML` in a loop forces repeated reflows. A few patterns help: ### Batch Reads Before Writes Interleaving reads and writes forces multiple layout recalculations: ```js // ✗ Read → write → read → write → forces 2 layouts const h1 = el1.offsetHeight // read (layout) el2.style.height = h1 + 'px' // write const h2 = el3.offsetHeight // read (layout again!) el4.style.height = h2 + 'px' // write // ✓ All reads first, then all writes — forces only 1 layout const h1 = el1.offsetHeight const h2 = el3.offsetHeight el2.style.height = h1 + 'px' el4.style.height = h2 + 'px' ``` ### DocumentFragment for Bulk Inserts Every `appendChild` call is a potential reflow trigger if it alters layout. Build large subtrees off-screen in a `DocumentFragment` and then attach in one shot: ```js // ✗ 100 reflows items.forEach(item => { const li = document.createElement('li') li.textContent = item ul.appendChild(li) // ← triggers layout each time }) // ✓ 1 reflow const frag = document.createDocumentFragment() items.forEach(item => { const li = document.createElement('li') li.textContent = item frag.appendChild(li) // off-screen, no reflow }) ul.appendChild(frag) // ← single reflow ``` Layout-triggering properties include: offsetWidth/Height, clientWidth/Height, scrollTop, getBoundingClientRect(), and computed style reads. If you find yourself calling these in a loop, consider caching the value before the loop starts. }> ```js // ✗ Triggers layout on every iteration items.forEach(el => { if (el.offsetWidth > 200) { /* … */ } }) // ✓ Cache outside the loop const containerWidth = container.offsetWidth items.forEach(el => { if (containerWidth > 200) { /* … */ } }) ``` ## A Mental Model for the DOM The DOM is not magic — it is a well-specified tree of objects with predictable rules: | Concept | Key fact | |---|---| | Node tree | `document` is the root; everything is a node with `nodeType` | | Querying | `querySelector` / `querySelectorAll` accept any CSS selector | | Traversal | Use `Element`-suffixed properties to skip text nodes | | Manipulation | `createElement` + `append` is safer and faster than `innerHTML` | | Events | Default phase is bubbling (target → ancestors); capture is reversed | | Delegation | One parent listener beats many child listeners | | Performance | Batch reads before writes; use `DocumentFragment` for bulk inserts | When the browser behaves unexpectedly, the first question is usually: *which node owns this behaviour, and what does the DOM's model say should happen here?* The answer is almost always in one of these seven rules. --- ## /llms.txt URL: https://1chooo.com/notes/llms-txt Date: 2026.03.06 Description: What llms.txt and llms-full.txt are, how the spec works, and how to add them to a Next.js site — including a per-page clean reader route. # /llms.txt AI assistants crawl websites all the time — to answer questions, build context before generating code, or ground a response in up-to-date documentation. But a real HTML page is full of noise: navigation bars, cookie banners, tracking scripts, ads, and duplicate footer links. The actual content a model cares about might be five percent of the bytes it receives. The `/llms.txt` standard is a simple proposal: put a clean, Markdown-formatted index of your site at that well-known URL so models and agents can find and read your content without stripping HTML. The spec: llmstxt.org — proposed by Jeremy Howard (Answer.AI). Covers the full file format, the Optional section convention, and the llms-full.txt variant. } /> ## Why It Exists Two constraints make a dedicated machine-readable file worth having. **Context windows are finite.** Even the largest models top out somewhere. A typical documentation site sent as raw HTML will exhaust a context window long before the model has seen everything useful. **HTML is noisy.** A model reading raw `` must mentally strip repeated nav links, script tags, inline styles, and accessibility attributes to reach the text. A plain Markdown file just has the text. The `/llms.txt` convention was proposed by [Jeremy Howard](https://answer.ai) and has been quickly adopted by documentation tooling, static site generators, and a growing list of sites that want AI-friendly content. ## The File Format `/llms.txt` is a Markdown file served as `text/plain` at the root of your domain. Its structure is deliberately minimal: The Optional section is a spec convention — AI tools may omit it when context is tight. Put links that are useful for humans (GitHub, contact) but not critical for understanding your site's content there. }> ```markdown # Site Name — domain.com > One-sentence description of what this site is. ## Section - [Page Title](/path): One-sentence description of the page. - [Another Page](/other): What it covers. ## Optional - [GitHub](https://github.com/…): Source code. ``` An H1 title, an optional blockquote description, H2 sections each containing a list of links, and an Optional section at the end. That's the entire spec. ## `llms.txt` vs `llms-full.txt` The standard defines two files with different tradeoffs. `/llms.txt` is an **index**: one line per page, each with a title, URL, and short description. A model can read the whole file in a single context window and then decide which individual pages to fetch for more detail. `/llms-full.txt` is the **complete content**: the same structure, but the full text of each page is included directly beneath its link entry. This is useful when you want an agent to have deep offline access without making further HTTP requests. | | `llms.txt` | `llms-full.txt` | |---|---|---| | Size | ~1–5 KB | 10–500 KB | | Use case | Discovery, navigation | Offline deep-read | | Follow-up fetches | Yes, per page | No | | Fits in context | Always | Usually not | ## Implementing in Next.js The cleanest way to handle these files is with [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) in a `(llms)` route group. The route group keeps the files organized without adding a URL segment. Three routes cover all the cases — a fast index, a full-content dump, and a per-page clean reader. ### Route Group Organisation Putting all three routes under `app/(llms)/` keeps the project tidy: Route groups (parenthesised folder names) are invisible in the URL. app/(llms)/llms.txt/route.ts responds at /llms.txt — the (llms) segment is just for organisation. }> ``` app/ (llms)/ llms.txt/ route.ts → GET /llms.txt llms-full.txt/ route.ts → GET /llms-full.txt llm/ [[...slug]]/ route.ts → GET /llm, /llm/bio, /llm/notes/css-layout … ``` ### Parsing MDX Frontmatter Without a Bundler The index route needs each article's `title`, `description`, and `date` — but it runs in a Route Handler, not through the MDX bundler pipeline. The metadata is stored as a JavaScript object literal at the top of every `.mdx` file: ```ts ``` Since this is valid JS — not JSON — you can extract and evaluate it with a `Function` constructor: new Function(`return ({"{"}${"{"}match[1]{"}"}{"}"})`)() wraps the extracted object body in parentheses and evaluates it as a return value. It works with any JS value that can appear in an object literal — strings, booleans, numbers — without needing to serialise to JSON first. Drafts are filtered out by checking the draft flag before adding an article to the output. }> ```ts function parseMetadata(content: string): ArticleMetadata { const match = content.match( /export\s+const\s+metadata\s*=\s*\{([\s\S]*?)\}/ ) if (!match) return { title: '' } try { return new Function(`return ({${match[1]}})`)() as ArticleMetadata } catch { return { title: '' } } } ``` ### Stripping MDX Syntax for the Full-Text File The full-content route reads each article's raw `.mdx` source and needs to strip everything that isn't prose — import statements, the metadata export block, and JSX component calls like `` or `…`. Self-closing component tags ({""}) are removed entirely — they're interactive demos that don't translate to plain text. Open/close component tags ({"…"}) are unwrapped — the inner children are kept because they contain the code blocks and prose that an LLM should still read. }> ```ts function stripMdx(content: string): string { return content // remove .replace(/export\s+const\s+metadata\s*=\s*\{[\s\S]*?\}\s*\n?/g, '') // remove import statements .replace(/^import\s+.*$/gm, '') // remove self-closing tags (no inner content to keep) .replace(/<[A-Z][A-Za-z]*[^>]*\/>/g, '') // unwrap … — keep inner children .replace(/<[A-Z][A-Za-z]*[^>]*>([\s\S]*?)<\/[A-Z][A-Za-z]*>/g, '$1') .replace(/^\n+/, '') .trimEnd() } ``` The full-text route concatenates every section with a `---` divider and includes static pages (home, bio, projects) alongside the article content: ```ts // llms-full.txt: one large document, all content inline sections.push(`## About\n\nURL: ${BASE_URL}\n\n${indexBody}`) for (const { slug, metadata, body } of notes) { sections.push([ `## ${metadata.title}`, `URL: ${BASE_URL}/notes/${slug}`, metadata.date ? `Date: ${metadata.date}` : null, metadata.description ? `Description: ${metadata.description}` : null, '', body, ].filter(Boolean).join('\n')) } return new NextResponse(sections.join('\n\n---\n\n'), { … }) ``` ### The Per-Page Clean Reader The most useful bonus route is `llm/[[...slug]]` — a catch-all that serves any individual page as stripped Markdown. An LLM that wants to deep-read one specific article can request it directly without downloading the whole site: The double-bracket syntax {"[[...slug]]"} makes the parameter optional — so /llm with no slug matches the homepage. Two-segment slugs like /llm/notes/css-layout map to the article's .mdx file. The route is fully statically generated via generateStaticParams — no server required at runtime. }> ``` GET /llm → homepage (page.mdx) GET /llm/bio → bio page GET /llm/projects → projects page GET /llm/notes/css-layout → that note's content GET /llm/thoughts/some-post → that thought's content ``` The implementation reads the `.mdx` file at the matching path, runs `stripMdx`, and returns it as `text/markdown`: ```ts export async function GET(_req: NextRequest, { params }) { const slug = (await params).slug ?? [] let content: string | null = null if (slug.length === 0) { content = await readMdx('app', '(home)', 'page.mdx') } else if (slug.length === 1) { // bio, projects, … content = await readMdx('app', '(home)', slug[0], 'page.mdx') } else if (slug.length === 2) { const [section, articleSlug] = slug content = await readMdx( 'app', '(home)', '(article)', section, '_articles', `${articleSlug}.mdx` ) } if (!content) notFound() return new NextResponse(content, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, }) } ``` ## Caching All three routes use `export const revalidate = false`, which tells Next.js to generate the responses once at build time and never revalidate them on the server. The CDN layer is then controlled entirely with `Cache-Control` headers: s-maxage controls the CDN cache TTL; stale-while-revalidate lets the CDN serve a stale copy while revalidating in the background. The browser sees a fresh response every time (no max-age). This combination is the standard pattern for Next.js pages that need predictable CDN caching without using Next.js's built-in ISR. }> ```ts export const revalidate = false // generated at build time // returned on every response 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400' ``` ## Content-Type `llms.txt` and `llms-full.txt` are served as `text/plain; charset=utf-8`. The per-page `/llm/…` route uses `text/markdown; charset=utf-8` — both are valid; the markdown type is more precise for a file whose format is explicitly Markdown. ## A Mental Model | File | Purpose | Format | Fetches needed | |---|---|---|---| | `/llms.txt` | Site index | `text/plain` (Markdown) | One per page to go deep | | `/llms-full.txt` | Full content dump | `text/plain` (Markdown) | None | | `/llm/[section]/[slug]` | Per-page clean reader | `text/markdown` | One per page | | `/sitemap.xml` | URL inventory for crawlers | XML | — | The three LLM-facing routes work together: an agent reads `/llms.txt` to understand the site's structure, requests `/llm/notes/css-layout` for a deep-read on a specific article, and downloads `/llms-full.txt` only if it needs the entire site in one shot. --- ## TCP URL: https://1chooo.com/notes/tcp Date: 2026.03.06 Description: A deep dive into the Transmission Control Protocol — segment structure, the three-way handshake, connection teardown, flow control, and the full state machine. # TCP The internet moves bytes. Most of those bytes travel over TCP — the **Transmission Control Protocol** — yet the protocol is easy to take for granted. You call `fetch()`, the browser fetches the page, and somewhere underneath a stream of data reliably crossed the network. How? TCP answers a deceptively hard question: *how do you build reliable, ordered, connection-oriented communication over an unreliable, unordered packet network?* The answer involves sequence numbers, acknowledgments, timers, flow control, and a carefully specified state machine. This article walks through all of it. ## Where TCP Fits TCP lives at the **Transport layer** (layer 4 in the OSI model, layer 3 in the simplified TCP/IP model). It sits between the application and the IP layer: IP delivers **packets** — individually routed, possibly reordered, possibly dropped. TCP builds a **byte stream** on top of that: ordered, lossless, and flow-controlled. Its connectionless sibling, **UDP**, skips those guarantees for lower overhead — useful for real-time audio/video (WebRTC), DNS queries, and QUIC. But wherever reliability and order matter, TCP is the default choice. ## The TCP Segment Every unit of work in TCP is called a **segment**. A segment has a fixed-format header (minimum 20 bytes) followed by optional extensions and the application payload. Click each field in the diagram above to see what it does. A few are worth calling out in detail. ### Sequence and Acknowledgment Numbers These two 32-bit numbers are the heart of TCP's reliability guarantee. TCP treats the data as a continuous byte stream identified by sequence numbers. The **sequence number** says "the first byte in this segment is byte N of the stream." The **acknowledgment number** says "I have received everything up to byte M — send me M next." Cumulative acknowledgment — one field covers the whole stream }> ``` Client sends: seq=1000, data=500 bytes → bytes 1000–1499 Server replies: ack=1500 → "got up to 1499, send 1500" Client sends: seq=1500, data=500 bytes → bytes 1500–1999 Server replies: ack=2000 → "got up to 1999, send 2000" ``` This design means a single ACK covers everything received so far — a dropped packet causes a gap, and the receiver simply does not advance its ACK number past the gap until the retransmission fills it. ### Flags The 6 control bits in the flags field determine the purpose of a segment: | Flag | Name | Used for | |------|------|----------| | `SYN` | Synchronize | Opening a connection | | `ACK` | Acknowledge | Carrier of acknowledgment numbers | | `FIN` | Finish | Closing a connection (graceful) | | `RST` | Reset | Aborting a connection immediately | | `PSH` | Push | Hint to deliver data to app now | | `URG` | Urgent | Urgent data pointer valid | Most segments in a normal data exchange carry only the `ACK` flag. `SYN` and `FIN` each consume one sequence number (they are treated as a 1-byte virtual payload). ### Window Size and Flow Control The **window size** field is TCP's built-in throttle. A receiver declares how many bytes it can currently buffer. The sender must not have more than that many unacknowledged bytes outstanding. ``` Receiver buffer = 65535 bytes Sender may have at most 65535 bytes "in flight" (sent, not yet ACKed) ``` The **Window Scale option** (negotiated during the handshake) multiplies this field by a power of two, enabling windows up to 1 GiB — essential for high-bandwidth, high-latency links. ## The Three-Way Handshake Before any data flows, the two endpoints must synchronise their Initial Sequence Numbers and confirm that both directions of communication work. TCP does this with three segments — the **three-way handshake**. Use the mode toggle above to step through both the connection setup (three-way handshake) and connection teardown (four-way teardown). Click **next** to advance one step at a time. ### Why Three Steps? Two steps are not enough. If a client sent SYN and the server replied ACK alone, the server would not know whether *its* direction is working — it has never received an acknowledgment from the client. The third step (client → ACK of the server's SYN-ACK) confirms that both directions are functional. SYN cookies — defending against SYN flood attacks }> ``` Normal: server allocates a half-open socket when it receives SYN. SYN flood: attacker sends millions of SYN segments with spoofed source IPs → server fills its backlog queue → legitimate connections are refused. SYN cookies: server encodes the connection state into the ISN (a cryptographic hash of src/dst addresses and ports plus a timestamp). No state is stored until the ACK arrives and the cookie is verified. ``` ### ISN Randomisation The Initial Sequence Number is **not** zero. TCP requires it to be chosen pseudo-randomly (RFC 6528 recommends a cryptographic hash). This prevents two hazards: 1. **Old duplicate segments** — leftover segments from a previous connection with the same 4-tuple being mistaken for new data. 2. **Blind injection attacks** — an off-path attacker guessing the sequence number and injecting fake data. ## Four-Way Connection Teardown TCP is full-duplex. Each direction is closed independently with a FIN + ACK exchange. That is why graceful close takes **four** segments instead of three. Half-close — each direction is independent }> ``` Client sends FIN → "I'm done sending data." Server sends ACK → "Understood; but I may still send to you." … server finishes its own sends … Server sends FIN → "I'm done too." Client sends ACK → "Connection fully closed." ``` In practice the server often has no data left and combines its ACK and FIN into a single segment, making it look like three segments — but conceptually it is always two independent half-closes. ### TIME_WAIT After sending the final ACK, the active closer does **not** immediately move to CLOSED. It waits for **2×MSL** (Maximum Segment Lifetime, typically 30–60 s, so the wait is 60–120 s). Two reasons: 1. **Ensure the final ACK was received.** If it was lost, the remote will retransmit its FIN, and the active closer needs to be alive to re-send the ACK. 2. **Absorb stale duplicates.** Any segment from this connection that was delayed in the network will expire before the port is reused — preventing it from contaminating a new connection with the same 4-tuple. TIME_WAIT is why a server that was restarted quickly sometimes cannot immediately reclaim its port. `SO_REUSEADDR` relaxes this restriction for server sockets. ## TCP State Machine Every TCP endpoint is always in exactly one of eleven states. Transitions happen in response to incoming segments, API calls (connect, close), or timers. Toggle between the **active opener** (client) and **passive opener** (server) paths. Click any state square to see what it means and which events drive the next transition. ### The Normal Client Path ``` CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED ``` ### The Normal Server Path ``` CLOSED → LISTEN → SYN_RECEIVED → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED ``` The `CLOSING` state appears only in the rare simultaneous close scenario — both sides send FIN at virtually the same time. ## Reliability: Retransmission and Timeouts When a segment is lost, the sender eventually notices that no ACK arrived, and retransmits. Two mechanisms trigger this: **Retransmission timeout (RTO)** — A timer starts when a segment is sent. If no ACK arrives before it fires, the segment is retransmitted. The timeout is computed dynamically from observed round-trip times using Jacobson's algorithm. **Fast retransmit** — If the sender receives three **duplicate ACKs** (the same ACK number repeated), it infers a hole in the stream and retransmits immediately, without waiting for the timer. This is faster because duplicates arrive quickly on modern networks. Fast retransmit — three duplicate ACKs trigger early recovery }> ``` Sender transmits: seg 1, seg 2, seg 3, seg 4, seg 5 Network drops: seg 2 Receiver got seg 1: ACK=2 (normal) Receiver got seg 3: ACK=2 (dup — still waiting for 2) Receiver got seg 4: ACK=2 (dup) Receiver got seg 5: ACK=2 (dup × 3 → sender retransmits seg 2) ``` ## Congestion Control Flow control prevents the receiver from being overwhelmed. **Congestion control** prevents the *network* from being overwhelmed. They work on the same lever — the amount of in-flight data — but with different signals. TCP maintains a **congestion window (cwnd)** alongside the receiver's window. The effective window is `min(cwnd, rwnd)`. **Slow start** — cwnd begins at 1 MSS (Maximum Segment Size, typically 1460 bytes) and doubles every RTT until either loss is detected or a threshold (`ssthresh`) is reached. **Congestion avoidance** — Above `ssthresh`, cwnd increases by 1 MSS per RTT (linear growth) instead of doubling. **On loss** — cwnd is cut. Exactly how depends on the TCP variant (Reno, CUBIC, BBR…). ``` cwnd growth during slow start: 1 → 2 → 4 → 8 → … (exponential) cwnd growth during avoidance: 8 → 9 → 10 → 11 → … (linear) ``` Modern variants like **TCP CUBIC** (Linux default) and **TCP BBR** (Google) use more sophisticated models that achieve higher throughput on long-fat-network paths. ## Putting It All Together A single `GET https://example.com/` involves: 1. **DNS** — resolve `example.com` to an IP. 2. **TCP three-way handshake** — open a connection to port 443. 3. **TLS handshake** — negotiate encryption (over the established TCP connection). 4. **HTTP/1.1 or HTTP/2 request** — bytes flow as TCP segments, each ACKed. 5. **Data transfer** — congestion control and flow control regulate the pace. 6. **TCP four-way teardown** (or `Connection: keep-alive` for reuse). Every reliability guarantee you rely on in a web browser, API client, or SSH session flows from the mechanisms above — sequence numbers, ACKs, retransmissions, flow control, the handshake, and the state machine. The transport layer is invisible until it goes wrong. At that point, knowing exactly what state each side believes it is in, what a SYN cookie is, or why TIME_WAIT cannot be skipped makes all the difference. --- ## CSS Layout URL: https://1chooo.com/notes/css-layout Date: 2026.03.05 Description: A complete guide to CSS layout — from normal flow and the box model through Flexbox, Grid, and positioning. # CSS Layout CSS layout is one of those topics that looks simple on the surface but reveals surprising depth the moment you try to build something real. Why does my `margin: auto` not center this element? Why did my absolutely positioned child escape its container? Why won't `z-index` work? The answer, almost always, is that you haven't fully met the layout model you are working with. This post walks through every major CSS layout mechanism — from the document's default flow all the way to modern Grid — and shows you what each one actually does. ## Normal Flow Before you add a single layout property, the browser already has opinions about where things go. This is **normal flow** (also called the *in-flow* layout model), and understanding it is the prerequisite for understanding everything else. In normal flow, elements are divided into two *formatting contexts*: - **Block formatting context** — elements stack vertically, one per "line", filling the full width of their container. ``, `

    `, `

    ` participate here by default. - **Inline formatting context** — elements sit side-by-side along a text baseline, wrapping when they run out of space. ``, ``, `` live here. ```css /* no layout properties needed — this IS normal flow */ div { display: block; } /* default */ span { display: inline; } /* default */ ``` The key insight: **any layout property you add — flex, grid, position — is a deliberate departure from this well-defined default.** You pull elements *out* of flow, or you replace the flow algorithm with a different one. Block elements stack vertically and expand to fill container width. Inline elements sit on the text baseline and wrap. On mobile both become full-width for readability. }> ```html
    Block A Block B Hello world ``` ## The Box Model Every element in CSS is a rectangular box made of four nested layers. Knowing their names — and their sizes — is crucial for any measurement or positioning work. The `box-sizing` property changes what `width` and `height` refer to: The four layers from inside out: content (your actual text or image), padding (transparent breathing room), border (the visible edge), margin (transparent space that pushes neighbouring elements away). Try the presets to see how each layer enlarges the total footprint. }> ```css /* default — width = content only */ .content-box { box-sizing: content-box; /* default */ width: 120px; padding: 16px; border: 2px solid; /* total = 120 + 32 + 4 = 156px */ } /* border-box — width = content + padding + border */ .border-box { box-sizing: border-box; /* preferred globally */ width: 120px; padding: 16px; border: 2px solid; /* total = 120px — no mental arithmetic */ } ``` `box-sizing: border-box` is the universally recommended baseline. Almost every modern project resets to it: ```css *, *::before, *::after { box-sizing: border-box; } ``` ### Margin Collapsing One counterintuitive consequence of block formatting context: **adjacent vertical margins collapse**. If element A has `margin-bottom: 24px` and element B directly below it has `margin-top: 16px`, the gap between them is `24px`, not `40px`. The larger margin wins; the smaller one is absorbed. Margin collapse only happens in the vertical direction, only between elements in the same block formatting context, and never with flex or grid children. It's not a bug — it prevents paragraph text from double-spacing between headings and body copy. }> ```css h2 { margin-bottom: 24px; } p { margin-top: 16px; } /* gap between h2 and p = max(24, 16) = 24px not 24 + 16 = 40px */ ``` ## Flexbox Flexbox is a **one-dimensional** layout algorithm. You pick an axis — row or column — and items are placed along it. The container controls how leftover space is distributed and how items align on the *cross axis*. The mental model that prevents confusion: Toggle the four core properties to see their effect in real time. The generated CSS block at the bottom reflects your exact selection. Note how align-items: stretch grows all items to equal height — handy for card grids. }> ``` Main axis → controlled by flex-direction (row = horizontal, column = vertical) Cross axis → the perpendicular direction justify-content → distributes space on the MAIN axis align-items → aligns items on the CROSS axis ``` ### Flex Items: `flex-grow`, `flex-shrink`, `flex-basis` The magic of flexbox is that items can **grow** to fill space or **shrink** to avoid overflow. These three properties — usually written in shorthand — describe that behaviour: ```css /* shorthand: flex: grow shrink basis */ .item { flex: 1 1 0%; } /* grow, shrink, start from 0 */ .item { flex: 0 0 200px; } /* rigid, always 200px */ .item { flex: 2; } /* grow twice as fast as flex: 1 siblings */ ``` flex: 1 is shorthand for flex: 1 1 0% — grow to fill space, shrink if needed, start from zero. It's the most common single value and turns all siblings into equal-width columns. }> ```css /* equal-width columns that fill the container */ .nav-item { flex: 1; } /* one item uses minimum space, the other takes the rest */ .icon { flex: 0 0 40px; } /* always 40px */ .label { flex: 1; } /* fills the remaining space */ ``` ## CSS Grid Grid is a **two-dimensional** layout algorithm. Where flexbox works along a single axis, grid manages rows *and* columns simultaneously. This makes it the right tool for page-level layout, dashboards, and anything that needs true 2D alignment. Two-dimensional alignment is the unique superpower of Grid: Switch between the three presets. Holy grail shows the classic header / sidebar / main / aside / footer pattern that used to require float hacks — it's four lines of CSS with Grid. Masonry-ish demonstrates span to let items occupy multiple tracks. }> ```css .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } /* item spans two columns */ .featured { grid-column: span 2; } /* item placed explicitly by line number */ .sidebar { grid-column: 3; /* column line 3 → 4 */ grid-row: 2 / 4; /* rows 2 through 4 */ } ``` ### `fr` Units and `minmax()` Grid introduces the `fr` (fraction) unit, which is only valid inside grid track definitions. One `fr` means "one share of the available space after fixed-size tracks are placed": ```css /* 3 equal columns */ grid-template-columns: 1fr 1fr 1fr; /* sidebar fixed, main flexible */ grid-template-columns: 200px 1fr; /* responsive without media queries */ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); ``` minmax(180px, 1fr) combined with auto-fill is one of the most powerful one-liners in CSS: it creates as many columns as will fit, each at least 180px wide and proportionally stretching to fill the row. No media queries needed. }> ```css .card-grid { display: grid; /* fill with columns ≥ 180px, stretch to fill row */ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; } ``` ### Grid vs Flexbox A common question: *which one should I use?* | Scenario | Reach for | |---|---| | Navigation bar, button group (items in a line) | **Flexbox** | | Page skeleton (header, sidebar, main, footer) | **Grid** | | Card grid with rows *and* columns aligned | **Grid** | | Centering a single element | Either (`flex`/`grid` + `place-content: center`) | | Unknown number of items that should wrap proportionally | **Flexbox** with `flex-wrap` | | Items that need to align to a shared row baseline | **Grid** | In practice you use both together — Grid for the outer shell, Flexbox within each cell. ## Positioning All the layout models above keep elements *in flow*. Positioning lets you step outside that flow entirely. Click through each positioning scheme. The dashed outline shows where the element would have been in normal flow — notice it disappears for absolute and fixed because those elements leave flow entirely. sticky is the hybrid: it starts in flow and locks to the scroll threshold. }> ``` static → default; offsets have no effect relative → offset from normal position; space preserved absolute → relative to nearest positioned ancestor fixed → relative to viewport; survives scroll sticky → flows normally then locks to scroll threshold ``` ### The Containing Block For an `absolute` element, the reference frame is its **containing block** — the nearest ancestor whose `position` is anything *other than* `static`. This is a frequent source of bugs: ```css /* Bug: child is positioned relative to the page, not the card */ .card { position: static; } /* ← this is the problem */ .tooltip { position: absolute; top: 0; right: 0; } /* Fix: give the parent a non-static position */ .card { position: relative; } .tooltip { position: absolute; top: 0; right: 0; } ``` position: relative with no offsets is a no-op visually — the element does not move. But it establishes a containing block, which is exactly why it is the standard fix when an absolutely positioned child escapes to the wrong ancestor. }> ```css /* Pattern: contain absolutely-positioned children */ .parent { position: relative; /* no visual change */ } .child { position: absolute; top: 0; right: 0; /* now anchors to .parent, not the page */ } ``` ### Stacking Order and `z-index` When non-static elements overlap, the browser uses a predictable stacking order. `z-index` only has effect on *positioned* elements (anything other than `static`) or flex/grid children. ```css .overlay { position: fixed; z-index: 100; /* bring to front */ } ``` **Stacking contexts** are the hidden rule: each `z-index` is local to its stacking context. A child with `z-index: 9999` cannot escape a parent stacking context with `z-index: 1`. Stacking contexts are created by `transform`, `opacity < 1`, `filter`, `will-change`, and a few other properties — which is why an element sometimes seems stuck behind another despite its large `z-index`. ## Multi-Column Layout CSS has a native newspaper-style column module. It is less common than Flex or Grid, but useful for text-heavy content: ```css .article { column-count: 2; column-gap: 2rem; column-rule: 1px solid #e2e8f0; /* optional divider */ } /* or set a minimum column width — columns created as needed */ .article { column-width: 280px; } ``` Use break-inside: avoid on elements like blockquotes or figures to prevent them from being sliced across a column break. Use column-span: all to let a heading stretch across every column. }> ```css figure, blockquote { break-inside: avoid; } h2 { column-span: all; } ``` ## A Mental Model for All of CSS Layout Every layout decision reduces to one question: *what formatting context am I in, and what rules does it follow?* | Context | Created by | Axis | Key property | |---|---|---|---| | Block | default (`display: block`) | vertical | `margin`, `width` | | Inline | `display: inline` | horizontal | `line-height`, `letter-spacing` | | Flex | `display: flex` | one axis | `justify-content`, `align-items` | | Grid | `display: grid` | two axes | `grid-template-*`, `gap` | | Positioned | `position: absolute/fixed` | free placement | `top`, `right`, `z-index` | When something looks wrong, the first question is always: *which context owns this element right now, and am I fighting its rules or working with them?* The cascade, the box model, the positioning scheme, the stacking contexts — they are all consistent, deterministic, and understandable. CSS layout only seems unpredictable until you know the system. Once you do, it is remarkably expressive. --- ## Landing Page URL: https://1chooo.com/notes/landing-page Date: 2026.03.02 Description: A landing page is a single, high-stakes surface. This post breaks down the anatomy section by section — hero, fold, social proof, CTAs — and introduces Magic UI, the animation library I reach for when I want motion without friction. # Landing Page With AI growing increasingly powerful, the barrier to code has essentially vanished. Suddenly, anyone can be a builder crafting their own product. In this new reality, a landing page is no longer just a digital brochure—it is your definitive pitch, your storefront, and often your only chance to make a crucial first impression to stand out from the noise. A landing page is a lie — in the best possible way. It presents your product in its most optimistic form: the clearest headline you can write, the most believable proof you can show, the most frictionless path to a single action. It is not the product itself. It is the argument that the product is worth trying. Because it serves as this high-stakes pitch, most of the craft is about reduction. Every sentence that survives the editing process is one you could not cut without losing something. Every section that stays on the page earns its place by moving visitors closer to a decision. This post walks through the anatomy of an effective landing page section by section, then looks at how animation has become a design primitive — and why [Magic UI](https://magicui.design) is my preferred toolkit for bringing it to life. ## What a Landing Page Actually Is A landing page is a focused web page with a single conversion goal: a sign-up, a purchase, a download, a demo request. Unlike a homepage (which tries to orient every kind of visitor), a landing page arrives with a target audience already in mind. ```ts // Homepage: general entry point // Tries to answer: "Who are you?" // Audience: anyone // Landing page: targeted entry point // Tries to answer: "Why should I try this?" // Audience: a visitor with a specific intent ``` The distinction matters because it dictates how you measure success. A homepage might optimise for time-on-site. A landing page optimises for a single conversion rate — the percentage of visitors who complete the target action. ## The Anatomy Every effective landing page is built from the same six sections, arranged in the same order. That order is not arbitrary — each section handles a different stage of visitor psychology. Click any section in the wireframe to read what it does and why it exists. Pay attention to how early the high-stakes sections arrive — the page never makes you wait for the core argument.
    }> The order is deliberately front-loaded. Your strongest asset — the headline — appears first. Your second-strongest — social proof — appears second. Feature details are pushed down, on the theory that no visitor should need to read them to decide whether to try. ## Above the Fold The fold is the line below which a visitor cannot see without scrolling. It comes from newspaper design: the stories above the physical fold of the printed paper sold more copies, because they were visible on the newsstand. On the web the fold is not a fixed line. It shifts with screen size, resolution, and zoom level. A visitor on a 360 px mobile phone will see a fraction of what a desktop user at 1440 px sees at the same scroll position. ```ts // Nothing is literally "above the fold" — the fold is a distribution of viewports const foldHeight = { mobile: 640, // px laptop: 768, desktop: 1080, } // The only safe assumption: // your hero must argue the case before a single scroll ``` Switch between viewports, or drag the fold slider, to see how much content lands above the line. On a small phone, only the navbar and hero are guaranteed visible — everything else requires intent.

    }> The practical conclusion: assume the fold sits right below your hero. The hero must contain the entire argument in compressed form — headline, sub-headline, and CTA. Everything below is either elaboration or reassurance. ## Writing the Hero The hero section is a five-second interview. Most visitors decide to stay or leave based on what is in the viewport when the page loads. A hero contains three things: - **Headline** — one sentence stating the outcome the product delivers. Not the features. Not the process. The outcome. - **Sub-headline** — one or two sentences of supporting context: who it's for, how it actually works, why now. - **Primary CTA** — one button. The action you most want the visitor to take. ```ts // Weak: feature-led const weakHeadline = "AI-powered task management with integrations" // Strong: outcome-led const strongHeadline = "Your team ships faster. Without the meetings." // The difference: weak tells you what the product does. // Strong tells you what your life looks like after using it. ``` Secondary CTAs (like "Watch Demo" or "Learn More") are acceptable, but they should be visually subordinate. Two equally weighted buttons force a decision before the visitor is ready to decide anything. ## Social Proof Trust is expensive to build. Social proof borrows it from parties the visitor already trusts. The mechanism is borrowed authority — if a company you respect uses this product, the product is probably worth investigating. The format matters less than the placement. Logos work. Testimonials work. Star counts work. What does not work: placing them at the bottom of the page, after the visitor has already left. | Format | Works when | |---|---| | Company logos | Recognisable names are available | | Testimonial quotes | Specific, numeric claims are possible | | Usage numbers | Numbers are large enough to be meaningful | | Press mentions | Publication is recognisable to the target audience | The key qualifier is *recognisable*. A logo the visitor has never seen provides no trust signal. A logo from a company they know and respect borrows the full weight of that recognition. ## Magic UI [Magic UI](https://magicui.design) is an open-source library of 150+ animated components built on React, TypeScript, Tailwind CSS, and [Motion](https://motion.dev). It was created by [dillion](https://twitter.com/dillionverma) and has become the de facto toolkit for design-engineer-style landing pages. Its most popular component category is landing page effects: animated gradient text, shimmer buttons, border beams, number tickers, and marquees. None of these are load-bearing UI — they carry no semantic meaning. Their job is to direct attention and communicate polish. ```bash # Install via shadcn CLI — each component is a local file, # not a managed dependency npx shadcn@latest add "https://magicui.design/r/shimmer-button" npx shadcn@latest add "https://magicui.design/r/text-animate" npx shadcn@latest add "https://magicui.design/r/number-ticker" ``` Every component is copied into your codebase via the shadcn CLI. You own the source; there is nothing to keep in sync, no peer dependency to manage. The tradeoff is intentional: ownership over convenience. Step through each effect with the buttons below. Each preview is built from scratch using CSS keyframes and React state — no Magic UI package installed. The descriptions trace exactly how each effect works under the hood.
  • }> The library is notably honest about what its components do. The docs describe them as "special effects" rather than UI primitives. That framing is worth keeping: a shimmer button should not replace a well-structured design system, it should accent one. ## The Physics of Attention Animation on a landing page works by co-opting a deeply wired reflex. The human visual system is hard-tuned to detect motion in peripheral vision — a throwback to predator detection. Moving elements capture attention with almost no cognitive cost to the viewer. Used well, this is powerful. A subtle shimmer on the CTA button tells the eye: *this is the action*. A number ticking upward says: *something is happening, and it is big*. Used carelessly, animation destroys credibility. A page where everything moves is a page where nothing is emphasised. The signal-to-noise ratio collapses. ```ts // Good: one animated element in a field of still ones const ctaShimmers = true const heroTextGradientAnimates = true const everythingElseIsStatic = true // ← this is the constraint // Bad: ambient animation everywhere const backgroundParticlesMove = true // ← pulls eye away from CTA const cardsHoverSway = true // ← visual noise const loadingSpinnersPersist = true // ← communicates chaos ``` The practical rule: animate at most one or two elements per viewport. Give everything else room to breathe. ## Conversion Rate is a Design Problem Most teams treat conversion rate as a marketing problem — something fixed by changing the ad copy or the audience targeting. It is more accurately a design problem. Visitors convert when the page removes every possible reason not to. The failure modes are mostly structural: | Failure | What it signals to the visitor | |---|---| | Weak headline | "I don't understand what this does" | | No social proof | "I have no reason to trust this" | | Multiple CTAs | "I don't know what they want me to do" | | Feature-led copy | "So what does that mean for me?" | | No fold awareness | "I didn't know there was more to see" | Each failure mode is fixable without a redesign — a headline rewrite, a logos section, a button consolidation. The page does not need to be beautiful to convert. It needs to be clear. ## Components at a Glance | Component | What it does | |---|---| | Gradient Text | Animates background-position on a wide gradient clipped to text | | Shimmer Button | Layered gradient shifts left-to-right at constant speed | | Border Beam | Conic gradient rotates via @property-animated CSS variable | | Number Ticker | IntersectionObserver triggers a cubic-eased rAF count-up | | Marquee | Duplicated list accelerates via translateX at constant rate | The implementation pattern is always the same: a CSS keyframe, a Tailwind class that enables `background-clip: text` or relative positioning, and a state hook if interactivity is needed. Magic UI's real contribution is not the individual effects — those are all re-implementable in an afternoon. It is the catalogue: a curated vocabulary of components that already fit together visually and conceptually. When I reach for it, I am not buying effects. I am buying a set of design decisions I do not have to remake. --- ## Image Compression URL: https://1chooo.com/notes/image-compression Date: 2026.03.01 Description: How JPEG and WebP shrink images without (visibly) breaking them — a tour of colour spaces, 8×8 blocks, quantisation, and a live quality demo. # Image Compression A raw photograph from a modern phone is around 25 MB. After JPEG compression at typical quality, it weighs under 3 MB, and most people cannot tell the difference. Something interesting is going on — the algorithm is discarding real data, yet the result looks fine. Why? The answer is that JPEG does not compress uniformly. It exploits two reliable features of human perception: we notice blur less than we notice noise, and we are much more sensitive to brightness variation than to colour variation. The algorithm finds the information we are least likely to miss, and throws it away first. This post walks through the pipeline step by step, with interactive demos at each stage. ## The Raw Size Problem Before any compression, every pixel in an image needs three bytes — one each for red, green, and blue, each value from 0 to 255. ```ts // pixels × channels × bytes per channel const rawBytes = width * height * 3 // 1920×1080 photo rawBytes = 1920 * 1080 * 3 // → 6,220,800 ≈ 6 MB ``` A 4K image runs to 25 MB. For a website that loads dozens of images on a scroll, that is prohibitive. Compression is not optional — it is the only reason the web works. ## The JPEG Pipeline JPEG compresses in five stages, each exploiting a different weakness in human perception. | Stage | What happens | |---|---| | 1. Colour space | RGB → YCbCr — separate brightness from colour | | 2. Chroma subsampling | Halve the resolution of the colour channels | | 3. Block division | Slice the image into 8×8 pixel tiles | | 4. DCT + quantisation | Convert each tile to frequencies, discard weak ones | | 5. Entropy coding | Huffman-encode the remaining values | The quality setting controls stage 4 — how aggressively the frequencies are rounded. Everything else is fixed. ## Step 1 — Colour Space: RGB → YCbCr RGB stores brightness and colour entangled in three equal channels. YCbCr separates them: - **Y** — luma, the brightness signal - **Cb** — blue–yellow colour difference - **Cr** — red–cyan colour difference ```ts function rgbToYCbCr(r: number, g: number, b: number) { const Y = 0.299 * r + 0.587 * g + 0.114 * b const Cb = 128 - 0.168736 * r - 0.331264 * g + 0.5 * b const Cr = 128 + 0.5 * r - 0.418688 * g - 0.081312 * b return [Y, Cb, Cr] } ``` Why bother? Because the human visual system is wired this way. The retina has many more luminance-sensitive receptors than colour-sensitive ones — you can read fine print in greyscale but would struggle if the same print used only colour variation. Step through the channels using the buttons below. Y preserves all the fine detail; Cb and Cr look blurry and uniform by comparison. Switch to Reconstruct and toggle 4:2:0 subsample — the colour channels are halved, yet the difference is subtle. }> ## Step 2 — Chroma Subsampling Once we have YCbCr, we can exploit the perception gap immediately. The simplest scheme is **4:2:0**: keep Y at full resolution, but share each Cb and Cr value between four neighbouring pixels (a 2×2 block). ``` 4:4:4 → Y Cb Cr per pixel (no subsampling) 4:2:2 → Y full, Cb/Cr halved horizontally 4:2:0 → Y full, Cb/Cr halved in both directions ``` For a 1920×1080 photo, 4:2:0 reduces the raw byte count from 6 MB to 3 MB before any further compression — a 50 % reduction at almost zero perceptual cost. ## Step 3 — Dividing into 8×8 Blocks JPEG does not process the full image at once. It slices it into **8×8 pixel tiles** and handles each independently. Every step from here on operates on a single tile. ```ts // Top-left pixel of block (bx, by) const x0 = bx * 8 const y0 = by * 8 // Read the 64 pixels that make up this block const block = pixels.slice(y0, y0 + 8).map(row => row.slice(x0, x0 + 8)) ``` The thick lines mark 8×8 block boundaries. Hover over any pixel to read its coordinates and RGB value. Click to highlight that pixel's block — notice that the local coordinates reset to (0, 0) at each block's top-left corner. }> Why 8×8 specifically? It is a trade-off. Larger blocks capture more spatial context but make local detail harder to preserve. Smaller blocks miss the correlations between nearby pixels that compression depends on. 8×8 was the sweet spot at the time JPEG was standardised in 1992, and it has stayed ever since. ## Step 4 — DCT and Quantisation The most important step. Each 8×8 block is transformed by the **Discrete Cosine Transform** (DCT) from pixel values into 64 *frequency coefficients*. ```ts // Conceptually: pixels → frequency amplitudes const [dc, ...ac] = dct2d(block) // dc = the average colour of the block (coefficient [0,0]) // ac = how much of each spatial frequency is present ``` Think of it like a Fourier analysis for that tiny tile. The first coefficient (`dc`) is the average tone. The remaining 63 `ac` coefficients represent progressively finer striped patterns — horizontal, vertical, diagonal — at increasing frequency. Then comes **quantisation**: each coefficient is divided by a number from a *quantisation table* and rounded to the nearest integer. ```ts quantised[u][v] = Math.round(dct[u][v] / Q[u][v]) ``` The quantisation table is derived from the quality setting. At low quality, the divisors are large — most high-frequency coefficients round to zero. At high quality, the divisors are small — almost all coefficients survive. Zero values compress extremely well in the next stage, so more zeros mean a smaller file. This is real JPEG encoding running in the browser via {'canvas.toDataURL("image/jpeg", q)'}. Drag the slider down towards 1 — watch the blocky 8×8 artefacts appear on the sharp edges and the checkerboard. The smooth gradient holds up much longer because it contains almost no high-frequency content. }> Notice that the artefacts follow block boundaries — each 8×8 tile is quantised independently, so neighbouring blocks can diverge at low quality. ## Step 5 — Entropy Coding After quantisation, JPEG applies **Huffman coding** — a lossless step that assigns shorter bit sequences to more common values. Zero-run-length encoding is used first: long runs of zero coefficients (common after aggressive quantisation) are represented with a single symbol. ``` before: 0 0 0 0 0 0 7 0 0 0 0 3 after: (6 zeros)(7)(4 zeros)(3) ``` This stage adds no new loss. It is pure lossless compression. ## WebP: the Same Idea, Extended WebP (the format used on this site) is built on VP8 video compression. It uses the same conceptual pipeline but with several improvements: - **Prediction coding** — encode the *difference* from a predicted value rather than the value itself - **Larger transform blocks** — up to 16×16 for smooth regions of the image - **Arithmetic coding** — more efficient than Huffman for the same entropy The result is roughly 25–35 % smaller than JPEG at equivalent visual quality. ## Why Smooth Regions Compress, Sharp Edges Bleed The DCT is the key to understanding both the strengths and the weaknesses of JPEG. A smooth gradient across an 8×8 block is dominated by the first few low-frequency coefficients — a handful of large numbers, the rest near zero. After quantisation, almost all of those numbers survive. The block is cheap to code. A sharp edge or a fine texture requires many high-frequency coefficients. After quantisation those get zeroed out. The edge becomes a halo, the texture turns to mud. ``` smooth gradient → few DCT coefficients → survive quantisation → small file checkerboard → many DCT coefficients → zeroed by quantisation → large file, bad quality ``` Photographic images sit in the middle: strong gradients with occasional sharp detail — which is exactly the trade-off JPEG was designed for. ## Concepts at a Glance | Concept | Key idea | |---|---| | Colour space | YCbCr separates brightness (keep full res) from colour (can discard) | | Chroma 4:2:0 | Halve both colour channels — 50 % size before the transform | | 8×8 blocks | Localise the transform; also the source of block artefacts | | DCT | Convert pixel values to frequency amplitudes per block | | Quantisation | Divide by quality-derived table and round — zeros compress for free | | Entropy coding | Huffman + run-length encode the quantised stream (no further loss) | The quality setting controls a single scaling factor applied to the entire quantisation table. Halving it roughly halves the file — but the perceptual cost falls unevenly. Gradients absorb the loss quietly; fine edges and sharp textures spend it loudly, and visibly. --- ## OpenGraph Image URL: https://1chooo.com/notes/opengraph-image Date: 2026.03.01 Description: A deep dive into OpenGraph images — the 1200 × 630 canvas that previews your content on every social platform — and how to build a live generator with React and canvas. # OpenGraph Image Every time you paste a link into Slack, iMessage, Twitter, or Facebook, a card blooms beneath it — title, description, a big thumbnail. That thumbnail is your **OpenGraph image**. It is the first impression your content makes before a single word of it is read. The protocol is simple: a `` tag in your HTML points to a URL, the platform crawls it, and the image appears in the preview card. But building an image that looks great, loads at the right size, and communicates the right things is its own design problem. In this post I walk through the anatomy of a good OG image, then build an interactive generator with React and `` — live below. ## The Standard: 1200 × 630 The Open Graph specification does not mandate a single dimension, but the industry has converged on **1200 × 630 pixels** as the canonical size. The aspect ratio is approximately `1.91 : 1` — wider than a standard photograph but shorter than a cinema letterbox. A few things fall out of this constraint: - **Twitter/X** crops to 2 : 1 at medium-card size, so anything in the outer ~5 % of height may be clipped. Keep all important text centred vertically away from the very top and bottom edges. - **iMessage and Slack** render the full 1.91 : 1 ratio uncropped. - Retina displays will show your image at 2× if you serve it large enough — at 1200 px wide you are already at the threshold; 600 px wide looks blurry on HiDPI screens. The canvas below maps the full 1200 × 630 space and labels each functional zone. **Hover over any coloured region** to read what it should contain. ## Anatomy of a Good OG Image Looking at the zones from the demo above, a well-structured OG image has five distinct layers. **Background.** The full canvas. It should establish the visual mood in one glance — dark, light, saturated — and be consistent with your brand. **Identity row.** A small avatar or logo (≈ 80 × 80 px) next to your site name anchored to the top-left. This is the *who* — the viewer should immediately know whose content this is. **Title.** The dominant text element, centred vertically in the canvas. Keep it under 60 characters; choose a typeface and weight that reads clearly at thumbnail size. At 1200 px canvas width, a font size of **56–72 px** is comfortable. **Description.** One sentence below the title. Smaller type (≈ 30 px), a muted colour. It reinforces the title without competing with it. Under 100 characters. **URL / metadata.** Bottom of the canvas — canonical URL, author, date. The smallest readable type size (≈ 24 px). A horizontal rule separating this from the description zone helps visually. ## Two Implementation Approaches Once you know the layout, there are two standard ways to produce the image at build or request time. ### 1 — `ImageResponse` (Next.js / Vercel OG) The recommended approach for Next.js projects is the [`@vercel/og`](https://vercel.com/docs/functions/og-image-generation) package, which renders a React tree to a PNG using Satori (a CSS-to-SVG engine) and Resvg (SVG-to-PNG). You write JSX that looks like HTML, and the library handles font loading and rendering: ```ts export const runtime = 'edge' export async function GET(req: Request) { const { searchParams } = new URL(req.url) const title = searchParams.get('title') ?? 'My Site' return new ImageResponse( ( {title} ), { width: 1200, height: 630 } ) } ``` The route handler lives at `app/opengraph-image.tsx` (for the root OG image) or `app/blog/[slug]/opengraph-image.tsx` for per-post images. Next.js automatically links the `` tag to it. ImageResponse runs on the Edge runtime, so it executes close to the user with near-zero cold-start. Pass query parameters to make the image dynamic — /opengraph-image?title=My+Post. }> ```ts // app/blog/[slug]/opengraph-image.tsx export async function generateImageMetadata({ params }) { const post = await getPost(params.slug) return [{ id: 'og', alt: post.title }] } export default async function OGImage({ params }) { const post = await getPost(params.slug) return new ImageResponse(, { width: 1200, height: 630, }) } ``` ### 2 — Canvas + `toDataURL` When you need complete control — custom blend modes, pixel-level effects, or a client-side generator that never hits a server — the browser `` API is the right tool. The same drawing primitives (`fillRect`, `fillText`, arc paths) that power the Snake game earlier in this blog produce a pixel-perfect 1200 × 630 PNG with a single call to `canvas.toDataURL('image/png')`. The key insight is to draw at **full resolution** (1200 × 630) even when the *display* canvas is scaled down. In the implementation below, two canvases coexist: - A **display canvas** scaled to fit the page — re-drawn on every state change, read by the user. - A **hidden export canvas** drawn at `scale = 1` only when the user clicks *Download*. ```ts // Display at ~600 px wide const scale = displayWidth / 1200 drawOG(displayCanvas, { ...opts, scale }) // Export at full 1200 × 630 drawOG(exportCanvas, { ...opts, scale: 1 }) const dataUrl = exportCanvas.toDataURL('image/png') ``` `drawOG` multiplies every coordinate and font size by `scale`, so the same function draws correctly at any resolution. All coordinates are in OG-space (1200 × 630). Multiply by scale before every drawing call to make the same function work at any output size. }> ```ts function drawOG(canvas: HTMLCanvasElement, opts: { scale: number, ... }) { const { scale, title, preset } = opts const W = 1200 * scale const H = 630 * scale canvas.width = W canvas.height = H const ctx = canvas.getContext('2d')! // Every measurement is multiplied by scale ctx.font = `bold ${68 * scale}px serif` ctx.fillText(title, 60 * scale, 265 * scale) } ``` ## The React Implementation The component uses the same ref-over-state pattern from the Snake game: mutable display config lives in React `useState` (since the DOM does need to re-render the inputs), but the canvas is always drawn imperatively via `useEffect` and `useCallback`. ```ts const [title, setTitle] = useState("Building an OpenGraph Image") const [preset, setPreset] = useState(PRESETS[0]) const render = useCallback(() => { const scale = displayWidth / 1200 drawOG(displayRef.current!, { title, preset, scale }) }, [title, preset, displayWidth]) useEffect(() => { render() }, [render]) ``` A `ResizeObserver` watches the container element and re-triggers `render` whenever the page layout changes — this keeps the canvas sharp on narrow mobile screens without any manual breakpoints. ## The Live Builder Type your own title, description, and site name. Pick a theme. Click **Download PNG** to get the 1200 × 630 file — ready to drop straight into a `` tag or a Next.js `opengraph-image.tsx` response. ## Putting It On Your Page Once you have the image file, the simplest way to serve it is as a static asset: ```html ``` For dynamic images that share the same layout but different content, use the Next.js route handler approach above — pass `?title=...&desc=...` as query parameters and read them inside `GET()`. To verify your image before publishing, paste your URL into [opengraph.xyz](https://www.opengraph.xyz/) or the [Twitter Card Validator](https://cards-dev.twitter.com/validator). Both show exactly what crawlers will return. ## Concepts at a Glance | Concept | Detail | |---|---| | Canonical size | 1200 × 630 px, ratio ≈ 1.91 : 1 | | Next.js route | `app/opengraph-image.tsx` → `ImageResponse` | | Scale-independent drawing | multiply all coords by `scale` factor | | Display vs. export | two canvases — one scaled for screen, one full-res | | Validation | opengraph.xyz, Twitter Card Validator | The OG image is a small canvas but a high-leverage one. A few hours of design work pays off every time someone shares your content — which is to say, every time anyone else markets it for you. --- ## Traceroute URL: https://1chooo.com/notes/traceroute Date: 2026.01.24 Description: An interactive traceroute and DNS explorer for understanding how packets travel and how domains resolve. # Traceroute Every time you open a website, your request doesn't travel in a straight line. It moves hop by hop across a chain of routers before reaching the destination server. **Traceroute** reveals that path — showing each intermediate hop, its IP address, hostname, geographic location, and round-trip latency. Alongside the route, DNS underpins almost everything. Before a connection is even attempted, your machine asks a nameserver to translate `google.com` into an IP address. The DNS tab below shows all the record types returned — `A`, `AAAA`, `MX`, `TXT`, `NS`, and `CNAME` — along with their TTL values. ## Try it Enter a domain to trace its path and inspect its DNS records. Hops stream in as they resolve. The globe and route diagram update in real time. The results are simulated in the browser. For precise results, run{" "} traceroute <domain> in your terminal. }> ## How traceroute works Traceroute relies on the **TTL** (Time To Live) field in every IP packet. Each router decrements the TTL by one. When it reaches zero, the router drops the packet and replies with an ICMP "Time Exceeded" message — revealing its own IP address. By sending packets with `TTL = 1, 2, 3, ...` traceroute discovers each hop along the path. ``` TTL=1 -> first router responds -> hop 1 TTL=2 -> second router responds -> hop 2 TTL=n -> destination responds -> done ``` The three RTT measurements per hop reveal jitter and asymmetric routing. A `*` means the router didn't respond — either firewalled or configured to silently drop ICMP. Higher latency on later hops is expected — transoceanic links alone add 80-150 ms. What matters is a sudden spike at a specific hop. That often signals congestion, routing inefficiencies, or poor peering between networks. --- ## Motion URL: https://1chooo.com/notes/motion Date: 2025.09.21 Description: A guide to building delightful UI animations with Motion (Framer Motion) — covering the core concepts and four real-world component examples. # Motion More and more websites and applications are incorporating motion into their designs and interactions. Motion can enhance the user experience by providing visual feedback, guiding attention, and making interactions feel more natural and engaging. However, I believe we should carefully consider when and how to use motion. The design principles I strive for prioritize simplicity, minimalism, and maintainability. In this article, I will walk you through the **core concepts** behind building UI animations with [Motion](https://motion.dev/) (previously known as Framer Motion), and [show you four concrete examples](#dynamic-island) I built to put those ideas into practice. ## Core Concepts Before diving into the examples, it helps to understand the handful of primitives that Motion gives you. Once you internalize these, you will be able to compose almost any animation imaginable. ### 1. `motion.*` components The foundation of every Motion animation is swapping a plain HTML element for its `motion.*` equivalent: ```tsx // Before // After – now it can be animated ``` `motion.div`, `motion.span`, `motion.svg`, and friends accept extra props (`animate`, `initial`, `exit`, `transition`, `whileHover`, `whileTap`, …) that describe how the element should move. The plain div on the left snaps to its new style instantly — no interpolation happens. The motion.div on the right smoothly drives between states using the browser's animation engine. Hit Trigger to compare. }> ### 2. `animate` and `initial` `initial` sets the element's starting state. `animate` describes the target state Motion will smoothly drive towards. ```tsx ``` When the component mounts, it fades in while sliding upward by 20 px. No manual `useEffect` or `requestAnimationFrame` required. initial is the off-screen starting pose. animate is where it lands. Hit Play to watch the card animate from {"{ opacity: 0, y: 32 }"} to {"{ opacity: 1, y: 0 }"}. }> ### 3. `transition` `transition` controls **how** the animation plays — duration, easing curve, delay, and type. Motion supports three physics models: | Type | Behaviour | |---|---| | `tween` | Classic CSS-like easing (linear, easeIn, easeOut, easeInOut) | | `spring` | Physics-based spring with `stiffness`, `damping`, and `mass` | | `stiff spring` | High stiffness, low damping — overshoots and bounces back | ```tsx // Smooth CSS-style transition={{ type: "tween", duration: 0.6, ease: "easeInOut" }} // Natural spring transition={{ type: "spring", stiffness: 300, damping: 20 }} // Bouncy spring transition={{ type: "spring", stiffness: 600, damping: 10 }} ``` All three dots travel the same distance to the same destination — only the transition differs. Notice how the stiff spring overshoots and bounces back; that subtle overshoot is what makes spring animations feel alive. }> ### 4. `layout` prop Adding `layout` to a `motion.*` element tells Motion to automatically animate whenever its **size or position changes** in the DOM. This is the secret ingredient behind smooth accordion expansions, list reordering, and collapsing cards — you simply change state and Motion handles the interpolation. ```tsx {isOpen && } ``` Click a card on either column to expand it. Without layout, siblings snap to their new position instantly. With layout, everything glides into place — no manual height calculation needed. }> ### 5. `AnimatePresence` and `exit` React removes elements from the DOM instantly. `AnimatePresence` wraps a group of conditionally rendered elements and gives them a chance to play an `exit` animation before they are removed. ```tsx {isVisible && ( )} ``` The `mode="wait"` option ensures the outgoing element finishes its exit animation before the incoming one starts — preventing two elements from overlapping. The top section shows a basic mount / unmount cycle with an exit scale animation. The bottom section uses mode="wait" for a directional tab swap — the outgoing tab slides out before the incoming one slides in. }> ### 6. Gesture props Motion exposes ergonomic props for common user interactions: ```tsx ``` These feel far more alive than a CSS `transition: transform 0.2s` because they respond in real time to the user's pointer speed. Hover and tap each card to feel how whileHover and whileTap compose together. The spring transition makes the scale feel responsive to the speed of your interaction, not just the state change. }> ### The Mental Model Think of a Motion animation as three questions: 1. **Where does it start?** → `initial` 2. **Where does it end up?** → `animate` 3. **How does it get there?** → `transition` Everything else (`exit`, `layout`, `whileHover`, …) is just layering more answers on top of those three questions. ## Dynamic Island Apple's Dynamic Island on iPhone 14 Pro is a brilliant example of purposeful motion — a hardware notch that transforms into context-aware UI. The demo below recreates that concept: a pill-shaped container that expands to show a silent / ring notification, then collapses back. The pill morphs its width and height using animate, while AnimatePresence mode="wait" crossfades between three content states. The looping bell shake is driven by a repeat: Infinity rotation sequence. }> ### How it works The key technique here is animating `width` and `height` directly on a `motion.div` combined with `layout`: ```tsx ``` Inside the pill, `AnimatePresence mode="wait"` swaps between three content views (`collapsed`, `expanded`, `controls`) with crossfade transitions. Each view is keyed so React and Motion can track which element is entering and which is leaving. The bell icon in the ring state uses a looping `rotate` animation to simulate ringing: ```tsx animate={{ rotate: [-5, 5, -5, 5, 0], }} transition={{ duration: 0.4, repeat: Infinity, repeatDelay: 0.3, }} ``` **Concept to remember:** Combine `layout` + `animate` size changes + `AnimatePresence` for any component that morphs its shape in response to state. --- ## Notifications The notification stack collapses multiple cards into a "fanned" pile when hidden, then fans out into a readable list when expanded — a pattern used by iOS, macOS, and many notification centre UIs. Each card gets a progressively larger y offset, smaller scaleX, and lower opacity when collapsed — creating a stacked depth illusion with a single boolean. Tap any card or the Collapse button to toggle. }> ### How it works Each notification card is a `motion.div` with an independent `y` (vertical offset) and optional `scaleX` / `opacity` target driven by the `show` boolean: ```tsx // Second card — partially visible behind the first // Third card — deeper in the stack ``` By giving each card a progressively larger `y` offset and a slightly longer `duration`, the cards collapse into a satisfying stacked silhouette at different speeds — the bottom card "catches up" last, which feels natural. **Concept to remember:** Stagger depth effects by varying `y`, `scaleX`, `opacity`, and `duration` across sibling elements driven by the same boolean. No `staggerChildren` needed for simple cases. --- ## Paginations A custom animated pagination indicator that tracks which carousel slide is active. The active dot expands into a label pill while the inactive dots shrink — a pattern popularised by the Apple AirPods Pro feature carousel. The active indicator grows via a type: "spring" scale and reveals its text label. All inactive dots shrink and lose their text. Dragging the carousel or clicking a dot both update the same current state. }> ### How it works The indicator is a list of `motion.li` elements. Each item's `scale` is driven by whether its index matches `current`: ```tsx api?.scrollTo(index)} > {current === index ? text : ""} ``` The carousel itself uses [shadcn/ui's Carousel](https://ui.shadcn.com/docs/components/carousel) under the hood, and the `CarouselApi` event `"select"` keeps `current` in sync: ```tsx api.on("select", () => { setCurrent(api.selectedScrollSnap()) }) ``` **Concept to remember:** Spring transitions on `scale` are perfect for indicator dots — they feel bouncy and alive without being distracting. Always use a `type: "spring"` transition for indicators and icon state changes. --- ## Payment A multi-step payment flow where the form card pops in from nothing, the button label morphs through states ("Make Payment" → "Send" → "Sent"), and a checkmark SVG path draws itself on completion. The modal scales in from scale: 0 via AnimatePresence. On confirmation, pathLength animates from 0 → 1 on the SVG checkmark path, producing a "draw-on" effect without any canvas or CSS tricks. }> ### How it works **Step 1 — Modal entrance:** `AnimatePresence` wraps the form, which scales in from `scale: 0` to `scale: 1`: ```tsx {showModal && ( )} ``` **Step 2 — SVG path drawing:** This is the most visually striking trick. Motion can animate `pathLength` on an SVG ``, which makes it look like the path is being drawn in real time: ```tsx ``` `pathLength` goes from `0` (invisible) to `1` (fully drawn). Combined with `AnimatePresence`, the checkmark draws itself when the payment is confirmed and undraws itself when reset. **Concept to remember:** `pathLength` animation on `motion.path` is one of Motion's hidden gems. It works on any SVG path and creates a "draw-on" effect that feels premium without any canvas or CSS tricks. ## Summary Here is a quick reference of the Motion concepts used across all examples: | Concept | Used in | |---|---| | `animate` + `layout` (size morphing) | Dynamic Island | | `AnimatePresence mode="wait"` (content swap) | Dynamic Island, Payment | | `whileHover` / `whileTap` (gesture feedback) | Dynamic Island, Gesture demo | | Staggered `y` + `opacity` offsets | Notifications | | `type: "spring"` on `scale` | Paginations, Transition demo | | `pathLength` SVG drawing | Payment | | `initial` + `animate` (entrance) | Animate demo, all components | The biggest shift when adopting Motion is moving from **CSS-driven** to **state-driven** animations. Instead of writing keyframes or managing class toggles, you describe the desired end state in `animate`, and Motion figures out how to get there. That mental model — *what* rather than *how* — is what makes Motion so ergonomic for building production UI. *Inspired by [ui.lndev.me](https://ui.lndev.me/) and the [Motion documentation](https://motion.dev/).* --- ## Server and Client Components URL: https://1chooo.com/notes/server-and-client-components Date: 2025.05.09 Description: A complete guide to React Server Components and Client Components in Next.js — what they are, what they can do, and how to compose them correctly. # Server and Client Components Next.js App Router ships with a deceptively simple mental model: every component is either a **Server Component** or a **Client Component**. Get the split right and you gain free server-side rendering, zero-cost data fetching, and a smaller JavaScript bundle. Get it wrong and you silently pull your entire page into the browser bundle, losing all of those benefits. Next.js official docs: Server and Client Components — covers the App Router mental model, directive rules, and composition patterns in depth. } /> This note walks through what each type actually is, what rules they follow, and the exact patterns for composing them together. ## Two Rendering Environments Before "Server Component" or "Client Component" means anything, you have to understand that your code now runs in two fundamentally different places. The **server environment** is a Node.js process. It has access to the file system, databases, environment secrets, and the full `node:` API surface. It has no `window`, no `document`, and no DOM. The **client environment** is the browser. It has `window`, `document`, event listeners, and React's stateful hooks. It cannot touch databases or secret environment variables directly. The table above is the single most useful thing to memorise. Every "why doesn't this work?" question about Server and Client Components traces back to one of these environment mismatches. ## React Server Components React Server Components (RSC) are the **default** in Next.js App Router. Any `.tsx` file in the `app/` directory that does not begin with `"use client"` is a Server Component. The headline feature is that an RSC can be an `async` function. You `await` your data directly in the component body — no `useEffect`, no loading state, no client-side waterfall. The query runs on the server; credentials never reach the browser. The HTML arrives pre-filled with data — no loading spinners, no client-side waterfall. }> ```tsx // app/articles/page.tsx — Server Component by default export default async function ArticlesPage() { const articles = await db.query('SELECT * FROM articles ORDER BY date DESC') return (
      {articles.map(a =>
    • {a.title}
    • )}
    ) } ``` Server Components run once per request on the server and stream their HTML output to the browser. The component source code and any libraries it imports never appear in the browser bundle. A 200 kB Markdown parser used only in a Server Component adds zero bytes to the client JS. }> ```tsx // No "use client" → Server Component // This entire file stays on the server. export default async function Post({ slug }: { slug: string }) { const post = await db.posts.findUnique({ where: { slug } }) const html = marked(post.content) // runs server-side return
    } ``` What a Server Component **cannot** do: - Use `useState`, `useReducer`, `useEffect`, or any React hook that requires a live browser runtime - Attach event handlers like `onClick` or `onChange` - Access `window`, `document`, `localStorage`, or any other browser API ## Client Components A Client Component is any component whose file begins with the `"use client"` directive. This is the **boundary declaration** — it tells Next.js that this file (and everything it imports) should be included in the browser JavaScript bundle and hydrated client-side. ```tsx // components/ThemeToggle.tsx "use client" // ↑ This one line is the entire "use client" directive. // It tells Next.js to include this file in the client bundle. export function ThemeToggle() { const [dark, setDark] = useState(false) return ( ) } ``` "use client" is a React directive, not a Next.js one. It must appear at the very top of the file, before any imports. It is a string literal — not a comment and not a function call. Files without this directive are treated as Server Components automatically in the App Router. }> ```tsx "use client" // ↑ must be the first line — before imports // This file now participates in the browser bundle. // useState, useEffect, onClick — all valid here. ``` What a Client Component **can** do that a Server Component cannot: - Call `useState`, `useReducer`, `useContext`, `useRef`, and all other React hooks - Register event handlers (`onClick`, `onChange`, `onSubmit`, …) - Access `window`, `document`, `navigator`, `localStorage` - Use browser-specific APIs like `IntersectionObserver`, `ResizeObserver`, or the Web Audio API - Re-render in response to user interaction without a server round-trip ## The `"use client"` Boundary `"use client"` does not mark a single component — it marks a **subtree boundary**. Every file that a Client Component imports directly becomes part of the client bundle too, regardless of whether those files have their own `"use client"` directive. Switch between the three scenarios above. The key insight: - **Leaf-level client** — `"use client"` appears only on the components that actually need it. The server forms the skeleton; client components are small, focused islands. - **Too much client** — `"use client"` is placed on a high-level component. Every child it imports becomes a client component, losing server features and inflating the JS bundle. - **SC as CC children** — Server Component output is passed as `children` to a Client Component. The SC still runs on the server. Think of the component tree like a one-way membrane. Server Components can freely render Client Components. But once you cross into a client boundary, every file that is statically imported must also be a Client Component. You cannot re-enter the server side from within a client file. }> ``` layout.tsx (Server) └─ Page (Server) └─ Sidebar (Server) └─ FilterForm ← "use client" boundary └─ FilterInput (Client) ← pulled in as client └─ ResetButton (Client) ← pulled in as client ``` ## Composition Patterns There are four patterns to know. Three are valid; one is a common mistake. | Pattern | Valid | Notes | |---|---|---| | SC renders SC | ✓ | Both run on the server; can be `async`; share DB access and secrets freely. | | SC renders CC | ✓ | The most common pattern. SC fetches data and passes it as props to a CC leaf that handles interactivity. | | CC imports SC | ✗ | The imported SC is silently pulled into the client bundle, losing all server-only capabilities. | | CC receives SC as `children` | ✓ | Escape hatch: the *parent* SC renders both and passes the SC output as `children`. The CC never imports the SC directly. | The children pattern deserves a concrete example. Instead of the Client Component importing the Server Component, the *Server Component parent* renders both and passes the SC's output as `children`: The parent page.tsx is a Server Component — it owns both imports and renders ArticleContent inside Modal. Modal never imports ArticleContent directly, so the SC stays out of the client bundle entirely. }> ```tsx // app/page.tsx — Server Component export default async function Page({ params }) { return ( ) } ``` Modal is a Client Component that manages open/close state. It receives children as already-rendered React nodes from the server — it does not need to know or import what is inside. }> ```tsx // components/Modal.tsx "use client" export function Modal({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(true) return open ? {children} : null } ``` The children prop (and any prop typed as React.ReactNode) is the bridge between the two worlds. Props crossing the server–client boundary must be serialisable — plain objects, strings, numbers, arrays — because they are encoded in the RSC payload and sent over the network. Functions and class instances cannot cross the boundary. }> ```tsx // Props that CAN cross the boundary: // Props that CANNOT cross the boundary: refetch()} /> // ✗ function // ✗ class instance // ✗ Map / Set // Exception: children (React nodes from SC are OK) // ✓ ``` ## A Practical Example: Building a Blog All of the rules above click into place fastest with a concrete project. A blog has a natural split: the content and data-fetching are purely server-side work; the interactive features — liking a post, adding a comment, copying a share link — belong to the client. Click any node in the tree to see its role and code. The structure has two routes: - `posts/page.tsx` — lists all posts. It fetches from the database, renders `PostList` → `PostCard`, and streams pre-filled HTML. No JavaScript is sent for any of these components. - `posts/[slug]/page.tsx` — renders a single post. It fetches the post, parses Markdown on the server using a potentially large library (zero bundle cost), and composes three Client Components at the leaves. The three Client Component leaves — `LikeButton`, `ShareMenu`, `CommentSection` — are small and focused. Each one has a single reason to be a Client Component: LikeButton needs useState for optimistic UI and onClick to fire the API call. The server passes initialLikes as a prop so the count is correct on first paint — no extra client fetch needed. }> ```tsx // components/LikeButton.tsx "use client" export function LikeButton({ postId, initialLikes }) { const [likes, setLikes] = useState(initialLikes) async function handleLike() { setLikes(l => l + 1) await fetch(`/api/posts/${postId}/like`, { method: "POST" }) } return } ``` ShareMenu needs window.location and navigator.clipboard — browser globals that do not exist on the server. A one-line check would throw at build time in a Server Component. }> ```tsx // components/ShareMenu.tsx "use client" export function ShareMenu({ title }) { const [open, setOpen] = useState(false) function copyLink() { navigator.clipboard.writeText(window.location.href) } return ( {open && } ) } ``` CommentSection manages a controlled form and an optimistic comment list — both require state that lives in the browser and updates without a page reload. }> ```tsx // components/CommentSection.tsx "use client" export function CommentSection({ postId }) { const [comments, setComments] = useState([]) const [text, setText] = useState("") async function submit(e) { e.preventDefault() setComments(c => [...c, { text, id: Date.now() }]) setText("") await fetch(`/api/posts/${postId}/comments`, { method: "POST", body: JSON.stringify({ text }), }) } return (
    {comments.map(c =>

    {c.text}

    )}