# pean.dev — Full Writing Archive > Full text of all articles published on pean.dev by Andrii Petlovanyi, a product-minded full stack developer from Rivne, Ukraine with practical AI product development experience. Author: Andrii Petlovanyi Role: Product-Minded Full Stack Developer Site: https://www.pean.dev GitHub: https://github.com/andrii-petlovanyi LinkedIn: https://www.linkedin.com/in/andriipetlovanyi/ For the site index, see: https://www.pean.dev/llms.txt --- ## AI-Driven Development: When Code Stops Being the Bottleneck URL: https://www.pean.dev/blog/ai-driven-development-when-code-is-not-the-bottleneck Published: 2026-07-16 Description: AI-driven development shortens the path from product idea to tested software—without handing the important decisions to an AI agent. There is a familiar moment in every product team. Someone writes, “Can we add this small feature?” Twenty minutes later, the “small feature” has three UI states, two awkward edge cases, a new database field, an integration, analytics, an email, and one very uncomfortable question about what happens when the user clicks the button twice. For a long time, the distance between that moment and something real was mostly implementation work. Find the right files. Reload the context. Look up the thing you half remember. Build the skeleton. Break the skeleton. Fix the skeleton. Explain all of it in a pull request. Now, an AI can cover a surprising amount of that ground. It can map an unfamiliar codebase, draft a plan, build a first pass of a screen, find every call site for a type change, write tests, explain a strange log, and even argue with your approach if you ask it to. That changes the pace of work. But here is the important correction: **AI-driven development is not about AI building the product instead of the team.** It is about making the path from a thought to a useful test much shorter. A person sets the direction and the bar. AI removes some of the mechanical drag. The team sees something real earlier, checks its assumptions sooner, and corrects course before getting emotionally attached to the wrong solution. Code has become cheaper. Being wrong about *what* to build has not. ## It is not autocomplete with a bigger ego AI-assisted development is the familiar version: an assistant explains an error, finishes a function, writes a test, or produces the regular expression nobody wants to be personally responsible for. AI-driven development is broader. AI becomes part of the whole loop: - turning a fuzzy idea into a clearer problem; - recovering codebase context faster than opening ten tabs in an editor; - comparing implementation options and surfacing risks; - building a small end-to-end slice instead of isolated snippets; - handling repeatable work such as types, tests, docs, migrations, and refactors; - running checks and helping investigate where a solution disagrees with reality; - shortening the feedback loop after a release. In other words, it is not just another item in the toolbox. It changes the rhythm of making software. I have written separately about [how I use AI coding agents on real projects](/blog/how-i-use-ai-coding-agents-on-real-projects): where they genuinely save time and where I still keep a very close eye on the work. This is the wider view. What happens when AI is not only touching a line of code, but entering every stage of how a product gets made? The old rhythm was easy to recognise: think for a long time, build for a long time, then show a big result. The healthier rhythm now is: clarify quickly, build a small but complete scenario, check whether it is telling the truth, then make the next decision with better information. Not because speed is a new religion. Because the earlier you see the real product, the less likely you are to spend a week producing a beautiful answer to a question nobody asked. ## The biggest change happens before the first line of code It is tempting to look at AI and think only about code generation. Fair enough: seeing a usable component appear in a minute is hard not to enjoy. But the most valuable part often starts *before* code. Imagine a backlog item that says: “Add repeat order.” An AI can help turn that into better questions: - Who is likely to repeat an order, and at what moment? - What happens when an item is no longer available or its price has changed? - Does the user see a pre-filled cart, or does the system try to charge them automatically? - What counts as success: a click, a filled cart, or a paid order? - Which data already exists, and which data would we need to add? - What deliberately stays out of version one? That does not replace product thinking. It forces it into the open. Instead of “please add a button,” a team can make a real agreement: > A customer should be able to repeat a previous order quickly, but before payment they must see current pricing, unavailable items, and have a chance to edit the cart. That is the kind of brief an AI can become genuinely useful with. It is no longer guessing what you meant. Give it a vague task, though, and it will give you vagueness back — confidently, neatly formatted, and with very respectable variable names. ## Progress is becoming a learning loop, not a growing diff It used to be easy to measure movement by what you could see: tickets closed, screens built, lines changed. Once a first version can appear almost instantly, that becomes a weak metric. It rewards more surface area, not more value. A better unit of progress looks like this: 1. We have a hypothesis about a user or system problem. 2. We build the smallest scenario that can test it. 3. We look at real behaviour: users, data, failures, latency, cost. 4. We make the next decision a little smarter. AI is excellent at compressing step two. It does not remove steps one, three, or four. That is the real shift. The advantage does not go to the team that generates interfaces fastest. It goes to the team that learns from reality fastest, and does not mistake the first working version for the answer to every question. ## What a healthy AI-driven workflow actually looks like There is no magic 800-word prompt here. There are just a few habits that make the difference between a genuine speed-up and an expensive lottery. ### 1. Start with context, not “build this” A prompt like “add authentication” almost guarantees a demo. It may be a nice demo. It may even have polished animation. But it is still a demo. Give the model a real frame instead: > This is a Next.js app with email and Google sign-in, an existing design system, and a separate backend. We need registration for a B2B product. New users must confirm their email. Do not add dependencies. First inspect the closest existing flow, then show the plan, files involved, risks, and edge cases. Only write code after that. Nothing about this is magic. It is just reality. An AI does not know your old trade-offs, the constraint your mobile team depends on, or why that slightly scary file in `lib/` exists. If something matters, it needs to be in the context, not only in your head. ### 2. Ask for a map before asking for changes For a non-trivial task, I want the AI to answer four things first: - where the relevant logic lives now; - which files and contracts the change will touch; - what could break; - what the smallest useful plan is. That can sound like extra time. In practice, it saves you from reading a huge diff that was heading in the wrong direction from the first decision. A plan is much easier to challenge than code. And killing a bad assumption before it touches ten files is one of the best kinds of productivity. ### 3. Cut work into vertical slices, not decorative chunks “Build us a CRM” is not a task. It is a way to make both the team and the model miserable. Take one complete scenario instead. A manager creates a contact, the system validates the email, the contact appears in the list, and a retry cannot create a duplicate. That slice can travel from UI to database, be covered by a test, and be shown to someone. It has boundaries, data, behaviour, and an outcome. This is where AI is especially useful: it can connect the frontend, API, types, data model, and checks quickly. Ask it to “do everything,” though, and it will just as eagerly spread logic across the whole project. A small, living slice is almost always more useful than an ambitious chunk that exists only as a list of tickets. ### 4. Let the AI build, then let it annoy you One of the best AI roles is an endlessly patient, slightly annoying opponent. After a first solution, ask it to: - find hidden states and failure paths; - explain what happens on a slow connection or a double click; - suggest a cheaper or simpler version; - check whether the same logic already exists somewhere else; - role-play the user who does everything in the wrong order; - write negative tests, not only the happy path. Do not treat the answer as truth. Treat it as a way to see your own blind spots before production finds them for you at 3am. ### 5. Build speed on checks, not trust When code is generated faster, bugs are generated faster too. No surprise there. That is why types, tests, linters, CI, migration checks, and a manual pass through the critical path are not bureaucracy. They are the seatbelt that lets you go faster. Let the AI run checks, read failures, and fix the obvious issues before review. But a person still needs to decide *what* deserves testing. A model can happily test the easy part and miss the part of the flow the user came for in the first place. ## What I would hand to AI — and what I would not There is no mystical “safe / unsafe” line. There is a cost-of-being-wrong line. I am happy to give AI work where coverage, speed, and consistency matter most: - codebase archaeology and dependency tracing; - mechanical refactors; - the first pass of a well-specified feature; - matching changes across types, DTOs, clients, forms, and API contracts; - first drafts of tests, documentation, and release notes; - log analysis, suspicious-pattern hunting, and checklists; - tedious changes across dozens of files where humans are likely to miss one. These are the decisions I still keep close: - the product “why” and priorities; - architecture, data models, and choices we will live with for years; - permissions, authentication, payments, and personal data; - migrations against real data; - critical integrations and trust-sensitive failure paths; - the boundary of a feature: what we are *deliberately not building* yet. This is not distrust for the sake of a nice slogan. Those decisions have history, consequences, and a price the model will not pay alongside you. AI can prepare very good options. The responsibility for choosing one is still human. ## Vibe coding is great. Just do not confuse it with the finish line Vibe coding brought a healthy sense of play back to development. You describe an idea in plain language, and an hour later there is something to click, show a colleague, or put in front of a test user. For prototypes, internal tools, demos, and early validation, that is genuinely fun. The problem starts when the prototype quietly becomes production. “It works on my machine” does not mean: - it is safe; - it survives a bad connection; - the next person understands how to maintain it; - data does not duplicate when someone clicks twice; - it can be changed in three months without an archaeological expedition. So the rule I like is simple: > Vibe code for discovery. Engineer for consequences. Let AI help you find the shape of an idea and give it a first life. Before you ship it, though, make sure the foundation exists: data integrity, access control, failure states, performance, clarity, and a sane way to change the thing later. ## Context is becoming part of engineering In the old model, context often lived in people’s heads, old Slack threads, and the phrase “you know how we do things here.” AI makes the weakness of that setup painfully obvious. If a project rule is nowhere to be found, an agent cannot infer it reliably. It will fill the gap with the most plausible answer. And “plausible” is not the same as “correct for this product.” That is why a good AI-driven team collects more than code. It builds usable context: - short architecture and style rules; - domain terms that should not be interpreted by guesswork; - examples of good implementations for similar flows; - security boundaries and data that must not go to external services; - commands to verify changes and clear definitions of done; - the history of important decisions: not only what happened, but why. This is not documentation for documentation’s sake. It means a new teammate, future you, or an AI agent does not need to start every task with an archaeological dig. And yes, good context is usually more valuable than another “genius prompt.” ## Roles are not disappearing. They are becoming more honest For developers, AI removes some mechanical typing and makes the valuable parts of the job more visible: seeing dependencies, feeling where a system is fragile, explaining complexity simply, and protecting quality over time. For designers, it can accelerate alternative flows, content, states, and small prototypes. It does not replace taste, empathy, or knowing what a person on the other side of the screen will actually feel. For product people, it is a chance to stop polishing a specification in a vacuum for weeks, build a living scenario in a day, listen to a reaction, and move past abstract arguments. For founders, it creates a way to test more bets for less money. It also creates a temptation to throw ten “AI features” at the market, none of which makes a user’s life easier. Across every role, one skill becomes more valuable: **being able to state an intent clearly.** Not “build a dashboard.” “Help an operations manager see where today’s process has stopped, and what to do next, in under thirty seconds.” When an instruction can be executed very quickly, a vague instruction becomes more expensive than ever. ## The most dangerous illusion: fast output equals progress AI is very good at making us feel productive. In one evening, there can be four screens, three integrations, a stack of commits, and a beautiful pull request. It is extremely tempting to say, “we are flying.” Sometimes you are. Sometimes you are just moving in the wrong direction at an impressive speed. So before a large change, I would make three questions non-negotiable: 1. What specific user or system behaviour are we trying to change? 2. What is the smallest piece of evidence that would prove the change worked? 3. What becomes expensive if we are wrong? Those questions do not slow a team down. They remove fake speed — the kind that ends with an urgent rollback, an apology to users, and half a day spent finding the weird side effect nobody meant to create. ## It is not “AI will replace developers.” Development is getting closer to the product. I am not very interested in the question of whether AI will “replace” developers. It is too flat. The more interesting thing is that code is no longer such an expensive, slow bottleneck. That exposes the work that was always hard: understanding people, choosing the right problem, building a system that holds up, spotting risk before it becomes an incident, and owning the result. AI does not remove the need to think. It removes some of the excuses for thinking too little. Used well, this does not make development less engineering-heavy. It makes it more product-aware, more honest, and hopefully a little more fun. AI can drive the pace. We still choose the direction. ## FAQ ### What is the difference between AI-driven development and AI-assisted development? AI-assisted development is point help: explaining code, writing a function, or generating a test. AI-driven development puts AI into the full workflow, from clarifying the problem and planning through implementation, verification, documentation, and learning after release. It accelerates the process without taking responsibility for the decisions. ### Does AI-driven development mean teams can skip specifications? No. The faster an AI can execute an instruction, the more important it is to make that instruction clear. You do not need a 40-page document, but you do need the user, desired behaviour, constraints, success criteria, and risks. Without that, AI will generate something plausible quickly, not necessarily something needed. ### Where does AI create the most value in software development? It is often strongest in large-codebase research, mechanical refactors, first passes of well-specified features, coordinated frontend and backend changes, tests, documentation, and running checks. It works best when a task has clear boundaries and a short feedback loop. ### What should not be delegated to AI without human review? Architecture, access control, authentication, payments, personal data, migrations, critical integrations, and big product decisions all deserve close human review. AI can help generate options and implementation, but people need to own the final decision and verification because the cost of being wrong is high. --- ## Next.js App Router Architecture in 2026: How I Structure Production Apps URL: https://www.pean.dev/blog/nextjs-app-router-architecture-in-2026 Published: 2026-07-13 Description: A practical way to structure a production Next.js App Router project: routes, Server Components, Server Actions, Route Handlers, auth, validation, caching, and the folders that keep it understandable. Most Next.js architecture advice looks clean because it stops before the hard part. You get a folder tree, a few arrows, and a confident rule like “keep business logic out of components.” Then the real product arrives: authenticated dashboards, public pages, forms, webhooks, background jobs, cached reads, file uploads, and a mobile app that needs the same data. That is when the neat diagram starts collecting exceptions. I have rebuilt the same kind of App Router structure enough times to notice which decisions keep paying rent and which ones only look impressive in a repository screenshot. The architecture that survives is rarely the one with the most layers. It is the one where every new piece of code has an obvious home, and where crossing a boundary is a deliberate act. So this is how I structure a production **Next.js App Router app in 2026**. It is not the only valid structure, and it is not a starter template disguised as a universal truth. It is a set of practical defaults for products that have real users, real permissions, and a good chance of still existing a year from now. ## The short answer My default architecture is: - **Routes compose the screen.** Pages and layouts decide what appears for a URL. - **Server Components read data.** They call server-side query functions directly. - **Client Components own interaction.** State, event handlers, browser APIs, and instant feedback stay in small client boundaries. - **Server Actions handle mutations from my own UI.** Forms and product actions do not need a private HTTP endpoint by default. - **Route Handlers expose HTTP contracts.** I use them for webhooks, public APIs, mobile clients, extensions, feeds, and file responses. - **A data access layer owns authorization.** Hiding a button is UX; checking access beside the data is security. - **Caching follows product semantics.** I cache because I know how stale a result may be, not because a framework feature exists. The sentence I keep in my head is: > Routes compose, domain modules do the work, and every boundary validates what > enters it. That is almost the whole architecture. ![A production Next.js App Router project organized around routes, domain modules, shared UI, and infrastructure](/img/blog/nextjs-production-app-structure.svg) ## Start with product boundaries, not technical layers The official [Next.js project structure documentation](https://nextjs.org/docs/app/getting-started/project-structure) is intentionally flexible. The framework gives meaning to files such as `page.tsx`, `layout.tsx`, `loading.tsx`, and `route.ts`, but it does not decide how the rest of your product should be organized. That freedom is useful. It is also how projects end up with folders called `services`, `utils`, `helpers`, `repositories`, and `managers`, where the only way to find code is to remember which synonym someone chose six months ago. I prefer product or domain boundaries. For a small-to-medium app, the structure often starts like this: ```txt src/ app/ (marketing)/ page.tsx pricing/ page.tsx (app)/ layout.tsx projects/ page.tsx loading.tsx error.tsx [projectId]/ page.tsx _components/ project-header.tsx actions.ts api/ webhooks/ stripe/ route.ts layout.tsx components/ ui/ button.tsx dialog.tsx lib/ auth/ dal.ts projects/ queries.ts mutations.ts schemas.ts integrations/ stripe.ts db.ts ``` This tree is not sacred. The useful part is the direction of ownership. The `app` directory owns routing and screen composition. `lib/projects` owns the reusable server-side rules for projects. `lib/auth` owns session and permission checks. `components/ui` contains genuinely shared interface pieces. The Stripe webhook is public because a `route.ts` makes it public; the Stripe client itself lives outside the route so other server code can reuse it. I colocate a component inside a route when that route is the only place using it. I move it to a shared folder only after it becomes shared. Starting with everything in `components` feels organized for about a week, then turns the folder into a warehouse. Route groups such as `(marketing)` and `(app)` are useful because they let me separate layouts and product areas without changing the URL. Private folders such as `_components` make it clear that a folder is an implementation detail, not another route segment. The goal is not a perfect tree. The goal is that someone opening `app/(app)/projects/[projectId]/page.tsx` can follow the feature without touring the entire repository. ## Let routes compose; do not make them carry the business A page should be readable as a description of the screen. ```tsx // app/(app)/projects/[projectId]/page.tsx import { notFound } from 'next/navigation'; import { getProjectForUser } from '@/lib/projects/queries'; import { ProjectHeader } from './_components/project-header'; import { ProjectActivity } from './_components/project-activity'; export default async function ProjectPage({ params, }: { params: Promise<{ projectId: string }>; }) { const { projectId } = await params; const project = await getProjectForUser(projectId); if (!project) notFound(); return (
); } ``` The route knows the URL, chooses the data needed for the screen, and composes the interface. It does not contain a forty-line database query, permission rules, Stripe mapping, and email side effect. I am not dogmatic about extracting every three-line query. A tiny page can be a tiny page. I extract logic when it represents a reusable product rule, contains security-sensitive behavior, or makes the route hard to scan. That threshold matters. Architecture can become a way of hiding simple code behind five jumps. If opening a user profile requires following `page -> service -> repository -> adapter -> client` and each layer forwards the same arguments, the layers are not protecting anything. They are charging navigation tax. ## Server Components are the default, not the whole strategy App Router pages and layouts are Server Components by default. I keep them that way for as long as the browser is not required. That means Server Components usually handle: - initial data reads - permission-aware rendering - page and layout composition - secret-dependent work - expensive formatting - SEO-critical content - passing small, serializable props into interactive children The browser boundary begins where interaction begins: state, event handlers, effects, refs, browser APIs, or a library that depends on the DOM. I do not measure success by having zero Client Components. A search input that feels immediate is doing useful client work. A sortable table may be easier to use when some state stays in the browser. The mistake is moving an entire page behind `'use client'` because one leaf needs `onClick`. Once a module becomes a client boundary, its imported module graph joins the client bundle. Keeping that boundary low protects the rest of the route without making the interface less interactive. I wrote the component-level rules in more detail in [Server vs Client Components in Next.js](/blog/nextjs-server-vs-client-components-article). The architectural version is simpler: the server owns the screen; the client owns interaction moments. ## Read data directly on the server One habit from older React architectures is surprisingly hard to drop: calling your own API from your own server-rendered page. ```tsx // Avoid this inside a Server Component const response = await fetch(`${process.env.APP_URL}/api/projects`); const projects = await response.json(); ``` That request leaves server code, enters an HTTP endpoint in the same application, parses a response, and often repeats authentication that was already available. It adds a network-shaped boundary without gaining an actual external client. I call the query function directly instead: ```tsx const projects = await getProjectsForCurrentUser(); ``` The query function can still enforce access, shape the returned data, and be tested independently. A Route Handler can call the same underlying function if a mobile app later needs an endpoint. This separation gives me two reusable things: - a server-side capability, such as `getProjectsForUser(userId)` - an optional HTTP representation of that capability The HTTP layer is no longer the business logic. It is one way into it. That is the distinction that keeps a Next.js backend from turning into a set of endpoints the frontend has to call even when both sides are already running in the same process. ## Put authorization beside data access Authentication answers “who is this?” Authorization answers “may this person do this specific thing?” The second question is where production apps usually get interesting. I centralize those checks in a small data access layer and in domain query or mutation functions. ```ts // lib/projects/queries.ts import { verifySession } from '@/lib/auth/dal'; import { db } from '@/lib/db'; export async function getProjectForUser(projectId: string) { const session = await verifySession(); return db.project.findFirst({ where: { id: projectId, members: { some: { userId: session.userId } }, }, select: { id: true, name: true, status: true, updatedAt: true, }, }); } ``` Notice that ownership is part of the query. I do not fetch any project by ID and then hope every caller remembers to compare a `userId` afterward. I also return only the fields the screen needs. That is less about ceremony and more about making accidental data exposure harder. A Server Component does not send all its code to the browser, but data passed into a Client Component still crosses the server-client boundary. Smaller, intentional objects are easier to reason about. Layouts and Proxy can perform optimistic checks for navigation and UX, but I do not treat them as the final security boundary. The official [Next.js authentication guide](https://nextjs.org/docs/app/guides/authentication) recommends secure checks close to data access and explicitly says to treat both Server Actions and Route Handlers like public-facing entry points. That matches the rule I use: every mutation re-checks authorization, even if the button that triggered it was visible only to an admin. ## Use Server Actions for mutations owned by your UI If a form or button in my Next.js interface triggers a mutation, I usually start with a Server Action. ```ts // app/(app)/projects/[projectId]/actions.ts 'use server'; import { updateTag } from 'next/cache'; import { verifySession } from '@/lib/auth/dal'; import { renameProjectForUser } from '@/lib/projects/mutations'; import { renameProjectSchema } from '@/lib/projects/schemas'; export async function renameProject(input: unknown) { const session = await verifySession(); const parsed = renameProjectSchema.safeParse(input); if (!parsed.success) { return { ok: false, errors: parsed.error.flatten().fieldErrors }; } await renameProjectForUser({ ...parsed.data, userId: session.userId, }); updateTag(`project-${parsed.data.projectId}`); return { ok: true }; } ``` This example assumes Cache Components are enabled; I will get to that detail in a moment. The important architecture is outside the syntax. The action is an entry point. It verifies the session, validates untrusted input, calls a domain mutation, and updates the relevant cached read. The deeper mutation owns the database transaction and the rule that this user may rename this project. That makes the action small enough to understand without making it useless. I do not put a Server Action in a global `actions.ts` just because all actions share a directive. I colocate route-specific actions with their route and move shared actions into the relevant domain module when reuse becomes real. For a deeper comparison, see [Server Actions vs API Routes in Next.js](/blog/server-actions-vs-api-routes-in-nextjs-rules-i-use). ## Use Route Handlers when HTTP is part of the product A Route Handler is the right tool when the URL itself is a contract. I use one for: - payment and authentication webhooks - endpoints called by a mobile app or browser extension - public or partner APIs - OAuth callbacks - RSS, XML, calendar, and file responses - endpoints that need explicit HTTP methods, headers, or status codes ```ts // app/api/webhooks/stripe/route.ts import { handleStripeEvent } from '@/lib/integrations/stripe'; export async function POST(request: Request) { const body = await request.text(); const signature = request.headers.get('stripe-signature'); if (!signature) { return new Response('Missing signature', { status: 400 }); } await handleStripeEvent({ body, signature }); return new Response('OK'); } ``` The route owns the HTTP concerns. The integration module verifies the signature, interprets the event, and performs idempotent work. That split makes it possible to test the integration without constructing a framework request for every case. I do not create a Route Handler only because “backend code belongs under `/api`.” In an App Router project, server code can live wherever the server-side domain needs it. A Route Handler is for a real request boundary. The complete decision rules are in [Next.js API Routes in 2026](/blog/nextjs-api-routes-in-2026-route-handlers-server-actions-when-to-use-each). ![How browser reads, UI mutations, and external requests move through a Next.js App Router application](/img/blog/nextjs-app-router-request-flow.svg) ## Validate at the boundary, then work with trusted data TypeScript does not validate a form submission, JSON body, URL parameter, or webhook payload. It describes what our code expects after the value has entered the program. So I validate at every place untrusted data enters: - Server Action arguments and `FormData` - Route Handler bodies, query strings, and headers - dynamic route parameters when the accepted format matters - environment variables during startup - responses from third-party services when a bad shape would be expensive After validation, I pass a narrow typed object into the domain function. I do not pass the whole `Request`, `FormData`, or framework-specific object through three layers. This keeps the center of the application boring in a good way. Domain code works with values such as `{ projectId, name, userId }`, while entry points deal with browsers, HTTP, parsing, and error responses. It also makes testing much more direct. A permission rule or database mutation does not need a fake Next.js request to prove that it works. ## Cache according to how the product may be stale Caching is the part of App Router architecture where generic advice ages fastest. In Next.js 16, Cache Components are available behind the `cacheComponents` configuration flag. When enabled, the model centers on `'use cache'`, `cacheLife`, `cacheTag`, `updateTag`, and `revalidateTag`. If the flag is not enabled, the previous caching model still applies. I make that choice explicit in the project instead of mixing examples from both models. ```ts // next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { cacheComponents: true, }; export default nextConfig; ``` For public data that can be a few minutes old, a cached query may look like this: ```ts // lib/catalog/queries.ts import { cacheLife, cacheTag } from 'next/cache'; import { db } from '@/lib/db'; export async function getPublicCatalog() { 'use cache'; cacheLife('minutes'); cacheTag('catalog'); return db.product.findMany({ where: { published: true }, orderBy: { updatedAt: 'desc' }, }); } ``` After an editor changes a product, the invalidation depends on what the user expects: - `updateTag('catalog')` expires it immediately. This fits a read-your-own-writes flow where the editor should see the change now. - `revalidateTag('catalog', 'max')` serves stale data while refreshing in the background. This fits content where a short delay is acceptable. - `revalidatePath('/catalog')` invalidates a route. It is useful, but less precise than invalidating the data shared by several routes. The official [Cache Components guide](https://nextjs.org/docs/app/getting-started/partial-prerendering) and [revalidation guide](https://nextjs.org/docs/app/getting-started/revalidating) describe the current APIs. The architecture decision still belongs to the product: how stale can this be, who must see a write immediately, and how large is the cost of recomputing it? I am conservative with personalized and permission-sensitive data. Passing a verified user ID into a carefully scoped cached function is very different from putting a broad cache around a function that reads session state and returns private rows. If the security story is not obvious, I leave the query dynamic until it is. A page that is correct and slightly slower is easier to improve than a fast page that occasionally shows the wrong person's data. ## Make loading, errors, and empty states part of the route Production architecture is not only where successful data comes from. It is also where waiting and failure live. The App Router gives each segment natural places for those states: ```txt projects/ page.tsx loading.tsx error.tsx not-found.tsx ``` I keep a loading boundary near the slow work it represents. A dashboard should not turn into one giant spinner because one activity panel is waiting on a slower query. Suspense boundaries and route-level loading files are architecture tools because they decide which parts of the screen can arrive independently. The same goes for errors. A useful `error.tsx` should let the user recover or retry when that makes sense. `not-found.tsx` should describe a missing resource, not catch every permission failure and server exception under the same vague message. Empty states belong in the normal component path. “No projects yet” is not an error; it is a valid product state, often with the most important call to action on the page. These files are easy to postpone because the happy path demos well. They are also what makes an application feel deliberate once the network, database, and users stop behaving perfectly. ## Keep third-party systems behind small server modules Payment providers, email clients, analytics APIs, AI models, and storage SDKs change faster than the product concepts around them. I keep those SDK details in small modules under something like `lib/integrations`. The rest of the app asks for an outcome: ```ts await sendProjectInvitation({ email, projectName, inviteUrl }); ``` It does not build a provider-specific payload inside a Server Action. This is one place where an abstraction earns its keep. It protects the product code from vendor types, centralizes retries and observability, and gives me one place to handle a provider's strange edge cases. I still avoid a grand universal `EmailService` with five implementations nobody plans to use. A small function with a product-shaped name is usually enough. Background jobs deserve a similarly explicit boundary. A Server Action or Route Handler can enqueue work, but long-running or retryable tasks should not pretend the original request will remain alive forever. The queue and worker may live outside Next.js; the important part is that the request path hands off the job intentionally and records enough state to retry it safely. ## The folder structure should grow after the problem does I have a rough progression for Next.js project structure. At the beginning, I colocate aggressively. A page, its small components, and an action can live together. Shared UI goes into `components/ui`. Database access can start in a focused `lib` module. As the product grows, I extract around pressure: - repeated permission checks become a data access function - repeated business rules become a domain mutation - an external client creates a real Route Handler - a provider SDK gets an integration module - several routes sharing data get a cache tag strategy - a slow section gets its own Suspense boundary I do not add a repository layer because a diagram says applications have one. I add it when database details genuinely need isolation or multiple callers are duplicating query behavior. I do not add a global state library because the app has state. I add one when URL state, server state, local state, and context no longer cover a real cross-screen interaction cleanly. The best Next.js architecture is usually one step ahead of current complexity, not six steps ahead of imaginary scale. ## A practical request walkthrough Imagine a user renames a project from a dashboard. Here is the full path I want: 1. A Server Component loads the project through `getProjectForUser`. 2. The page passes the project name and ID to a small interactive form. 3. The form calls a colocated Server Action. 4. The action verifies the session and validates the submitted values. 5. A domain mutation updates only a project the user may edit. 6. The mutation completes before the relevant cache tag is invalidated. 7. The UI shows the new name immediately and handles validation errors without losing the form state. Now imagine Stripe changes that project's subscription. 1. Stripe calls a Route Handler at a stable public URL. 2. The handler reads the raw body and required signature header. 3. The integration module verifies the signature. 4. The event handler checks idempotency before changing the database. 5. The relevant subscription and project cache tags are revalidated. 6. The handler returns the HTTP status Stripe expects. The database mutation may touch the same project in both flows. The entry points are different because the callers and contracts are different. That is what I mean by boundaries. They are not folders for their own sake. They describe who is calling, what can be trusted, and what kind of response is required. ## Mistakes I would avoid in a new App Router project These are the patterns I now treat as early warnings: - putting `'use client'` on a page because one nested control is interactive - fetching your own Route Handler from a Server Component - trusting a layout redirect as the only authorization check - putting every Server Action in one global file - caching private data before the ownership model is clear - invalidating the entire route when one tagged query changed - returning full database records to interactive components - passing `Request` or `FormData` deep into domain code - creating `utils.ts` as the default home for unrelated logic - adding service and repository layers that only forward arguments - performing a long retryable job inside the original request - treating `loading.tsx`, `error.tsx`, and empty states as final-week polish None of these choices automatically ruins a project. I have shipped several of them. The problem is accumulation. Each one makes the next feature slightly harder to place, slightly harder to secure, or slightly harder to debug. Good architecture is mostly the absence of that friction. ## The production checklist I actually use Before I call an App Router feature complete, I ask: - Can I understand the route by reading its page and layout? - Is the client boundary limited to code that needs browser capability? - Does server-rendered code call domain queries directly? - Does every Server Action validate input and re-check authorization? - Does every Route Handler behave like a public endpoint? - Are sensitive reads scoped by ownership in the data access layer? - Is the returned data narrower than the database record? - Does the cache policy state how stale the data may be? - Will the user see their own write when they expect to? - Are loading, empty, not-found, and error states intentional? - Can external events be retried without duplicating work? - Is shared code actually shared, or merely predicted to be shared? If the answers are clear, the folder tree is usually fine. ## FAQ ### What is the best Next.js App Router architecture in 2026? There is no single best folder tree. A strong default is to let routes compose screens, use Server Components for reads, keep Client Components focused on interaction, use Server Actions for UI-owned mutations, use Route Handlers for real HTTP contracts, and centralize authorization close to data access. ### How should I organize a Next.js App Router project? Organize routes by URL and product area inside `app`, colocate route-specific components and actions, keep truly shared UI in a shared components folder, and group reusable server logic by domain such as `projects`, `billing`, or `auth`. Add technical layers only when they remove real duplication or isolate a real boundary. ### Should a Server Component call a Next.js Route Handler? Usually no. If both live in the same application, call the shared server-side query or domain function directly. Use a Route Handler when an external client or an HTTP-specific contract actually needs the endpoint. ### Where should authentication and authorization live in Next.js? Session verification can be centralized in a data access layer, while secure authorization checks should also happen close to each sensitive query and mutation. Layouts and Proxy can improve navigation and UX, but they should not be the only protection for private data or actions. ### Should I use Server Actions or Route Handlers for mutations? Use a Server Action when the mutation is triggered by your own Next.js UI. Use a Route Handler when a webhook, mobile app, extension, partner, or other HTTP client needs a stable endpoint. Both must validate input and check permissions. ### Do I need a service and repository layer in Next.js? Not by default. Add a layer when it isolates meaningful complexity, protects a domain rule, or removes duplication. If it only forwards the same arguments to the next file, it is probably making the code harder to follow without making it safer. ## Conclusion The App Router gives us more useful primitives than older Next.js applications had: Server Components, nested layouts, streaming boundaries, Server Actions, Route Handlers, and explicit caching controls. More primitives do not require a more elaborate architecture. They require clearer decisions about where each kind of work belongs. My production Next.js architecture in 2026 is intentionally plain. Routes compose screens. Server Components read. Client Components interact. Server Actions mutate for the UI. Route Handlers speak HTTP. Domain modules protect the rules that should survive all of those entry points. The structure will change as the product grows. That is healthy. The important part is that it grows in response to real pressure, while the path from a user action to a permission check to a database write stays short enough to hold in your head. That is the kind of architecture I trust in production: not the one that looks most advanced on day one, but the one that still makes sense when the simple app is no longer simple. --- ## MCP Is Not the Product: What AI Agents Actually Need Before Tool Access URL: https://www.pean.dev/blog/mcp-is-not-the-product-ai-agent-tool-access Published: 2026-07-06 Description: A human, practical look at MCP, AI agents, tool access, permissions, context, and why connecting an agent to more systems is not the same as designing a useful AI product. There is a funny moment that happens with almost every new technical layer. At first, nobody knows what it is. Then suddenly everyone talks about it like it explains the future. MCP is in that phase now. If you spend time around AI products, coding agents, developer tools, or agentic workflows, you have probably seen it everywhere. MCP servers. MCP clients. MCP tools. MCP marketplaces. MCP for databases. MCP for browsers. MCP for internal systems. MCP as the missing piece that will finally let AI agents do real work. Some of that excitement is justified. [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) gives AI applications a more standard way to connect to external systems: files, databases, APIs, search tools, business software, workflows, and other sources of context. [Anthropic introduced it](https://www.anthropic.com/news/model-context-protocol) as an open standard for connecting AI assistants to the systems where data lives, and OpenAI's [Apps SDK](https://developers.openai.com/apps-sdk) also builds on MCP for ChatGPT apps. That matters. But I do not think MCP is the product. It is infrastructure. Useful infrastructure, yes. Important infrastructure, probably. But still infrastructure. The product question is different: **what should the agent be allowed to know, decide, and do for the user?** That is the question I care about more. ## The old AI feature was mostly a box A lot of early AI features had the same shape. There was an input. There was a prompt. There was an answer. Maybe the answer was a summary. Maybe it rewrote text. Maybe it generated a checklist. Maybe it answered a question from a document. That kind of feature can be useful, but it is still mostly a box. The user puts something in. The model replies. The product displays the reply. The newer version is different. Now the agent can reach outside the box. It can read a file. Search a codebase. Query a database. Open a browser. Create a ticket. Update a CRM record. Draft a pull request. Look at a calendar. Call another internal service. Combine several steps into one workflow. That is a real shift. It is also where the stakes change. When an AI answer is wrong, the user can ignore it. When an AI action is wrong, the system may already have changed something. That difference is where product design starts to matter. ## More tools do not automatically make a better agent The tempting idea is simple: > If the agent can access more tools, it will become more useful. Sometimes that is true. An AI coding agent is more useful when it can read the repo, run tests, inspect errors, and understand the local project. A support assistant is more useful when it can see the ticket history and product docs. A research agent is more useful when it can search, cite sources, and keep notes. But the sentence is still incomplete. More tools help only when the agent knows when to use them, what not to touch, and how to recover when something is uncertain. Without that, tool access becomes noise. The agent sees too much. It calls the wrong thing. It mixes stale data with live data. It performs an action before the user has approved it. It sounds more capable while becoming harder to trust. That is why I do not think "we added MCP" is a product milestone by itself. It is closer to saying: > We added a door. Good. Where does it lead? Who has the key? What happens when someone walks through it? Can they delete anything? Can they spend money? Can they email a customer? Can they read private data? Can we see what they did afterward? Those are the product questions. ## MCP solves one kind of mess Before MCP, connecting AI tools to external systems was often a pile of one-off integrations. One app had its own way to connect GitHub. Another had its own database connector. Another had a custom browser tool. Another had a plugin system. Every tool needed its own shape, every client needed its own integration, and the same work repeated again and again. MCP helps with that. The official docs describe MCP as an open standard for connecting AI applications to external systems. In plain language: it gives AI clients and external tools a common way to talk. That is a good thing. Standards reduce glue work. They make integrations more reusable. They make it easier to expose a tool once and connect it to different AI applications. They give developers a shared mental model instead of another custom connector for every product. I like that. But MCP does not answer the harder product questions for you. It does not decide whether your agent should be allowed to update production data. It does not decide when a user confirmation is required. It does not decide how much context is too much. It does not decide which actions should be reversible. It does not decide how the interface should explain what happened. It gives you a way to connect the agent to things. You still have to decide what kind of relationship the agent should have with those things. ## Tool access needs a job, not just a capability When I think about giving an agent access to a tool, I try to start with the job. Not the technology. Not the protocol. Not "would it be cool if the agent could call this?" The job. For example: - help a developer understand why a test is failing; - help a user find the right moment in a long video; - help a support person prepare a reply with the right account context; - help a founder compare several landing pages before a launch; - help an operator spot broken links before publishing a site; - help a team turn a messy request into a scoped implementation plan. Once the job is clear, tool access becomes easier to reason about. The agent does not need every possible tool. It needs the tools that help complete that job without creating unnecessary risk. For a coding workflow, reading files and running tests may be reasonable. Pushing to `main` without review probably is not. For a support workflow, reading customer history may be useful. Sending a refund without confirmation may be too much. For a browser workflow, reading the current page may be enough. Submitting a form, accepting permissions, or purchasing something should be a separate boundary. The point is not to make agents weak. The point is to make them legible. The user should understand what the agent can do, what it cannot do, and where the user is still in control. ## Context is not the same as permission One mistake I see in AI product thinking is treating all access as the same thing. It is not. There is a big difference between: - reading a document; - searching a database; - seeing metadata; - drafting a change; - writing to a system; - deleting something; - sending a message; - spending money; - changing permissions; - publishing content. Those are not just different API calls. They are different levels of trust. An agent may need read access to understand a situation. That does not mean it should have write access. It may need to draft an update. That does not mean it should apply the update. It may need to inspect a customer's account. That does not mean it should change billing. This sounds obvious when written out. It becomes less obvious when a product team says: > Let's connect the agent to our internal tools. That sentence hides everything important. Which internal tools? Which data? Which actions? Which users? Which environments? Which logs? Which approval steps? Which failure modes? The product is not "connected to internal tools." The product is the boundary around that connection. ## The best agent flows have a pause in the right place I do not think every agent action needs a confirmation. That would make the product painful. If the agent has to ask before every harmless read operation, the user will stop using it. If it needs approval to summarize a document the user just uploaded, the workflow becomes silly. But high-impact actions need a pause. Not a fake pause. Not a tiny "Are you sure?" modal that everyone clicks through. A meaningful pause. Something like: > I found the issue. I can update these three files and run the tests. Or: > I drafted the customer reply from the ticket history. Review it before sending. Or: > I can create this Linear issue with the following scope and acceptance criteria. Or: > This action will change production data. I need your approval before I continue. That pause does a few things. It gives the user a chance to correct the agent's understanding. It makes the agent show its work before acting. It turns a black-box operation into a shared decision. And it creates a natural place to explain risk. That is good product design. It is also good safety design. ## Audit trails are part of the interface When an AI agent can use tools, logs stop being only a backend concern. They become part of the user experience. The user should be able to answer: - what did the agent read? - what did it call? - what did it change? - what did it decide not to do? - what failed? - what needs my review? - where did this answer come from? This does not mean showing raw JSON to everyone. Most users do not want that. But the product should expose enough traceability that the agent does not feel like a ghost moving through the system. In developer tools, that might mean showing commands, files touched, test output, and diffs. In support tools, it might mean showing the sources used for a reply. In research tools, it might mean showing citations and search history. In browser extensions, it might mean showing what page data was inspected and what stayed local. The exact interface depends on the product. The principle is the same: **if the agent can act, the user needs a way to inspect the action.** Without that, trust becomes vibes. And vibes are not enough when software starts changing real things. ## A real example from coding agents Coding agents make this easier to see because the workflow is concrete. When I use an AI coding agent, I am comfortable giving it a lot of read access inside the repo. It can search files, inspect call sites, read tests, and understand how a feature is wired. I am also comfortable letting it run checks. Types, linting, tests, local builds, maybe a browser preview if the task touches UI. That kind of tool access is useful because the job is clear: understand the codebase and verify a change. But I still want boundaries. I want to review the diff. I want to decide whether an abstraction belongs in the codebase. I want to approve commits and pushes. I want anything touching auth, data access, migrations, payments, or destructive operations to be treated differently from a copy change. The agent can do a lot. It should not own the decision. That is the balance I keep coming back to: let the agent do the work that benefits from speed and context, but keep the expensive decisions visible. MCP can make the connections cleaner. It does not remove the need for judgment. ## A real example from product workflows Imagine a small SaaS dashboard with an AI assistant. The naive version is: > The assistant can access everything in the dashboard and take actions for the user. That sounds powerful. It is also too vague to be useful. A better version is more specific: > The assistant helps users understand account activity, draft follow-up tasks, and prepare changes, but it asks for confirmation before sending messages, changing billing data, deleting records, or updating permissions. Now the product has shape. You can design the tool list around it. Read account activity. Search docs. Draft a note. Create a task. Suggest a setting change. Show a confirmation before applying it. The user can understand that. The team can test that. The logs can reflect that. The permissions model can support that. This is where AI product work becomes interesting. Not at "add agent." Not at "add MCP server." At the moment when you decide what the agent is actually allowed to do on behalf of a person. ## The UI matters more than people think A lot of agent discussion is backend-heavy. Protocols. Servers. Tools. Schemas. Transports. Auth. Hosting. All of that matters. But the user meets the agent through the interface. If the interface makes the agent look more certain than it is, the product becomes risky. If the interface hides the sources, the user cannot verify. If the interface hides pending actions, the user does not know what is about to happen. If the interface does not separate "drafted" from "sent", people will misunderstand the state. If the interface buries permissions in settings nobody reads, tool access becomes invisible. That is why I like agent interfaces that show state clearly: - reading; - thinking; - planning; - asking for approval; - running a tool; - waiting; - failed; - completed; - needs review. Those states sound boring. They are not. They are how the user keeps their footing. The more capable the agent becomes, the more important those small state cues become. ## The dangerous version feels effortless There is a version of AI product design that tries to remove every bit of friction. One prompt. The agent does everything. No steps. No confirmation. No exposed reasoning. No visible sources. No boundaries. No audit trail. Just a smooth answer or a completed task. That can feel impressive in a demo. It can also be the wrong shape for real work. Some friction is useful. Reviewing a diff is friction. Confirming a payment is friction. Approving an email before it is sent is friction. Choosing which data source the agent should use is friction. Reading a warning before a production change is friction. But that friction exists because the action matters. The trick is not to remove all friction. The trick is to put friction where it protects the user, and remove it where it only slows them down. That is the difference between a powerful agent and a reckless one. ## What I would decide before building Before building an AI agent feature with MCP or any other tool-access layer, I would answer a few questions in plain language. Not architecture language. Product language. What job is the agent helping with? What data does it need to read? What data should it never see? Which actions can it perform automatically? Which actions require confirmation? Which actions should only be drafted? Can the user undo the action? What should be logged? What should be shown to the user? What happens when the tool call fails? What happens when the agent is unsure? What happens when the user asks for something outside the allowed boundary? Those answers should shape the MCP server, the tool definitions, the prompts, the UI, the permissions model, and the tests. If those answers are unclear, adding a protocol will not make the product clearer. It may only make the unclear product more capable of doing the wrong thing. ## Where MCP fits in my mental model I do think MCP matters. I would not ignore it. If I were building an AI product that needed to connect to external tools, internal systems, user files, business data, or cross-app workflows, I would want to understand MCP properly. I would want to know where it fits, where it is overkill, and where a simple direct integration is still enough. But I would keep it in the right layer. MCP is the connection layer. The product is the workflow. The trust comes from boundaries. The usefulness comes from context. The quality comes from choosing the right job. The safety comes from permissions, confirmations, and logs. The user experience comes from making all of that understandable. That is the part I do not want to lose in the excitement. It is easy to talk about agents like the main problem is giving them more power. I think the better problem is giving them the right amount of power, in the right moment, with the user still able to see and steer what is happening. That is less flashy than "connect everything." It is also much closer to a product people can trust. ## Conclusion MCP is useful because it gives AI applications a more standard way to reach tools, data, and workflows. That is real progress. But the protocol is not the product. The product is what you decide to expose. What you decide to hide. What you ask the user to confirm. What you log. What you make reversible. What context you provide. What actions you refuse to automate. An AI agent with tool access is not just a smarter chatbot. It is a user acting through software with a second layer of interpretation in the middle. That deserves careful design. Not fear. Not hype. Care. Because once the agent can do things, the most important question is no longer: > Can it? The question is: > Should it, right now, for this user, with this context? That is where the real product work begins. ## FAQ ### Is MCP only for developers? No. Developers build MCP servers and clients, but the product impact is broader. MCP can affect how AI assistants connect to business tools, documents, databases, internal workflows, and customer-facing applications. ### Does every AI product need MCP? No. If a product only needs one narrow integration, a direct API connection may be simpler. MCP becomes more interesting when the product needs reusable tool access across multiple AI clients, workflows, or external systems. ### Is tool access safe if it goes through MCP? Not automatically. A standard protocol can make integration cleaner, but safety still depends on permissions, authentication, tool design, confirmations, logging, and what actions the product allows the agent to perform. ### What is the first product decision to make? Decide the job before deciding the tools. Once the job is clear, it becomes much easier to decide what the agent should read, what it can draft, what it can change, and where the user must approve the next step. --- ## What an AI-Ready Website Actually Means in 2026 URL: https://www.pean.dev/blog/what-an-ai-ready-website-actually-means-in-2026 Published: 2026-06-26 Description: A practical, human look at AI-ready websites in 2026: what still matters for people and search, where llms.txt can help, and why most GEO tricks are less important than clear, crawlable, useful content. Every few months, the web gets a new phrase that sounds like everyone should panic. Right now one of those phrases is **AI-ready website**. Sometimes it is called GEO. Sometimes AEO. Sometimes LLMO. Sometimes someone adds another acronym and says SEO is dead again. I understand why people pay attention. Search is changing. AI answers are showing up in more places. People ask ChatGPT, Perplexity, Google AI Mode, Claude, and browser agents questions they used to type into a search box. The path from "I need something" to "I found a website" is less predictable than it was a few years ago. But when I look at the actual work, I keep coming back to a less dramatic version: **an AI-ready website is mostly a website that explains itself clearly.** Not only to a person. To search engines. To crawlers. To AI systems trying to pull a useful answer from a messy internet. To agents that may open your site, read a few pages, and decide whether your content is relevant to the task. That sounds smaller than the hype. It is also more useful. ## The part I do not want to pretend I do not think there is a secret AI switch you can add to a site. There is no magic meta tag that makes a page cited by AI systems. There is no guaranteed "AI answer engine ranking factor" that works like a vending machine. Add `llms.txt`, receive citations. Add schema, receive traffic. Rename SEO to GEO, become future-proof. That is not how any of this works. [Google's own guidance for AI features in Search](https://developers.google.com/search/docs/appearance/ai-features) says the same basic thing in a more official voice: there are no extra special requirements for AI Overviews or AI Mode beyond being eligible for Search and following the fundamentals. Pages still need to be crawlable, indexable, useful, and eligible to show snippets. Preview controls such as `nosnippet`, `data-nosnippet`, `max-snippet`, and `noindex` still matter. So the first thing I would say to a founder, client, or developer is: > Do not rebuild your whole site around AI search anxiety. Start with the boring questions. Can the page be crawled? Can the page be indexed? Does the title say what the page is? Does the content answer a real question? Can someone tell who wrote it or who is responsible for it? Are important pages connected with internal links? Is the content visible in the HTML, not trapped inside a fragile client-only experience? Does the page have enough context to stand on its own? That is still the center of the work. The AI layer does not remove the basics. It makes weak basics more obvious. ## What actually changed The old mental model was simple: 1. write a page; 2. let Google index it; 3. hope a search result sends traffic. That model still exists, but it is no longer the only path. Now a user might ask an AI assistant a broad question: > What are the best tools for checking whether my website is ready for AI search? Or a more specific one: > Is this developer a good fit for building a Chrome extension with an AI feature? Or: > Compare these three product pages and tell me which one is clearer. In those moments, your site might not be visited in the old way first. It might be read, summarized, compared, quoted, skipped, or used as one source among several. That changes what "good website content" needs to do. It is not enough for a page to look nice in a browser. It has to survive being reduced to text. It has to make sense when pulled out of its layout. It has to contain the important facts in a way that a machine can find without guessing too much. That does not mean writing for robots. It means not making robots guess what humans would also struggle to understand. ## I think about three readers now When I work on a page in 2026, I usually think about three readers. The first reader is still a human. They are impatient. They scan. They want to know if they are in the right place. They care about tone, trust, examples, friction, and whether the page feels like it was written by someone who understands the problem. The second reader is search. Search needs crawlable URLs, internal links, canonical signals, titles, headings, metadata, structured data where it makes sense, and content that is not hidden behind needless technical friction. The third reader is an AI system or agent. This reader is strange because it is not really a reader in the human sense. It does not admire your layout. It does not care that a section had a beautiful animation. It may only see a small slice of the page. It may compress the content. It may use your page as context for a user request rather than as a destination. But it still benefits from the same things people benefit from: - clear page purpose; - plain explanations; - specific examples; - stable URLs; - visible text; - useful internal links; - author and organization context; - schema that matches the page; - concise summaries of what matters; - no fake authority; - no walls of vague marketing copy. That is why I dislike the phrase "write for AI". Most of the time the better instruction is: **write so the page can be understood without you standing beside it explaining what you meant.** ## `llms.txt` is useful, but not magic I like the idea behind [`llms.txt`](https://llmstxt.org/). The proposal is simple: put a Markdown file at `/llms.txt` that gives language models and agents a cleaner map of the important parts of your site. It can link to docs, product pages, articles, project pages, policies, or any other content that helps a system understand what the site is about. That is a sensible idea. In fact, this site has an [`/llms.txt`](/llms.txt) route and a fuller [`/llms-full.txt`](/llms-full.txt) route because it fits the kind of site pean.dev is: a personal portfolio, project hub, and writing archive. If an agent wants a clean index of who I am, what I build, and which articles matter, I would rather provide that context directly than make it scrape random navigation. But I would not sell `llms.txt` as an AI SEO cheat code. It is better to think of it as a front desk. It says: > Here is what this site is. Here are the important rooms. Here is what you should read first. That can help agents and tools. It can make your site easier to process. It can be especially useful for documentation, product catalogs, personal sites, universities, API references, and any site where a curated map is genuinely helpful. But if the underlying pages are thin, confusing, or untrustworthy, `llms.txt` will not rescue them. A clean map to weak content is still weak content. ## The page has to answer better than the snippet One thing AI search has changed for me is the bar for content. A lot of older SEO content was built around capturing a keyword, answering the obvious question, and keeping the reader moving just long enough to convert. That can still get clicks in some places, but it feels weaker now. If an AI answer can summarize the generic version in five seconds, the page needs a reason to exist beyond the generic version. That reason might be: - real experience; - original examples; - a clear point of view; - implementation details; - tradeoffs; - screenshots or product evidence; - a better explanation than the average answer; - a strong comparison; - trust that comes from showing how something was built. This is why I keep writing posts from the angle of "how I think about this while building real products" instead of trying to produce neutral encyclopedia pages. Neutral summaries are easy to generate. Experience is harder to fake. So if someone asks me how to make a site more ready for AI discovery, I do not start with a plugin. I start with the content. Does this page say anything a generic answer would not say? Does it include details that prove someone actually worked through the problem? Does it help the reader make a decision? Could another developer, founder, or product person use this page as a real reference? If the answer is no, the problem is not only AI readiness. The problem is that the page is not useful enough yet. ## The technical layer still matters I do not want to make this sound like writing alone fixes everything. The technical layer matters a lot. For a normal content or product site, I would check: - `robots.txt` and whether important pages are allowed to be crawled; - `sitemap.xml` and whether important URLs are included; - canonical URLs; - page titles and meta descriptions; - Open Graph images and social preview data; - article, product, organization, or local business schema where appropriate; - clean headings; - server-rendered or easily extractable content; - internal links between related pages; - redirects and broken links; - whether preview controls are intentionally set; - whether public pages are actually public to the crawlers you care about. None of that is new. But the reason is slightly broader now. You are not only helping a search result page understand your content. You are helping a wider set of systems decide what your page is, whether it can be trusted, and whether it is worth using as context. This is where I think tools like [Crowra](https://www.crowra.pean.dev/) make practical sense. Not because they can guarantee AI citations. They cannot. But because they can help review the signals around the page without turning the process into twenty tabs and a spreadsheet. The goal is not to please a machine. The goal is to remove unnecessary ambiguity. ## Control is becoming part of the conversation There is another side to this that people sometimes skip. AI readiness is not only about being included. It is also about deciding what access you actually want. Some websites want maximum discovery. Some publishers want tighter control. Some companies want search indexing but do not want their content used in certain AI training contexts. Some businesses want product pages to be easy to cite, but support docs to stay behind authentication. Some creators are asking whether crawler access should be free, paid, blocked, or negotiated. That conversation is getting more real. Cloudflare's [Pay Per Crawl](https://blog.cloudflare.com/introducing-pay-per-crawl/) experiment is one visible sign of that shift. It is not something every small site needs to implement tomorrow. But it shows where the web is moving: crawler access is becoming a product and business decision, not only a technical default. For a small business site, personal site, or product landing page, the decision can be simpler: - what should be public? - what should be indexed? - what should be easy for agents to understand? - what should not be exposed at all? - which crawlers do we want to allow? - are we using `Google-Extended`, `nosnippet`, or other controls intentionally? I like making those choices explicitly. Default openness can be fine. Default blocking can be fine too. The mistake is not knowing which one you chose. ## What I would actually fix first If I had one afternoon to make a site more AI-ready, I would not start by chasing every new acronym. I would do this: 1. Pick the 5 to 10 pages that matter most. 2. Make sure each page has a clear purpose in the title, H1, opening paragraph, and metadata. 3. Add or improve internal links so important pages are not isolated. 4. Check that the main content is crawlable and visible without requiring a fragile client-only flow. 5. Add structured data only where it honestly matches the page. 6. Create a simple `/llms.txt` if the site has enough public content to justify a curated map. 7. Review `robots.txt`, sitemap, canonical URLs, and preview controls. 8. Remove vague copy that sounds good but says very little. 9. Add real examples, proof, screenshots, project details, or decision-making context. 10. Re-read the page as if an AI assistant had to answer questions from it without guessing. That last step is surprisingly useful. Ask questions like: > What does this company actually do? > Who is this for? > What problems can this person solve? > What projects prove it? > What should I read next? > Is this page a real source, or just a polished brochure? If the page cannot answer those questions for a person, an AI system will not magically understand it either. ## The best version still sounds human This is the part I care about most. A lot of AI optimization advice quietly pushes people toward worse writing. More definitions. More repeated keywords. More "comprehensive guide" language. More awkward sections created because someone thinks an answer engine might like them. More pages that feel like they were written for a content machine, reviewed by a content machine, and published for another content machine to summarize. That is a miserable direction for the web. The better version is not less human. It is more human and more structured. Say what you mean. Use headings that help. Explain tradeoffs. Link to sources. Show your work. Do not pretend certainty where there is none. Make the page easy to scan, but do not flatten every thought into a bullet list. Use schema and metadata as support, not as a substitute for substance. If you are writing from experience, let the experience show. That is what I trust when I read a page. It is also what I want AI systems to find when they read mine. ## How I think about pean.dev For this site, "AI-ready" does not mean turning every page into an SEO landing page. That would make it worse. pean.dev is a personal site. It should feel like a real developer's home on the web: projects, writing, contact, context, and enough technical clarity that someone can understand what I build without needing a sales call first. So the useful work is pretty grounded: - keep project pages specific; - write posts from actual product and engineering decisions; - expose clean metadata; - keep the sitemap and canonical URLs healthy; - provide `llms.txt` and `llms-full.txt` for agent-friendly context; - make public pages easy to read as text; - connect related posts and projects; - avoid pretending that any of this guarantees AI visibility. That is a calmer strategy. It is also the one I believe in more. ## Conclusion An AI-ready website in 2026 is not a website that blindly chases AI. It is a website with less ambiguity. Clearer pages. Better source signals. Useful content. Honest structure. Crawlable routes. Thoughtful access controls. A good map for humans, search engines, and agents. Some of the work is technical. Some of it is editorial. Some of it is just having enough respect for the reader to say something concrete. That last part is easy to underestimate. But the more AI gets involved in how people discover and compare information, the more important it becomes to have a site that can stand on its own. Not because AI needs special treatment. Because clarity travels better. ## FAQ ### Does an AI-ready website need `llms.txt`? No. `llms.txt` is not required for Google AI Overviews or AI Mode. I still like it for sites with useful public content because it gives agents and tools a clean, curated map of the site. ### Is GEO replacing SEO? I do not think so. The work overlaps too much. Crawlability, useful content, internal links, structured data, and trust signals still matter. GEO is better treated as an extra lens on good web publishing, not a full replacement. ### Should I block AI crawlers? It depends on the site. A public portfolio, product site, or documentation site may benefit from being easy to discover. A publisher, paid knowledge product, or private community may want stricter controls. The important thing is to choose intentionally. ### What is the first thing I would fix? I would fix the pages that matter most. Make their purpose obvious, make the main content crawlable, connect them with internal links, and add enough specific context that the page is useful even when the design is stripped away. --- ## How I Actually Use AI Coding Agents on Real Projects (and Where I Still Don’t Trust Them) URL: https://www.pean.dev/blog/how-i-use-ai-coding-agents-on-real-projects Published: 2026-06-10 Description: A practical, senior-level look at using AI coding agents like Claude Code on production codebases: where they save real time, where I keep full control, and how I structure work so the output is actually shippable. A year ago, if someone told me they were shipping production code where an AI agent wrote most of it, I'd have asked "which production." A landing page, sure. A real backend with real users and real data? I wasn't convinced. I'm a lot less skeptical now. Not because the models suddenly got smarter overnight — they got better, but that's not the main thing. What changed is how I work with them. So this isn't a "10x your output" post. It's closer to notes from someone who's been doing this daily for months: what an agent is genuinely good at on a real codebase, what it still gets wrong, and the habits that keep the wrong parts from costing me anything. I work across a Next.js site, an Expo app with a NestJS backend and PostgreSQL, and a few Chrome extensions (Cuelio, Crowra, TableSnap). Different stacks, different stakes, same agent. The lessons mostly transfer. ## The mistake I made early on My first instinct was to treat the agent like a junior dev I could hand a ticket to and walk away. "Here's what I need. Go build it." For small, boring, low-risk stuff, that's actually fine. But the moment a task touches architecture, data flow, or "how we do things in this codebase," that approach falls apart in a specific way: you get code that compiles, looks reasonable on a skim, and is wrong in some quiet way you only notice a week later. It took me a while to realize this isn't really an "AI" problem. "Here's what I need, go build it" is a bad spec for a human too — we just don't feel it, because a human teammate fills in the gaps using context they already have. They remember why the last attempt at this broke. They know which shortcut is fine here and which one bit us in production six months ago. An agent doesn't have any of that unless you hand it over. And it won't sit there confused — it'll just fill the gap with whatever sounds plausible. Plausible and correct-for-your-codebase are not the same thing, and the gap between them is exactly where the subtle bugs live. What actually changed for me wasn't "trust it more" or "trust it less." It was treating context as part of the job, not an optional extra. I now think of the agent as a very capable engineer who started an hour ago and hasn't read the codebase yet — because, functionally, that's what it is on every single task. That one mental shift did more for the quality of the output than any prompt trick I've tried. ![An AI coding agent workflow: plan, scoped task with context, agent execution, human review, ship](/img/blog/ai-coding-agent-workflow.svg) ## Where it actually earns its keep Once I stopped expecting it to "just know" things, a few areas turned out to be genuinely strong — stronger than I expected. **Codebase archaeology.** "Where does this value actually get computed?" "What breaks if I change this function's signature?" "Why does this component refetch on every route change?" These used to be ten minutes of grep and scrolling through files I half-remembered. Now it's one prompt and an answer with file paths and line numbers I can check in seconds. Honestly, this alone would justify the workflow change for me — it's the thing I do most, all day, on every project. **Mechanical refactors.** Renaming a prop across forty components, migrating a batch of API routes to Server Actions, updating every call site after a helper's signature changes. Correctness here is mostly about not missing a spot, and an agent doesn't get sloppy on file thirty-one the way I do at 4pm. **First drafts of well-specified features.** If I can describe the shape of something precisely — inputs, outputs, edge cases, where it slots into the existing structure — the agent gets to a working first version faster than I'd type it myself. Not the final version. Maybe 80% there. But 80% there, on the first try, is a great place to start steering from. **Wiring things across the stack.** Add a field, and it needs to exist in the PostgreSQL schema, the NestJS DTO and service, the sync payload, the Expo screen, and the Next.js dashboard. That used to be an afternoon of repetitive, low-creativity work. Now the agent reads how one similar field is wired and drafts the whole chain in a few minutes. **Edge cases I forgot.** Empty arrays, null timestamps, duplicate sync writes, what happens if the device comes back online mid-write. I don't agree every single one matters, but having the list in front of me beats trying to brainstorm it cold. **Debugging across boundaries.** When something breaks between, say, the Apple Watch app and the dashboard, the agent can hold the whole chain in its head at once — mobile write, NestJS endpoint, Postgres constraint, frontend read. I can do that too, it just takes me longer to load it all back in after I've been doing something else for an hour. None of this is the agent designing my product. It's the agent closing the gap between "I know what needs to happen" and "it exists in the codebase." ## Where I don't let it run unsupervised This is the part that matters more, because it's where things go wrong if you get lazy. **Architecture and data model decisions.** One table with a status column, or two tables and a join? Does this state live in the URL, a server component, or client state? These choices ripple for months. The agent will give you an answer — often a perfectly reasonable one — but it doesn't carry "we'll be living with this for two years and three other features will depend on it." That weight is mine, so the call stays mine. The agent can help me think it through. It doesn't get a vote on the final answer. **Anything touching auth or data access.** This is where I'm closest to paranoid, on purpose. Code can look like it checks permissions correctly and not actually do it — especially across an ownership chain like "user owns a place, place belongs to a shared group, group membership decides who can see it." I read every line of this myself. Doesn't matter who or what wrote it. **Scope creep.** Agents are eager to be helpful, which sounds nice until you ask for a one-line fix and get the fix plus a "while I was in there" refactor, plus a new abstraction "for reusability," plus a validation layer nobody asked for. On a side project that's mildly annoying. On anything shared, it turns a five-minute review into a forty-minute one. A diff that's bigger than I expected is my cue to stop and ask why before I read another line. **Naming and abstractions, over time.** It'll happily produce a working `useThing` hook or another `helpers.ts`. What it's much worse at is noticing "we already have three things that almost do this, and this is now a fourth, slightly different one." That kind of drift doesn't show up in any single diff — it shows up six months later when you're staring at the folder structure wondering how it got like this. Catching that is still on me. **Anything where "it compiles and the demo looks fine" isn't the actual bar.** Sync logic that has to survive a phone losing signal mid-write. An extension that has to behave when the tab closes mid-request. A migration that has to run safely against a table with real rows in it. The agent can write code for all of these — if I describe the scenario. It won't reliably think of the scenario itself, because it's never been the one paged at 2am when this kind of thing goes sideways. I have. That experience is still doing work here, even if I'm not the one typing. If there's a pattern across all of these, it's this: the agent is excellent at execution once a problem is well-defined, and it's not the one who should decide what the problem is or what "done" actually means. That's still the job. ![Where an AI coding agent is trusted to execute versus where a human keeps the decision: architecture, security, scope, and abstractions stay with the engineer](/img/blog/ai-coding-agent-trust-boundaries.svg) ## How I actually work, day to day A handful of habits made the biggest difference, and none of them are clever. I plan before I let it touch any files. For anything that isn't trivial, I have it read the relevant code first and tell me how it's going to approach the change before writing a line. Reading a plan takes ten seconds. Reading a thousand-line diff to discover we disagreed from the start takes a lot longer, and by then it's already written. I scope things the way I'd brief a contractor I trust but haven't worked with yet — specific files, specific behavior, specific things not to break. "This has to keep working offline." "Don't touch the API shape, the mobile app depends on it." "Match the pattern in `lib/posts.ts`, don't invent a new one." Every piece of context I make explicit is one less thing it has to guess, and guessing is where things drift. I try to keep diffs small enough to actually read. If something's going to touch twenty files, I'd rather it happen in a few steps I can review properly than one big change I skim because I'm tired by file twelve. I let it run its own checks. Type checker, linter, tests, even firing up the dev server to look at the actual page — having it do this and fix what it finds before showing me anything has probably been the single biggest time saver. The "run it, copy the error, paste it back" loop basically disappears. And I review its code differently than I'd review a teammate's PR — not less carefully, just differently. With a person's PR I'm partly checking whether they understood the requirement. With the agent, I already know what I asked for, so the review is: did it do that, did it do *only* that, and does it look like it belongs in this codebase or like it was dropped in from somewhere else. That last one is the one I see people skip most. The rest — what to build, what not to build, when "good enough" is actually good enough versus a problem waiting to happen — stays mine. The agent doesn't have an opinion on whether a feature is worth building in the first place, and on a side project, that question is most of the actual work. ## A real example: the Expo/NestJS sync layer The clearest case from my own work is the [sync architecture for the Expo app](/blog/expo-nestjs-postgresql-sync-architecture) — Apple Watch and iPhone capture GPS places, sync through NestJS, land in PostgreSQL, show up later in the Next.js dashboard. Here's what the agent handled well: - scaffolding the NestJS DTOs and service methods once I'd nailed down the sync payload shape - writing the Postgres migration for the new sync columns, matching the style of the existing migrations - generating the Expo-side API client and TypeScript types from the NestJS contract - a first pass at tests for "what happens if the same place gets synced twice" What I kept for myself: - how conflicts get resolved when the same place is edited offline on two devices — last-write-wins, per-field merge, manual resolution. That's a product decision wearing a technical costume, and it has UX consequences I had to sit with myself - the idempotency strategy for sync writes, because getting it wrong means duplicate places quietly appearing, and that's the kind of bug that makes people stop trusting the app - reading the auth check on the sync endpoint myself, line by line, since that's the line between "your data" and someone else's By lines of code, the agent probably wrote more than I did. But that's not really the split that matters. It handled the surface area; I handled the handful of decisions where being wrong is expensive. That's roughly the ratio I aim for on most real features. ## The same pattern shows up in the Chrome extensions Cuelio, Crowra, and TableSnap are smaller and more contained than the Expo/NestJS side, which makes the same pattern easier to see at a different scale. With [Crowra](/blog/why-i-built-crowra-side-panel-seo-ai-readiness-inspector) — a side-panel tool that audits a page for SEO, schema, and AI-readiness signals — the agent is great at adding a new check once the existing ones establish the pattern. "Also flag pages missing an `og:image`" is mechanical: the existing checks are basically the spec, and getting one wrong has a small blast radius. What I keep for myself is deciding whether a check is worth adding at all. A side panel like this lives or dies on not being noisy, and every new check is a tradeoff between "more thorough" and "more overwhelming." That's a product call, and the agent has no stake in it either way. With [TableSnap](/blog/building-tablesnap-local-first-chrome-extension-web-table-workflows), the constraint that matters most is that table data never leaves the browser — it's local-first by design. That's exactly the kind of rule an agent will respect if you say it out loud, and might quietly step around if you don't, the moment "add an export option" starts to sound like it'd be easier with some convenient API call. I don't read that as the agent being careless. It's a reminder that a constraint that only lives in my head doesn't exist as far as it's concerned. ## The one rule that covers most of this If I had to boil it down: give the agent a well-defined problem with a short feedback loop, and hold on to the decisions that are expensive to get wrong. "Well-defined" means it isn't guessing at your conventions or priorities, because if it has to guess, it will — confidently, plausibly, and sometimes wrong in ways that are hard to spot until later. "Short feedback loop" means it can check its own work against the types, the tests, the running app, before you're the one finding the bug. And "expensive to get wrong" is a short list — architecture, data integrity, security boundaries, scope. Almost everything else is fair game. That list isn't fixed, either. A year ago mine was longer. It'll probably be shorter again next year. But right now, on real projects with real users and real data, that's roughly where I draw the line — and it's why I can lean on these tools heavily without it feeling like a gamble. ## Conclusion Whether an AI agent can write code isn't really the interesting question anymore. It can, and it's gotten good at it fast. What's more interesting is what changes about the job once that's true. For me, the answer has been: less than the hype would have you believe, and more than I expected when I started. The work shifts from typing out the implementation to specifying the problem precisely, checking the result honestly, and owning the decisions that are hard to undo. That was always the harder half of the job. It's just that now it's nearly all of it — and if I'm honest, it's the half I find more interesting anyway. --- ## Context Optimization in AI Products: How to Make AI Features Faster, Cheaper, and More Useful URL: https://www.pean.dev/blog/context-optimization-in-ai-products-en Published: 2026-05-30 Description: AI product quality often depends less on the model and more on the context you give it. A practical look at context optimization, cost, latency, source grounding, and real product workflows. When people talk about AI features, they often start with the model. Which model should we use? How big is the context window? Can we send the whole document? Can it read a full video transcript? Would the answer be better if we just gave it more context? Those are fair questions. I ask them too. But in real products, I rarely start there. The more useful question is usually this: **what context does the AI actually need to help the user right now?** Not the maximum context. Not everything we have. Not a long prompt with every possible detail just in case. The right context. That is the part I keep coming back to when I design AI features. The model matters, of course. But the product often becomes faster, cheaper, and more useful when we get better at choosing what the model should see. That is how I think about context optimization. ## The model is not always the problem It is easy to blame the model. If the answer is weak, use a stronger model. If the answer is vague, use a bigger context window. If the feature is slow, try another provider. If the feature is expensive, switch to a cheaper model. Sometimes that is the right move. But very often the real issue is simpler: we are giving the model the wrong material to work with. Take a long YouTube video as an example. The lazy version is to extract the entire transcript and send it to the model every time the user asks a question. For a demo, that can look good enough. The user asks something, the AI replies, and the feature feels impressive for a few minutes. Then the product starts to behave like a real product. Long videos contain a lot of text. A lot of text means more tokens. More tokens mean higher cost, slower responses, and more room for the model to drift around the transcript instead of focusing on the exact part that matters. At that point, the issue is not that the AI is bad. The issue is that we asked it to work with a pile of unsorted context. ## More context is not always better There is a natural temptation to think: > If we give the model more context, the answer will be better. That can be true for broad tasks. If the user asks, “What is this document about?” or “Can you summarize the main ideas from this video?”, then wider context helps. The model needs a larger view to understand the whole thing. But many product questions are not broad. They are specific. A user asks: > What did the speaker say about pricing? Or: > Where does the tutorial explain the deployment step? Or: > Did anyone mention this issue in the comments? In those cases, sending everything can make the answer worse, not better. The model might find the right part. It might also pick a similar section, blend two different moments together, or return a generic answer that sounds correct but does not really help. For specific questions, I usually prefer a different flow: 1. find the most relevant pieces first; 2. send those pieces to the model; 3. ask the model to answer from that context; 4. show the user where the answer came from. That is less magical than “AI reads everything”. But it is usually better product design. ## Context is a product decision I do not see context optimization as only a backend or prompt engineering detail. It is a product decision. The context we send to AI directly affects the user experience: - how fast the answer feels; - how specific the answer is; - whether the user can verify it; - how much each request costs; - how predictable the system is; - how painful the product will be to scale; - whether the AI feature feels useful or random. A lot of AI products have the same basic shape: there is an input, a button, a loading state, and an answer. That is not enough. The harder questions are hidden underneath: - what did the model actually see? - did it see the right source? - can the user go back to that source? - what happens when the content is long or noisy? - what should happen when the answer is incomplete? - what data should never be sent at all? This is where the real work starts. Not in adding an AI box to the interface, but in deciding how AI fits into the workflow. ## What I try to send to the model When I design an AI feature, I try not to ask “how much can we send?” first. I ask: **what is the smallest useful context for this moment?** Depending on the product, that might include: - relevant text fragments; - sections with the closest semantic match; - a page title, video title, or document metadata; - a small amount of surrounding context; - a user-selected item or range; - timestamps or source references; - a short instruction about how the answer should behave; - constraints the model should not ignore. For example, if a user asks about one topic in a video, I do not need to send the whole transcript by default. I need to find the parts that are most likely to contain the answer, send those, and ask the model to stay grounded in them. That is less exciting than a huge prompt. But it is much closer to how a reliable product should work. ## What I try not to send Choosing what not to send is just as important. I try to avoid sending: - long chunks of text just in case; - repeated content; - irrelevant sections; - raw HTML noise; - navigation text, boilerplate, and layout clutter; - content that does not affect the answer; - private or sensitive data unless it is truly required; - an entire document when the user only needs one part. This is not only about saving money. Less noise often means a better answer. When the model receives cleaner context, it has fewer distractions. The answer becomes more direct. The user does not get a polished paragraph that vaguely touches the topic. They get something they can actually use. That is the difference between an AI demo and an AI feature that belongs in a product. ![Context optimization as a product layer between raw content and AI answers](/img/blog/ai-context-optimization-products.svg) ## Cost matters earlier than people think At the beginning of a product, it is easy to say: > We do not have many users yet, so cost does not matter. I understand that thinking, but I do not fully agree with it. AI cost is not only a billing problem. It is also an architecture signal. If the first version of a product sends too much context on every request, you are not just spending more money. You are building the user experience around a slow and expensive operation. The code starts to assume it can send everything. The interface starts to assume the user will wait. The product behavior starts to depend on a pattern that may not survive real usage. Then usage grows, and suddenly every active session is more expensive than expected. That is a bad moment to realize the architecture was wasteful from the start. So I like thinking about cost early. Not because every feature needs to be cheap at all costs. Some AI features are worth paying for. But a product should know where the money is going and why. If we can make the answer faster and cheaper by sending better context, that is usually worth doing. ## Latency changes how the feature feels A correct answer can still feel bad if it takes too long. This matters a lot in browser workflows. When someone is inside a YouTube video, a dashboard, a document, or an internal tool, they are already in the middle of a task. They do not want a separate AI process that interrupts everything. They want help in the same flow. Find the thing. Check the detail. Jump to the source. Copy the result. Move on. Latency is not just a technical metric here. It is part of the product experience. If the AI responds quickly, it feels like a tool. If it takes too long, it starts to feel like a separate workflow the user has to wait for. Context optimization helps with that. Less irrelevant context means fewer tokens, faster processing, and less friction. That does not make every AI feature instant. But it does make the feature feel more intentional. ## Grounded answers need traceable context One of the things I care about most in AI products is whether the user can check the answer. AI should not feel like a black box. If the product answers based on a video, document, page, transcript, or comment thread, the user should have a way to understand where the answer came from. This is especially important for video. A summary can be helpful, but it often removes the path back to the original source. You get the conclusion, but not the moment that supports it. I prefer a more grounded flow: - find the relevant part; - generate the answer; - show the timestamp or source reference; - let the user jump back to the original moment; - keep the answer inspectable. That is not just a nice detail. It is trust design. A timestamp is not only navigation. It is a way for the user to verify the AI instead of blindly accepting it. ## How this applies to Cuelio This way of thinking directly affects how I am building [Cuelio](/blog/building-cuelio-search-first-ai-extension-for-youtube). Cuelio is a YouTube extension I am working on and testing. The goal is not to make another AI summarizer. I want YouTube to feel more like a searchable knowledge base. The first version is focused on the workflow that feels most useful in long videos: - search the transcript; - jump to the exact timestamp; - ask AI questions based on the video content; - keep answers connected to sources; - search comments without endless scrolling; - save videos for later; - export transcripts when needed. Context optimization is a very practical problem there. If a user asks a question about a long video, sending the full transcript to the model every time is not the best default. It can be slow, expensive, and less focused than it should be. A better approach is to find the relevant transcript parts first, pass those to the AI, and return an answer that still points back to the original video moment. That makes the product cheaper to run, but also more honest for the user. The answer is not floating in the air. It has a source. ## How this applies to client work This problem is not unique to YouTube. It shows up in almost every product where AI needs to work with real content: - internal knowledge bases; - CRM notes; - support tickets; - educational platforms; - documentation; - video and audio archives; - browser extensions; - SEO and content audit tools; - internal research workflows. A client might describe the request very simply: > We want to add AI to the product. But the real questions start after that. What data should the AI see? What should it never see? How do we find the relevant context? How do we make the answer verifiable? How do we avoid burning money on tokens? How do we keep the UX fast? How do we fit the feature into the real workflow instead of adding a chat box on top? That is where AI product engineering becomes more interesting than just connecting an API. The value is not only in making the model respond. The value is in making the response useful at the exact point where the user needs it. ## The practical rule I use My rule is simple: **find the right context first, then ask AI to help.** Not the other way around. If an AI feature starts with “let's send everything to the model”, I usually want to pause and look closer. What is the user actually asking? Where is the answer likely to be? Which parts of the content matter? Can we show the source? Can this be faster? Can this be cheaper? Can this be easier to trust? Very often, those questions improve the product more than a bigger model or a longer prompt. The best AI features I have used rarely feel like magic. They feel like the product understands the task, brings the right context forward, and uses AI only where it adds value. ## Conclusion AI products do not become useful just because they include AI. They become useful when the AI sees the right context, appears in the right part of the workflow, and helps the user do something faster, clearer, or with more confidence. That is why I treat context optimization as a product decision, not only a technical detail. Especially at the beginning of a product. Early on, it is easy to overbuild, overspend, and hide messy thinking behind a large prompt. I would rather start smaller: understand the task, find the relevant context, give the model what it needs, and keep a path back to the source. It does not sound like magic. But in real products, that is often what makes AI useful. --- ## Why AI Summaries Are Not Enough for Long YouTube Videos URL: https://www.pean.dev/blog/why-ai-summaries-are-not-enough-for-long-youtube-videos Published: 2026-05-29 Description: AI summaries are useful, but long YouTube videos often need something more: transcript search, timestamped answers, source checking, comment search, and a way to return to the exact moment. AI summaries are useful. I use them. I understand why people want them. If a YouTube video is 48 minutes long and you only want a rough idea of what it covers, a good summary can save time. But the more I work with long videos, tutorials, interviews, technical talks, product reviews, and research-heavy content, the more I notice the same thing: a summary is only one layer of the problem. Sometimes I do not need a shorter version of the video. I need to find the exact moment where something is explained. I need the timestamp. I need the original context. I need to check what the speaker actually said. I need to search the transcript. Sometimes I also need to see whether people in the comments corrected something, shared a link, added a warning, or asked the same question I had. That is a different workflow. It is not only about summarizing YouTube videos with AI. It is about making long videos searchable, inspectable, and easier to return to. That is the thinking behind Cuelio, a YouTube extension I am currently testing. I do not want it to be another AI YouTube summarizer. The first version is focused on transcript search, timestamped AI answers, comment search, saved videos, and transcript export. ![AI summaries are useful, but long videos often need search, timestamps, comments, and source-linked answers.](/img/blog/ai-summaries-not-enough-youtube.svg) ## Summaries solve only one part of the problem A summary is good when the question is broad. For example: - What is this video about? - Is this worth watching? - What are the main points? - Can I get a quick overview before spending time on it? That is a valid use case. The problem starts when people expect a summary to solve every long-video workflow. A summary flattens the content. It compresses the video into a few paragraphs or bullets. That can be helpful, but it also removes a lot of the structure that makes the original video useful: timing, order, emphasis, examples, small details, and the ability to verify the answer. For some videos, that does not matter much. For others, it matters a lot. If I am watching a programming tutorial, I do not only want to know that the author talked about authentication. I want to find the exact part where they explain the bug with cookies, headers, middleware, or callback URLs. If I am watching a product review, I do not only want “the product is good but has trade-offs.” I want to find the moment where the reviewer talks about battery life, build quality, software issues, or long-term use. If I am watching a lecture, I do not only want the topic list. I may need the definition, the example, or the part where the lecturer compares two ideas. A summary gives me the shape of the content. Search gives me access to the content. That distinction is important. ## Long videos are often search problems The longer the video, the more it starts to behave like a small knowledge base. A one-hour interview, a course lesson, a conference talk, or a technical walkthrough can contain dozens of useful moments. The value is not always evenly distributed. Sometimes only three minutes matter to me. Sometimes I remember a phrase but not the timestamp. Sometimes I watched the video last week and want to return to one section without scrubbing through the timeline again. This is why “search inside YouTube video” is a more interesting problem than it first looks. The user does not always want an AI-generated answer immediately. Sometimes the user wants to search the YouTube transcript first, see matching lines, and jump to the right moment. That is a simpler, more transparent workflow. I think a good AI YouTube extension should respect that. AI should not be the first answer to every problem. Sometimes plain transcript search is faster, cheaper, and easier to trust. That is one of the product decisions I care about with Cuelio: search first, AI when the question needs context. ## A summary removes the path back to the source This is the part that bothers me most about many AI summary tools. They produce a clean answer, but they often make it harder to inspect the source. For casual browsing, that might be fine. For learning, research, technical work, or decision-making, it is not enough. If an AI answer says that the video recommends a specific approach, I want to know where that came from. Was it stated directly? Was it inferred? Did the speaker qualify it? Did they mention an exception ten seconds later? Without a path back to the original moment, the answer becomes harder to trust. That is why timestamped answers matter. A timestamp is not just a convenience feature. It is part of the trust model. When an AI answer points back to the exact part of the video, the user can verify it. They can listen to the original wording. They can check the context around the answer. They can decide whether the AI understood the video correctly. For me, that is the difference between an AI feature that feels useful and one that feels like a black box. ## Timestamps are not a bonus feature A lot of tools treat timestamps as a nice extra. I see them as part of the interface. Long videos are temporal content. The timeline is the source structure. If a tool ignores that structure, it loses one of the most important ways users navigate the video. A good timestamped workflow should let the user move between three states: 1. The question or search query. 2. The relevant answer or transcript match. 3. The exact moment in the original video. That loop matters. Search result → timestamp → video moment. AI answer → source timestamp → video moment. Comment result → surrounding discussion → video context. This is also where AI product design gets practical. It is not only about model quality. It is about whether the interface helps the user understand and verify the result. For long YouTube videos, I do not think AI answers should float separately from the video. They should stay connected to the transcript, timestamps, and original source. ## Transcript search is different from summarization Transcript search and summarization solve different problems. Summarization says: > “Here is what the video is mostly about.” Transcript search says: > “Here are the exact places where your topic appears.” Both are useful. But they should not be treated as the same feature. If I search for “pricing,” “OAuth,” “camera overheating,” “protein intake,” “Next.js caching,” or “export settings,” I do not necessarily want the AI to rewrite the whole video for me. I want matches. I want context around those matches. I want timestamps. I want to decide which part to open. This is why I like the search-first approach. It keeps the user in control. The AI can still help, especially when the question is more complex. But the basic layer should be searchable and inspectable before it becomes generative. That is also better for performance. Searching a transcript is usually much cheaper and faster than sending a large amount of text to a model. For an early product, those details matter. ## Comments can be part of the knowledge layer YouTube comments are messy. They can be noisy, repetitive, and sometimes useless. But for many types of videos, they are also valuable. In technical tutorials, comments often contain fixes, version updates, errors people hit, or alternative approaches. In product reviews, comments often include long-term user experience. In educational videos, someone may add a correction or a better explanation. In creator videos, the discussion can reveal what people actually cared about. That makes comment search more than a convenience. If I am researching a video, I do not always want to scroll through hundreds or thousands of comments. I want to search them. I want to find whether someone mentioned a specific tool, issue, feature, mistake, link, or follow-up question. This is one of the reasons I want Cuelio to include YouTube comment search as part of the workflow. Not because comments should replace the transcript. Because they can add another layer of context around the video. For some use cases, the comments are where the practical reality shows up. ## AI answers need grounded context The most useful AI answer is not always the most confident one. It is the one that can show where it came from. For a YouTube video, that means grounding the answer in the transcript and connecting it to timestamps. If the answer is based on a specific part of the video, the user should be able to jump there. This matters because long videos create a lot of room for misunderstanding. A model can compress too much. It can miss a condition. It can blend two parts of the video together. It can turn a small example into a general recommendation. It can answer in a way that sounds right but is hard to verify. The interface should make verification easy. That is why I think AI answers for YouTube videos should be designed less like chat messages and more like source-linked navigation. The answer is useful. The source is what makes it trustworthy. ## Context optimization matters more than it sounds One of the technical decisions behind Cuelio is context optimization. Long YouTube videos can contain thousands of words. Sending the entire transcript to an AI model for every question is the lazy solution. It can work for a prototype, but it is not always the best product decision. It can be slower. It can be more expensive. It can be less predictable. It can also make the answer worse if the model receives too much irrelevant context. For an early product, this matters a lot. If every user question becomes expensive and slow, the product becomes harder to test, harder to price, and harder to scale. So the better question is not “how much context can I send?” The better question is: > What is the smallest useful context the model needs to answer this question > well? That is the direction I am thinking about with Cuelio. Find the relevant transcript parts first. Keep the answer grounded. Avoid sending unnecessary text. Make the product faster and cheaper without making it feel worse. This is also the kind of AI engineering decision that often gets hidden behind the word “AI.” From the outside, a feature looks simple: ask a question, get an answer. Inside the product, the important work is deciding what context the model sees, how the user verifies the output, and how the system stays fast enough to feel useful. ![A search-first AI video workflow starts with transcript and comments, then uses AI with grounded context and timestamps.](/img/blog/youtube-search-first-ai-workflow.svg) ## How this thinking shaped Cuelio Cuelio is still in the final testing stage, so I am intentionally keeping the first version focused. The goal is not to ship every possible AI feature at once. The goal is to make one workflow reliable: - search the YouTube transcript; - ask AI questions based on the video content; - connect answers back to timestamps and sources; - search comments without endless scrolling; - save useful videos for later; - export the transcript when needed. That is enough for a first version. I do not want to position Cuelio as a generic AI summarizer. I also do not want the first version to pretend it can do everything: full comment analytics, automatic transcript generation for every possible video, large account systems, or a complete research platform. Those things may become useful later. But early products need focus. For Cuelio, the focus is simple: turn YouTube into something closer to a searchable knowledge base. Not by replacing the video. By making the useful parts easier to find, ask about, verify, and return to. ## What this means for AI product design The bigger lesson for me is that AI features should not start with the model. They should start with the workflow. For long YouTube videos, the workflow is not always “summarize this.” Sometimes it is: - find the exact explanation; - check the original wording; - jump to the right timestamp; - search a phrase in the transcript; - ask a question that needs context; - inspect the comments; - save the video for later; - export the transcript into another tool. Once that workflow is clear, AI becomes easier to place. It does not need to do everything. It needs to help at the right moment. That is the product direction I care about more and more: AI that stays close to the user’s context, reduces friction, and makes the output easier to verify. Cuelio is a small product, but the same decisions show up in client work too. When I build custom AI features, browser extensions, internal tools, or product workflows, the hard part is rarely “connect an AI API.” The hard part is deciding what the user actually needs, what context the system should use, where the answer should appear, how the user verifies it, and how to keep the product fast and affordable enough to use. That is where AI product engineering becomes interesting. Not in making everything shorter. In making the right information easier to find. --- ## Building Cuelio: Why I’m Designing a Search-First AI Extension for YouTube URL: https://www.pean.dev/blog/building-cuelio-search-first-ai-extension-for-youtube Published: 2026-05-26 Description: A behind-the-scenes look at how I am designing Cuelio, a search-first AI browser extension for YouTube with transcript search, timestamped AI answers, comment search, saved videos, and transcript export. Most AI tools for YouTube start with the same promise: summarize this video. That is useful sometimes. If I open a 90-minute interview and only want a quick idea of what it covers, a summary helps. But when I use YouTube as a learning tool, a research source, or a technical reference, a generic summary is rarely the real thing I need. I usually need something much more specific. I want to find the exact moment where someone explains a concept. I want to search the transcript instead of dragging the timeline. I want to ask a question and see where the answer came from. I want to jump back to the source before trusting the AI response. Sometimes I also want to search the comments, because that is where people add corrections, extra context, links, warnings, and practical experience. That is why I am building **Cuelio** as a search-first AI extension for YouTube, not just another AI summarizer. Cuelio is currently in the final testing stage, so this is not a launch announcement. It is a product and engineering note about the decisions behind it: transcript search, timestamped AI answers, comment search, saved videos, transcript export, and context optimization for better performance and lower early-stage cost. ![Cuelio search-first YouTube workflow](/img/blog/building-cuelio-search-first-youtube-extension.svg) ## YouTube is becoming a knowledge base YouTube is not only entertainment anymore. For many people it is where they learn software, compare products, watch lectures, follow tutorials, research tools, understand market opinions, and listen to long technical discussions. A single video can contain the answer you need, but the answer might be hidden at minute 38, inside a small explanation, or in a comment under the video. That changes the product problem. The issue is not always _watching_ the video. The issue is _finding_ the right piece of information inside it. That is especially true for: - students reviewing lectures or tutorials; - developers looking for one implementation detail; - researchers collecting arguments or references; - marketers analyzing audience reactions; - creators reviewing feedback in comments; - product people comparing opinions across long videos. In all of these cases, YouTube behaves less like a video platform and more like a messy knowledge base. But the default interface still makes you consume content mostly linearly. Cuelio is my attempt to make that knowledge easier to search, question, verify, and return to. ## I do not want to build another YouTube summarizer I have nothing against summaries. They are useful when the user genuinely needs a quick overview. But I do not think “summarize this video” should be the default answer to every YouTube productivity problem. A summary compresses the video. That is the point. But compression also removes details, examples, nuance, and sometimes the exact part the user actually needed. If the user is trying to learn, cite, debug, compare, or verify something, the summary is only a starting point. The more useful workflow is often this: 1. search inside the video; 2. find the relevant section; 3. ask a focused AI question; 4. check the answer against the timestamped source; 5. jump back to the original moment. That is the core difference I care about. Cuelio is not designed as a tool that hides the video behind an AI answer. It is designed as a tool that helps the user reach the right part of the video faster. ## The real problem is not watching. It is finding. Long videos are not always a waste of time. Sometimes they are valuable because they contain depth. The problem is that the useful part is hard to reach. If I am watching a technical tutorial, I may already know 80% of the topic. I do not want to sit through the whole video just to find one configuration detail. If I am watching a product review, I may only care about one comparison point. If I am watching a lecture, I may want to revisit the explanation of one term. This is why transcript search is such an important base layer. Before AI answers, before summaries, before any advanced workflow, the user needs a fast way to search the content that is already there. That is where Cuelio starts. ## How Cuelio helps search inside a YouTube video The basic workflow is intentionally simple: 1. open a YouTube video; 2. search the transcript; 3. jump to the exact timestamp; 4. ask an AI question if the answer needs context; 5. check the source before trusting the response; 6. search comments when the discussion around the video matters too. That flow matters because it keeps the user close to the original material. A lot of AI interfaces create a separate layer between the user and the source. They answer confidently, but the user still has to wonder where the answer came from. For YouTube, I think that is the wrong experience. A good AI YouTube extension should not only answer. It should help the user verify. ## Transcript search should feel like search, not scrolling YouTube transcripts are useful, but they are not always comfortable to work with. The user may need to scan a long transcript, search for a phrase, understand where it appears, and then move from text back to video. That sounds simple, but the experience can become slow if it is treated like a secondary feature. In Cuelio, transcript search is one of the main interactions. The goal is to make it feel like searching inside the video itself: - type a keyword or phrase; - see matching transcript parts; - understand the surrounding context; - click the timestamp; - continue watching from the exact moment. This is also why I like the phrase **search-first**. It describes the real user behavior better than “summary-first”. Most people do not open a long video because they want a smaller version of the whole thing. They open it because they believe the answer is somewhere inside. ## AI answers need sources and timestamps AI answers are only useful if the user can trust them. For Cuelio, that means answers should stay connected to the transcript and to the video timeline. If the extension answers a question about the video, the user should be able to see which parts of the transcript supported that answer and jump to the related timestamp. That is important for two reasons. First, it reduces the feeling that the AI response came from nowhere. The answer is not just a generated paragraph. It is tied to source material. Second, it keeps the video as the primary source. Cuelio should help users understand the video faster, not replace the video with an unverifiable answer. This is especially important for educational and technical content. If a developer asks about a command, a student asks about a concept, or a marketer asks about a claim, the exact context matters. A timestamp is not just a navigation feature. It is part of the trust model. ## Comments are part of the knowledge layer YouTube comments are messy, but they are often useful. For tutorials, reviews, product comparisons, and educational videos, comments can contain: - corrections from other viewers; - links to related resources; - warnings about outdated information; - practical examples; - answers from the creator; - alternative opinions; - follow-up questions. The problem is that comments are hard to search manually. Scrolling through them is slow, and YouTube’s default interface is not designed for focused research. That is why comment search belongs in Cuelio’s workflow. It is not the same as full AI comment analytics, and I do not want to overpromise that as the main current feature. The first practical value is simpler: help the user find relevant comments without endless scrolling. For some videos, the transcript explains the content. The comments explain how people reacted to it. Both can matter. ## Saved videos and transcript export are small but important workflow features Not every useful feature has to be AI. Saved videos and transcript export are simple, but they support the real workflow around research and learning. If a user finds a useful video, they may want to return to it later. If they are collecting notes, they may want to export the transcript. If they are comparing multiple videos, they may want to keep the source material organized. These features are not flashy, but they make the product feel less like a one-time tool and more like part of a working process. That is something I try to keep in mind when building small products. The best feature is not always the most impressive one. Sometimes it is the one that removes the next small friction point. ## Why context optimization matters in an AI YouTube extension Long YouTube videos can contain a lot of transcript text. The lazy approach would be to send the entire transcript to an AI model every time the user asks a question. Sometimes that may work in a prototype, but it is not a great product decision for an early extension. It can be slower. It can be more expensive. It can make responses less focused. And if the product is still trying to prove its first workflow, unnecessary AI cost can become a real constraint too early. So context optimization is an important part of how I think about Cuelio. The goal is not to send more context. The goal is to send the right context. For a question about a specific part of the video, the extension should first identify relevant transcript segments, then use those segments to generate an answer that still points back to the original timestamps. That gives the user a better experience and gives the product a more realistic cost structure. This is one of the places where AI product engineering becomes more interesting than just connecting an API. You have to think about: - what the user is really asking; - which transcript parts are likely relevant; - how much context is enough; - how to keep the answer grounded; - how to reduce unnecessary model calls; - how to make the workflow fast enough to feel native. For an early product, these decisions matter a lot. ## What I am intentionally not building yet Cuelio is still in the final testing stage, so I am intentionally keeping the first version focused. I am not trying to turn it into a full research platform from day one. I am not positioning it as a generic AI chat app. I am not treating full AI comment analytics as the main promise of the current product. I also do not want to promise automatic transcript generation for every video without subtitles as if that is already the core experience. The first version is about one clear workflow: find useful information inside a YouTube video faster, understand it with enough context, and jump back to the original source when needed. That focus helps with product quality. It also helps with engineering decisions. When the workflow is clear, it becomes easier to decide what belongs in the first version and what should wait. ## Who Cuelio is for Cuelio is useful for people who treat YouTube as a source of information, not just a feed. That includes students who need to search lectures, researchers who collect references, developers who look for exact technical explanations, marketers who analyze videos and comments, creators who review audience feedback, and anyone who wants to return to useful videos later. The common pattern is simple: the user does not want to consume everything linearly. They want to find the right part. That is the product space I care about with Cuelio. ## What this taught me about AI product engineering Building Cuelio has reinforced a simple idea for me: AI features are strongest when they are designed around a workflow, not around a demo. A demo can summarize a video. A product needs to help a user get from question to source, from source to answer, and from answer back to trust. That is why I care about transcript search, timestamps, comment search, saved videos, transcript export, and context optimization as much as the AI response itself. The value is not in having AI somewhere in the interface. The value is in making YouTube easier to use as a searchable knowledge base. That same thinking applies to custom development work too. When I build AI-assisted tools, browser extensions, or product workflows, I do not want to add AI as decoration. I want to understand where the user gets stuck, what context the system already has, what should stay verifiable, and how to make the first version useful without overbuilding it. Cuelio is a small product, but the product decisions behind it are the same decisions that matter in larger software: start with the real workflow, keep the interface close to the user’s context, optimize costs early, and make the output traceable back to the source. --- ## Building TableSnap: How I designed a local-first Chrome extension for web table workflows URL: https://www.pean.dev/blog/building-tablesnap-local-first-chrome-extension-web-table-workflows Published: 2026-05-22 Description: Why I built TableSnap: a local-first Chrome extension for copying web tables, cleaning messy extracts in preview, and exporting structured data as CSV, TSV, Markdown, HTML, or JSON. Copying a table sounds simple until the table is messy, the destination expects structure, and the user needs to trust the result before it reaches the clipboard. That was the starting point for [TableSnap](https://www.ts.pean.dev/). I kept running into this in places that looked boring on the surface and costly in practice: pricing pages, comparison tables, docs tables, admin screens, and public datasets that were clearly meant to be read, but not clearly meant to be reused. At first glance, the problem looks almost too small to deserve a product. You see a table on a page. You copy it. You paste it into Excel, Google Sheets, Notion, Airtable, or a Markdown doc. Done. Except that it is usually not done. The rows break. The headers shift. Pricing cards pretend to be tables without actually behaving like tables. Comparison layouts mix icons, notes, buttons, and duplicated labels into something that looks structured on screen but falls apart the moment you try to reuse it somewhere else. That is the gap I wanted to solve. Not a giant scraping platform. Not a heavy data pipeline. Not another dashboard you open in a separate tab. I did not want to build something that sounds impressive in a product deck and still feels annoying the first time someone actually uses it. A focused local-first Chrome extension for the moment when you need to move structured data from the web into a tool that expects structure. ![Illustration of TableSnap turning a messy webpage table into a clean structured export](/img/blog/tablesnap-copy-vs-structured-export.svg) ## The problem was never "copy" The more I looked at this workflow, the more obvious it became that the hard part was not copying. The hard part was preserving meaning. When someone says they want to copy a web table, they usually mean something closer to this: > I need the useful structure from this page to survive the trip into the next > tool. That is a very different product problem. It is not about whether text can reach the clipboard. Browsers already do that. It is about whether the exported result still feels usable after it leaves the page. That matters because the destination is not neutral. A spreadsheet wants rows and columns that line up cleanly. A Markdown doc wants something readable in plain text. JSON wants a predictable shape for scripts or automation. A research workflow may need something quick to clean, annotate, and share. A product team might want to drop the result into Notion or Airtable without another round of manual repair. So the real task is not "copy web tables." It is: > Detect the structure, let the user verify it, clean the messy parts, and > export it in the format the next tool actually needs. That became the center of the product. I like product problems like this because they look small until you try to make them feel reliable. That is usually where the real work is. ## Why this became a Chrome extension I did not want this workflow to start with a dashboard. If the table is already on the page, the product should stay close to the page. That sounds obvious, but it rules out a lot of awkward product shapes. A hosted tool that asks for a URL can work for some extraction jobs. It feels wrong for this one. The user is already looking at the source. They are already scrolling, comparing columns, checking whether the page version is the one they actually want, and deciding what should make it into the export. Sending them away from that moment creates unnecessary friction. So the browser itself became the right product surface. That decision shaped the browser extension UX: - the workflow had to feel immediate - the result had to stay visually close to the source page - the extension had to help with trust, not just output - the product had to stay focused instead of becoming general-purpose scraping TableSnap works better as a browser-native tool because web table workflows are already happening inside the browser. The extension does not need to invent a new workspace. It needs to make the existing one less fragile. ## Why I did not start with a bigger data product It would have been easy to frame this as a larger SaaS from day one. Projects. Saved URLs. Cloud jobs. Team workspaces. Extraction history. Shared pipelines. Maybe even scheduled scraping. Some of that could make sense later. I did not want to start there. The first product question was much narrower: > Can I make copying web tables feel trustworthy enough that people stop doing > repair work after every paste? If that part is weak, a bigger backend does not fix the product. It only hides the weakness behind more surface area. Starting with a local-first Chrome extension keeps the product honest. The table detection has to work. The preview has to help. The export has to make sense immediately. The workflow has to earn its place the first time someone needs it. That constraint is useful. ## The workflow had to create trust before export One thing I did not want was a "click and hope" product. That is the easiest way to make table extraction feel unreliable. If the extension instantly copies something and the user only discovers the damage after pasting it into another tool, the workflow already lost. The error arrives too late. That is why TableSnap is built around a compact three-step flow: 1. detect the table on the live page 2. clean the extracted structure in preview 3. export in the format the destination expects The preview step matters more than it might seem. Preview is not decorative UI. It is the trust boundary. It gives the user a chance to answer the questions that actually matter: - Did the structure come through correctly? - Are the headers useful? - Did noise from the page leak into the result? - Is this ready for Sheets, Notion, Markdown, or JSON? Without that checkpoint, the extension becomes a black box. With it, the workflow starts to feel dependable. That is a product rule I keep coming back to: > When the output is meant to travel, users need confidence before the handoff, > not after it. ## "HTML table to CSV" is only part of the story If I described TableSnap too narrowly, I could call it an `HTML table to CSV` tool. That would be technically true and product-wise incomplete. Some pages use clean, textbook `` markup. Many do not. Some use ARIA grids. Some use comparison layouts that are built from stacked containers and still behave like tables to the human eye. Some pages mix icons, badges, empty cells, repeated labels, or sticky headers into the layout. Some tables are clearly meant for reading, but not clearly authored for export. That is where DOM table extraction stops being trivial. The job is not just to grab text from the page. The job is to interpret enough structure that the result is still useful somewhere else. That is also why I wanted TableSnap to support more than ideal HTML tables, including ARIA grids and supported layout-based comparison tables. Real-world workflows do not happen on perfect demo pages. They happen on pricing pages, documentation sites, vendor comparisons, admin surfaces, public datasets, and random pages where the author cared more about presentation than export. If the product only works on polite markup, it misses the point. ## Why local-first mattered TableSnap is local-first because this workflow should feel lightweight. I did not want the main experience to depend on creating an account, waiting for remote processing, or sending a simple page-level extraction task through a backend before the user gets value. For this kind of product, local-first is not only a technical preference. It is part of the UX. The user is already inside a page. They want a quick result. They want to tweak the output, export it, and move on. That pushes the product in a clear direction: - the core extraction workflow should stay in the browser - settings and presets should stay close to the workflow - site-specific recipes should stay local to the user - the product should feel useful before any account-shaped idea enters the room Local-first also helps the permission story stay understandable. Broad browser access is only acceptable when the product purpose is narrow and clear. In this case, the purpose is simple: > The user opens a page and asks TableSnap to help them extract a table from > that page. That is a much healthier boundary than vague background behavior. ![Local-first TableSnap browser flow from page detection to preview cleanup and export](/img/blog/tablesnap-local-first-browser-flow.svg) ## Export formats are product decisions, not just checkboxes I wanted the export layer to reflect real destinations, not just technical possibilities. That is why TableSnap exports to CSV, TSV, Markdown, HTML, and JSON. Each format solves a different handoff: - CSV and TSV are for spreadsheets and quick data cleanup in Excel or Google Sheets - Markdown is for docs, notes, issues, and workflows that need readable plain text - HTML is useful when table structure needs to travel with richer formatting - JSON is for scripts, automation, and structured downstream processing This part matters because people do not extract tables for fun. They extract them because the data needs to go somewhere. The export format is not the end of the feature. It is the start of the next workflow. That sounds small, but it changes how the product should be designed. A table to Markdown flow should feel different from a table to JSON flow. One is optimized for human reading. The other is optimized for machine handling. A spreadsheet export often needs a shape that feels clean immediately, because the user will notice broken columns in seconds. Good product design respects the destination, not just the source. ## What I wanted TableSnap to avoid Focused tools get worse when they drift into feature theater. For TableSnap, I want to avoid a few common traps: - pretending that extraction alone is enough without a cleanup step - hiding the result until after the export - turning the product into a generic scraping platform - requiring an account before the first useful action - supporting only one export shape and forcing every workflow into it - collecting permissions that do not map back to the core job - optimizing for perfect demo pages instead of messy real ones The product should stay honest about what it is. TableSnap is not trying to replace every data pipeline. It is trying to make one frequent, annoying browser task feel clean and trustworthy. That is enough. ## What building TableSnap reinforced for me TableSnap reminded me that some of the best product opportunities are hiding inside workflows that look too small to notice. "Copy a table" sounds boring. But a lot of useful software lives in exactly that territory: moments where the job sounds simple, yet the current workflow is full of friction, uncertainty, and tiny repeated cleanup costs. Those are often good product surfaces because the pain is real, even if it does not sound glamorous. I trust repeated friction more than big category language. If people keep hitting the same annoying edge between one tool and the next, there is usually product space there. Building this also reinforced a broader product-engineering lesson: > The product is not the extraction. The product is the confidence that the > extracted result will still make sense in the next tool. That is why TableSnap is not only about DOM table extraction. It is also about browser extension UX, trust before export, local-first product boundaries, and choosing formats that match how people actually work with structured data after it leaves the page. If the product does those things well, then a very ordinary action starts to feel much less fragile. And that is usually a good sign. ## FAQ ### What is TableSnap? TableSnap is a local-first Chrome extension for detecting web tables, cleaning messy extracts in preview, and exporting usable data as CSV, TSV, Markdown, HTML, or JSON. ### Is TableSnap just an HTML table to CSV tool? No. CSV export is part of the workflow, but the product is broader than that. TableSnap is designed around web table workflows: detection, preview cleanup, and export for different destinations such as spreadsheets, docs, and automation. ### Does TableSnap only work with normal HTML tables? No. TableSnap is designed to support more than textbook HTML tables, including ARIA grids and supported layout-based comparison tables where the page behaves like a table even if the markup is less direct. ### Why make it local-first? Because the workflow should feel lightweight, fast, and close to the page. Keeping the main extraction flow in the browser reduces friction and makes the product useful without turning a simple task into an account-first system. ### What export formats does TableSnap support? TableSnap supports CSV, TSV, Markdown, HTML, and JSON export formats. ### Who is TableSnap for? TableSnap is for people who move structured data from webpages into spreadsheets, docs, databases, research notes, Notion, Airtable, or JSON-based automation workflows. --- Related reading: - [TableSnap](https://www.ts.pean.dev/) - [Why I built Crowra as a side-panel SEO and AI readiness inspector](/blog/why-i-built-crowra-side-panel-seo-ai-readiness-inspector) - [How I think about building products as a developer, not just features](/blog/product-minded-developer-building-products-not-features) --- ## Next.js API Routes in 2026: Route Handlers, Server Actions, and When to Use Each URL: https://www.pean.dev/blog/nextjs-api-routes-in-2026-route-handlers-server-actions-when-to-use-each Published: 2026-05-19 Description: A practical guide to API routes in modern Next.js: what changed with the App Router, when to use Route Handlers, when Server Actions are enough, and how to avoid building the wrong abstraction. People still search for **Next.js API Routes** because the phrase is familiar. For years, the answer was simple: put a file in `pages/api`, export a handler, and call it from the client. That mental model still exists if you are using the Pages Router. But in a modern App Router project, the conversation changed. You now have Server Components, Server Actions, Route Handlers, server-side `fetch`, cached reads, revalidation, and a lot of small decisions that did not exist in older Next.js apps. So when someone asks: > Should I create an API route in Next.js? The real answer is usually: > What kind of server boundary do you actually need? That question matters more than the file name. This article is how I think about API routes in Next.js in 2026. Not as a framework history lesson. Not as a documentation rewrite. Just the practical rules I use when building App Router projects that need forms, dashboards, webhooks, mobile clients, integrations, and real production behavior. ## Quick answer If you are using the **Pages Router**, API Routes live in `pages/api`. If you are using the **App Router**, the closest equivalent is usually a **Route Handler** inside the `app` directory: ```txt app/api/users/route.ts ``` Use **Route Handlers** when you need a real HTTP endpoint. Use **Server Actions** when the server code belongs to your own app UI: forms, buttons, dashboard mutations, user settings, internal product workflows, and other actions triggered by your interface. Use **Server Components** when you only need to read data and render a page. That is the simple version. The rest of the article is about the messy part: knowing which one fits the job when a real product starts growing. ![Decision diagram for Next.js API Routes, Route Handlers, and Server Actions](/img/blog/nextjs-api-routes-2026.svg) ## API Routes are not gone The first thing to clear up: API Routes are not gone. If your project uses the Pages Router, `pages/api` is still a valid way to build server-side endpoints inside a Next.js app. A classic API Route looks like this: ```ts // pages/api/hello.ts import type { NextApiRequest, NextApiResponse } from 'next'; type ResponseData = { message: string; }; export default function handler( req: NextApiRequest, res: NextApiResponse ) { res.status(200).json({ message: 'Hello from Next.js' }); } ``` That model is easy to understand. A request comes in, a handler runs, a response goes out. The confusion starts when the project uses the App Router. In the App Router, you normally do not create `pages/api` routes. You create **Route Handlers** with `route.ts` or `route.js`. ```ts // app/api/hello/route.ts import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json({ message: 'Hello from Next.js' }); } ``` The goal is similar: expose an HTTP endpoint. The shape is different: Route Handlers use the Web `Request` and `Response` model and live naturally inside the App Router. So when people say “Next.js API Routes” in 2026, they may mean one of two things: - the older `pages/api` feature from the Pages Router - the broader idea of creating server endpoints in Next.js, which usually means Route Handlers in the App Router That is why search results and conversations often feel slightly mixed. Everyone is using the same phrase, but not always talking about the same file convention. ## Route Handlers are the App Router version of API endpoints For most new App Router projects, Route Handlers are the endpoint primitive. A Route Handler is useful when you need a URL that can be requested directly: ```txt POST /api/webhooks/stripe GET /api/public/products POST /api/mobile/places GET /api/feed.xml ``` The important part is not that it sits under `/api`. The important part is that it is a real HTTP interface. That means another system, browser, client, service, or script can call it without knowing anything about your React component tree. That is the main reason I reach for a Route Handler. Not because it is “more backend”. Because the thing I am building needs an endpoint. ## Where Server Actions fit Server Actions solve a different problem. A Server Action is server code that can be called from your own app. It is very useful for product interactions that start inside your interface. For example: ```ts // app/settings/actions.ts 'use server'; export async function updateProfile(formData: FormData) { const name = String(formData.get('name') || '').trim(); if (!name) { return { error: 'Name is required' }; } // check auth // update database // revalidate UI return { success: true }; } ``` Then a form can use it directly: ```tsx
``` That is the part that feels different from older Next.js code. In many older apps, I would create an API Route only because I needed a server place to handle a form submission. The client would call `/api/profile`, the API Route would validate the input, update the database, and return JSON. In an App Router project, that API endpoint may not be necessary. If the action is only used by my own UI, a Server Action is often a cleaner fit. That does not mean Server Actions replace every API route. It means they replace a specific kind of API route: the internal endpoint that only existed to support one app-specific mutation. ## The question I ask first Before choosing between a Route Handler and a Server Action, I ask one question: > Does this need to be an HTTP endpoint, or is it just an app action? That question removes a lot of noise. If the answer is “this needs a stable URL that another client can call”, I use a Route Handler. If the answer is “this happens inside my own Next.js interface”, I usually start with a Server Action. If the answer is “I just need to read data for a page”, I probably do not need either one. I can fetch data in a Server Component or call a server-side query function directly. The worst default is creating an API endpoint for everything just because that was the old habit. It works, but it often adds boilerplate without adding clarity. ## When I use a Route Handler I use a Route Handler when the server code needs to behave like an API. That usually means one of these cases. ### 1. Webhooks Webhooks are the easiest example. Stripe, Lemon Squeezy, GitHub, Clerk, Resend, and many other services need to call your application at a public URL. That is not a Server Action job. The request is not coming from your form or button. It is coming from an external service. ```txt POST /api/webhooks/stripe ``` A Route Handler gives you the right shape for that: ```ts // app/api/webhooks/stripe/route.ts export async function POST(request: Request) { const body = await request.text(); // verify signature // process event // return response return new Response('OK', { status: 200 }); } ``` You get the request body, headers, status codes, and response control you need. ### 2. External clients If a mobile app, browser extension, another website, or external dashboard needs to call your backend, use a Route Handler. For example, imagine a mobile app that saves places to the same database your Next.js web platform uses. ```txt POST /api/mobile/places ``` That is an API contract. It should not depend on a React form existing in the web app. The mobile client needs a URL, a request shape, authentication, validation, and a clear response. That is Route Handler territory. ### 3. Public or shared APIs Sometimes you want to expose data intentionally. ```txt GET /api/public/tools GET /api/public/status GET /api/integrations/projects ``` If other clients are supposed to call it, treat it like an API. That means a Route Handler, not a Server Action. ### 4. Custom response formats Route Handlers are also the right choice when the response is not a normal UI mutation result. Examples: - file downloads - CSV exports - RSS feeds - XML - text responses - streamed responses - custom cache headers - redirects from an endpoint - special status codes A Server Action is not meant to be your general response formatting layer. If you need to control the HTTP response, create an endpoint. ### 5. Integration boundaries This is the most important product reason. A Route Handler creates a boundary. That boundary can be useful when different parts of a system need to talk to the same backend behavior. For example: - web app - mobile app - browser extension - admin panel - automation script - third-party service If all of them need the same operation, hiding that operation inside a Server Action tied to one UI is probably the wrong shape. A shared service layer behind a Route Handler will usually age better. ## When I use a Server Action I use a Server Action when the action belongs to the app interface. Examples: - submit a contact form - update profile settings - create a dashboard item - delete a saved record - change a project status - invite a team member - save a preference - mark a notification as read - run a simple internal mutation from a button Those are not public API problems. They are product interaction problems. A Server Action keeps that flow close to the UI without forcing me to create a separate endpoint, write a client-side `fetch`, parse JSON, return custom status codes, and manually connect all the pieces. For many internal mutations, that is less code and a clearer mental model. But there is one important warning. A Server Action is not magic security dust. It still runs on the server, and you still need to validate input, check auth, check ownership, handle errors, and avoid trusting the client. The fact that an action is called from your UI does not mean the input is safe. I treat Server Actions as server entry points. Small ones are fine. Careless ones are not. ## When I use neither A lot of Next.js code does not need a Route Handler or a Server Action. This is easy to forget. If a page needs to read data and render it, I usually start with a Server Component: ```tsx // app/projects/page.tsx import { getProjectsForCurrentUser } from '@/features/projects/queries'; export default async function ProjectsPage() { const projects = await getProjectsForCurrentUser(); return ; } ``` No `/api/projects` call. No client-side loading state just for the first render. No unnecessary endpoint. The page is already running on the server, so it can call server-side code. That is one of the biggest mindset shifts in App Router projects. You do not need to fetch from your own API every time you need data on a page. If the code is already on the server, call the server function directly. ## The mistake I see most often The most common mistake is carrying the old SPA habit into the App Router. The old habit looks like this: ```txt React component -> fetch('/api/something') -> API route -> database ``` That was a reasonable pattern in many apps. But in the App Router, it is not always the best default. For a server-rendered page, this can often become: ```txt Server Component -> server query -> database ``` For a form mutation, this can often become: ```txt Form -> Server Action -> service -> database ``` For a real endpoint, it should still be: ```txt External request -> Route Handler -> service -> database ``` The goal is not to avoid API routes at all costs. The goal is to stop creating them when the app does not need an API boundary. ![Comparison between Route Handlers and Server Actions in a Next.js app](/img/blog/route-handlers-vs-server-actions.svg) ## My practical decision table This is the table I usually keep in my head. | Scenario | What I usually use | Why | | --- | --- | --- | | Render a dashboard page with user data | Server Component | The page is already on the server | | Submit a settings form inside the app | Server Action | It is an internal mutation | | Delete an item from an admin table | Server Action | It belongs to the UI workflow | | Handle a Stripe webhook | Route Handler | External service needs an endpoint | | Build an endpoint for a mobile app | Route Handler | External client needs a stable API | | Return a CSV export | Route Handler | You need custom response behavior | | Serve an RSS feed | Route Handler | It is a URL-based response | | Update data from a browser extension | Route Handler | The extension is a separate client | | Fetch data for a Server Component | Direct server function | No endpoint needed | | Share business logic between several entry points | Service function | Keep the rules outside the transport layer | The last row is the one that saves projects from becoming messy. Server Actions and Route Handlers are entry points. They should not become the only place your business logic exists. ## Keep business logic out of the transport layer A Route Handler is a transport layer. A Server Action is also an entry point. Neither one should automatically become the home for all product logic. For real projects, I prefer this shape: ```txt app/ api/ projects/ route.ts dashboard/ projects/ actions.ts page.tsx features/ projects/ service.ts queries.ts validation.ts ``` Then the entry points stay small. A Server Action can parse input, check the current user, call a service, and revalidate the right page. A Route Handler can parse a request, verify auth or signatures, call the same service, and return a response. The product rules live in the service layer. That matters when the app grows. At first, you may only have a dashboard button. Later, you may add a mobile app, browser extension, webhook, scheduled job, or public integration. If the logic is trapped inside one Server Action, reuse becomes awkward. If the logic lives behind a service function, the entry point can change without rewriting the product rules. ## Example: contact form A contact form on your own site is usually a Server Action. The user fills the form, clicks send, and your app sends an email or stores a lead. You do not need a public API just for that. ```tsx