[{"data":1,"prerenderedAt":9853},["ShallowReactive",2],{"site-en":3,"post-en-ecommerce-development-cost":131,"blog-en":749},{"id":4,"contacts":5,"cta":15,"extension":18,"footer":19,"meta":68,"nav":69,"serviceUi":84,"stem":129,"__hash__":130},"site_en\u002Fen\u002Fsite.yml",{"email":6,"telegram":7,"viber":8,"whatsapp":9,"hours":10,"city":11,"linkedin":12,"github":13,"founderLinkedin":14,"founderGithub":14},"hello@tochnodev.com","https:\u002F\u002Ft.me\u002F","viber:\u002F\u002Fchat?number=","https:\u002F\u002Fwa.me\u002F","Mon–Fri 10:00–19:00, Kyiv time","Kyiv","https:\u002F\u002Fwww.linkedin.com\u002F","https:\u002F\u002Fgithub.com\u002F","",{"label":16,"to":17},"Discuss your project","\u002Fbryf","yml",[20,35,56],{"title":21,"links":22},"Services",[23,26,29,32],{"label":24,"to":25},"Website development","\u002Fposluhy\u002Fwebsite-development",{"label":27,"to":28},"E-commerce","\u002Fposluhy\u002Fecommerce-development",{"label":30,"to":31},"QA audit","\u002Fposluhy\u002Fecommerce-qa-audit",{"label":33,"to":34},"Tech Lead","\u002Fposluhy\u002Ftech-lead",{"title":36,"links":37},"Company",[38,41,44,47,50,53],{"label":39,"to":40},"About us","\u002Fpro-nas",{"label":42,"to":43},"Process","\u002Fproces",{"label":45,"to":46},"Cases","\u002Fkeisy",{"label":48,"to":49},"Blog","\u002Fblog",{"label":51,"to":52},"FAQ","\u002Ffaq",{"label":54,"to":55},"Glossary","\u002Fhlosarii",{"title":57,"links":58},"Legal",[59,62,65],{"label":60,"to":61},"Privacy Policy","\u002Fpolityka-konfidentsiinosti",{"label":63,"to":64},"Cookies","\u002Fcookies",{"label":66,"to":67},"Terms of Cooperation","\u002Fumovy",{},[70,72,75,78,79,80,81],{"label":21,"to":71},"\u002Fposluhy",{"label":73,"to":74},"Turnkey","\u002Fpid-kliuch",{"label":76,"to":77},"Pricing","\u002Ftsiny",{"label":45,"to":46},{"label":42,"to":43},{"label":48,"to":49},{"label":82,"to":83},"Contacts","\u002Fkontakty",{"whoFits":85,"whatsIncluded":86,"pricingEyebrow":87,"pricingAnchor":88,"allPrices":89,"sampleResult":90,"process":91,"details":92,"faqEyebrow":51,"faqTitle":93,"related":94,"seeAlso":95,"seeAlsoCases":96,"seeAlsoPosts":97,"price":98,"crumbServices":21,"flagship":99,"ctaLead":117,"ctaSteps":118,"needs":122},"WHO IT'S FOR","WHAT'S INCLUDED","PRICING","Packages and pricing","All prices","SAMPLE RESULT","PROCESS","LEARN MORE","Questions about the service","OFTEN ORDERED TOGETHER","SEE ALSO","Case studies","Articles","price",{"crumb":30,"expressCheck":100,"badge":101,"discuss":102,"bookDays":103,"sampleAria":104,"reportTitle":105,"steps":106,"actual":107,"expected":108,"impact":109,"colAction":110,"colExpected":111,"colStatus":112,"top3":113,"screenshot":114,"faqTitle":115,"related":116},"Free express check","FLAGSHIP","Discuss","Book days","Sample","This is what the report you receive looks like","STEPS","ACTUAL","EXPECTED","IMPACT","ACTION","EXPECTED RESULT","STATUS","TOP 3 TO FIX","report screenshot","Questions about testing","AFTER AN AUDIT YOU USUALLY NEED","We reply within 2 business hours. An estimate with a price and timeline range follows within 1 day.",[119,120,121],"Reply within 2 hours","20-minute call","Estimate within 1 day",[123,124,30,125,33,126,127,128],"Turnkey website","Online store","Testing","Design","Developer for your team","Support","en\u002Fsite","yowiXCE5f_PBbMxOr-Cl8oFTSBoiVPGSthZ0C1HAFyQ",{"id":132,"title":133,"author":134,"body":135,"cover":704,"coverFile":705,"coverLines":706,"coverStat":718,"date":719,"description":141,"excerpt":704,"extension":720,"faq":721,"featured":734,"key":735,"meta":736,"navigation":737,"path":738,"readingTime":739,"seo":740,"seoDescription":741,"seoTitle":742,"slug":743,"stem":744,"tags":745,"updated":747,"__hash__":748},"blog_en\u002Fen\u002Fblog\u002Fecommerce-development-cost.md","How Much Does an Online Store Cost in 2026? A Cost Breakdown","Євгеній",{"type":136,"value":137,"toc":687},"minimark",[138,142,151,156,159,194,197,201,204,210,221,232,235,239,242,342,345,349,357,360,367,371,374,378,381,443,446,453,456,472,476,479,521,524,528,531,570,573,604,608,611,617,623,626,630,633,656,663,667,670,673],[139,140,141],"p",{},"In 2026, building an online store costs anything from a few thousand hryvnias on a hosted builder to several hundred thousand on a custom solution. For a custom Laravel store with a catalog, payments, Nova Poshta and testing, our benchmark is this: from UAH 150,000 and roughly 10–14 weeks of work. Below we break down what that sum is made of, what pushes it up and what you can cut without hurting sales.",[139,143,144,145,150],{},"All figures below come from our ",[146,147,149],"a",{"href":148},"\u002Fen\u002Fpricing","pricing page"," and calculator, and where we give an estimate we say so explicitly. A real estimate always depends on your project, so treat this text as a map, not a price list.",[152,153,155],"h2",{"id":154},"why-how-much-does-an-online-store-cost-has-no-single-answer","Why \"how much does an online store cost\" has no single answer",[139,157,158],{},"The question is like \"how much does a car cost\". There is a bicycle, a used sedan and a four-wheel-drive crossover, and they all \"drive\". Stores are the same. Five things determine the cost.",[160,161,162,170,176,182,188],"ul",{},[163,164,165,169],"li",{},[166,167,168],"strong",{},"Platform:"," a hosted builder, a ready-made CMS, or custom development.",[163,171,172,175],{},[166,173,174],{},"Catalog size and product complexity:"," 50 products without variants or 5,000 with combinations of sizes and colors.",[163,177,178,181],{},[166,179,180],{},"Design:"," a template, an adapted layout, or a bespoke design.",[163,183,184,187],{},[166,185,186],{},"Integrations:"," payments, delivery, CRM, accounting system, marketplaces.",[163,189,190,193],{},[166,191,192],{},"Quality and risk:"," whether there is testing, automated tests, monitoring and a warranty.",[139,195,196],{},"The last item most often drops out of the comparison of proposals. Two quotes of UAH 150,000 can differ in that one tests the store on 12 devices, while in the other your first customer does the first test.",[152,198,200],{"id":199},"three-price-tiers-from-a-builder-to-custom","Three price tiers: from a builder to custom",[139,202,203],{},"Simplified, the market splits into three tiers. The approaches are approximate and are not anyone's official price list.",[139,205,206,209],{},[166,207,208],{},"Hosted builder (Shopify, Prom, Horoshop and similar)."," The start is fast and upfront costs are small. You pay a monthly subscription, commissions and paid extensions. Limits appear when you need non-standard logic: complex discounts, special shipping calculation, integration with your accounting system.",[139,211,212,215,216,220],{},[166,213,214],{},"Ready-made CMS (OpenCart, WooCommerce, PrestaShop) with a theme."," This is the middle path: the price is lower than custom, and there is an ecosystem of modules. The cost hides in customizations, updates and support for modules by different authors. We compared this in detail in ",[146,217,219],{"href":218},"\u002Fen\u002Fblog\u002Flaravel-vs-opencart-wordpress","Laravel vs OpenCart and WordPress for Ecommerce",".",[139,222,223,226,227,231],{},[166,224,225],{},"Custom development (Laravel + Nuxt)."," The highest upfront price and the most flexibility: the code belongs to you, there is no dependence on third-party modules, and complex logic is easy to add. This is our tier: ",[146,228,230],{"href":229},"\u002Fen\u002Fservices\u002Fecommerce-development","ecommerce development"," from UAH 150,000.",[139,233,234],{},"The rule for choosing is simple: if your business logic fits the platform's standard settings, custom is an overpayment. If you are already hitting limits or know you will within a year, saving at the start turns into a more expensive migration.",[152,236,238],{"id":237},"what-a-custom-store-estimate-is-made-of","What a custom store estimate is made of",[139,240,241],{},"Below is a typical budget split. The percentages are our estimates and shift on a specific project.",[243,244,245,261],"table",{},[246,247,248],"thead",{},[249,250,251,255,258],"tr",{},[252,253,254],"th",{},"Item",[252,256,257],{},"Approximate share",[252,259,260],{},"What is included",[262,263,264,276,287,298,309,320,331],"tbody",{},[249,265,266,270,273],{},[267,268,269],"td",{},"Analysis and planning",[267,271,272],{},"8–12%",[267,274,275],{},"Brief, catalog structure, user flow, requirements",[249,277,278,281,284],{},[267,279,280],{},"UI\u002FUX design",[267,282,283],{},"15–20%",[267,285,286],{},"Prototypes, key page design, responsive 375\u002F768\u002F1440, UI kit",[249,288,289,292,295],{},[267,290,291],{},"Frontend development",[267,293,294],{},"20–25%",[267,296,297],{},"Catalog, product page, cart, checkout, account",[249,299,300,303,306],{},[267,301,302],{},"Backend development and admin panel",[267,304,305],{},"25–30%",[267,307,308],{},"Product model, orders, roles, API, admin panel",[249,310,311,314,317],{},[267,312,313],{},"Integrations",[267,315,316],{},"8–15%",[267,318,319],{},"Payments, delivery, CRM, emails",[249,321,322,325,328],{},[267,323,324],{},"QA and fixes",[267,326,327],{},"10–15%",[267,329,330],{},"Manual testing, checkout automated tests, fixes",[249,332,333,336,339],{},[267,334,335],{},"Launch and warranty",[267,337,338],{},"3–5%",[267,340,341],{},"Deployment, redirects, analytics setup, 30-day warranty",[139,343,344],{},"Pay attention to QA. If the estimate has no testing at all, that is not savings but a deferred payment: you will pay with fixes after launch, when every bug costs sales.",[346,347,269],"h3",{"id":348},"analysis-and-planning",[139,350,351,352,356],{},"This is where we decide what exactly we are building. Poor analysis produces the most expensive mistakes, because they multiply across design and code. At this stage we find out how many categories there are, what attributes products have, how discounts are calculated, which payment and delivery methods are needed, and whether a B2B account area is required. That is why our ",[146,353,355],{"href":354},"\u002Fen\u002Fbrief","brief"," is detailed: it reduces the number of surprises in the middle of the project.",[346,358,126],{"id":359},"design",[139,361,362,363,366],{},"Bespoke design costs more, but a well-made store design affects conversion directly: from a clear checkout to a readable product page. If you already have a brand book, design gets cheaper. According to our calculator, an existing brand book lowers the cost by about 7%, and a ready layout by about 18%. For details on the process, see the ",[146,364,280],{"href":365},"\u002Fen\u002Fservices\u002Fui-ux-design"," page.",[346,368,370],{"id":369},"development","Development",[139,372,373],{},"This is the biggest item. It is driven by the number of entities (products, variants, brands, reviews, promotions), the complexity of pricing rules and the number of admin roles. Search and filters on a catalog of 2,000+ products also need a dedicated solution (full-text search, facet caching), not a \"magic button\".",[152,375,377],{"id":376},"what-pushes-the-price-up-extra-modules","What pushes the price up: extra modules",[139,379,380],{},"In our calculator, extra options are counted as a percentage of the base amount. It is a convenient model for estimating.",[243,382,383,393],{},[246,384,385],{},[249,386,387,390],{},[252,388,389],{},"Option",[252,391,392],{},"Cost increase",[262,394,395,403,411,419,427,435],{},[249,396,397,400],{},[267,398,399],{},"Online payment",[267,401,402],{},"about +6%",[249,404,405,408],{},[267,406,407],{},"Nova Poshta",[267,409,410],{},"about +5%",[249,412,413,416],{},[267,414,415],{},"CRM \u002F 1C",[267,417,418],{},"about +12%",[249,420,421,424],{},[267,422,423],{},"Second language",[267,425,426],{},"about +10%",[249,428,429,432],{},[267,430,431],{},"Playwright automated tests",[267,433,434],{},"about +8%",[249,436,437,440],{},[267,438,439],{},"Rush launch",[267,441,442],{},"+25%",[139,444,445],{},"A calculation example. Take a base of UAH 150,000. Add payment (+6%), Nova Poshta (+5%) and CRM (+12%): a total of +23%, or about UAH 34,500. The sum is roughly UAH 184,500. This is a benchmark, not an offer: the exact amount comes from an estimate after the brief.",[139,447,448,449,220],{},"Why are integrations expensive? Because the cost is not only about writing code. Every external system adds documentation to study, a sandbox environment, error handling (what to do when the API does not respond), retries, logging and testing in all states. We described the trouble spots of such integrations in ",[146,450,452],{"href":451},"\u002Fen\u002Fblog\u002Fnova-poshta-and-payment-integration","Nova Poshta and Payment Integration: Where It Breaks Most Often",[139,454,455],{},"A rush launch costs more because several people work in parallel: more coordination is needed, there is less time for checking, and mistakes cost more.",[457,458,459],"blockquote",{},[139,460,461,464,465,468,469,471],{},[166,462,463],{},"Want to see numbers for your store?"," Use the calculator on the ",[146,466,467],{"href":148},"pricing"," page: four questions and you get a range. We prepare an exact estimate within 1 business day after the ",[146,470,355],{"href":354},", with no obligation.",[152,473,475],{"id":474},"hidden-costs-people-forget","Hidden costs people forget",[139,477,478],{},"Besides development itself, a store budget includes things that are not in the developer's contract.",[160,480,481,487,493,499,509,515],{},[163,482,483,486],{},[166,484,485],{},"Hosting and domain."," For a store with moderate traffic this is not a lot of money, but you need a proper server, not the cheapest shared hosting. The domain and accounts must be in your name.",[163,488,489,492],{},[166,490,491],{},"Paid services:"," transactional email, SMS, CRM, search services. Payment provider fees are usually a percentage of turnover.",[163,494,495,498],{},[166,496,497],{},"Content."," Photos, descriptions, product specifications. Filling a catalog of 1,000 items takes more time than people expect.",[163,500,501,504,505,366],{},[166,502,503],{},"Support."," After launch the store lives on: library updates, fixes, new promotions, seasonal changes. Our support plans start from UAH 8,000 per month; details are on the ",[146,506,508],{"href":507},"\u002Fen\u002Fservices\u002Fwebsite-support","website support",[163,510,511,514],{},[166,512,513],{},"SEO."," The technical foundation is part of development, but promotion (copy, links, work with search queries) is a separate line.",[163,516,517,520],{},[166,518,519],{},"Marketing."," A store without traffic does not sell. Count the ad budget separately from development.",[139,522,523],{},"A safe rule: set aside 15–25% of the development cost per year for support and improvements. This is our practical estimate, not a standard.",[152,525,527],{"id":526},"how-to-reduce-the-budget-without-harm","How to reduce the budget without harm",[139,529,530],{},"You can save, and sensibly. Here is what works.",[532,533,534,540,546,552,558,564],"ol",{},[163,535,536,539],{},[166,537,538],{},"Start with an MVP."," Launch with 3–4 key flows (catalog, cart, payment, delivery) and add the rest based on data. Complex modules (a loyalty program, an account area with history, B2B prices) are easy to postpone to a second phase.",[163,541,542,545],{},[166,543,544],{},"Prepare materials in advance."," A brand book, photos and copy before the start shorten both the schedule and the cost.",[163,547,548,551],{},[166,549,550],{},"Use standard solutions where they are enough."," Do not write your own CRM if an existing one with an integration will do.",[163,553,554,557],{},[166,555,556],{},"Pick fewer integrations at launch."," One payment method and one delivery service instead of five.",[163,559,560,563],{},[166,561,562],{},"Avoid urgency"," if possible: +25% is a noticeable price for haste.",[163,565,566,569],{},[166,567,568],{},"Agree the scope in writing."," A vague scope leads to endless rework.",[139,571,572],{},"And here is where you should not save:",[160,574,575,586,592,598],{},[163,576,577,580,581,585],{},[166,578,579],{},"Testing checkout and payment."," A bug here means lost money every day. The ",[146,582,584],{"href":583},"\u002Fen\u002Fblog\u002Fecommerce-testing-checklist","60-point checklist"," will help you control the result even if you are not technical.",[163,587,588,591],{},[166,589,590],{},"Backups and security."," A store without backups is a lottery.",[163,593,594,597],{},[166,595,596],{},"Code ownership."," The contract must state clearly that the code belongs to you and that you hold access to the repository and the server.",[163,599,600,603],{},[166,601,602],{},"Documentation and handover."," Otherwise you become hostage to one developer.",[152,605,607],{"id":606},"two-sample-scenarios-for-comparison","Two sample scenarios for comparison",[139,609,610],{},"To keep the numbers from being abstract, consider two hypothetical stores. These are illustrations of the calculation, not real client projects.",[139,612,613,616],{},[166,614,615],{},"Scenario A: a small store at the start."," Up to 100 products, bespoke design, one online payment method and Nova Poshta. The base is about UAH 150,000, plus payment (+6%) and delivery (+5%): roughly UAH 166,500. Timeline: 8–10 weeks. This is a typical entry into custom development.",[139,618,619,622],{},[166,620,621],{},"Scenario B: a store with a catalog of several thousand items."," 2,000–10,000 products, a brand book is available, with payment, Nova Poshta, CRM integration and automated checkout tests. The base is higher because the catalog is larger (per the calculator, the range for a store is UAH 150,000–220,000), with a discount for a ready brand book (about 7%), and the markup for options is +31% (6% + 5% + 12% + 8%). The final estimate easily goes beyond UAH 220,000, and the timeline is closer to 12 weeks.",[139,624,625],{},"The difference between the scenarios shows the main point: the price is determined not by \"pages\" but by the volume of data, integrations and quality requirements. That is why an estimate without a brief should be treated only as a range.",[152,627,629],{"id":628},"how-to-check-a-contractors-estimate","How to check a contractor's estimate",[139,631,632],{},"When you receive three proposals with different prices, compare not the total but the composition. Ask each one:",[160,634,635,638,641,644,647,650,653],{},[163,636,637],{},"What exactly is included in the price and what is not (design, copy, testing, integrations, hosting)?",[163,639,640],{},"What assumptions are built in: number of products, pages, roles, languages?",[163,642,643],{},"Is there testing and who does it? Will there be automated tests?",[163,645,646],{},"What is the payment schedule? Our typical scheme is 30 \u002F 40 \u002F 30 by milestones, and it ties money to results.",[163,648,649],{},"Who owns the code, design and access after completion?",[163,651,652],{},"How long is the warranty and what does it cover?",[163,654,655],{},"What is the rate for out-of-scope changes? Our developer hourly rate is $20 to $35, depending on level.",[139,657,658,659,220],{},"A suspiciously low price usually means one of three things: something is missing from the scope, money is saved on testing, or the project will run on change requests that are paid separately. We collected more questions for choosing a vendor in ",[146,660,662],{"href":661},"\u002Fen\u002Fblog\u002Fhow-to-choose-web-development-contractor","How to Choose a Web Development Contractor: 12 Questions to Ask",[152,664,666],{"id":665},"summary-what-to-bring-to-a-contractor","Summary: what to bring to a contractor",[139,668,669],{},"A realistic budget for a custom store in 2026 starts from UAH 150,000, and with payment, Nova Poshta and CRM it usually lands in the UAH 180,000–220,000 range. This is our benchmark for a project of medium complexity, not a guarantee for any store.",[139,671,672],{},"To get an exact estimate, prepare five things: the approximate number of products, the list of integrations, whether you have a design or brand book, the desired timeline and the budget ceiling. With that set, an estimate takes a day, not a week, and you compare proposals on equal terms.",[457,674,675],{},[139,676,677,680,681,683,684,686],{},[166,678,679],{},"Ready to price your store?"," Fill in the ",[146,682,355],{"href":354}," in 10–15 minutes, and within 1 business day we will come back with a price range, timeline and list of risks. Or look at the ",[146,685,230],{"href":229}," page to see what the package includes.",{"title":14,"searchDepth":688,"depth":689,"links":690},2,3,[691,692,693,698,699,700,701,702,703],{"id":154,"depth":688,"text":155},{"id":199,"depth":688,"text":200},{"id":237,"depth":688,"text":238,"children":694},[695,696,697],{"id":348,"depth":689,"text":269},{"id":359,"depth":689,"text":126},{"id":369,"depth":689,"text":370},{"id":376,"depth":688,"text":377},{"id":474,"depth":688,"text":475},{"id":526,"depth":688,"text":527},{"id":606,"depth":688,"text":607},{"id":628,"depth":688,"text":629},{"id":665,"depth":688,"text":666},null,"estimate.xlsx",[707,710,712,715],{"marker":708,"text":709},"[INFO]","catalog and filters",{"marker":708,"text":711},"payment + Nova Poshta",{"marker":713,"text":714},"[WARN]","CRM \u002F 1C: +12%",{"marker":716,"text":717},"[PASS]","QA before launch","from UAH 150,000","2026-10-02","md",[722,725,728,731],{"q":723,"a":724},"How much does a custom online store cost in 2026?","A custom store from us starts from UAH 150,000 and takes 10–14 weeks. With payment, Nova Poshta and a CRM the total grows to roughly UAH 165,000–185,000. Ready-made CMS platforms and site builders are cheaper at the start, but come with limitations.",{"q":726,"a":727},"What hidden costs come with building an online store?","Hosting and domain, licenses for paid modules, payment system fees, content and photos, testing, and support and updates after launch. It is best to include them in the budget upfront. Support with us starts from UAH 8,000 per month.",{"q":729,"a":730},"How can I reduce a store budget without hurting quality?","Launch with a minimal feature set, move complex modules and integrations to a second phase, and use ready-made components where they do not limit you. Do not save on testing and security, since fixing them after launch costs more. You reach sales sooner and add features based on real data.",{"q":732,"a":733},"What does the price of building an online store depend on?","On the catalog size, the number of integrations (payment, delivery, CRM, warehouse), design complexity, languages and deadlines. Each extra module raises the base price. So ask for a detailed estimate by stage rather than one lump sum. It makes it easier to compare proposals from different contractors.",false,"skilky-koshtuie-rozrobka-internet-mahazynu",{},true,"\u002Fen\u002Fblog\u002Fecommerce-development-cost",11,{"title":133,"description":141},"How much does an online store cost in 2026: from UAH 150,000 for a custom build. What the estimate is made of, what drives the price and where to save.","How Much Does an Online Store Cost in 2026 — Tochno.","ecommerce-development-cost","en\u002Fblog\u002Fecommerce-development-cost",[746],"ecom","2026-10-30","xWdM70w9rvr_szF1ew5ECuwM_wkx9NyPJOB-5PfYW8o",[750,1291,2056,2817,3581,3970,4499,4995,5553,6320,7546,8318,9292],{"id":751,"title":752,"author":134,"body":753,"cover":704,"coverFile":1256,"coverLines":1257,"coverStat":1266,"date":747,"description":757,"excerpt":704,"extension":720,"faq":1267,"featured":734,"key":1280,"meta":1281,"navigation":737,"path":1282,"readingTime":1283,"seo":1284,"seoDescription":1285,"seoTitle":1286,"slug":1287,"stem":1288,"tags":1289,"updated":704,"__hash__":1290},"blog_en\u002Fen\u002Fblog\u002Fhoroshop-prom-opencart-or-custom-store.md","Horoshop, Prom, OpenCart or Custom: What to Choose for Your Store in 2026",{"type":136,"value":754,"toc":1240},[755,758,761,765,768,794,801,805,808,811,825,828,831,835,838,841,845,1030,1033,1037,1043,1052,1055,1064,1070,1081,1085,1088,1120,1128,1132,1135,1139,1153,1156,1160,1171,1174,1178,1189,1193,1210,1213,1217,1220,1229],[139,756,757],{},"For a small store with a catalog of up to a few thousand products and a standard checkout, a SaaS builder such as Horoshop usually works best: a low monthly fee, a quick start and zero sales commission. You need Prom if the ready-made traffic of a marketplace matters to you. OpenCart remains an option when you have your own developer. Order custom development when the platform's limits are already costing you money.",[139,759,760],{},"Below we go through the four paths from an owner's point of view: what you pay every month, what commissions and limits apply, how much control you have over SEO, how Nova Poshta, LiqPay and 1C work, and what happens when you want to leave. The author, Evgeniy, has written PHP\u002FLaravel for 11 years and tests stores, so we sell custom work and have an obvious bias. That is why we say plainly where it is not needed.",[152,762,764],{"id":763},"four-paths-not-one-choice","Four paths, not one choice",[139,766,767],{},"These options are often compared as if they were the same thing, but they are different models.",[160,769,770,776,782,788],{},[163,771,772,775],{},[166,773,774],{},"SaaS builder (Horoshop and similar)."," You rent a ready-made store: hosting, admin panel and updates are included in the fee. You do not own the platform code; you change themes and settings.",[163,777,778,781],{},[166,779,780],{},"Marketplace (Prom)."," You sell in someone else's catalog with ready-made traffic and pay for a package plus a commission on orders. Prom also has a standalone-site mode, but the trading logic stays platform-bound.",[163,783,784,787],{},[166,785,786],{},"Self-hosted CMS (OpenCart)."," Free software: you choose the hosting, modules and developer yourself and are responsible for updates and security.",[163,789,790,793],{},[166,791,792],{},"Custom (Laravel, Nuxt and others)."," The store is built around your processes. The code and data are yours, but the start is longer and more expensive.",[139,795,796,797,800],{},"So the question is not \"which is better\" but \"who carries which risk, and for how much money\". The answer depends on the catalog, the budget and whether someone on the team is responsible for the technical side. We covered self-hosted CMS separately in our comparison of ",[146,798,799],{"href":218},"Laravel, OpenCart and WordPress",", so we do not repeat it here and focus on SaaS and the marketplace.",[152,802,804],{"id":803},"money-fees-commissions-and-hidden-costs","Money: fees, commissions and hidden costs",[139,806,807],{},"Horoshop has a fixed monthly fee that depends on the number of products and features (API, wholesale prices, B2B): roughly from a few hundred to about two thousand hryvnias per month on annual billing. The platform takes no sales commission, and hosting and SSL are included. Tariffs change, so check current prices on the official site horoshop.ua before deciding.",[139,809,810],{},"But the fee is not the whole cost. Add:",[160,812,813,816,819,822],{},[163,814,815],{},"setup and design (a template or a studio's work);",[163,817,818],{},"paid integrations and services that are not in the basic plan;",[163,820,821],{},"payment fees: LiqPay and other acquirers take their percentage regardless of the platform;",[163,823,824],{},"content: photos, descriptions, import.",[139,826,827],{},"Prom works differently. According to support.prom.ua, a seller pays for a yearly package or a basic subscription, plus a commission on completed orders, and its size depends on the category and the package terms. Separate commission rules exist for a site with your own domain. Prom's exact percentages have changed several times in recent years and public sources disagree, so we do not quote specific figures: before you start, work out the commission for your own category in the seller account.",[139,829,830],{},"OpenCart is free as software but not as a project: you need hosting, paid modules and themes, a developer's work and regular updates. A typical scenario is a cheap first year, then growing costs for customizations and fixes. Custom is the most expensive at the start: a store at Tochno costs from UAH 150,000 and takes 10–14 weeks, but afterwards you do not pay rent to a platform.",[346,832,834],{"id":833},"how-to-calculate-the-cost-over-three-years","How to calculate the cost over three years",[139,836,837],{},"Compare not a month but three years, which is about how long a first version of a store lives. Suppose the fee is UAH 800 per month: over 36 months that is about UAH 29,000. Add setup and design, say another few tens of thousands of hryvnias, and you get roughly UAH 50,000–70,000 over three years with no platform commission. This is a hypothetical example of our own arithmetic, not a tariff or a studio's offer, so plug in your own numbers.",[139,839,840],{},"With custom the math is different: from UAH 150,000 at the start, then hosting and support (ours starts from UAH 8,000 per month if you need it). So over a three-year horizon custom is almost always more expensive than a builder. It wins not on price but on having no limits: when the catalog has grown, you need your own pricing rules, or the builder makes you pay for workarounds. If you cannot name a specific limit that is stopping you, custom is premature.",[152,842,844],{"id":843},"comparison-by-owners-criteria","Comparison by owner's criteria",[243,846,847,866],{},[246,848,849],{},[249,850,851,854,857,860,863],{},[252,852,853],{},"Criterion",[252,855,856],{},"Horoshop (SaaS)",[252,858,859],{},"Prom",[252,861,862],{},"OpenCart",[252,864,865],{},"Custom",[262,867,868,885,902,917,932,949,966,983,1000,1014],{},[249,869,870,873,876,879,882],{},[267,871,872],{},"Start",[267,874,875],{},"Days or weeks",[267,877,878],{},"Days",[267,880,881],{},"Weeks",[267,883,884],{},"10–14 weeks",[249,886,887,890,893,896,899],{},[267,888,889],{},"Monthly costs",[267,891,892],{},"Fixed fee",[267,894,895],{},"Package and order commission",[267,897,898],{},"Hosting, modules, support",[267,900,901],{},"Hosting and support",[249,903,904,907,910,913,915],{},[267,905,906],{},"Sales commission",[267,908,909],{},"None",[267,911,912],{},"Yes, depends on category",[267,914,909],{},[267,916,909],{},[249,918,919,922,925,928,930],{},[267,920,921],{},"Own brand and customer base",[267,923,924],{},"Yes",[267,926,927],{},"Partly, the customer is in the Prom catalog",[267,929,924],{},[267,931,924],{},[249,933,934,937,940,943,946],{},[267,935,936],{},"SEO control",[267,938,939],{},"Medium: standard settings",[267,941,942],{},"Limited",[267,944,945],{},"High, but depends on setup",[267,947,948],{},"Full",[249,950,951,954,957,960,963],{},[267,952,953],{},"Non-standard logic",[267,955,956],{},"Limited by plan",[267,958,959],{},"Minimal",[267,961,962],{},"Via modules",[267,964,965],{},"No limits",[249,967,968,971,974,977,980],{},[267,969,970],{},"Nova Poshta, LiqPay",[267,972,973],{},"Ready modules",[267,975,976],{},"Built-in Prom services",[267,978,979],{},"Modules from various authors",[267,981,982],{},"Built around your process",[249,984,985,988,991,994,997],{},[267,986,987],{},"1C integration",[267,989,990],{},"Typical, via exchange",[267,992,993],{},"Import and API",[267,995,996],{},"Modules, quality varies",[267,998,999],{},"Built around your setup",[249,1001,1002,1005,1008,1010,1012],{},[267,1003,1004],{},"Code ownership",[267,1006,1007],{},"No",[267,1009,1007],{},[267,1011,924],{},[267,1013,924],{},[249,1015,1016,1019,1022,1025,1028],{},[267,1017,1018],{},"Leaving the platform",[267,1020,1021],{},"Data export, new URLs",[267,1023,1024],{},"Export, the customer stays with Prom",[267,1026,1027],{},"Simple",[267,1029,1027],{},[139,1031,1032],{},"The table is simplified. The real difference depends on the specific plan, template and contractor, but it shows the direction correctly.",[152,1034,1036],{"id":1035},"limits-seo-and-integrations-where-it-hurts-most","Limits, SEO and integrations: where it hurts most",[139,1038,1039,1042],{},[166,1040,1041],{},"Catalog and plan limits."," In builders, the number of products, API access, wholesale prices and multi-warehouse features are tied to the plan. While you stay within them it is a good deal. As you grow, you move to a more expensive package and the calculation changes.",[139,1044,1045,1047,1048,220],{},[166,1046,513],{}," Builders can already generate meta tags, a sitemap and basic markup. For most small stores that is enough. Problems appear when you need complex filters with indexable pages, your own canonical URL rules, non-standard markup or decent mobile speed. With custom you control everything: URL structure, internal linking, speed, markup. More on this in the article ",[146,1049,1051],{"href":1050},"\u002Fen\u002Fblog\u002Fcore-web-vitals-for-store-owners","Core Web Vitals for store owners",[139,1053,1054],{},"On Prom you compete inside someone else's catalog, and your own site is not the main source of buyers there. That is fine, but do not expect Prom to grow your brand in Google.",[139,1056,1057,1060,1061,220],{},[166,1058,1059],{},"Integrations."," Nova Poshta and LiqPay are available almost everywhere, but \"available\" and \"works without losing orders\" are different things. Typical problems are mismatched waybill (TTN) statuses, double charges and an order stuck after a failed payment. With ready-made modules you depend on the module's author; with custom, on the quality of the tests. More on where this breaks in the article about ",[146,1062,1063],{"href":451},"Nova Poshta and payment integration",[139,1065,1066,1069],{},[166,1067,1068],{},"1C and accounting."," If stock and prices live in 1C, most platforms support file exchange. For complex logic (several warehouses, special customer prices, partial shipments) a builder often becomes too tight, and you have to look for workarounds.",[457,1071,1072],{},[139,1073,1074,1077,1078,220],{},[166,1075,1076],{},"Not sure a builder will be enough?"," Tell us about your catalog, integrations and plans for the year, and we will honestly say whether you need custom development. If you do not, we will say so. ",[146,1079,1080],{"href":354},"Fill in the brief",[152,1082,1084],{"id":1083},"ownership-and-migration-what-happens-when-you-want-to-leave","Ownership and migration: what happens when you want to leave",[139,1086,1087],{},"This is the question people remember too late. Store data (products, categories, customers) can usually be exported from any platform, but everything around it moves less easily.",[160,1089,1090,1096,1102,1108,1114],{},[163,1091,1092,1095],{},[166,1093,1094],{},"Page URLs."," A new platform almost always has a different URL structure. Without 301 redirects you will lose search positions, and recovering them takes months. This is the main migration risk.",[163,1097,1098,1101],{},[166,1099,1100],{},"Order history and customers."," Moved as a separate piece of work, and not always completely. Customer passwords cannot be transferred and have to be reset.",[163,1103,1104,1107],{},[166,1105,1106],{},"Reviews and content."," Depend on the export format. Sometimes it is easier to take them from the site than from the system.",[163,1109,1110,1113],{},[166,1111,1112],{},"Design."," A builder's theme does not move with you. It means a new design or a new layout.",[163,1115,1116,1119],{},[166,1117,1118],{},"Prom customers."," Buyers who came through the marketplace remain Prom's buyers. You will only have a mailing base from those who ordered on your own site.",[139,1121,1122,1123,1127],{},"So when choosing a platform, ask right away: what can I take with me and how much will it cost. Before migrating to a new platform, it is useful to run a ",[146,1124,1126],{"href":1125},"\u002Fen\u002Fservices\u002Fecommerce-qa-audit","QA audit of the online store"," on the old and new sites: check redirects, meta tags, the order form, payment and delivery before the switch, not after the first customer complaint. An audit with us starts from UAH 6,000.",[152,1129,1131],{"id":1130},"when-to-choose-what-decision-rules","When to choose what: decision rules",[139,1133,1134],{},"Here are simple rules. They are not perfect, but they work in most situations.",[346,1136,1138],{"id":1137},"choose-horoshop-or-a-similar-builder-if","Choose Horoshop or a similar builder if",[160,1140,1141,1144,1147,1150],{},[163,1142,1143],{},"the catalog is up to a few thousand products with standard processes: cart, payment, delivery by Nova Poshta;",[163,1145,1146],{},"the budget is limited and you need to launch in weeks, not months;",[163,1148,1149],{},"there is no technical person to maintain the site;",[163,1151,1152],{},"you are testing a niche and do not know yet whether it will sell.",[139,1154,1155],{},"In this case custom for UAH 150,000 and up simply will not pay off, and we do not advise ordering it.",[346,1157,1159],{"id":1158},"choose-prom-if","Choose Prom if",[160,1161,1162,1165,1168],{},[163,1163,1164],{},"you need first sales without your own traffic;",[163,1166,1167],{},"the product is easily found through marketplace categories;",[163,1169,1170],{},"you are ready to build the commission into the price.",[139,1172,1173],{},"Prom pairs well with your own site: the marketplace brings new buyers, and the site grows repeat sales without a commission.",[346,1175,1177],{"id":1176},"choose-opencart-if","Choose OpenCart if",[160,1179,1180,1183,1186],{},[163,1181,1182],{},"you have your own developer or a long-term support contract;",[163,1184,1185],{},"you need more freedom than a builder gives, but custom is still too high for the budget;",[163,1187,1188],{},"you are ready to keep up with updates and security.",[346,1190,1192],{"id":1191},"choose-custom-if","Choose custom if",[160,1194,1195,1198,1201,1204,1207],{},[163,1196,1197],{},"the platform already limits you: catalog, prices for customer groups, complex discount logic, a B2B account;",[163,1199,1200],{},"you need non-standard integrations with 1C, an ERP or a warehouse;",[163,1202,1203],{},"speed and SEO are critical and template solutions do not deliver;",[163,1205,1206],{},"you count not the first year but three to five years and see that platform rent plus workarounds cost more than your own code;",[163,1208,1209],{},"you need automated checkout tests and quality control before every release.",[139,1211,1212],{},"Three questions help you check yourself. Can I describe my sales process in standard steps? Do the plan's features cover me both now and in two years? Do I have someone who is technically responsible for the site? If the answer to all three is \"yes\", take a builder.",[152,1214,1216],{"id":1215},"conclusion","Conclusion",[139,1218,1219],{},"The most expensive solution is rarely the best, and the cheapest often just postpones the costs. For a small store, Horoshop or a similar builder is usually a sensible choice in 2026, Prom complements it with traffic, OpenCart needs a technical owner, and custom wins when the platform's limits are already felt in money. Before you pay, calculate the cost over three years and check what exactly you can take with you if you decide to leave.",[139,1221,1222,1223,1226,1227,366],{},"We covered cost benchmarks for custom development in the article ",[146,1224,1225],{"href":738},"how much does online store development cost",", and Tochno's public prices are collected on the ",[146,1228,467],{"href":148},[457,1230,1231],{},[139,1232,1233,1236,1237,220],{},[166,1234,1235],{},"Choosing a platform for your store?"," Describe your catalog, integrations and budget, and we will tell you what fits: a builder or custom. If you need development, see ",[146,1238,1239],{"href":229},"an online store from UAH 150,000, 10–14 weeks",{"title":14,"searchDepth":688,"depth":689,"links":1241},[1242,1243,1246,1247,1248,1249,1255],{"id":763,"depth":688,"text":764},{"id":803,"depth":688,"text":804,"children":1244},[1245],{"id":833,"depth":689,"text":834},{"id":843,"depth":688,"text":844},{"id":1035,"depth":688,"text":1036},{"id":1083,"depth":688,"text":1084},{"id":1130,"depth":688,"text":1131,"children":1250},[1251,1252,1253,1254],{"id":1137,"depth":689,"text":1138},{"id":1158,"depth":689,"text":1159},{"id":1176,"depth":689,"text":1177},{"id":1191,"depth":689,"text":1192},{"id":1215,"depth":688,"text":1216},"platform.choice",[1258,1260,1262,1264],{"marker":716,"text":1259},"Horoshop: fast start, zero commission",{"marker":713,"text":1261},"Prom: traffic, but the customer is not yours",{"marker":708,"text":1263},"OpenCart: freedom + you maintain it",{"marker":716,"text":1265},"Custom: when limits already hurt","4 paths",[1268,1271,1274,1277],{"q":1269,"a":1270},"What should a small store choose in 2026: Horoshop or Prom?","If you want your own brand, a site in Google search and your own customer base, choose Horoshop or a similar builder: a fixed monthly fee and no sales commission. Prom fits when you need the ready-made traffic of a marketplace. The two channels are often combined: a site for repeat customers, Prom for new buyers.",{"q":1272,"a":1273},"Can I move from Horoshop or Prom to my own site?","Yes. Products and categories can be exported to a file, but there are catches: page URLs will change, so you need 301 redirects, and order history and reviews must be moved separately. A proper migration takes a few weeks and needs SEO and checkout checks before you switch over.",{"q":1275,"a":1276},"How much does a custom online store cost compared with a builder?","A builder costs from a few hundred hryvnias a month plus setup. A custom store at Tochno starts from UAH 150,000 and takes 10–14 weeks. The difference pays off not at launch but when a builder starts limiting your catalog, pricing logic, integrations or speed.",{"q":1278,"a":1279},"Is OpenCart still a good choice in 2026?","It works for a mid-sized catalog if you have a developer who updates the system and checks module compatibility. The platform itself is free, but hosting, security, updates and customizations are on you. Without technical support, an OpenCart store gradually becomes slow and vulnerable.","horoshop-prom-opencart-chy-kastom",{},"\u002Fen\u002Fblog\u002Fhoroshop-prom-opencart-or-custom-store",10,{"title":752,"description":757},"Horoshop, Prom, OpenCart or a custom store: compared by fees, commissions, SEO, integrations and migration. What to choose in 2026 and when.","Horoshop, Prom or Custom Store in 2026 — Tochno.","horoshop-prom-opencart-or-custom-store","en\u002Fblog\u002Fhoroshop-prom-opencart-or-custom-store",[746],"H4-dlUiHrJQMammD0fVkBs7YQdzcjMMlXW6_LyMY5-g",{"id":1292,"title":1293,"author":134,"body":1294,"cover":704,"coverFile":2020,"coverLines":2021,"coverStat":2031,"date":719,"description":1298,"excerpt":704,"extension":720,"faq":2032,"featured":734,"key":2045,"meta":2046,"navigation":737,"path":2047,"readingTime":739,"seo":2048,"seoDescription":2049,"seoTitle":2050,"slug":2051,"stem":2052,"tags":2053,"updated":747,"__hash__":2055},"blog_en\u002Fen\u002Fblog\u002Fbug-report-and-test-case-example.md","How We Test: A Bug Report and Test Case Example",{"type":136,"value":1295,"toc":1997},[1296,1299,1302,1306,1309,1312,1332,1340,1344,1347,1351,1354,1362,1365,1369,1372,1389,1393,1396,1400,1403,1407,1410,1449,1452,1455,1481,1485,1488,1492,1495,1583,1586,1590,1622,1626,1629,1632,1711,1715,1761,1764,1768,1771,1799,1802,1806,1850,1857,1868,1872,1875,1889,1892,1896,1899,1931,1934,1938,1941,1962,1972,1976,1979,1986],[139,1297,1298],{},"A good bug report lets a developer reproduce the problem in two minutes without sending the tester a single follow-up question. A good test case lets anyone on the team walk through a scenario the same way and get a comparable result. These are the two basic things that website testing rests on, and they show whether QA is really working or just \"clicking around\".",[139,1300,1301],{},"In this article we show our approach: the structure of a bug report, the structure of a test case, examples based on a checkout scenario and how we assess severity. All the examples below are illustrative, made up for explanation, and do not belong to any specific client.",[152,1303,1305],{"id":1304},"why-standardize-bug-reports","Why standardize bug reports",[139,1307,1308],{},"Bad bug reports cost more than they seem. The phrase \"payment doesn't work on mobile\" starts a chain: the developer asks which device, the tester answers the next day, the developer cannot reproduce it, marks it \"cannot reproduce\", the ticket is closed, and the bug stays in production.",[139,1310,1311],{},"A standard format gives three things:",[160,1313,1314,1320,1326],{},[163,1315,1316,1319],{},[166,1317,1318],{},"speed",": the developer sees at once what to do and does not spend the morning on an interrogation;",[163,1321,1322,1325],{},[166,1323,1324],{},"comparability",": ten bugs in the same structure are easy to sort by impact;",[163,1327,1328,1331],{},[166,1329,1330],{},"memory",": half a year later you can find what has already broken and add a regression test.",[139,1333,1334,1335,1339],{},"That is why in our ",[146,1336,1338],{"href":1337},"\u002Fen\u002Fservices\u002Fwebsite-testing","website testing"," every bug in the report has the same form and a video.",[152,1341,1343],{"id":1342},"anatomy-of-a-bug-report","Anatomy of a bug report",[139,1345,1346],{},"Here are the fields we always fill in.",[346,1348,1350],{"id":1349},"title","Title",[139,1352,1353],{},"The title answers three questions: where, what, under what conditions. Compare:",[160,1355,1356,1359],{},[163,1357,1358],{},"Bad: \"Cart doesn't work\".",[163,1360,1361],{},"Good: \"Cart: after deleting an item the coupon stays and the discount is calculated from the old total\".",[139,1363,1364],{},"The title should be clear without opening the ticket, because in the list the developer sees only that.",[346,1366,1368],{"id":1367},"environment","Environment",[139,1370,1371],{},"Without the environment a bug may be impossible to reproduce. We record:",[160,1373,1374,1377,1380,1383,1386],{},[163,1375,1376],{},"the URL and the build version or deployment date;",[163,1378,1379],{},"the device and OS (iPhone 13, iOS 17.5; Windows 11);",[163,1381,1382],{},"the browser and version;",[163,1384,1385],{},"the user role (guest, logged in, administrator);",[163,1387,1388],{},"the test data (which product, which coupon, which payment method).",[346,1390,1392],{"id":1391},"steps-to-reproduce","Steps to reproduce",[139,1394,1395],{},"A numbered list, each step one action. Not \"place an order\" but specific clicks. The steps should let an unfamiliar person reproduce the bug without guessing.",[346,1397,1399],{"id":1398},"expected-and-actual-result","Expected and actual result",[139,1401,1402],{},"These are two separate lines. The expected result is based on requirements or logic (\"after an item is deleted the discount is recalculated\"), and the actual one describes what happened. That way the developer sees the gap at once. If the expected behavior contradicts the documentation, state the source.",[346,1404,1406],{"id":1405},"severity-and-priority","Severity and priority",[139,1408,1409],{},"These are different concepts, and they are mixed up most often.",[243,1411,1412,1425],{},[246,1413,1414],{},[249,1415,1416,1419,1422],{},[252,1417,1418],{},"Parameter",[252,1420,1421],{},"What it means",[252,1423,1424],{},"Who decides",[262,1426,1427,1438],{},[249,1428,1429,1432,1435],{},[267,1430,1431],{},"Severity",[267,1433,1434],{},"how badly the bug breaks the product technically",[267,1436,1437],{},"QA",[249,1439,1440,1443,1446],{},[267,1441,1442],{},"Priority",[267,1444,1445],{},"how fast it must be fixed from a business point of view",[267,1447,1448],{},"the client, the product owner",[139,1450,1451],{},"A bug with a typo in the logo on the home page has low severity but may have high priority. A bug that breaks only on Internet Explorer 11 may have high severity but zero priority, because there is no traffic from that browser.",[139,1453,1454],{},"Our severity scale:",[160,1456,1457,1463,1469,1475],{},[163,1458,1459,1462],{},[166,1460,1461],{},"S1 (critical)",": blocks the main flow, loss of money or data. Example: payment does not go through.",[163,1464,1465,1468],{},[166,1466,1467],{},"S2 (high)",": the main flow works but with a noticeable distortion. Example: a wrong discount amount.",[163,1470,1471,1474],{},[166,1472,1473],{},"S3 (medium)",": there is a workaround. Example: the filter resets when going back.",[163,1476,1477,1480],{},[166,1478,1479],{},"S4 (low)",": cosmetics, small UI details.",[346,1482,1484],{"id":1483},"evidence","Evidence",[139,1486,1487],{},"A screenshot with the problem highlighted, a screen recording (almost mandatory for dynamic bugs), and console and network logs if available. A video saves dozens of messages in the thread. For mobile bugs it helps if the recording shows the device indicators.",[152,1489,1491],{"id":1490},"a-bug-report-example","A bug report example",[139,1493,1494],{},"Below is a typical bug we find in stores. The product and numbers are invented for illustration.",[457,1496,1497,1502,1516,1522,1533,1538,1555,1561,1567,1577],{},[139,1498,1499],{},[166,1500,1501],{},"BUG-017. Cart: after deleting an item the coupon stays and the discount is calculated from the old total",[139,1503,1504,1507,1508,1511,1512,1515],{},[166,1505,1506],{},"Severity:"," S2. ",[166,1509,1510],{},"Priority:"," P1. ",[166,1513,1514],{},"Status:"," new.",[139,1517,1518,1521],{},[166,1519,1520],{},"Environment:"," staging, build of 2026-10-01, Chrome 129 \u002F Windows 11 and Safari \u002F iPhone 13 (iOS 17.5). Role: guest.",[139,1523,1524,1527,1528,1532],{},[166,1525,1526],{},"Preconditions:"," the catalog has product A (UAH 1,000) and product B (UAH 500). Coupon ",[1529,1530,1531],"code",{},"SALE10"," is active (10% off orders from UAH 1,200).",[139,1534,1535],{},[166,1536,1537],{},"Steps:",[532,1539,1540,1543,1549,1552],{},[163,1541,1542],{},"Add product A and product B to the cart (total UAH 1,500).",[163,1544,1545,1546,1548],{},"Apply coupon ",[1529,1547,1531],{},". Total: UAH 1,350.",[163,1550,1551],{},"Remove product A from the cart.",[163,1553,1554],{},"Look at the total.",[139,1556,1557,1560],{},[166,1558,1559],{},"Expected result:"," product B for UAH 500 remains. The coupon does not apply (minimum UAH 1,200), the total is UAH 500, and the user sees a message that the coupon was cancelled.",[139,1562,1563,1566],{},[166,1564,1565],{},"Actual result:"," the total is UAH 450, the coupon stays applied. At checkout the amount is UAH 450.",[139,1568,1569,1572,1573,1576],{},[166,1570,1571],{},"Reproducibility:"," 3 of 3 attempts. Evidence: video ",[1529,1574,1575],{},"bug-017.mp4",", a screenshot of the cart.",[139,1578,1579,1582],{},[166,1580,1581],{},"Note:"," a similar bug is possible when changing the product quantity with the \"minus\" button.",[139,1584,1585],{},"Notice the details: preconditions are described (otherwise the developer will not find the right coupon), there is a note about a neighboring scenario, and reproducibility is given as a number. This is not bureaucracy; it saves the time of two people.",[346,1587,1589],{"id":1588},"common-mistakes-in-bug-reports","Common mistakes in bug reports",[160,1591,1592,1598,1604,1610,1616],{},[163,1593,1594,1597],{},[166,1595,1596],{},"Several bugs in one ticket."," The developer fixes one and closes the ticket. One bug, one ticket.",[163,1599,1600,1603],{},[166,1601,1602],{},"Opinion instead of fact."," \"Search works terribly.\" Fact: \"search for the word 'jacket' returns 0 results although there are 14 products.\"",[163,1605,1606,1609],{},[166,1607,1608],{},"No expected result."," Without it a ticket becomes a matter of taste.",[163,1611,1612,1615],{},[166,1613,1614],{},"A bug in someone else's environment."," Check that the problem is not a browser extension or the cache before writing the report.",[163,1617,1618,1621],{},[166,1619,1620],{},"A ticket closed without verification."," \"Fixed\" should mean \"QA repeated the steps and confirmed\".",[152,1623,1625],{"id":1624},"anatomy-of-a-test-case","Anatomy of a test case",[139,1627,1628],{},"A bug report records a problem that has been found, while a test case describes exactly what we check before any problems are found. It is a tool for repeatability: the same scenario can be run before every release.",[139,1630,1631],{},"Test case fields:",[243,1633,1634,1644],{},[246,1635,1636],{},[249,1637,1638,1641],{},[252,1639,1640],{},"Field",[252,1642,1643],{},"What we write",[262,1645,1646,1657,1664,1672,1680,1688,1695,1703],{},[249,1647,1648,1651],{},[267,1649,1650],{},"ID",[267,1652,1653,1654],{},"a unique code, for example ",[1529,1655,1656],{},"CHK-004",[249,1658,1659,1661],{},[267,1660,1350],{},[267,1662,1663],{},"briefly, what is being checked",[249,1665,1666,1669],{},[267,1667,1668],{},"Preconditions",[267,1670,1671],{},"system state, test data, role",[249,1673,1674,1677],{},[267,1675,1676],{},"Steps",[267,1678,1679],{},"a numbered list of actions",[249,1681,1682,1685],{},[267,1683,1684],{},"Expected result",[267,1686,1687],{},"for each step or for the scenario",[249,1689,1690,1692],{},[267,1691,1442],{},[267,1693,1694],{},"how often and how critically it should be run",[249,1696,1697,1700],{},[267,1698,1699],{},"Type",[267,1701,1702],{},"positive, negative, boundary",[249,1704,1705,1708],{},[267,1706,1707],{},"Status",[267,1709,1710],{},"pass \u002F fail \u002F blocked \u002F not run",[346,1712,1714],{"id":1713},"a-test-case-example","A test case example",[457,1716,1717,1722,1727,1732,1736,1756],{},[139,1718,1719],{},[166,1720,1721],{},"CHK-004. Guest checkout with card payment (positive scenario)",[139,1723,1724,1726],{},[166,1725,1510],{}," high (part of the smoke suite).",[139,1728,1729,1731],{},[166,1730,1526],{}," the product is in stock, the payment provider is in sandbox mode, the user is not logged in.",[139,1733,1734],{},[166,1735,1537],{},[532,1737,1738,1741,1744,1747,1750,1753],{},[163,1739,1740],{},"Open the product page, add it to the cart.",[163,1742,1743],{},"Go to the cart, click \"Checkout\".",[163,1745,1746],{},"Fill in the contact details (name, phone, email).",[163,1748,1749],{},"Choose Nova Poshta delivery: city, branch.",[163,1751,1752],{},"Choose card payment, click \"Confirm\".",[163,1754,1755],{},"Enter the test card on the payment system page.",[139,1757,1758,1760],{},[166,1759,1559],{}," after step 6 the \"Thank you\" page opens, the order number matches the confirmation email, the order status in the admin panel is \"paid\", and the product stock decreased by 1.",[139,1762,1763],{},"Exactly this kind of case goes into our smoke suite: a short list of a dozen scenarios that are run after every deployment in 15–20 minutes. If even one fails, the release stops.",[346,1765,1767],{"id":1766},"positive-negative-and-boundary-cases","Positive, negative and boundary cases",[139,1769,1770],{},"For every feature we write at least three groups of cases. Take the \"Phone\" field at checkout:",[160,1772,1773,1783,1793],{},[163,1774,1775,1778,1779,1782],{},[166,1776,1777],{},"Positive:"," ",[1529,1780,1781],{},"+380501234567"," is accepted.",[163,1784,1785,1788,1789,1792],{},[166,1786,1787],{},"Negative:"," letters, an empty field, a number without a country code, an SQL-like string (",[1529,1790,1791],{},"' OR 1=1 --",") do not break the form and do not reach the database as code.",[163,1794,1795,1798],{},[166,1796,1797],{},"Boundary:"," 9 digits, 10, 13, 14; leading spaces; pasting a number with parentheses and hyphens.",[139,1800,1801],{},"Most real bugs live in the negative and boundary scenarios, because a developer by default checks what is supposed to work.",[152,1803,1805],{"id":1804},"from-test-case-to-result-what-our-process-looks-like","From test case to result: what our process looks like",[532,1807,1808,1814,1820,1826,1832,1838,1844],{},[163,1809,1810,1813],{},[166,1811,1812],{},"Requirements analysis."," We read the spec, the mockups and the site itself, looking for ambiguities. Questions to the client at this stage cost less than bugs.",[163,1815,1816,1819],{},[166,1817,1818],{},"Test plan."," We record what we test, what we do not, on which devices, in which browsers and in what time.",[163,1821,1822,1825],{},[166,1823,1824],{},"Test cases."," We write them or take them from our ecommerce checklist (150+ items) and adapt them to the project.",[163,1827,1828,1831],{},[166,1829,1830],{},"Execution."," We walk through the scenarios and record the result. In parallel we do exploratory testing: free-form scenarios outside the list, where the most interesting bugs are often found.",[163,1833,1834,1837],{},[166,1835,1836],{},"Bug reports."," Every bug is written up immediately using the template, with a video.",[163,1839,1840,1843],{},[166,1841,1842],{},"Report."," A summary: what was checked, what was found, the distribution by severity, and a \"ready to release or not\" recommendation.",[163,1845,1846,1849],{},[166,1847,1848],{},"Retest."," After fixes we verify every fixed bug and run regression around it.",[139,1851,1852,1853,220],{},"A separate word about regression. Fixing one bug often breaks neighboring functionality, so we do not limit ourselves to checking the fix itself. If a scenario is key, it is worth moving into an automated test: we wrote about when that is justified in the article on ",[146,1854,1856],{"href":1855},"\u002Fen\u002Fblog\u002Fmanual-vs-automated-testing","manual vs automated testing",[457,1858,1859],{},[139,1860,1861,1864,1865,220],{},[166,1862,1863],{},"Need a report like this for your site?"," We test manually, record every bug using the template with a video, and prioritize by impact on money. ",[146,1866,1867],{"href":1337},"Learn about website testing",[152,1869,1871],{"id":1870},"how-we-prioritize-what-we-find","How we prioritize what we find",[139,1873,1874],{},"After testing, the report holds dozens of bugs, and the budget for fixes is limited. So we sort them not in the order found but by impact on money and trust. A simple approach that works in practice:",[532,1876,1877,1880,1883,1886],{},[163,1878,1879],{},"First everything that blocks purchase: cart, checkout, payment, delivery.",[163,1881,1882],{},"Then bugs that corrupt data: wrong amounts, duplicate orders, lost emails.",[163,1884,1885],{},"Then problems that push the customer away: slow pages, a broken mobile version, confusing form errors.",[163,1887,1888],{},"Last, cosmetics and small inaccuracies.",[139,1890,1891],{},"That way the client sees at once where to start and can fix the 20% of bugs that cause 80% of the problems.",[152,1893,1895],{"id":1894},"tools-what-we-need-and-what-we-do-not","Tools: what we need and what we do not",[139,1897,1898],{},"You do not need an expensive system to do quality testing. For small teams the following is enough:",[160,1900,1901,1907,1913,1919,1925],{},[163,1902,1903,1906],{},[166,1904,1905],{},"A bug tracker:"," Jira, Linear, GitHub Issues or even Trello. The main thing is that the ticket template is the same.",[163,1908,1909,1912],{},[166,1910,1911],{},"Test cases:"," a spreadsheet (Google Sheets) or a TestRail-like tool. For 30–60 cases a spreadsheet is enough.",[163,1914,1915,1918],{},[166,1916,1917],{},"Screen recording:"," the built-in recording in macOS and iOS, Android; OBS or Loom for desktop.",[163,1920,1921,1924],{},[166,1922,1923],{},"An inspector:"," DevTools (the Console and Network tabs) for logs and requests.",[163,1926,1927,1930],{},[166,1928,1929],{},"Emulators and real devices:"," DevTools emulation is useful, but mobile bugs with Safari and the keyboard are better checked on real iPhones and Android phones.",[139,1932,1933],{},"A useful habit: in the ticket template make the \"Environment\" and \"Expected result\" fields mandatory. They filter out about half of bad reports before they ever reach a developer.",[152,1935,1937],{"id":1936},"how-to-judge-the-quality-of-a-qa-report","How to judge the quality of a QA report",[139,1939,1940],{},"If you order testing and want to know whether you received quality work, check:",[160,1942,1943,1946,1949,1952,1959],{},[163,1944,1945],{},"whether every bug has steps, an expected and actual result, and evidence;",[163,1947,1948],{},"whether there is a breakdown by severity rather than just a long list;",[163,1950,1951],{},"whether environments are stated (devices, browsers, build version);",[163,1953,1954,1955,1958],{},"whether there is a list of what was ",[166,1956,1957],{},"not"," checked, and why;",[163,1960,1961],{},"whether there is a conclusion: is the product ready for release or not.",[139,1963,1964,1965,1967,1968,220],{},"A report without the last item is incomplete. Testing exists to help make a decision, not to create a quantity of tickets. For what exactly we check in a store, see the ",[146,1966,584],{"href":583},", and for one of the most painful groups of bugs, the article on ",[146,1969,1971],{"href":1970},"\u002Fen\u002Fblog\u002Fcheckout-bugs-lost-orders","lost orders at checkout",[152,1973,1975],{"id":1974},"summary","Summary",[139,1977,1978],{},"A bug report and a test case are not a formality but a way to make testing predictable. A bug report should answer \"what, where, how to reproduce, how bad is it\", and a test case \"what and how do we check in every release\". If you do not have them, start with two things: a single ticket template and a smoke suite of 10 scenarios.",[139,1980,1981,1982,1985],{},"If you want to see what this looks like on your project, there is a free express check in 48 hours, and a full ",[146,1983,1984],{"href":1125},"ecommerce QA audit"," costs from UAH 6,000.",[457,1987,1988],{},[139,1989,1990,1993,1994,220],{},[166,1991,1992],{},"Want to start with an express check?"," Send the site address in the brief, and within 48 hours we will return 10–15 key problems with video. ",[146,1995,1996],{"href":354},"Send the brief",{"title":14,"searchDepth":688,"depth":689,"links":1998},[1999,2000,2008,2011,2015,2016,2017,2018,2019],{"id":1304,"depth":688,"text":1305},{"id":1342,"depth":688,"text":1343,"children":2001},[2002,2003,2004,2005,2006,2007],{"id":1349,"depth":689,"text":1350},{"id":1367,"depth":689,"text":1368},{"id":1391,"depth":689,"text":1392},{"id":1398,"depth":689,"text":1399},{"id":1405,"depth":689,"text":1406},{"id":1483,"depth":689,"text":1484},{"id":1490,"depth":688,"text":1491,"children":2009},[2010],{"id":1588,"depth":689,"text":1589},{"id":1624,"depth":688,"text":1625,"children":2012},[2013,2014],{"id":1713,"depth":689,"text":1714},{"id":1766,"depth":689,"text":1767},{"id":1804,"depth":688,"text":1805},{"id":1870,"depth":688,"text":1871},{"id":1894,"depth":688,"text":1895},{"id":1936,"depth":688,"text":1937},{"id":1974,"depth":688,"text":1975},"bug-report.template",[2022,2025,2027,2029],{"marker":2023,"text":2024},"[FAIL]","coupon not removed when item is deleted",{"marker":708,"text":2026},"steps, expected and actual result",{"marker":713,"text":2028},"severity S2, priority P1",{"marker":716,"text":2030},"reproduced in 3 of 3 attempts","BUG-017",[2033,2036,2039,2042],{"q":2034,"a":2035},"What should a good bug report contain?","A short title, the environment, clear steps to reproduce, expected and actual results, priority, and a screenshot or video. A developer should be able to reproduce the bug without further questions. The more precise the steps, the faster the fix. Such a report also helps verify later that the fix really worked.",{"q":2037,"a":2038},"How is a test case different from a bug report?","A test case describes what and how to check before testing starts, while a bug report records a discrepancy that was found. A test case contains preconditions, steps and an expected result, and a bug report adds the actual one. One test case can produce several bug reports. Together they form the basis of a transparent testing process.",{"q":2040,"a":2041},"How do I prioritize the bugs I find?","By impact on money and customer trust: first what blocks a purchase (cart, checkout, payment), then bugs that corrupt data, then problems that put customers off, and cosmetic issues last. That way the client sees at once where to start.",{"q":2043,"a":2044},"What tools do I need for bug tracking?","A small team needs a tracker (Jira, Linear, GitHub Issues or even Trello) and a spreadsheet for test cases. What matters is a consistent ticket template, with the Environment and Expected result fields required. You do not need an expensive system. A spreadsheet is usually enough for a few dozen test scenarios.","yak-my-testuiemo-bah-report-i-test-keis",{},"\u002Fen\u002Fblog\u002Fbug-report-and-test-case-example",{"title":1293,"description":1298},"What a good bug report and test case look like in website testing: templates, a checkout example, severity levels and common mistakes, with ready samples.","Bug Report and Test Case Example — Tochno.","bug-report-and-test-case-example","en\u002Fblog\u002Fbug-report-and-test-case-example",[2054],"qa","l8oi0JDZN5A09GO3tXa5w3l3-GpzC29QifQXOKLiQ-w",{"id":2057,"title":2058,"author":134,"body":2059,"cover":704,"coverFile":2783,"coverLines":2784,"coverStat":2793,"date":719,"description":2063,"excerpt":704,"extension":720,"faq":2794,"featured":734,"key":2807,"meta":2808,"navigation":737,"path":1970,"readingTime":2809,"seo":2810,"seoDescription":2811,"seoTitle":2812,"slug":2813,"stem":2814,"tags":2815,"updated":747,"__hash__":2816},"blog_en\u002Fen\u002Fblog\u002Fcheckout-bugs-lost-orders.md","Why Orders Get Lost at Checkout: 7 Common Bugs",{"type":136,"value":2060,"toc":2771},[2061,2064,2067,2071,2074,2077,2091,2097,2101,2107,2112,2126,2132,2138,2287,2298,2302,2307,2311,2322,2327,2331,2345,2350,2354,2359,2363,2374,2379,2383,2394,2398,2403,2408,2437,2442,2447,2451,2456,2460,2474,2479,2483,2497,2501,2506,2511,2528,2533,2538,2598,2601,2605,2610,2614,2628,2633,2649,2660,2664,2667,2673,2679,2685,2696,2710,2716,2720,2723,2746,2749,2752,2767],[139,2062,2063],{},"Orders get lost at checkout for a small number of recurring reasons: double clicks, lost responses from the payment provider, an expired session, overly strict field validation, failures in dependent services, price mismatches and mobile problems. Below are seven typical checkout bugs we find in online stores during testing, with steps to reproduce, the cause and the fix. The material is useful both for owners (to know what to ask a developer) and for developers (to check their own code).",[139,2065,2066],{},"A quick note: we do not cite other people's cases or statistics. Everything below is a typical scenario that is easy to reproduce on any store. We find these in our work as QA, and you should check for them before a customer finds them for you.",[152,2068,2070],{"id":2069},"why-checkout-is-a-high-risk-zone","Why checkout is a high-risk zone",[139,2072,2073],{},"Checkout combines the most moving parts in the shortest time: a form with validation, total calculation, delivery choice, an external payment system, order creation in the database, emails and notifications. Any part can fail, and the customer sees nothing useful: they either get a confusing error or pay and never receive an order.",[139,2075,2076],{},"Two situations are especially dangerous:",[160,2078,2079,2085],{},[163,2080,2081,2084],{},[166,2082,2083],{},"The customer paid, but there is no order."," This is the worst scenario: the money is charged and the store knows nothing about it.",[163,2086,2087,2090],{},[166,2088,2089],{},"The customer could not finish and left."," You will never find out: there is nothing in the database, and analytics shows only a \"bounce\".",[139,2092,2093,2094,2096],{},"That is why checkout testing is needed not only for all the \"happy\" flows but also for abnormal ones: a slow network, double clicks, a closed tab, a bank decline. We collected the basic checks in the ",[146,2095,584],{"href":583},", and here we go through the most expensive mistakes in detail.",[152,2098,2100],{"id":2099},"bug-1-a-double-click-creates-two-orders-or-none","Bug 1. A double click creates two orders (or none)",[139,2102,2103,2106],{},[166,2104,2105],{},"Symptom."," The customer clicks \"Confirm order\" and the page thinks for a few seconds. They click again. The system ends up with two identical orders, or the second click triggers an error and the first order \"hangs\".",[139,2108,2109],{},[166,2110,2111],{},"Steps to reproduce.",[532,2113,2114,2117,2120,2123],{},[163,2115,2116],{},"Add a product to the cart and go to checkout.",[163,2118,2119],{},"Turn on Slow 3G in DevTools.",[163,2121,2122],{},"Click the confirm button twice quickly.",[163,2124,2125],{},"Check the number of orders in the admin panel.",[139,2127,2128,2131],{},[166,2129,2130],{},"Cause."," The button is not disabled after the first click, and the server does not check whether the request is a repeat.",[139,2133,2134,2137],{},[166,2135,2136],{},"Fix."," Two layers of protection are needed. On the client, the button becomes inactive and shows an indicator. On the server, an idempotency key is added: the client generates a unique token when the form opens and sends it with the request, and the server rejects a repeated request with the same token.",[2139,2140,2144],"pre",{"className":2141,"code":2142,"language":2143,"meta":14,"style":14},"language-php shiki shiki-themes github-dark-default","\u002F\u002F Laravel: protect against a duplicate order with an idempotency key\n$order = Order::firstOrCreate(\n    ['idempotency_key' => $request->input('idempotency_key')],\n    $orderData\n);\n\nif (! $order->wasRecentlyCreated) {\n    return response()->json(['order_id' => $order->id]); \u002F\u002F same result\n}\n","php",[1529,2145,2146,2155,2179,2208,2214,2220,2226,2246,2281],{"__ignoreMap":14},[2147,2148,2151],"span",{"class":2149,"line":2150},"line",1,[2147,2152,2154],{"class":2153},"sH3jZ","\u002F\u002F Laravel: protect against a duplicate order with an idempotency key\n",[2147,2156,2157,2161,2165,2169,2172,2176],{"class":2149,"line":688},[2147,2158,2160],{"class":2159},"sZEs4","$order ",[2147,2162,2164],{"class":2163},"suJrU","=",[2147,2166,2168],{"class":2167},"sFSAA"," Order",[2147,2170,2171],{"class":2163},"::",[2147,2173,2175],{"class":2174},"sc3cj","firstOrCreate",[2147,2177,2178],{"class":2159},"(\n",[2147,2180,2181,2184,2188,2191,2194,2197,2200,2203,2205],{"class":2149,"line":689},[2147,2182,2183],{"class":2159},"    [",[2147,2185,2187],{"class":2186},"s9uIt","'idempotency_key'",[2147,2189,2190],{"class":2163}," =>",[2147,2192,2193],{"class":2159}," $request",[2147,2195,2196],{"class":2163},"->",[2147,2198,2199],{"class":2174},"input",[2147,2201,2202],{"class":2159},"(",[2147,2204,2187],{"class":2186},[2147,2206,2207],{"class":2159},")],\n",[2147,2209,2211],{"class":2149,"line":2210},4,[2147,2212,2213],{"class":2159},"    $orderData\n",[2147,2215,2217],{"class":2149,"line":2216},5,[2147,2218,2219],{"class":2159},");\n",[2147,2221,2223],{"class":2149,"line":2222},6,[2147,2224,2225],{"emptyLinePlaceholder":737},"\n",[2147,2227,2229,2232,2235,2238,2241,2243],{"class":2149,"line":2228},7,[2147,2230,2231],{"class":2163},"if",[2147,2233,2234],{"class":2159}," (",[2147,2236,2237],{"class":2163},"!",[2147,2239,2240],{"class":2159}," $order",[2147,2242,2196],{"class":2163},[2147,2244,2245],{"class":2159},"wasRecentlyCreated) {\n",[2147,2247,2249,2252,2255,2258,2260,2263,2266,2269,2271,2273,2275,2278],{"class":2149,"line":2248},8,[2147,2250,2251],{"class":2163},"    return",[2147,2253,2254],{"class":2174}," response",[2147,2256,2257],{"class":2159},"()",[2147,2259,2196],{"class":2163},[2147,2261,2262],{"class":2174},"json",[2147,2264,2265],{"class":2159},"([",[2147,2267,2268],{"class":2186},"'order_id'",[2147,2270,2190],{"class":2163},[2147,2272,2240],{"class":2159},[2147,2274,2196],{"class":2163},[2147,2276,2277],{"class":2159},"id]); ",[2147,2279,2280],{"class":2153},"\u002F\u002F same result\n",[2147,2282,2284],{"class":2149,"line":2283},9,[2147,2285,2286],{"class":2159},"}\n",[139,2288,2289,2290,2293,2294,2297],{},"For this the ",[1529,2291,2292],{},"orders"," table needs a unique index on ",[1529,2295,2296],{},"idempotency_key",", otherwise two nearly simultaneous requests will still race and create a duplicate.",[152,2299,2301],{"id":2300},"bug-2-the-payment-provider-reported-payment-but-the-order-did-not-update","Bug 2. The payment provider reported payment, but the order did not update",[139,2303,2304,2306],{},[166,2305,2105],{}," The customer paid by card and the bank charged the money, but the order stayed in the \"awaiting payment\" status. The manager does not ship the goods, and the customer calls furious.",[139,2308,2309],{},[166,2310,2111],{},[532,2312,2313,2316,2319],{},[163,2314,2315],{},"Place an order with card payment in the sandbox.",[163,2317,2318],{},"Close the tab right after payment, without returning to the site.",[163,2320,2321],{},"Check the order status.",[139,2323,2324,2326],{},[166,2325,2130],{}," The store updates the status only when the customer returns to the \"Thank you\" page. But the customer can close the tab or lose connection. The payment provider sends a separate server-to-server message (a callback, or webhook), and that must be the source of truth.",[139,2328,2329],{},[166,2330,2136],{},[160,2332,2333,2336,2339,2342],{},[163,2334,2335],{},"The payment status changes only on a server callback verified by signature.",[163,2337,2338],{},"The customer's return to the site only displays the state and does not change it.",[163,2340,2341],{},"The callback must be idempotent: the provider may send it twice, and the second handling must neither duplicate emails nor change the status back.",[163,2343,2344],{},"For orders that sit in \"awaiting\" for a long time, a background job is needed that asks the provider for the status directly.",[139,2346,2347,2348,220],{},"Payment and delivery integrations are a big topic of their own, covered in ",[146,2349,452],{"href":451},[152,2351,2353],{"id":2352},"bug-3-the-session-expired-and-the-cart-disappeared","Bug 3. The session expired and the cart disappeared",[139,2355,2356,2358],{},[166,2357,2105],{}," The customer fills in the form for 15 minutes, gets distracted, clicks \"Place order\" and instead of an order sees \"419 Page Expired\" or an empty cart.",[139,2360,2361],{},[166,2362,2111],{},[532,2364,2365,2368,2371],{},[163,2366,2367],{},"Add a product to the cart and open checkout.",[163,2369,2370],{},"Leave the page for longer than the session lifetime.",[163,2372,2373],{},"Try to submit the form.",[139,2375,2376,2378],{},[166,2377,2130],{}," The session (or CSRF token) has expired, and the site responds with a dry technical error and does not keep the entered data. It happens especially often on mobile: the browser unloads the tab from memory while the customer switches to the banking app.",[139,2380,2381],{},[166,2382,2136],{},[160,2384,2385,2388,2391],{},[163,2386,2387],{},"The cart is stored in the database (tied to a guest identifier in a cookie), not only in the session.",[163,2389,2390],{},"The protection token refreshes automatically, and on error the user sees a clear message with the form data preserved.",[163,2392,2393],{},"Entered fields are temporarily stored on the client (without passwords or card data) and restored after a reload.",[152,2395,2397],{"id":2396},"bug-4-validation-rejects-normal-data","Bug 4. Validation rejects normal data",[139,2399,2400,2402],{},[166,2401,2105],{}," The customer enters correct data, but the form says \"invalid format\" and will not let them continue. They leave, and you never even see that the attempt happened.",[139,2404,2405],{},[166,2406,2407],{},"Typical cases.",[160,2409,2410,2417,2420,2427,2430],{},[163,2411,2412,2413,2416],{},"A phone number in the format ",[1529,2414,2415],{},"+38 (050) 123-45-67"," against a mask that expects digits only.",[163,2418,2419],{},"An email with a trailing space, added by smartphone autofill.",[163,2421,2422,2423,2426],{},"A surname with an apostrophe (",[1529,2424,2425],{},"O'Brien",") or a double-barrelled name with a hyphen.",[163,2428,2429],{},"An address with Cyrillic and Latin letters in the street name.",[163,2431,2432,2433,2436],{},"A postcode or house number with a letter (",[1529,2434,2435],{},"12\u002FA",").",[139,2438,2439,2441],{},[166,2440,2111],{}," Make a small list of problem values and enter them into each field in turn. Check browser autofill on a phone separately.",[139,2443,2444,2446],{},[166,2445,2136],{}," Normalize input instead of rejecting it: strip spaces from email, reduce the phone to a single format on the server, do not restrict characters in names. The rule: better to accept too much and verify afterwards than to turn a customer away. The error should appear next to the specific field and explain what is wrong.",[152,2448,2450],{"id":2449},"bug-5-nova-poshta-an-empty-or-unavailable-branch-selector","Bug 5. Nova Poshta: an empty or unavailable branch selector",[139,2452,2453,2455],{},[166,2454,2105],{}," The customer chooses \"Nova Poshta\", but the branch list does not load, or the city is found but there are no branches, or the selected branch disappears after the item quantity changes.",[139,2457,2458],{},[166,2459,2111],{},[532,2461,2462,2465,2468,2471],{},[163,2463,2464],{},"Choose Nova Poshta delivery.",[163,2466,2467],{},"Search for a city by part of its name, and also for a settlement that has namesakes in other regions.",[163,2469,2470],{},"Select a branch, then change the product quantity and see whether the selection resets.",[163,2472,2473],{},"Turn off the network in the middle of selection and try again.",[139,2475,2476,2478],{},[166,2477,2130],{}," The store depends on an external API and has no fallback. If the API is slow or unavailable, the form freezes. Another variant: the branch list is cached for too long, and the customer is offered a closed branch.",[139,2480,2481],{},[166,2482,2136],{},[160,2484,2485,2488,2491,2494],{},[163,2486,2487],{},"A timeout and retries for API requests, and a clear message on failure.",[163,2489,2490],{},"A cache of the city and branch directory with regular background refresh.",[163,2492,2493],{},"Preserving the selection when the cart changes.",[163,2495,2496],{},"A fallback: a \"manual address\" field or the option to place the order with delivery clarified by a manager, so another party's API outage does not block a sale.",[152,2498,2500],{"id":2499},"bug-6-the-checkout-price-differs-from-the-catalog-price","Bug 6. The checkout price differs from the catalog price",[139,2502,2503,2505],{},[166,2504,2105],{}," In the catalog the product costs UAH 1,200, in the cart 1,250, at checkout 1,190. The customer gets confused and leaves, or worse: buys at the wrong price, and you work at a loss.",[139,2507,2508],{},[166,2509,2510],{},"Causes.",[160,2512,2513,2516,2519,2522,2525],{},[163,2514,2515],{},"A discount is calculated at different stages with different formulas (before or after delivery).",[163,2517,2518],{},"Rounding happens in different places, and the cents drift apart.",[163,2520,2521],{},"The catalog cache shows the old price after a change.",[163,2523,2524],{},"A promo code applies to products that should not be part of the promotion.",[163,2526,2527],{},"Currency or VAT is calculated differently on the frontend and the backend.",[139,2529,2530,2532],{},[166,2531,2111],{}," Take 3–4 products with different discounts and compare the price in the listing, the product page, the cart, checkout, the email and the admin panel. Do this with and without a promo code. Verify each calculation with a calculator.",[139,2534,2535,2537],{},[166,2536,2136],{}," A single place where the price is calculated. The frontend only shows what the server returned. The order total is fixed at the moment of creation (a price snapshot), so later catalog changes do not affect orders already placed. Money is stored in minor units as an integer, not as a floating-point number:",[2139,2539,2541],{"className":2141,"code":2540,"language":2143,"meta":14,"style":14},"\u002F\u002F risky: float\n$total = 19.99 * 3; \u002F\u002F 59.97, but 59.969999... in some cases\n\n\u002F\u002F reliable: minor units as integers\n$totalMinor = 1999 * 3; \u002F\u002F 5997\n",[1529,2542,2543,2548,2570,2574,2579],{"__ignoreMap":14},[2147,2544,2545],{"class":2149,"line":2150},[2147,2546,2547],{"class":2153},"\u002F\u002F risky: float\n",[2147,2549,2550,2553,2555,2558,2561,2564,2567],{"class":2149,"line":688},[2147,2551,2552],{"class":2159},"$total ",[2147,2554,2164],{"class":2163},[2147,2556,2557],{"class":2167}," 19.99",[2147,2559,2560],{"class":2163}," *",[2147,2562,2563],{"class":2167}," 3",[2147,2565,2566],{"class":2159},"; ",[2147,2568,2569],{"class":2153},"\u002F\u002F 59.97, but 59.969999... in some cases\n",[2147,2571,2572],{"class":2149,"line":689},[2147,2573,2225],{"emptyLinePlaceholder":737},[2147,2575,2576],{"class":2149,"line":2210},[2147,2577,2578],{"class":2153},"\u002F\u002F reliable: minor units as integers\n",[2147,2580,2581,2584,2586,2589,2591,2593,2595],{"class":2149,"line":2216},[2147,2582,2583],{"class":2159},"$totalMinor ",[2147,2585,2164],{"class":2163},[2147,2587,2588],{"class":2167}," 1999",[2147,2590,2560],{"class":2163},[2147,2592,2563],{"class":2167},[2147,2594,2566],{"class":2159},[2147,2596,2597],{"class":2153},"\u002F\u002F 5997\n",[139,2599,2600],{},"For a store this matters especially, because rounding accumulates on large orders and shows up as \"it somehow doesn't match the accounting\".",[152,2602,2604],{"id":2603},"bug-7-a-mobile-checkout-where-the-button-cannot-be-tapped","Bug 7. A mobile checkout where the button cannot be tapped",[139,2606,2607,2609],{},[166,2608,2105],{}," On an iPhone or Android phone the customer cannot complete the order: the button is covered by a sticky banner, the keyboard hides the field, the dropdown will not open, the form \"jumps\".",[139,2611,2612],{},[166,2613,2111],{},[532,2615,2616,2619,2622,2625],{},[163,2617,2618],{},"Open checkout on a real phone (an iPhone with Safari and an Android with Chrome), not only in an emulator.",[163,2620,2621],{},"Fill in the form, switching between fields.",[163,2623,2624],{},"See whether anything covers the confirm button when the keyboard is open.",[163,2626,2627],{},"Try rotating the screen and changing the system text size.",[139,2629,2630,2632],{},[166,2631,2130],{}," The layout was checked on desktop. Mobile browsers handle screen height, fixed elements and input autozoom differently. In Safari a field with a font size below 16 pixels zooms in automatically on focus, which breaks the layout.",[139,2634,2635,2637,2638,2641,2642,2641,2645,2648],{},[166,2636,2136],{}," A field font size of 16 pixels or more, the right field types (",[1529,2639,2640],{},"type=\"tel\"",", ",[1529,2643,2644],{},"inputmode",[1529,2646,2647],{},"autocomplete","), a completion button that is not fixed where the keyboard can cover it, and checking on real devices. In most stores mobile traffic makes up the majority of visits, so this bug is especially expensive.",[457,2650,2651],{},[139,2652,2653,2656,2657,2659],{},[166,2654,2655],{},"Want to know where orders get lost in your own store?"," Our free 48-hour express check will show 10–15 key problems: we check checkout, the mobile version and speed. If there are many problems, we will offer an ",[146,2658,1984],{"href":1125}," from UAH 6,000.",[152,2661,2663],{"id":2662},"how-to-find-such-bugs-before-customers-do","How to find such bugs before customers do",[139,2665,2666],{},"One thing unites all seven bugs: they are invisible on the \"happy path\". A developer went through checkout once, everything worked, and the task was closed. To catch these problems systematically you need an approach.",[139,2668,2669,2672],{},[166,2670,2671],{},"Test negative scenarios."," For every step, ask: \"What happens if this does not work?\" A payment error, no network, a double click, a session timeout, a repeated request.",[139,2674,2675,2678],{},[166,2676,2677],{},"Use the services' test environments."," All popular payment providers and Nova Poshta have a sandbox. A test card with successful payment, a decline and a 3-D Secure failure is the minimum.",[139,2680,2681,2684],{},[166,2682,2683],{},"Log and monitor."," If orders get lost, you should know before the customer's call. Set up:",[160,2686,2687,2690,2693],{},[163,2688,2689],{},"recording of all payment provider callbacks with body and signature;",[163,2691,2692],{},"alerts (to Telegram or email) for orders that sit in \"awaiting\" for more than 30 minutes;",[163,2694,2695],{},"funnel metrics: how many people reached checkout and how many completed it. A sharp drop is a signal.",[139,2697,2698,2701,2702,2706,2707,220],{},[166,2699,2700],{},"Automate the critical path."," The most important scenario, \"product → cart → payment → order\", should be covered by an automated test that runs on every release. We use Playwright and run tests in CI; read more about ",[146,2703,2705],{"href":2704},"\u002Fen\u002Fservices\u002Ftest-automation","test automation"," and what to automate first in ",[146,2708,2709],{"href":1855},"Manual vs Automated Testing",[139,2711,2712,2715],{},[166,2713,2714],{},"Repeat regression after changes."," Every module update, pricing change or new delivery method can break what worked. Before a release, run the key checks, not only the new feature.",[152,2717,2719],{"id":2718},"short-summary","Short summary",[139,2721,2722],{},"Seven typical reasons orders get lost at checkout:",[532,2724,2725,2728,2731,2734,2737,2740,2743],{},[163,2726,2727],{},"A double click without idempotency.",[163,2729,2730],{},"The payment status depends on the customer returning, not on the callback.",[163,2732,2733],{},"Sessions and tokens expire without saving the cart.",[163,2735,2736],{},"Validation rejects normal data.",[163,2738,2739],{},"Dependence on the Nova Poshta API without a fallback.",[163,2741,2742],{},"Price mismatches between stages.",[163,2744,2745],{},"An unusable mobile checkout.",[139,2747,2748],{},"None of these bugs needs complex technology to fix, only attention to negative scenarios and testing on real devices. Their price is in direct losses: every lost order is not only a lost sale but a lost customer.",[139,2750,2751],{},"Start with two things today. First, go through checkout yourself on a phone with a weak connection, pressing buttons twice. Second, make sure you have alerts for orders that did not complete payment. It takes an hour and may save weeks of revenue.",[457,2753,2754],{},[139,2755,2756,2759,2760,2762,2763,2766],{},[166,2757,2758],{},"Need a checkout review from people who do this every day?"," Describe your store in the ",[146,2761,355],{"href":354}," or see how ",[146,2764,2765],{"href":1337},"website and store testing"," works: regression, cross-browser checks, and a report with video and priorities.",[2768,2769,2770],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":14,"searchDepth":688,"depth":689,"links":2772},[2773,2774,2775,2776,2777,2778,2779,2780,2781,2782],{"id":2069,"depth":688,"text":2070},{"id":2099,"depth":688,"text":2100},{"id":2300,"depth":688,"text":2301},{"id":2352,"depth":688,"text":2353},{"id":2396,"depth":688,"text":2397},{"id":2449,"depth":688,"text":2450},{"id":2499,"depth":688,"text":2500},{"id":2603,"depth":688,"text":2604},{"id":2662,"depth":688,"text":2663},{"id":2718,"depth":688,"text":2719},"checkout.bugs",[2785,2787,2789,2791],{"marker":2023,"text":2786},"double click: two orders",{"marker":2023,"text":2788},"payment callback lost",{"marker":713,"text":2790},"phone with spaces rejected",{"marker":708,"text":2792},"checked on 12 devices","7 bugs",[2795,2798,2801,2804],{"q":2796,"a":2797},"Why did the customer pay but the order did not appear in the admin panel?","Most often the callback from the payment system never arrived or was not processed correctly, so the order status did not update. Check the callback logs and compare them with the payment provider statement. Callback handling must be reliable and safe against repeated calls.",{"q":2799,"a":2800},"How can I tell that orders are being lost at checkout?","Compare started and completed checkouts in your analytics and look at the server error log. A sharp drop at one step points to the problem. It also helps to place test orders yourself from different devices and with different payment methods, and to ask support about customer complaints.",{"q":2802,"a":2803},"Why can the checkout price differ from the catalog price?","Usually because of cached prices, different rounding, or discounts and coupons calculated in different places in the code. The final amount should come from a single server-side calculation. Check promotional items, coupons and quantity changes in the cart. Mismatches erode trust and cause drop-offs right before payment.",{"q":2805,"a":2806},"How often should checkout be tested?","After every release that touches the cart, payment, delivery or forms, and after CMS and plugin updates. It helps to keep a short regression set for the critical purchase path. If problems have piled up, a one-off QA audit from UAH 6,000 can help.","chomu-hubliatsia-zamovlennia-na-checkout",{},12,{"title":2058,"description":2063},"Why orders get lost at checkout: 7 common online store bugs with examples, steps to reproduce, and ways to fix and test them before customers hit them.","Why Orders Get Lost at Checkout: 7 Bugs — Tochno.","checkout-bugs-lost-orders","en\u002Fblog\u002Fcheckout-bugs-lost-orders",[746,2054],"v8FUN6A9PBWS7VQkxn46yMJGKFA5z8_nu2bRW9kEytk",{"id":2818,"title":2819,"author":134,"body":2820,"cover":704,"coverFile":3547,"coverLines":3548,"coverStat":3557,"date":719,"description":2824,"excerpt":704,"extension":720,"faq":3558,"featured":734,"key":3571,"meta":3572,"navigation":737,"path":1050,"readingTime":739,"seo":3573,"seoDescription":3574,"seoTitle":3575,"slug":3576,"stem":3577,"tags":3578,"updated":3579,"__hash__":3580},"blog_en\u002Fen\u002Fblog\u002Fcore-web-vitals-for-store-owners.md","Core Web Vitals for Store Owners: What They Are and How to Check",{"type":136,"value":2821,"toc":3525},[2822,2825,2828,2832,2835,2917,2927,2930,2955,2959,2962,2966,2969,2989,2992,2996,2999,3003,3006,3023,3026,3030,3033,3037,3040,3044,3047,3073,3077,3084,3145,3149,3157,3161,3168,3172,3175,3178,3204,3207,3221,3225,3228,3231,3263,3270,3343,3346,3350,3353,3397,3400,3410,3414,3417,3431,3437,3441,3447,3450,3464,3468,3500,3506,3508,3511,3522],[139,2823,2824],{},"Core Web Vitals are three Google metrics that show how fast an online store loads, how quickly it responds to actions and whether it \"jumps around\" while you read: LCP, INP and CLS. A store owner does not need to know how to calculate them. It is enough to know the good values, be able to check the site in 15 minutes with free tools, and understand what to ask a developer for.",[139,2826,2827],{},"We build sites with these metrics in mind, so below is a practical guide without excess theory: what is measured, where to look at the numbers, what most often makes them worse in stores and what to do about it.",[152,2829,2831],{"id":2830},"what-core-web-vitals-are-and-why-they-matter-for-a-store","What Core Web Vitals are and why they matter for a store",[139,2833,2834],{},"Google measures \"page experience\" with three indicators. These are not abstract scores but a reflection of what a buyer actually feels: \"nothing shows up for a long time\", \"the button does not respond\", \"I wanted to tap one thing and tapped another\".",[243,2836,2837,2855],{},[246,2838,2839],{},[249,2840,2841,2844,2846,2849,2852],{},[252,2842,2843],{},"Metric",[252,2845,1421],{},[252,2847,2848],{},"Good",[252,2850,2851],{},"Needs improvement",[252,2853,2854],{},"Poor",[262,2856,2857,2877,2897],{},[249,2858,2859,2865,2868,2871,2874],{},[267,2860,2861,2864],{},[166,2862,2863],{},"LCP"," (Largest Contentful Paint)",[267,2866,2867],{},"When the largest element on screen appeared: usually the main photo or heading",[267,2869,2870],{},"up to 2.5 s",[267,2872,2873],{},"2.5–4 s",[267,2875,2876],{},"over 4 s",[249,2878,2879,2885,2888,2891,2894],{},[267,2880,2881,2884],{},[166,2882,2883],{},"INP"," (Interaction to Next Paint)",[267,2886,2887],{},"How quickly the page responds to clicks, taps and input",[267,2889,2890],{},"up to 200 ms",[267,2892,2893],{},"200–500 ms",[267,2895,2896],{},"over 500 ms",[249,2898,2899,2905,2908,2911,2914],{},[267,2900,2901,2904],{},[166,2902,2903],{},"CLS"," (Cumulative Layout Shift)",[267,2906,2907],{},"How much elements shift during loading",[267,2909,2910],{},"up to 0.1",[267,2912,2913],{},"0.1–0.25",[267,2915,2916],{},"over 0.25",[139,2918,2919,2920,2926],{},"These are the thresholds ",[146,2921,2925],{"href":2922,"rel":2923},"https:\u002F\u002Fweb.dev\u002Farticles\u002Fvitals",[2924],"nofollow","Google publishes"," (checked in October 2026). The assessment uses the 75th percentile: that is, three quarters of visitors should get a \"good\" result.",[139,2928,2929],{},"Why this matters to you as an owner:",[160,2931,2932,2938,2943,2949],{},[163,2933,2934,2937],{},[166,2935,2936],{},"Money."," A slow catalog or product page loses buyers before they have seen the range. Exact numbers depend on the niche, so we do not promise \"+20% conversion\", but the direction is obvious.",[163,2939,2940,2942],{},[166,2941,513],{}," Core Web Vitals are part of the \"page experience\" signals. They do not replace useful content, but at equal quality a faster site has the advantage.",[163,2944,2945,2948],{},[166,2946,2947],{},"Advertising."," If your traffic is paid, a slow landing page makes every click more expensive.",[163,2950,2951,2954],{},[166,2952,2953],{},"Mobile users."," Most buyers open a store from a phone, often over an unstable mobile connection.",[152,2956,2958],{"id":2957},"how-to-check-core-web-vitals-yourself-in-15-minutes","How to check Core Web Vitals yourself in 15 minutes",[139,2960,2961],{},"You need a browser and Google's free tools. One rule: check not only the home page but also the typical templates: category, product page, cart.",[346,2963,2965],{"id":2964},"step-1-pagespeed-insights","Step 1. PageSpeed Insights",[139,2967,2968],{},"Open PageSpeed Insights, paste the page address and choose the \"Mobile\" tab. You will see two blocks.",[532,2970,2971,2983],{},[163,2972,2973,2976,2977,2982],{},[166,2974,2975],{},"Real-user data"," (field data) at the top. These are the metrics of visitors over the last 28 days from the ",[146,2978,2981],{"href":2979,"rel":2980},"https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fcrux",[2924],"CrUX report",". This is what matters to Google. If there is little traffic, this block may be missing.",[163,2984,2985,2988],{},[166,2986,2987],{},"Diagnostics"," (lab data) below. This is a simulation of loading on a weak device. It shows the causes of problems but is not the \"official\" assessment.",[139,2990,2991],{},"Do not chase just the overall Lighthouse score. Look at LCP, INP (in lab mode TBT is shown instead) and CLS separately, as well as the list of \"Opportunities\" and \"Diagnostics\" tips.",[346,2993,2995],{"id":2994},"step-2-the-report-in-google-search-console","Step 2. The report in Google Search Console",[139,2997,2998],{},"If the site is connected to Search Console, open the \"Core Web Vitals\" section. Pages are grouped there by status: good, needs improvement, poor. They are grouped by typical templates, so you can see that \"all product pages are slow\" rather than just one page.",[346,3000,3002],{"id":3001},"step-3-a-check-on-a-real-phone","Step 3. A check on a real phone",[139,3004,3005],{},"Open the store from your smartphone over a mobile network, not your home Wi-Fi. Ask yourself five questions:",[160,3007,3008,3011,3014,3017,3020],{},[163,3009,3010],{},"After how many seconds do I see the main product photo?",[163,3012,3013],{},"Does the page shift when banners and recommendations load?",[163,3015,3016],{},"Does the \"Buy\" button respond immediately, or is there a delay?",[163,3018,3019],{},"Does the catalog filter open without \"freezing\"?",[163,3021,3022],{},"Does a cookie banner or chat cover the content?",[139,3024,3025],{},"This simple test finds half of the problems without any tool.",[346,3027,3029],{"id":3028},"step-4-chrome-devtools-for-advanced-users","Step 4. Chrome DevTools (for advanced users)",[139,3031,3032],{},"In Chrome press F12, open the Performance or Lighthouse tab and run an audit in mobile device mode with network throttling. The Performance tab shows which scripts block the main thread. If you are not a developer, you can simply take a snapshot and pass it to the contractor as an argument in the conversation.",[152,3034,3036],{"id":3035},"lcp-why-the-main-element-appears-slowly","LCP: why the main element appears slowly",[139,3038,3039],{},"In stores LCP is almost always a large photograph: a banner on the home page, a product photo on the product page or the first image in the catalog. The causes of slowness are typical.",[346,3041,3043],{"id":3042},"heavy-images","Heavy images",[139,3045,3046],{},"A 2–3 MB JPEG photo takes seconds to load on mobile. What to do:",[160,3048,3049,3052,3059,3062],{},[163,3050,3051],{},"serve modern formats (WebP, AVIF);",[163,3053,3054,3055,3058],{},"generate several sizes and serve the one matching the screen through ",[1529,3056,3057],{},"srcset",";",[163,3060,3061],{},"compress without visible quality loss;",[163,3063,3064,3065,3068,3069,3072],{},"set ",[1529,3066,3067],{},"width"," and ",[1529,3070,3071],{},"height"," so the browser reserves space.",[346,3074,3076],{"id":3075},"an-image-that-lazy-loads-when-it-should-not","An image that lazy-loads when it should not",[139,3078,3079,3080,3083],{},"The ",[1529,3081,3082],{},"loading=\"lazy\""," attribute is useful for images below the first screen. But if you put it on the main photo, the browser will postpone loading it. For the LCP element you need the opposite:",[2139,3085,3089],{"className":3086,"code":3087,"language":3088,"meta":14,"style":14},"language-html shiki shiki-themes github-dark-default","\u003Cimg src=\"\u002Fimg\u002Fhero.avif\" width=\"1200\" height=\"600\"\n     fetchpriority=\"high\" alt=\"New collection\">\n","html",[1529,3090,3091,3124],{"__ignoreMap":14},[2147,3092,3093,3096,3100,3103,3105,3108,3111,3113,3116,3119,3121],{"class":2149,"line":2150},[2147,3094,3095],{"class":2159},"\u003C",[2147,3097,3099],{"class":3098},"sPWt5","img",[2147,3101,3102],{"class":2167}," src",[2147,3104,2164],{"class":2159},[2147,3106,3107],{"class":2186},"\"\u002Fimg\u002Fhero.avif\"",[2147,3109,3110],{"class":2167}," width",[2147,3112,2164],{"class":2159},[2147,3114,3115],{"class":2186},"\"1200\"",[2147,3117,3118],{"class":2167}," height",[2147,3120,2164],{"class":2159},[2147,3122,3123],{"class":2186},"\"600\"\n",[2147,3125,3126,3129,3131,3134,3137,3139,3142],{"class":2149,"line":688},[2147,3127,3128],{"class":2167},"     fetchpriority",[2147,3130,2164],{"class":2159},[2147,3132,3133],{"class":2186},"\"high\"",[2147,3135,3136],{"class":2167}," alt",[2147,3138,2164],{"class":2159},[2147,3140,3141],{"class":2186},"\"New collection\"",[2147,3143,3144],{"class":2159},">\n",[346,3146,3148],{"id":3147},"a-slow-server-response","A slow server response",[139,3150,3151,3152,3156],{},"If the server generates the page in 1.5 seconds, no image optimization will fix that. For Laravel stores the cause is often N+1 database queries and no cache: we wrote about that in ",[146,3153,3155],{"href":3154},"\u002Fen\u002Fblog\u002Fwhat-is-code-audit","What Is a Code Audit and When Do You Need One",". If pages can be served from a static cache or prerendered, the gain is sometimes the biggest.",[346,3158,3160],{"id":3159},"blocking-fonts-styles-and-scripts","Blocking fonts, styles and scripts",[139,3162,3163,3164,3167],{},"Fonts from an external CDN, many CSS files and scripts in ",[1529,3165,3166],{},"\u003Chead>"," delay the first render. The solution: host fonts on your own domain, load only the weights and languages you need, and defer scripts you do not need immediately.",[152,3169,3171],{"id":3170},"inp-why-the-store-lags-after-a-click","INP: why the store \"lags\" after a click",[139,3173,3174],{},"INP is the newest metric and replaced FID. It measures the delay between a user action and the screen update throughout the whole visit, not only the first click.",[139,3176,3177],{},"What makes it worse:",[160,3179,3180,3186,3192,3198],{},[163,3181,3182,3185],{},[166,3183,3184],{},"Too much JavaScript."," Libraries for sliders, widgets, chats, pixels, A\u002FB tests: each adds load to the main thread.",[163,3187,3188,3191],{},[166,3189,3190],{},"Third-party analytics and ad scripts."," A tag manager with a dozen tags can slow a page more than all the store's own code.",[163,3193,3194,3197],{},[166,3195,3196],{},"Heavy event handlers."," For example, a filter that redraws the entire list of thousands of items on every click.",[163,3199,3200,3203],{},[166,3201,3202],{},"Hydration of large pages"," in SPA frameworks, when the page looks ready but does not respond yet.",[139,3205,3206],{},"Practical steps:",[532,3208,3209,3212,3215,3218],{},[163,3210,3211],{},"List all third-party scripts and for each ask: \"Who uses this and what decision do they make based on it?\" Remove what nobody uses.",[163,3213,3214],{},"Load analytics and marketing pixels after user consent and with a delay. This is also a privacy requirement.",[163,3216,3217],{},"Break up long tasks: scrolling, filters and the cart should respond first.",[163,3219,3220],{},"For large catalogs use pagination or list virtualization.",[152,3222,3224],{"id":3223},"cls-why-content-jumps","CLS: why content \"jumps\"",[139,3226,3227],{},"Layout shift is annoying and hurts conversion: a person is about to tap \"Add to cart\", and a recommendations block appears and pushes the button away.",[139,3229,3230],{},"Typical culprits:",[160,3232,3233,3239,3245,3251,3257],{},[163,3234,3235,3238],{},[166,3236,3237],{},"Images without dimensions."," The browser does not know the height and adjusts it after loading.",[163,3240,3241,3244],{},[166,3242,3243],{},"Banners, cookie panels and promo strips"," that appear at the top and push content down.",[163,3246,3247,3250],{},[166,3248,3249],{},"Fonts that get swapped"," after loading and change text size.",[163,3252,3253,3256],{},[166,3254,3255],{},"Dynamic blocks:"," reviews, \"frequently bought together\", delivery widgets without reserved space.",[163,3258,3259,3262],{},[166,3260,3261],{},"Embeds (iframes) without dimensions:"," video, maps, payment widgets.",[139,3264,3265,3266,3269],{},"The simplest rule: for any element that loads later, reserve space in advance. This is done by setting dimensions or ",[1529,3267,3268],{},"aspect-ratio",":",[2139,3271,3275],{"className":3272,"code":3273,"language":3274,"meta":14,"style":14},"language-css shiki shiki-themes github-dark-default",".product-card__image { aspect-ratio: 4 \u002F 5; width: 100%; height: auto; }\n.promo-bar { min-height: 40px; }\n","css",[1529,3276,3277,3323],{"__ignoreMap":14},[2147,3278,3279,3282,3285,3287,3290,3293,3296,3299,3301,3303,3305,3308,3311,3313,3315,3317,3320],{"class":2149,"line":2150},[2147,3280,3281],{"class":2167},".product-card__image",[2147,3283,3284],{"class":2159}," { ",[2147,3286,3268],{"class":2167},[2147,3288,3289],{"class":2159},": ",[2147,3291,3292],{"class":2167},"4",[2147,3294,3295],{"class":2159}," \u002F ",[2147,3297,3298],{"class":2167},"5",[2147,3300,2566],{"class":2159},[2147,3302,3067],{"class":2167},[2147,3304,3289],{"class":2159},[2147,3306,3307],{"class":2167},"100",[2147,3309,3310],{"class":2163},"%",[2147,3312,2566],{"class":2159},[2147,3314,3071],{"class":2167},[2147,3316,3289],{"class":2159},[2147,3318,3319],{"class":2167},"auto",[2147,3321,3322],{"class":2159},"; }\n",[2147,3324,3325,3328,3330,3333,3335,3338,3341],{"class":2149,"line":688},[2147,3326,3327],{"class":2167},".promo-bar",[2147,3329,3284],{"class":2159},[2147,3331,3332],{"class":2167},"min-height",[2147,3334,3289],{"class":2159},[2147,3336,3337],{"class":2167},"40",[2147,3339,3340],{"class":2163},"px",[2147,3342,3322],{"class":2159},[139,3344,3345],{},"For a cookie banner it is better to use a fixed panel at the bottom that does not push content.",[152,3347,3349],{"id":3348},"what-to-fix-first-order-of-action","What to fix first: order of action",[139,3351,3352],{},"Do not rewrite the site right away. Follow the principle \"biggest effect, least work\".",[532,3354,3355,3364,3370,3376,3385,3391],{},[163,3356,3357,3360,3361,3363],{},[166,3358,3359],{},"Images."," Formats, sizes, ",[1529,3362,3057],{},", correct lazy loading. This often gives the biggest LCP gain.",[163,3365,3366,3369],{},[166,3367,3368],{},"Third-party scripts."," Audit and remove what is unnecessary, defer the rest.",[163,3371,3372,3375],{},[166,3373,3374],{},"Reserving space"," for banners, images and widgets. It usually fixes CLS in a few hours.",[163,3377,3378,3381,3382,220],{},[166,3379,3380],{},"Fonts."," Self-hosting, a minimum of weights, correct ",[1529,3383,3384],{},"font-display",[163,3386,3387,3390],{},[166,3388,3389],{},"Caching and server speed."," Page cache, database query optimization, a CDN.",[163,3392,3393,3396],{},[166,3394,3395],{},"Architecture."," If the basics are fixed and the metrics are still weak, it is time to look at the platform: the amount of JavaScript, the rendering method.",[139,3398,3399],{},"After each change, check the result in PageSpeed Insights. Field data updates with a delay of up to 28 days, so do not panic if Search Console shows no improvement the next day: rely on lab measurements, and you will see the real effect later.",[457,3401,3402],{},[139,3403,3404,3407,3408,366],{},[166,3405,3406],{},"Don't know what exactly slows your store down?"," We will run an express check of speed and the mobile version and show 10–15 key problems in a PDF within 48 hours. It is free; details are on the ",[146,3409,1984],{"href":1125},[152,3411,3413],{"id":3412},"what-targets-to-set-for-developers","What targets to set for developers",[139,3415,3416],{},"When you order a new site or optimization, write down measurable goals instead of a general \"make it fast\". Our internal requirements for the sites we build:",[160,3418,3419,3422,3425,3428],{},[163,3420,3421],{},"Lighthouse on mobile: Performance 95+ on key pages, SEO and Best Practices at 100;",[163,3423,3424],{},"LCP up to 2.5 s, INP up to 200 ms, CLS up to 0.1 by field data (75th percentile);",[163,3426,3427],{},"minimal JavaScript on the home page, fonts from your own domain, images in modern formats;",[163,3429,3430],{},"third-party scripts only after user consent.",[139,3432,3433,3434,220],{},"These figures are goals, not guarantees for any project: the metrics are affected by hosting, the number of integrations and the content you add. But the measurement method and the thresholds can be agreed before work begins. This is one of the 12 points we recommend checking when you are ",[146,3435,3436],{"href":661},"choosing a web development contractor",[152,3438,3440],{"id":3439},"speed-and-design-what-is-decided-before-the-code","Speed and design: what is decided before the code",[139,3442,3443,3444,3446],{},"Many metric problems are baked in at the mockup stage. A slider of six large images on the first screen, a video background, three fonts with a dozen weights, scroll animations: all of this reliably hurts LCP and INP. Performance-aware design is ",[146,3445,280],{"href":365}," in which every block has a \"price\" in milliseconds and a justification.",[139,3448,3449],{},"Practical rules for the designer and the client:",[160,3451,3452,3455,3458,3461],{},[163,3453,3454],{},"one main image or heading on the first screen, not a carousel;",[163,3456,3457],{},"a limited number of fonts and weights;",[163,3459,3460],{},"space for dynamic blocks (reviews, recommendations) planned in the mockup;",[163,3462,3463],{},"animation where it helps understand the interface, not just decorates it.",[152,3465,3467],{"id":3466},"typical-mistakes-of-store-owners","Typical mistakes of store owners",[160,3469,3470,3476,3482,3488,3494],{},[163,3471,3472,3475],{},[166,3473,3474],{},"Looking only at the desktop Lighthouse score."," Mobile results are usually worse and matter more.",[163,3477,3478,3481],{},[166,3479,3480],{},"Checking only the home page."," The product page and catalog are often slower.",[163,3483,3484,3487],{},[166,3485,3486],{},"Adding plugins without checking the consequences."," Every widget has a price in milliseconds.",[163,3489,3490,3493],{},[166,3491,3492],{},"Measuring once."," Metrics degrade gradually: a new promotion, a new banner, a new pixel.",[163,3495,3496,3499],{},[166,3497,3498],{},"Treating speed as a \"technical matter\"."," It is part of the business result, so it should be on the site acceptance checklist.",[139,3501,3502,3503,3505],{},"For regular control it is enough to run through steps 1–3 once a month and record the numbers in a table. If the project is supported separately, speed and regression are worth including in a ",[146,3504,508],{"href":507}," plan.",[152,3507,1975],{"id":1974},[139,3509,3510],{},"Core Web Vitals are not about \"scores in Google\" but about whether it is comfortable for a buyer to use your store. Three metrics, three simple questions: does it appear fast? does it respond fast? does it stay put? You can check this for free in a quarter of an hour, and most problems are fixed with images, third-party scripts and reserved space.",[457,3512,3513],{},[139,3514,3515,3518,3519,3521],{},[166,3516,3517],{},"Want a store that opens fast on phones?"," We build sites with Core Web Vitals targets from the very start. Leave a ",[146,3520,355],{"href":354}," and we will estimate the project within 1 business day.",[2768,3523,3524],{},"html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}",{"title":14,"searchDepth":688,"depth":689,"links":3526},[3527,3528,3534,3540,3541,3542,3543,3544,3545,3546],{"id":2830,"depth":688,"text":2831},{"id":2957,"depth":688,"text":2958,"children":3529},[3530,3531,3532,3533],{"id":2964,"depth":689,"text":2965},{"id":2994,"depth":689,"text":2995},{"id":3001,"depth":689,"text":3002},{"id":3028,"depth":689,"text":3029},{"id":3035,"depth":688,"text":3036,"children":3535},[3536,3537,3538,3539],{"id":3042,"depth":689,"text":3043},{"id":3075,"depth":689,"text":3076},{"id":3147,"depth":689,"text":3148},{"id":3159,"depth":689,"text":3160},{"id":3170,"depth":688,"text":3171},{"id":3223,"depth":688,"text":3224},{"id":3348,"depth":688,"text":3349},{"id":3412,"depth":688,"text":3413},{"id":3439,"depth":688,"text":3440},{"id":3466,"depth":688,"text":3467},{"id":1974,"depth":688,"text":1975},"web-vitals.report",[3549,3551,3553,3555],{"marker":716,"text":3550},"LCP ≤ 2.5 s",{"marker":716,"text":3552},"INP ≤ 200 ms",{"marker":713,"text":3554},"CLS 0.14: banner shifts content",{"marker":708,"text":3556},"real-user data, 28 days","3 metrics",[3559,3562,3565,3568],{"q":3560,"a":3561},"What are Core Web Vitals in plain English?","They are three Google metrics for the page experience: LCP shows how quickly the main content appears, INP how quickly the page reacts to actions, and CLS whether the layout jumps around. They affect how comfortable buying is. You can check them for free in PageSpeed Insights.",{"q":3563,"a":3564},"Where can I check my site's Core Web Vitals for free?","The simplest way is PageSpeed Insights: paste the page URL and look at the field data. The Core Web Vitals report in Google Search Console and Lighthouse in Chrome are also useful. Check not only the home page but also a category, a product page and checkout, as these are usually heavier.",{"q":3566,"a":3567},"What Core Web Vitals values count as good?","By Google's guidelines, good values are LCP up to 2.5 s, INP up to 200 ms and CLS up to 0.1. They are assessed at the 75th percentile of real visits. A lab test in a single browser is therefore only a hint, not a final verdict.",{"q":3569,"a":3570},"Do Core Web Vitals affect Google rankings?","Yes, but moderately: they are one page experience signal and do not replace relevant content. Still, a slow or jumpy store loses buyers regardless of rankings. So fix speed first for the sake of conversion and treat any SEO effect as a bonus.","core-web-vitals-dlia-vlasnyka-mahazynu",{},{"title":2819,"description":2824},"Core Web Vitals in plain English: what LCP, INP and CLS mean, which values are good, how to check your online store yourself and what to fix first.","Core Web Vitals for Store Owners — Tochno.","core-web-vitals-for-store-owners","en\u002Fblog\u002Fcore-web-vitals-for-store-owners",[746,359],"2026-10-31","P8VMId0yiykBkBo6yxWHBzrr96DEyqNu1QinQOSm-Is",{"id":132,"title":133,"author":134,"body":3582,"cover":704,"coverFile":705,"coverLines":3957,"coverStat":718,"date":719,"description":141,"excerpt":704,"extension":720,"faq":3962,"featured":734,"key":735,"meta":3967,"navigation":737,"path":738,"readingTime":739,"seo":3968,"seoDescription":741,"seoTitle":742,"slug":743,"stem":744,"tags":3969,"updated":747,"__hash__":748},{"type":136,"value":3583,"toc":3942},[3584,3586,3590,3592,3594,3616,3618,3620,3622,3626,3632,3638,3640,3642,3644,3714,3716,3718,3722,3724,3728,3730,3732,3734,3736,3784,3786,3790,3792,3802,3804,3806,3834,3836,3838,3840,3866,3868,3888,3890,3892,3896,3900,3902,3904,3906,3922,3926,3928,3930,3932],[139,3585,141],{},[139,3587,144,3588,150],{},[146,3589,149],{"href":148},[152,3591,155],{"id":154},[139,3593,158],{},[160,3595,3596,3600,3604,3608,3612],{},[163,3597,3598,169],{},[166,3599,168],{},[163,3601,3602,175],{},[166,3603,174],{},[163,3605,3606,181],{},[166,3607,180],{},[163,3609,3610,187],{},[166,3611,186],{},[163,3613,3614,193],{},[166,3615,192],{},[139,3617,196],{},[152,3619,200],{"id":199},[139,3621,203],{},[139,3623,3624,209],{},[166,3625,208],{},[139,3627,3628,215,3630,220],{},[166,3629,214],{},[146,3631,219],{"href":218},[139,3633,3634,226,3636,231],{},[166,3635,225],{},[146,3637,230],{"href":229},[139,3639,234],{},[152,3641,238],{"id":237},[139,3643,241],{},[243,3645,3646,3656],{},[246,3647,3648],{},[249,3649,3650,3652,3654],{},[252,3651,254],{},[252,3653,257],{},[252,3655,260],{},[262,3657,3658,3666,3674,3682,3690,3698,3706],{},[249,3659,3660,3662,3664],{},[267,3661,269],{},[267,3663,272],{},[267,3665,275],{},[249,3667,3668,3670,3672],{},[267,3669,280],{},[267,3671,283],{},[267,3673,286],{},[249,3675,3676,3678,3680],{},[267,3677,291],{},[267,3679,294],{},[267,3681,297],{},[249,3683,3684,3686,3688],{},[267,3685,302],{},[267,3687,305],{},[267,3689,308],{},[249,3691,3692,3694,3696],{},[267,3693,313],{},[267,3695,316],{},[267,3697,319],{},[249,3699,3700,3702,3704],{},[267,3701,324],{},[267,3703,327],{},[267,3705,330],{},[249,3707,3708,3710,3712],{},[267,3709,335],{},[267,3711,338],{},[267,3713,341],{},[139,3715,344],{},[346,3717,269],{"id":348},[139,3719,351,3720,356],{},[146,3721,355],{"href":354},[346,3723,126],{"id":359},[139,3725,362,3726,366],{},[146,3727,280],{"href":365},[346,3729,370],{"id":369},[139,3731,373],{},[152,3733,377],{"id":376},[139,3735,380],{},[243,3737,3738,3746],{},[246,3739,3740],{},[249,3741,3742,3744],{},[252,3743,389],{},[252,3745,392],{},[262,3747,3748,3754,3760,3766,3772,3778],{},[249,3749,3750,3752],{},[267,3751,399],{},[267,3753,402],{},[249,3755,3756,3758],{},[267,3757,407],{},[267,3759,410],{},[249,3761,3762,3764],{},[267,3763,415],{},[267,3765,418],{},[249,3767,3768,3770],{},[267,3769,423],{},[267,3771,426],{},[249,3773,3774,3776],{},[267,3775,431],{},[267,3777,434],{},[249,3779,3780,3782],{},[267,3781,439],{},[267,3783,442],{},[139,3785,445],{},[139,3787,448,3788,220],{},[146,3789,452],{"href":451},[139,3791,455],{},[457,3793,3794],{},[139,3795,3796,464,3798,468,3800,471],{},[166,3797,463],{},[146,3799,467],{"href":148},[146,3801,355],{"href":354},[152,3803,475],{"id":474},[139,3805,478],{},[160,3807,3808,3812,3816,3820,3826,3830],{},[163,3809,3810,486],{},[166,3811,485],{},[163,3813,3814,492],{},[166,3815,491],{},[163,3817,3818,498],{},[166,3819,497],{},[163,3821,3822,504,3824,366],{},[166,3823,503],{},[146,3825,508],{"href":507},[163,3827,3828,514],{},[166,3829,513],{},[163,3831,3832,520],{},[166,3833,519],{},[139,3835,523],{},[152,3837,527],{"id":526},[139,3839,530],{},[532,3841,3842,3846,3850,3854,3858,3862],{},[163,3843,3844,539],{},[166,3845,538],{},[163,3847,3848,545],{},[166,3849,544],{},[163,3851,3852,551],{},[166,3853,550],{},[163,3855,3856,557],{},[166,3857,556],{},[163,3859,3860,563],{},[166,3861,562],{},[163,3863,3864,569],{},[166,3865,568],{},[139,3867,572],{},[160,3869,3870,3876,3880,3884],{},[163,3871,3872,580,3874,585],{},[166,3873,579],{},[146,3875,584],{"href":583},[163,3877,3878,591],{},[166,3879,590],{},[163,3881,3882,597],{},[166,3883,596],{},[163,3885,3886,603],{},[166,3887,602],{},[152,3889,607],{"id":606},[139,3891,610],{},[139,3893,3894,616],{},[166,3895,615],{},[139,3897,3898,622],{},[166,3899,621],{},[139,3901,625],{},[152,3903,629],{"id":628},[139,3905,632],{},[160,3907,3908,3910,3912,3914,3916,3918,3920],{},[163,3909,637],{},[163,3911,640],{},[163,3913,643],{},[163,3915,646],{},[163,3917,649],{},[163,3919,652],{},[163,3921,655],{},[139,3923,658,3924,220],{},[146,3925,662],{"href":661},[152,3927,666],{"id":665},[139,3929,669],{},[139,3931,672],{},[457,3933,3934],{},[139,3935,3936,680,3938,683,3940,686],{},[166,3937,679],{},[146,3939,355],{"href":354},[146,3941,230],{"href":229},{"title":14,"searchDepth":688,"depth":689,"links":3943},[3944,3945,3946,3951,3952,3953,3954,3955,3956],{"id":154,"depth":688,"text":155},{"id":199,"depth":688,"text":200},{"id":237,"depth":688,"text":238,"children":3947},[3948,3949,3950],{"id":348,"depth":689,"text":269},{"id":359,"depth":689,"text":126},{"id":369,"depth":689,"text":370},{"id":376,"depth":688,"text":377},{"id":474,"depth":688,"text":475},{"id":526,"depth":688,"text":527},{"id":606,"depth":688,"text":607},{"id":628,"depth":688,"text":629},{"id":665,"depth":688,"text":666},[3958,3959,3960,3961],{"marker":708,"text":709},{"marker":708,"text":711},{"marker":713,"text":714},{"marker":716,"text":717},[3963,3964,3965,3966],{"q":723,"a":724},{"q":726,"a":727},{"q":729,"a":730},{"q":732,"a":733},{},{"title":133,"description":141},[746],{"id":3971,"title":3972,"author":134,"body":3973,"cover":704,"coverFile":4466,"coverLines":4467,"coverStat":4475,"date":719,"description":4476,"excerpt":704,"extension":720,"faq":4477,"featured":737,"key":4490,"meta":4491,"navigation":737,"path":583,"readingTime":739,"seo":4492,"seoDescription":4493,"seoTitle":4494,"slug":4495,"stem":4496,"tags":4497,"updated":747,"__hash__":4498},"blog_en\u002Fen\u002Fblog\u002Fecommerce-testing-checklist.md","Ecommerce Testing Checklist Before Launch: 60 Checks",{"type":136,"value":3974,"toc":4454},[3975,3981,3984,3988,3991,4011,4016,4020,4023,4059,4062,4066,4069,4095,4099,4105,4160,4163,4167,4172,4205,4209,4230,4234,4237,4266,4270,4351,4361,4365,4368,4394,4397,4402,4406,4409,4441,4444],[139,3976,3977,3978,3980],{},"An ecommerce testing checklist is for store owners who are about to launch or relaunch a site and want to be sure a customer can find a product, add it to the cart and pay. Below are 60 checks grouped by the stages of the buyer's journey. You can work through them yourself in an evening or hand them to a tester. We use exactly this skeleton when we prepare an ",[146,3979,1984],{"href":1125},", although in practice the list is much longer: around 150 checks.",[139,3982,3983],{},"The main idea is simple. Not everything in a store matters equally. A bug in the footer costs you some nerves; a bug in the cart costs you money. So we test in order of money: payment and checkout first, then the cart and catalog, then everything else.",[152,3985,3987],{"id":3986},"how-to-use-the-checklist","How to use the checklist",[139,3989,3990],{},"Prepare three things before you start.",[160,3992,3993,3999,4005],{},[163,3994,3995,3998],{},[166,3996,3997],{},"A test environment"," (staging) that is as close to production as possible: the same PHP and database versions, the same integrations, but in sandbox mode. There is no need to test payments with a real card on production.",[163,4000,4001,4004],{},[166,4002,4003],{},"A set of test data:"," several products in different states (in stock, running low, out of stock, with variants, discounted), 2–3 accounts, promo codes and the payment provider's test cards.",[163,4006,4007,4010],{},[166,4008,4009],{},"Devices:"," at least one iPhone with Safari, one Android phone with Chrome, and a desktop with Chrome and Firefox. DevTools emulation is useful, but real iOS Safari breaks in different ways.",[139,4012,4013,4014,220],{},"Mark every item as passed, failed or not applicable. For every failure, record the steps to reproduce, the expected and actual result, the device, and a screenshot or video. We walk through a good bug description in the article ",[146,4015,1293],{"href":2047},[152,4017,4019],{"id":4018},"_1-catalog-and-search-10-checks","1. Catalog and search (10 checks)",[139,4021,4022],{},"The catalog creates the first impression and brings in search traffic, so here we check both functionality and data.",[532,4024,4025,4028,4031,4034,4037,4040,4047,4050,4053,4056],{},[163,4026,4027],{},"All categories open, with no empty pages and no 500 errors in the list.",[163,4029,4030],{},"The product count in the category counter matches the number of products on the page.",[163,4032,4033],{},"Filters work alone and in combination; the URL changes when they are applied, and the link brings you back to the same result.",[163,4035,4036],{},"Resetting filters returns the full list, not \"nothing found\".",[163,4038,4039],{},"Price sorting really sorts numbers, not strings: 900 does not come after 10,000.",[163,4041,4042,4043,4046],{},"Pagination: the last page and an out-of-range page (",[1529,4044,4045],{},"?page=999",") give a clear response, not an error.",[163,4048,4049],{},"Search finds a product by name, by SKU and with a one-letter typo; Cyrillic and Latin work the same.",[163,4051,4052],{},"An empty search result shows a hint and popular categories.",[163,4054,4055],{},"Product page: photos, price, description, variants and availability display correctly; products without photos get a placeholder.",[163,4057,4058],{},"An out-of-stock product cannot be bought but can still be found, and there is a \"notify me\" button or similar products.",[139,4060,4061],{},"Check prices separately. The price in the listing, on the product page, in the cart and at checkout must be identical to the cent, taking discounts and rounding into account.",[152,4063,4065],{"id":4064},"_2-cart-8-checks","2. Cart (8 checks)",[139,4067,4068],{},"The cart looks simple, but it holds the most logic: prices, stock, coupons.",[532,4070,4071,4074,4077,4080,4083,4086,4089,4092],{"start":739},[163,4072,4073],{},"Adding a product from the product page, from a listing and from \"quick view\" works the same way.",[163,4075,4076],{},"Changing the quantity recalculates the total instantly; a quantity of 0 or a negative quantity does not break the cart.",[163,4078,4079],{},"You cannot add more than is in stock, and the message about it is clear.",[163,4081,4082],{},"Removing a product and clicking \"undo\" leaves no \"ghosts\" in the cart icon counter.",[163,4084,4085],{},"The cart survives a page refresh, closing the tab and logging in (the guest cart merges with the account cart).",[163,4087,4088],{},"Promo code: valid, expired, already used, letter case, combination with another discount, minimum amount. The error explains the reason.",[163,4090,4091],{},"Free shipping above a threshold: the threshold is displayed and works when the total crosses it both upward and downward.",[163,4093,4094],{},"If the price or stock changed while the product sat in the cart, the user sees a message instead of finding out at payment.",[152,4096,4098],{"id":4097},"_3-checkout-ordering-without-registration-12-checks","3. Checkout: ordering without registration (12 checks)",[139,4100,4101,4102,4104],{},"This is where money gets lost. We cover the typical bugs of this stage in detail in ",[146,4103,2058],{"href":1970},"; here is the basic list.",[532,4106,4108,4111,4114,4127,4130,4139,4142,4145,4148,4151,4154,4157],{"start":4107},19,[163,4109,4110],{},"An order can be placed as a guest, without mandatory registration.",[163,4112,4113],{},"Field validation appears next to the field, not as one message at the top; entered data does not disappear after an error.",[163,4115,4116,4117,2641,4120,3068,4123,4126],{},"The phone field accepts ",[1529,4118,4119],{},"+380…",[1529,4121,4122],{},"380…",[1529,4124,4125],{},"0…"," formats, with spaces and parentheses, and stores them in a single format.",[163,4128,4129],{},"An email with a trailing space (a typical result of phone autofill) is handled, not rejected.",[163,4131,4132,4133,3068,4136,2436],{},"Cyrillic, apostrophes and quotes in the name and address do not break the order (try ",[1529,4134,4135],{},"O'Connor",[1529,4137,4138],{},"Ґанок",[163,4140,4141],{},"Choosing a delivery method changes the cost and the total immediately, without a reload.",[163,4143,4144],{},"Pickup shows the address and opening hours.",[163,4146,4147],{},"Address: for courier delivery the required fields are mandatory; for a branch they are hidden and do not block submission.",[163,4149,4150],{},"Double-clicking \"Confirm order\" creates one order, not two.",[163,4152,4153],{},"The browser's Back button does not reset the form or duplicate the order.",[163,4155,4156],{},"After a successful order the customer sees a thank-you page with the order number; refreshing the page does not create a new order.",[163,4158,4159],{},"If the session expires in the middle of checkout, the user does not lose the cart.",[139,4161,4162],{},"Add a \"slow connection\" test here: turn on Slow 3G in DevTools and place an order. That is where buttons without a loading indicator and double clicks show up.",[152,4164,4166],{"id":4165},"_4-payment-and-nova-poshta-10-checks","4. Payment and Nova Poshta (10 checks)",[139,4168,4169,4170,220],{},"Integrations break at the joints between systems, so we test success and every kind of failure. A separate deep dive on this topic: ",[146,4171,452],{"href":451},[532,4173,4175,4178,4181,4184,4187,4190,4193,4196,4199,4202],{"start":4174},31,[163,4176,4177],{},"Successful card payment in the sandbox: the order gets the \"paid\" status and the customer receives an email.",[163,4179,4180],{},"Declined card: the order stays \"awaiting payment\", the customer sees a clear message and can try again.",[163,4182,4183],{},"The customer closed the tab on the bank page: the order does not hang, and there is a way to pay again via a link.",[163,4185,4186],{},"The payment provider sent the callback twice: the order is not paid twice and emails are not duplicated (idempotency).",[163,4188,4189],{},"The callback arrived before the customer returned to the site: the status is still correct.",[163,4191,4192],{},"Cash on delivery has its own rules (limits, fees) and is shown correctly in the order.",[163,4194,4195],{},"Nova Poshta: city search by part of the name works, and settlements with the same name in different regions are distinguished.",[163,4197,4198],{},"Branches and parcel lockers load, search by number works, and a closed branch is not offered.",[163,4200,4201],{},"Shipping cost and delivery times match the carrier's tariff, and the waybill (TTN) is created with the correct dimensions and weight.",[163,4203,4204],{},"A carrier API error does not block the order: the manager sees that the waybill was not created and can retry.",[152,4206,4208],{"id":4207},"_5-account-and-emails-6-checks","5. Account and emails (6 checks)",[532,4210,4212,4215,4218,4221,4224,4227],{"start":4211},41,[163,4213,4214],{},"Registration, login, password reset: the email arrives, and the link works once and has an expiry.",[163,4216,4217],{},"The same email cannot be registered twice, even in a different letter case.",[163,4219,4220],{},"Order history shows the correct statuses and amounts.",[163,4222,4223],{},"Transactional emails (confirmation, payment, shipping) do not land in spam, look right in Gmail on a phone and contain the correct data.",[163,4225,4226],{},"Newsletter signup and the contact form work, and there is consent to data processing.",[163,4228,4229],{},"Admin emails contain no personal data that does not belong there, and Telegram or CRM notifications arrive with all order fields.",[152,4231,4233],{"id":4232},"_6-mobile-and-compatibility-6-checks","6. Mobile and compatibility (6 checks)",[139,4235,4236],{},"For most stores more than half of the traffic is mobile, so this is not an \"extra\".",[532,4238,4240,4243,4254,4257,4260,4263],{"start":4239},47,[163,4241,4242],{},"Check widths of 375, 768 and 1440 pixels: no horizontal scroll and no clipped text.",[163,4244,4245,4246,4249,4250,4253],{},"Buttons and fields are finger-friendly (roughly 44 pixels or more), and fields use the right keyboard type: ",[1529,4247,4248],{},"tel"," for phone, ",[1529,4251,4252],{},"email"," for email.",[163,4255,4256],{},"A sticky header or banner does not cover the \"Buy\" button or the input field when the keyboard is open.",[163,4258,4259],{},"iOS Safari: checkout, payment, filters, dropdowns.",[163,4261,4262],{},"Chrome on Android, Firefox and Edge on desktop: the main purchase flow.",[163,4264,4265],{},"Modals close, focus does not escape behind them, and the page underneath does not scroll.",[152,4267,4269],{"id":4268},"_7-speed-seo-and-security-8-checks","7. Speed, SEO and security (8 checks)",[532,4271,4273,4279,4296,4309,4318,4328,4331,4338],{"start":4272},53,[163,4274,4275,4276,220],{},"Run Lighthouse on mobile for the home page, a category and a product page. Target: LCP under 2.5 s, CLS under 0.1. We explain how to read these metrics without a developer in ",[146,4277,4278],{"href":1050},"Core Web Vitals for Store Owners",[163,4280,4281,4282,2641,4284,4287,4288,4291,4292,4295],{},"Every product and category has a unique ",[1529,4283,1349],{},[1529,4285,4286],{},"description",", a single ",[1529,4289,4290],{},"h1"," and a ",[1529,4293,4294],{},"canonical","; filtered pages do not create endless duplicates.",[163,4297,4298,3068,4301,4304,4305,4308],{},[1529,4299,4300],{},"robots.txt",[1529,4302,4303],{},"sitemap.xml"," are in place, and the test environment is closed to indexing (and that ",[1529,4306,4307],{},"noindex"," did not travel to production with the release).",[163,4310,4311,3068,4314,4317],{},[1529,4312,4313],{},"Product",[1529,4315,4316],{},"BreadcrumbList"," structured data is present with the correct price and availability.",[163,4319,4320,4321,3068,4324,4327],{},"The site works over HTTPS with no mixed-content warnings; ",[1529,4322,4323],{},"http",[1529,4325,4326],{},"www"," redirect to a single version.",[163,4329,4330],{},"The 404 page is useful, and old URLs have 301 redirects.",[163,4332,4333,4334,4337],{},"Basic security: the password is not passed in the URL, the admin panel is closed and not sitting unprotected at ",[1529,4335,4336],{},"\u002Fadmin",", and forms are protected from spam.",[163,4339,4340,4341,2641,4344,3068,4347,4350],{},"Analytics: ",[1529,4342,4343],{},"purchase",[1529,4345,4346],{},"add_to_cart",[1529,4348,4349],{},"begin_checkout"," events arrive, and tracking does not fire before consent in the cookie banner.",[457,4352,4353],{},[139,4354,4355,4358,4359,1985],{},[166,4356,4357],{},"Don't want to go through 60 checks yourself?"," We will run a free express check in 48 hours: 10–15 key problems with video and priorities. A full ",[146,4360,1984],{"href":1125},[152,4362,4364],{"id":4363},"what-to-do-with-the-results","What to do with the results",[139,4366,4367],{},"After working through the checklist you will have a list of problems. Do not try to fix everything at once; sort them by impact.",[160,4369,4370,4376,4382,4388],{},[163,4371,4372,4375],{},[166,4373,4374],{},"Blockers:"," the customer cannot buy or pay. Fix before launch, no exceptions.",[163,4377,4378,4381],{},[166,4379,4380],{},"Critical:"," there is a workaround, but some customers will leave (a bug only in Safari, a promo code that does not work).",[163,4383,4384,4387],{},[166,4385,4386],{},"Significant:"," they spoil the experience or SEO, but no money is lost.",[163,4389,4390,4393],{},[166,4391,4392],{},"Cosmetic:"," fix when there is time.",[139,4395,4396],{},"A good launch rule: zero blockers and zero critical issues on the \"product → cart → payment\" path. The rest can launch with a list of known issues and a fix plan.",[139,4398,4399,4400,220],{},"One more practical tip: after every release, repeat at least 12–15 key checks (sections 2–4). This is called regression testing. If it takes hours of manual work every time, it is worth thinking about automating the most important scenarios. We describe when that pays off and when it does not in ",[146,4401,2709],{"href":1855},[152,4403,4405],{"id":4404},"common-mistakes-when-you-test-it-yourself","Common mistakes when you test it yourself",[139,4407,4408],{},"In our experience, owners and developers who test their own store make the same mistakes again and again.",[160,4410,4411,4417,4423,4429,4435],{},[163,4412,4413,4416],{},[166,4414,4415],{},"They test the \"happy path\"."," Customers do the opposite: they type extra spaces, press Back and close the tab in the middle of payment.",[163,4418,4419,4422],{},[166,4420,4421],{},"They test on their own computer."," The developer works on a fast laptop with a wide screen, while the buyer is on an old Android phone on the subway.",[163,4424,4425,4428],{},[166,4426,4427],{},"They do not record the steps."," A bug that cannot be reproduced will not be fixed. Record your screen.",[163,4430,4431,4434],{},[166,4432,4433],{},"They do not retest after the fix."," Fixing one thing breaks neighboring functionality, so after every fix run through the related checks.",[163,4436,4437,4440],{},[166,4438,4439],{},"They skip emails and notifications."," The order was created, but the email never arrived, and the customer thinks they were ignored.",[139,4442,4443],{},"If you have a developer, ask them to add a \"sandbox\" page with all product states, or better still, set up automated tests for the cart and checkout. It is the cheapest insurance against repeating old mistakes.",[457,4445,4446],{},[139,4447,4448,2759,4451,4453],{},[166,4449,4450],{},"Need a second pair of eyes before launch?",[146,4452,355],{"href":354},", and we will tell you how much time and money it takes to test your specific project.",{"title":14,"searchDepth":688,"depth":689,"links":4455},[4456,4457,4458,4459,4460,4461,4462,4463,4464,4465],{"id":3986,"depth":688,"text":3987},{"id":4018,"depth":688,"text":4019},{"id":4064,"depth":688,"text":4065},{"id":4097,"depth":688,"text":4098},{"id":4165,"depth":688,"text":4166},{"id":4207,"depth":688,"text":4208},{"id":4232,"depth":688,"text":4233},{"id":4268,"depth":688,"text":4269},{"id":4363,"depth":688,"text":4364},{"id":4404,"depth":688,"text":4405},"checkout.checklist",[4468,4469,4471,4473],{"marker":716,"text":709},{"marker":716,"text":4470},"cart and promo codes",{"marker":713,"text":4472},"payment: repeated callback",{"marker":2023,"text":4474},"Nova Poshta: empty branch","60 checks","An ecommerce testing checklist is for store owners who are about to launch or relaunch a site and want to be sure a customer can find a product, add it to the cart and pay. Below are 60 checks grouped by the stages of the buyer's journey. You can work through them yourself in an evening or hand them to a tester. We use exactly this skeleton when we prepare an ecommerce QA audit, although in practice the list is much longer: around 150 checks.",[4478,4481,4484,4487],{"q":4479,"a":4480},"What should I test in an online store before launch?","Check the whole buyer journey: catalog and search, cart, checkout, payment, delivery, emails and the mobile version. Most revenue is lost at checkout and payment, so start there, and only then move on to secondary pages, speed and small visual details.",{"q":4482,"a":4483},"How long does it take to test a store before launch?","Working through the checklist yourself usually takes a few days, depending on catalog size and the number of integrations. A full store QA audit at our studio takes 3–5 days and starts from UAH 6,000. You get a report with fixes ranked by priority.",{"q":4485,"a":4486},"Can I test my store myself without a QA engineer?","Yes, you can walk through the basic catalog, cart and order scenarios yourself using a checklist. But owners know their site too well and do not see what a new buyer sees. Harder cases, such as payment failures or different browsers, are better handed to a fresh pair of eyes.",{"q":4488,"a":4489},"Which devices and browsers should I check before launching a store?","At a minimum: current Chrome and Safari on desktop, an iPhone with Safari and an Android phone with Chrome. Run checkout from a phone separately, since that is where buttons most often stop working and forms break. If you have analytics, focus on the devices your real visitors use.","chek-lyst-testuvannia-internet-mahazynu",{},{"title":3972,"description":4476},"Ecommerce testing checklist for pre-launch QA: 60 checks across catalog, cart, checkout, payments, mobile and SEO. Use it as is or hand it to a tester.","Ecommerce Testing Checklist: 60 Checks — Tochno.","ecommerce-testing-checklist","en\u002Fblog\u002Fecommerce-testing-checklist",[2054,746],"jwmUahW4mFURNq-hyZHOPQa-N3Q8iA7aWC0L8VY2ASI",{"id":4500,"title":662,"author":134,"body":4501,"cover":704,"coverFile":4962,"coverLines":4963,"coverStat":4972,"date":719,"description":4505,"excerpt":704,"extension":720,"faq":4973,"featured":734,"key":4986,"meta":4987,"navigation":737,"path":661,"readingTime":739,"seo":4988,"seoDescription":4989,"seoTitle":4990,"slug":4991,"stem":4992,"tags":4993,"updated":747,"__hash__":4994},"blog_en\u002Fen\u002Fblog\u002Fhow-to-choose-web-development-contractor.md",{"type":136,"value":4502,"toc":4935},[4503,4506,4509,4513,4516,4519,4545,4548,4552,4556,4559,4563,4569,4572,4576,4579,4582,4586,4589,4593,4597,4600,4603,4607,4613,4616,4620,4623,4627,4631,4634,4643,4647,4652,4656,4659,4670,4674,4678,4681,4687,4691,4694,4722,4725,4729,4732,4770,4774,4777,4872,4875,4879,4882,4917,4920,4922,4925],[139,4504,4505],{},"To choose a web development contractor for a website or online store, you do not need to understand code. It is enough to ask 12 questions about process, code ownership, testing, price and support, and to listen closely to how specific the answers are. A reliable team answers with numbers, examples and documents; an unreliable one answers with general phrases about an \"individual approach\".",[139,4507,4508],{},"Below are the questions we ourselves recommend clients ask, including us. A tech lead with eleven years of experience in PHP and Laravel has seen enough projects stall halfway to know that problems start not with the code but with vague agreements at the start.",[152,4510,4512],{"id":4511},"why-choosing-a-contractor-is-risk-management","Why choosing a contractor is risk management",[139,4514,4515],{},"A website or store is not something you can return to the shop. If the contractor disappears after six months, you are left with partially written code, someone else's access credentials and lost time. So the choice should be made as risk management: what exactly can go wrong, and what can you verify right now.",[139,4517,4518],{},"The main risks are typical:",[160,4520,4521,4527,4533,4539],{},[163,4522,4523,4526],{},[166,4524,4525],{},"Loss of control."," The code, domain, hosting and services are registered to the contractor.",[163,4528,4529,4532],{},[166,4530,4531],{},"A bloated budget."," The starting price is understated, and then every change is \"extra work\".",[163,4534,4535,4538],{},[166,4536,4537],{},"Low quality."," The site \"works on the developer's laptop\" but breaks on phones or at payment.",[163,4540,4541,4544],{},[166,4542,4543],{},"Dependence on one person."," Nobody can pick up the project when the developer gets ill or leaves.",[139,4546,4547],{},"The twelve questions below are grouped by these risks. Ask them in writing: answers in an email are easier to compare and to quote later.",[152,4549,4551],{"id":4550},"questions-about-the-result-and-the-price","Questions about the result and the price",[346,4553,4555],{"id":4554},"_1-what-exactly-will-i-get-at-each-stage","1. What exactly will I get at each stage?",[139,4557,4558],{},"A good answer is a list of artifacts: page structure, Figma mockups, a working prototype, a staging environment, the final release. A bad one is \"design first, then development\". Every stage should end with something you can see and accept.",[346,4560,4562],{"id":4561},"_2-what-is-the-price-made-of-and-what-is-not-included","2. What is the price made of, and what is not included?",[139,4564,4565,4566,4568],{},"Ask for a breakdown by stage or by feature, not a single figure. We publish starting prices openly on the ",[146,4567,149],{"href":148},": a landing page from UAH 25,000, a corporate website from UAH 60,000, a store from UAH 150,000. The exact cost depends on scope, design, integrations and deadlines, and it should be explained, not just \"depend\".",[139,4570,4571],{},"Separately ask about the things that usually stay outside the estimate: copy and photos, paid licenses and fonts, hosting, domain, third-party services. If they are not named, you will find out about them at the end.",[346,4573,4575],{"id":4574},"_3-how-are-changes-in-requirements-priced","3. How are changes in requirements priced?",[139,4577,4578],{},"Changes are inevitable. The question is how they are handled: a fixed price with a separate estimate for changes, or hourly work with a cap. The right answer is that every change is first estimated in writing and you approve it before work starts. The wrong one is \"we'll do it, we'll settle up later\".",[139,4580,4581],{},"For comparison: hourly rates on our team are open, from $20 per hour for a designer to $32–35 for a tech lead. You do not have to work hourly, but you should understand what any figure is made of.",[346,4583,4585],{"id":4584},"_4-how-long-will-it-take-and-what-affects-the-timeline","4. How long will it take and what affects the timeline?",[139,4587,4588],{},"Realistic benchmarks: a landing page takes 2–3 weeks, a corporate website 5–7 weeks, a store 10–14 weeks. If a contractor promises a store with payment, delivery and CRM in two weeks, they either did not account for testing or do not grasp the complexity of the integrations. Add your own time to this: approving mockups and content often delays projects more than development does.",[152,4590,4592],{"id":4591},"questions-about-code-access-and-ownership","Questions about code, access and ownership",[346,4594,4596],{"id":4595},"_5-who-owns-the-code-and-will-i-get-access-to-the-repository","5. Who owns the code, and will I get access to the repository?",[139,4598,4599],{},"The only acceptable answer: after payment, the code and all materials belong to you, and the repository is created on your account from the start or transferred to you. If development goes in the contractor's private repository and you get an \"archive at the end\", that is a risk: you cannot see progress and have no guarantees.",[139,4601,4602],{},"The same applies to the domain, hosting, payment provider accounts and analytics. Everything tied to money and customer data must be registered to you.",[346,4604,4606],{"id":4605},"_6-what-will-the-site-be-built-on-and-why-that-stack","6. What will the site be built on, and why that stack?",[139,4608,4609,4610,4612],{},"Not to test your knowledge of terms, but to hear the reasoning. For a store with non-standard logic, integrations and its own admin panel, Laravel is a reasonable answer; we discuss this in more detail in ",[146,4611,219],{"href":218},". For a quick landing page or content site, simpler solutions are enough. It is suspicious if a contractor offers the same stack for every task just because \"that's how we work\".",[139,4614,4615],{},"Ask separately: will it be easy for another team to take over the project? A popular stack, documentation and code structure that follows common practices are insurance against dependence on a single vendor.",[346,4617,4619],{"id":4618},"_7-is-there-documentation-and-what-happens-if-you-stop-working-with-us","7. Is there documentation, and what happens if you stop working with us?",[139,4621,4622],{},"At a minimum: deployment instructions, a description of environment variables, the database schema and a list of integrations. Ask directly: \"If in a year we decide to work with another team, what will you hand over and how fast?\" A good contractor does not take offense at this question; they have a ready answer.",[152,4624,4626],{"id":4625},"questions-about-quality-and-testing","Questions about quality and testing",[346,4628,4630],{"id":4629},"_8-who-tests-the-site-before-launch-and-how","8. Who tests the site before launch, and how?",[139,4632,4633],{},"This question filters out the most. The typical answer from a weak team: \"developers check it themselves\". A developer tests what they wrote, the way they intended. Independent testing looks for what nobody anticipated: an empty cart, pressing \"Place order\" twice, a long surname in a form, an old iPhone.",[139,4635,4636,4637,4639,4640,4642],{},"Ask for specifics: is there a test plan, which devices and browsers are checked, how bugs are recorded and how many rounds of fixes are included in the price. For a store, checkout testing is a topic of its own; we described the typical mistakes in ",[146,4638,2058],{"href":1970},". If the contractor has no QA of its own, you can order independent ",[146,4641,1338],{"href":1337}," separately.",[346,4644,4646],{"id":4645},"_9-which-speed-and-accessibility-metrics-do-you-guarantee","9. Which speed and accessibility metrics do you guarantee?",[139,4648,4649,4650,220],{},"Not \"a fast site\" but measurable thresholds: for example Lighthouse on mobile and Core Web Vitals. Ask where and how these numbers will be measured, and whether optimizations are included in the price. How to check speed yourself is explained in ",[146,4651,4278],{"href":1050},[346,4653,4655],{"id":4654},"_10-how-do-you-handle-seo-during-development","10. How do you handle SEO during development?",[139,4657,4658],{},"SEO added \"after launch\" almost always means rework. Ask whether every page will have a unique title and description, a canonical, structured data, a sitemap and correct redirects from the old site. If you already have a site and are migrating, redirects from old URLs are a separate question that must be in the plan before development starts, not after traffic drops.",[457,4660,4661],{},[139,4662,4663,4666,4667,366],{},[166,4664,4665],{},"Not sure about a contractor's answers?"," In a 90-minute consultation a tech lead will go through the commercial proposal and the requirements and tell you which risks they see. The price is from UAH 3,000; details are on the ",[146,4668,33],{"href":4669},"\u002Fen\u002Fservices\u002Ftech-lead",[152,4671,4673],{"id":4672},"questions-about-process-contract-and-support","Questions about process, contract and support",[346,4675,4677],{"id":4676},"_11-what-does-communication-look-like-and-who-is-my-contact","11. What does communication look like, and who is my contact?",[139,4679,4680],{},"Find out before work begins: who manages the project, how often there will be reports, where tasks are tracked, and how fast questions get answered. Good practice is a regular short report (what is done, what is in progress, what is blocking) and access to the task tracker. A bad sign is communication only through a manager who passes questions to \"the developers\" and comes back a week later.",[139,4682,4683,4684,366],{},"Also ask who exactly will write the code. In agencies a switch happens: one specialist sells, and another, more junior, does the work. We work openly: you know who the tech lead is, who the QA is and who builds the layout, and the process is described on the ",[146,4685,42],{"href":4686},"\u002Fen\u002Fprocess",[346,4688,4690],{"id":4689},"_12-what-does-the-contract-say-about-payment-and-post-launch-support","12. What does the contract say about payment and post-launch support?",[139,4692,4693],{},"Check four points:",[532,4695,4696,4702,4708,4714],{},[163,4697,4698,4701],{},[166,4699,4700],{},"A formal contract"," with scope, deadlines, milestones and the parties' liability.",[163,4703,4704,4707],{},[166,4705,4706],{},"Payment by milestones"," tied to acceptance, not to the calendar. Our scheme is 30 \u002F 40 \u002F 30: an advance, payment after the main development and a final settlement after acceptance.",[163,4709,4710,4713],{},[166,4711,4712],{},"A warranty period"," for fixing defects. Ours is 30 days after launch.",[163,4715,4716,4719,4720,366],{},[166,4717,4718],{},"Support after release",": what is included, what it costs, who responds to incidents. Support plans start from UAH 8,000 per month; more on the ",[146,4721,508],{"href":507},[139,4723,4724],{},"It is very useful to spell out what counts as acceptance of the work: for example, the absence of critical and serious defects from the test plan list. Without it, \"done\" means something different to each side.",[152,4726,4728],{"id":4727},"red-flags-that-do-not-depend-on-the-answers","Red flags that do not depend on the answers",[139,4730,4731],{},"Sometimes a contractor's behavior during negotiations already shows what will come next. Watch for these signs:",[160,4733,4734,4740,4746,4752,4758,4764],{},[163,4735,4736,4739],{},[166,4737,4738],{},"A price without a breakdown and without a conversation about requirements."," If the estimate came after a five-minute call, it is a price \"out of thin air\".",[163,4741,4742,4745],{},[166,4743,4744],{},"Time pressure."," \"The discount is valid until Friday\" is almost always marketing, not business logic.",[163,4747,4748,4751],{},[166,4749,4750],{},"No questions for you."," A good contractor asks many questions about the audience, sales processes and existing systems. If there are no questions, they do not intend to dig in.",[163,4753,4754,4757],{},[166,4755,4756],{},"A portfolio with no explanations."," Screenshots without a description of the task, the role and the result. Ask them to show what exactly the team did and what changed for the business.",[163,4759,4760,4763],{},[166,4761,4762],{},"A refusal to show examples of code or process."," It is not necessary to publish someone else's code, but talking about the approach should be possible.",[163,4765,4766,4769],{},[166,4767,4768],{},"A price far below the market."," If a proposal is half the others, testing, design or time reserve has been removed somewhere.",[152,4771,4773],{"id":4772},"how-to-compare-proposals-in-practice","How to compare proposals in practice",[139,4775,4776],{},"Collect at least three proposals and put them into a table. Do not compare only the total; compare the composition.",[243,4778,4779,4794],{},[246,4780,4781],{},[249,4782,4783,4785,4788,4791],{},[252,4784,853],{},[252,4786,4787],{},"Contractor A",[252,4789,4790],{},"Contractor B",[252,4792,4793],{},"Contractor C",[262,4795,4796,4807,4818,4829,4840,4851,4861],{},[249,4797,4798,4801,4803,4805],{},[267,4799,4800],{},"Artifacts at each stage",[267,4802],{},[267,4804],{},[267,4806],{},[249,4808,4809,4812,4814,4816],{},[267,4810,4811],{},"Independent testing included",[267,4813],{},[267,4815],{},[267,4817],{},[249,4819,4820,4823,4825,4827],{},[267,4821,4822],{},"Code and access in your name",[267,4824],{},[267,4826],{},[267,4828],{},[249,4830,4831,4834,4836,4838],{},[267,4832,4833],{},"Warranty, support",[267,4835],{},[267,4837],{},[267,4839],{},[249,4841,4842,4845,4847,4849],{},[267,4843,4844],{},"Speed metrics",[267,4846],{},[267,4848],{},[267,4850],{},[249,4852,4853,4855,4857,4859],{},[267,4854,4706],{},[267,4856],{},[267,4858],{},[267,4860],{},[249,4862,4863,4866,4868,4870],{},[267,4864,4865],{},"Price and what it excludes",[267,4867],{},[267,4869],{},[267,4871],{},[139,4873,4874],{},"Once filled in, it often becomes clear that the cheaper proposal simply lacks rows that the more expensive one has. If you are unsure how to assess the technical points, you can bring in an outside specialist for a few hours: a review of the proposal and requirements costs less than one rework.",[152,4876,4878],{"id":4877},"a-one-week-selection-mini-process","A one-week selection mini-process",[139,4880,4881],{},"To avoid dragging the choice out for months, you can fit it into five steps:",[532,4883,4884,4893,4899,4905,4911],{},[163,4885,4886,4889,4890,4892],{},[166,4887,4888],{},"Day 1."," Write a short task description: goal, audience, mandatory features, integrations, desired deadline and budget range. The ",[146,4891,355],{"href":354}," is convenient for this.",[163,4894,4895,4898],{},[166,4896,4897],{},"Days 2–3."," Send the task to 3–4 candidates and ask them to answer the 12 questions in writing.",[163,4900,4901,4904],{},[166,4902,4903],{},"Day 4."," Hold short calls: check how well people understand your task.",[163,4906,4907,4910],{},[166,4908,4909],{},"Day 5."," Put the proposals into a table and review the contract.",[163,4912,4913,4916],{},[166,4914,4915],{},"Days 6–7."," Ask for a small trial task or a paid discovery stage before signing a big contract.",[139,4918,4919],{},"A small paid pilot is the best check. In a week you can see how the team communicates, keeps deadlines and reacts to feedback.",[152,4921,1975],{"id":1974},[139,4923,4924],{},"A good contractor is not afraid of any of these 12 questions, because they already have answers: about artifacts, price, code, testing, contract and support. Bad answers do not always mean dishonesty, but they do mean a risk that you should consciously accept or remove before signing the contract.",[457,4926,4927],{},[139,4928,4929,680,4932,4934],{},[166,4930,4931],{},"Want us to answer these 12 questions about your project?",[146,4933,355],{"href":354},", and within 1 business day we will send an estimate with a breakdown by stage, a timeline and a list of what is not included in the price.",{"title":14,"searchDepth":688,"depth":689,"links":4936},[4937,4938,4944,4949,4954,4958,4959,4960,4961],{"id":4511,"depth":688,"text":4512},{"id":4550,"depth":688,"text":4551,"children":4939},[4940,4941,4942,4943],{"id":4554,"depth":689,"text":4555},{"id":4561,"depth":689,"text":4562},{"id":4574,"depth":689,"text":4575},{"id":4584,"depth":689,"text":4585},{"id":4591,"depth":688,"text":4592,"children":4945},[4946,4947,4948],{"id":4595,"depth":689,"text":4596},{"id":4605,"depth":689,"text":4606},{"id":4618,"depth":689,"text":4619},{"id":4625,"depth":688,"text":4626,"children":4950},[4951,4952,4953],{"id":4629,"depth":689,"text":4630},{"id":4645,"depth":689,"text":4646},{"id":4654,"depth":689,"text":4655},{"id":4672,"depth":688,"text":4673,"children":4955},[4956,4957],{"id":4676,"depth":689,"text":4677},{"id":4689,"depth":689,"text":4690},{"id":4727,"depth":688,"text":4728},{"id":4772,"depth":688,"text":4773},{"id":4877,"depth":688,"text":4878},{"id":1974,"depth":688,"text":1975},"vendor.checklist",[4964,4966,4968,4970],{"marker":716,"text":4965},"code and access belong to you",{"marker":716,"text":4967},"payment milestones tied to acceptance",{"marker":713,"text":4969},"\"price depends\" with no range",{"marker":2023,"text":4971},"no testing before release","12\u002F12",[4974,4977,4980,4983],{"q":4975,"a":4976},"How do I choose a contractor for website development?","Compare not only the price but also the result, the process, access, testing and support. Ask for an estimate broken down by stages and for the terms of handing over code and access. Ask every contractor the same questions and compare the answers in writing. That keeps the comparison objective rather than based on impressions.",{"q":4978,"a":4979},"What red flags should I watch for when talking to a contractor?","A price with no breakdown and no discussion of requirements, time pressure, no questions for you, a portfolio with no explanation, and a refusal to describe their approach. A quote half the size of the others is also a warning: most likely testing, design or a time buffer has been cut.",{"q":4981,"a":4982},"Should I pick the cheapest website development offer?","Usually not. A low price often means testing, support or some features are not included and will be billed separately later. Compare the scope of work: two offers with the same total can contain very different things. This helps you avoid overpaying for fixes later.",{"q":4984,"a":4985},"How much does website development cost at Tochno?","A landing page from UAH 25,000 (2–3 weeks), a corporate website from UAH 60,000 (5–7 weeks), an online store from UAH 150,000 (10–14 weeks), an MVP from $5,000. Website support starts from UAH 8,000 per month. We calculate the exact amount after a brief.","yak-obraty-pidriadnyka-na-rozrobku-saitu",{},{"title":662,"description":4505},"How to choose a web development contractor for a site or online store: 12 questions on code, contracts, testing and price, plus red flags to watch for.","How to Choose a Web Development Contractor — Tochno.","how-to-choose-web-development-contractor","en\u002Fblog\u002Fhow-to-choose-web-development-contractor",[746],"ByvT-5aQPmYNkyFlE-NlG9cDOv0qTf7wlrMb5Rmr054",{"id":4996,"title":4997,"author":134,"body":4998,"cover":704,"coverFile":5519,"coverLines":5520,"coverStat":5529,"date":719,"description":5002,"excerpt":704,"extension":720,"faq":5530,"featured":734,"key":5543,"meta":5544,"navigation":737,"path":218,"readingTime":1283,"seo":5545,"seoDescription":5546,"seoTitle":5547,"slug":5548,"stem":5549,"tags":5550,"updated":747,"__hash__":5552},"blog_en\u002Fen\u002Fblog\u002Flaravel-vs-opencart-wordpress.md","Laravel vs OpenCart and WordPress for Ecommerce: An Honest Comparison",{"type":136,"value":4999,"toc":5502},[5000,5003,5006,5010,5013,5032,5035,5039,5149,5152,5156,5159,5165,5171,5176,5182,5185,5189,5192,5196,5199,5213,5216,5220,5225,5228,5231,5234,5238,5241,5258,5261,5265,5268,5308,5312,5315,5348,5358,5362,5365,5397,5400,5426,5430,5433,5441,5470,5473,5475,5478,5481,5487],[139,5001,5002],{},"An online store on Laravel beats OpenCart or WordPress only when you have non-standard business logic, complex integrations or plans for fast growth. For a small catalog with a standard cart, a ready-made CMS is cheaper and faster to launch, and saying otherwise would be dishonest. Below we compare the platforms by the criteria that affect an owner's money: total cost of ownership, speed, security, flexibility and risk.",[139,5004,5005],{},"We work with Laravel and Nuxt, so we have an obvious bias. To compensate, we say plainly where our choice loses. The author, Eugene, has written PHP\u002FLaravel for 11 years and has handled store migrations from both camps: to Laravel and away from it.",[152,5007,5009],{"id":5008},"what-exactly-are-we-comparing","What exactly are we comparing",[139,5011,5012],{},"First, a clarification of terms, because they are often mixed up.",[160,5014,5015,5020,5026],{},[163,5016,5017,5019],{},[166,5018,862],{}," is a ready-made PHP platform built specifically for online stores: catalog, cart, orders, and payment and delivery modules out of the box.",[163,5021,5022,5025],{},[166,5023,5024],{},"WordPress + WooCommerce"," is a content management system with a store plugin. It is strong at content and blogging, while commerce runs through a plugin.",[163,5027,5028,5031],{},[166,5029,5030],{},"Laravel"," is not a CMS but a PHP framework. A store on it is written as an application for your processes: you get a skeleton (routes, database, queues, authentication, tests) and build everything else yourself or plug in proven packages.",[139,5033,5034],{},"In other words, we are comparing a finished product with a framework you build on. These are different philosophies: \"configure what exists\" versus \"build what you need\".",[152,5036,5038],{"id":5037},"quick-overview-in-a-table","Quick overview in a table",[243,5040,5041,5055],{},[246,5042,5043],{},[249,5044,5045,5047,5049,5052],{},[252,5046,853],{},[252,5048,862],{},[252,5050,5051],{},"WooCommerce",[252,5053,5054],{},"Laravel (custom)",[262,5056,5057,5070,5083,5096,5110,5123,5136],{},[249,5058,5059,5061,5064,5067],{},[267,5060,872],{},[267,5062,5063],{},"Fast",[267,5065,5066],{},"Very fast",[267,5068,5069],{},"Slower",[249,5071,5072,5075,5078,5080],{},[267,5073,5074],{},"Initial budget",[267,5076,5077],{},"Low",[267,5079,5077],{},[267,5081,5082],{},"Higher",[249,5084,5085,5088,5090,5093],{},[267,5086,5087],{},"Logic flexibility",[267,5089,962],{},[267,5091,5092],{},"Via plugins",[267,5094,5095],{},"Unlimited",[249,5097,5098,5101,5104,5107],{},[267,5099,5100],{},"Default performance",[267,5102,5103],{},"Average",[267,5105,5106],{},"Depends on plugins",[267,5108,5109],{},"You control it",[249,5111,5112,5115,5118,5120],{},[267,5113,5114],{},"Security",[267,5116,5117],{},"Depends on modules",[267,5119,5106],{},[267,5121,5122],{},"Your responsibility, but smaller attack surface",[249,5124,5125,5128,5131,5133],{},[267,5126,5127],{},"Tests and CI",[267,5129,5130],{},"Rare",[267,5132,5130],{},[267,5134,5135],{},"Standard practice",[249,5137,5138,5141,5144,5146],{},[267,5139,5140],{},"Cost of changes after 2 years",[267,5142,5143],{},"Grows",[267,5145,5143],{},[267,5147,5148],{},"More stable",[139,5150,5151],{},"The table is simplified: a good developer will build a decent store on any platform, and a bad one will ruin any of them. But it conveys the trends.",[152,5153,5155],{"id":5154},"where-a-ready-made-cms-wins","Where a ready-made CMS wins",[139,5157,5158],{},"Let's start with the honest part: in many cases OpenCart or WooCommerce is the right choice.",[139,5160,5161,5164],{},[166,5162,5163],{},"Fast start and a low barrier."," A ready theme, payment modules, a configured catalog: a first store can launch in a few weeks for little money. For testing the hypothesis \"will people buy this product\" it is ideal.",[139,5166,5167,5170],{},[166,5168,5169],{},"Ecosystem and community."," Many standard tasks (discounts, coupons, Nova Poshta integration, Google Merchant export) are already solved. There is documentation and thousands of developers on the market, so finding a contractor is easy.",[139,5172,5173,5175],{},[166,5174,497],{}," WordPress is traditionally strong at content management: blog, articles, landing pages, editor. If the store is only part of a content site, WooCommerce is a logical choice.",[139,5177,5178,5181],{},[166,5179,5180],{},"A small catalog with standard processes."," If you have 100–300 products, one delivery method, one payment provider and a standard cart, custom is unlikely to pay off.",[139,5183,5184],{},"So our first rule: if the task fits the standard, do not overcomplicate it. We genuinely sometimes recommend a ready-made solution to clients.",[152,5186,5188],{"id":5187},"where-a-ready-made-cms-starts-to-cost-more","Where a ready-made CMS starts to cost more",[139,5190,5191],{},"The problems of ready-made platforms are rarely visible at the start. They appear a year or two later.",[346,5193,5195],{"id":5194},"modules-from-different-authors","Modules from different authors",[139,5197,5198],{},"A store on OpenCart or WooCommerce is assembled from a dozen modules: payment, delivery, filters, SEO, a loyalty program. Each has its own author, its own update pace and its own code quality. Typical consequences:",[160,5200,5201,5204,5207,5210],{},[163,5202,5203],{},"A core update breaks a module, and the module's author no longer responds.",[163,5205,5206],{},"Two modules conflict because both change the same part of the system.",[163,5208,5209],{},"A module works but makes extra database queries, and the catalog page slows down.",[163,5211,5212],{},"To fix something you have to dig into someone else's code, and the next update wipes your patch.",[139,5214,5215],{},"For a site that gets 10 orders a day these are minor things. For a store where every minute of downtime is money, it is a real risk.",[346,5217,5219],{"id":5218},"performance","Performance",[139,5221,5222,5223,220],{},"A store's speed depends on how many database queries and scripts run on a page. Ready-made platforms ship with a lot of excess by default: dozens of plugins load their own styles and scripts on every page. It can be tuned, but it takes experience, and the result is fragile. Speed is also affected by the metrics that search engines see, which we covered in ",[146,5224,4278],{"href":1050},[139,5226,5227],{},"In a custom solution you control every query: eager loading versus N+1, caching, queues for heavy operations, a separate Nuxt frontend with server-side rendering. This does not make the site fast automatically, but it gives you the tools.",[346,5229,5114],{"id":5230},"security",[139,5232,5233],{},"The most popular platform is also the most frequent target. Most WordPress site compromises happen through outdated plugins and weak passwords, not through the core. Every extra plugin widens the attack surface. This does not mean Laravel is \"secure by itself\": code errors happen everywhere. The difference is that a custom project has less third-party code, and your code goes through code review.",[346,5235,5237],{"id":5236},"complex-business-logic","Complex business logic",[139,5239,5240],{},"This is what justifies custom work. Examples of logic that is hard to squeeze into a ready-made platform without crutches:",[160,5242,5243,5246,5249,5252,5255],{},[163,5244,5245],{},"prices depend on the customer group, order volume and contract (B2B);",[163,5247,5248],{},"stock is spread across several locations with different reservation rules;",[163,5250,5251],{},"an order goes through approval or consists of several shipments;",[163,5253,5254],{},"two-way real-time synchronization with an accounting system (1C or another);",[163,5256,5257],{},"bundles, sets and product dependencies with a non-standard price formula.",[139,5259,5260],{},"On a ready-made CMS this is implemented through a series of modules and patches. Every patch complicates updates, and together they turn into a \"fork\" that can no longer be updated. On Laravel this logic is simply part of your code, with tests.",[152,5262,5264],{"id":5263},"where-custom-laravel-wins","Where custom Laravel wins",[139,5266,5267],{},"Let's sum up the advantages of custom development.",[532,5269,5270,5276,5282,5290,5296,5302],{},[163,5271,5272,5275],{},[166,5273,5274],{},"The code belongs to you."," No dependence on third-party modules and licenses. The contract fixes the code rights, and you can change contractors.",[163,5277,5278,5281],{},[166,5279,5280],{},"Architecture built for you."," The data model describes your business, not an abstract store.",[163,5283,5284,5287,5288,220],{},[166,5285,5286],{},"Automated tests."," On Laravel it is normal practice to cover orders, prices, discounts and payment with tests. That lets you change code without fear of breaking something. For what is worth automating, read ",[146,5289,2709],{"href":1855},[163,5291,5292,5295],{},[166,5293,5294],{},"Predictable speed."," Profiling, caching, queues, a separate frontend: all under control.",[163,5297,5298,5301],{},[166,5299,5300],{},"Integrations without crutches."," APIs, webhooks, retry queues, idempotency: everything needed to work reliably with payment providers and delivery services.",[163,5303,5304,5307],{},[166,5305,5306],{},"Lower cost of change in the long run."," The first year is more expensive, but adding a new feature a year later does not mean fighting modules.",[346,5309,5311],{"id":5310},"nuances-nobody-mentions","Nuances nobody mentions",[139,5313,5314],{},"Custom is not free and not without trade-offs. There are downsides, and it is important to know them.",[160,5316,5317,5327,5336,5342],{},[163,5318,5319,5322,5323,5326],{},[166,5320,5321],{},"A higher entry cost."," What OpenCart already has (an admin panel, coupons, currency settings) has to be built or pulled in as packages. That is why our minimum budget for a ",[146,5324,5325],{"href":229},"custom store"," starts from UAH 150,000, while a template launch can be several times cheaper.",[163,5328,5329,5332,5333,220],{},[166,5330,5331],{},"Dependence on the team."," If the developer disappears and there is no documentation or tests, you are as stuck as with a ready-made CMS. That is why we require documentation, repository access and code review. It is part of our ",[146,5334,5335],{"href":4686},"process",[163,5337,5338,5341],{},[166,5339,5340],{},"A longer start."," Roughly 10–14 weeks versus 3–5 for a template solution.",[163,5343,5344,5347],{},[166,5345,5346],{},"Fewer ready-made \"features\"."," A blog, SEO plugins, a visual editor: on Laravel they have to be added separately. For the content part we usually use Nuxt with file-based content.",[457,5349,5350],{},[139,5351,5352,5355,5356,366],{},[166,5353,5354],{},"Not sure what you need?"," We will run a 90-minute tech lead consultation: we will look at your business process and honestly tell you whether you need custom or a ready-made CMS will do. Details are on the ",[146,5357,33],{"href":4669},[152,5359,5361],{"id":5360},"how-we-decide-five-questions","How we decide: five questions",[139,5363,5364],{},"Here are the questions we ask a client before recommending a platform.",[532,5366,5367,5373,5379,5385,5391],{},[163,5368,5369,5372],{},[166,5370,5371],{},"Does your pricing and order logic fit a standard cart?"," If yes, lean toward a ready-made solution.",[163,5374,5375,5378],{},[166,5376,5377],{},"How many integrations do you need, and how critical are they?"," One payment method and one delivery service is standard. Five systems with two-way sync is custom.",[163,5380,5381,5384],{},[166,5382,5383],{},"What will the catalog size and traffic be in a year or two?"," From a few thousand products and significant traffic, performance becomes a serious question.",[163,5386,5387,5390],{},[166,5388,5389],{},"Who will maintain the site?"," If the team already has a WordPress developer and the budget is limited, that is a weighty argument.",[163,5392,5393,5396],{},[166,5394,5395],{},"What does downtime or an error cost?"," The more a minute of unavailability costs, the more sense it makes to invest in tests, architecture and monitoring.",[139,5398,5399],{},"A rough summary of our recommendations:",[160,5401,5402,5408,5414,5420],{},[163,5403,5404,5407],{},[166,5405,5406],{},"Idea validation, a small catalog, standard processes:"," a ready-made CMS or a hosted builder.",[163,5409,5410,5413],{},[166,5411,5412],{},"A content project with a storefront:"," WordPress with WooCommerce.",[163,5415,5416,5419],{},[166,5417,5418],{},"A store with non-standard logic, B2B, many integrations or scaling plans:"," Laravel.",[163,5421,5422,5425],{},[166,5423,5424],{},"An existing OpenCart store that can no longer cope:"," an audit first, and only then a decision about migration.",[152,5427,5429],{"id":5428},"migration-when-you-outgrow-a-ready-made-cms","Migration: when you outgrow a ready-made CMS",[139,5431,5432],{},"The most common story in our experience: a store launched on OpenCart or WooCommerce, it grew, and the number of modules passed 30. Pages open slowly, every update means a week of testing, and a new feature is a patch on top of a patch. The owner decides to migrate.",[139,5434,5435,5436,5440],{},"It pays not to rush here. Before deciding on migration we recommend a ",[146,5437,5439],{"href":5438},"\u002Fen\u002Fservices\u002Fcode-audit","code audit",": sometimes the problem is solved by cleaning up modules, caching and query optimization, and a full migration is not needed. If the audit shows the architecture has run its course, the migration is planned in stages:",[160,5442,5443,5449,5455,5461],{},[163,5444,5445,5448],{},[166,5446,5447],{},"Data first."," The catalog, customers and order history are moved with integrity checks.",[163,5450,5451,5454],{},[166,5452,5453],{},"URLs and SEO."," Old addresses get 301 redirects, otherwise you lose search rankings.",[163,5456,5457,5460],{},[166,5458,5459],{},"Parallel run."," The new version is tested on a copy of real data before switching.",[163,5462,5463,5466,5467,5469],{},[166,5464,5465],{},"Testing before launch."," We check the buyer's journey from the ",[146,5468,584],{"href":583},", plus a rollback in case of problems.",[139,5471,5472],{},"Migration is always a risk, so it is planned for a low-traffic period, not on the eve of the sales season.",[152,5474,1975],{"id":1974},[139,5476,5477],{},"There is no \"best platform\" in general, only the platform that suits your task. A ready-made CMS gives a fast, cheap start but accumulates the cost of change. Laravel gives control and predictability but requires a bigger upfront investment and a stronger team.",[139,5479,5480],{},"A practical rule: compare not the cost of launch but the cost of ownership over 2–3 years. Include updates, support, speed, downtime risk and the cost of new features. The picture often shifts in favor of the platform you first rejected.",[139,5482,5483,5484,220],{},"In short: a standard storefront without special logic is OpenCart or WooCommerce; your business process dictating the code, rather than the other way round, is Laravel. If you hesitate, it is better to spend an hour on a consultation than months on a migration. We covered what this can cost in ",[146,5485,5486],{"href":738},"How Much Does an Online Store Cost in 2026",[457,5488,5489],{},[139,5490,5491,5493,5494,5496,5497,5501],{},[166,5492,1235],{}," Tell us about your project in the ",[146,5495,355],{"href":354}," or see how we approach ",[146,5498,5500],{"href":5499},"\u002Fen\u002Fservices\u002Flaravel-development","Laravel development",". We will give an honest recommendation, even if it is not Laravel.",{"title":14,"searchDepth":688,"depth":689,"links":5503},[5504,5505,5506,5507,5513,5516,5517,5518],{"id":5008,"depth":688,"text":5009},{"id":5037,"depth":688,"text":5038},{"id":5154,"depth":688,"text":5155},{"id":5187,"depth":688,"text":5188,"children":5508},[5509,5510,5511,5512],{"id":5194,"depth":689,"text":5195},{"id":5218,"depth":689,"text":5219},{"id":5230,"depth":689,"text":5114},{"id":5236,"depth":689,"text":5237},{"id":5263,"depth":688,"text":5264,"children":5514},[5515],{"id":5310,"depth":689,"text":5311},{"id":5360,"depth":688,"text":5361},{"id":5428,"depth":688,"text":5429},{"id":1974,"depth":688,"text":1975},"stack.compare",[5521,5523,5525,5527],{"marker":716,"text":5522},"WooCommerce: fast start",{"marker":713,"text":5524},"OpenCart: modules from many authors",{"marker":716,"text":5526},"Laravel: code and logic are yours",{"marker":708,"text":5528},"the choice depends on the task","3 platforms",[5531,5534,5537,5540],{"q":5532,"a":5533},"Which is better for an online store: Laravel or OpenCart?","For a typical store with a standard catalog and a modest budget, a ready-made CMS such as OpenCart is faster and cheaper to start with. Laravel wins where you need non-standard logic, deep integrations and scaling. The decision depends on your processes, not on fashion.",{"q":5535,"a":5536},"How much does a Laravel store cost compared with a ready-made CMS?","A custom store from us starts from UAH 150,000 and usually takes 10–14 weeks. A ready-made CMS is cheaper at the start, but costs can grow through paid modules, customizations and limitations. So compare total cost over several years, not the price of launch.",{"q":5538,"a":5539},"When should I move from OpenCart or WordPress to a custom solution?","When you pay for more and more modules, keep working around CMS limits, and every update breaks your customizations. Other signs are slow performance and fear of touching the code. Plan the migration in stages and keep page URLs and SEO rankings, so you do not lose traffic during the move.",{"q":5541,"a":5542},"Can I build an SEO-friendly store on Laravel?","Yes, but what a CMS gives you out of the box you must build yourself: meta tags, canonical URLs, a sitemap, clean URLs and speed. This is part of the estimate, so discuss it with the developer before the start and verify it before launch. The result will be no worse than a ready-made CMS, and often faster.","laravel-chy-opencart-wordpress-dlia-mahazynu",{},{"title":4997,"description":5002},"Laravel vs WordPress and OpenCart for ecommerce: cost, speed, security and flexibility compared. When a ready-made CMS wins and when custom pays off.","Laravel vs OpenCart and WordPress for Ecommerce — Tochno.","laravel-vs-opencart-wordpress","en\u002Fblog\u002Flaravel-vs-opencart-wordpress",[5551,746],"laravel","isy3FzpQxfmJlrmmkOS_cEOLnjWRCoSQ1Rht_uCrTmE",{"id":5554,"title":5555,"author":134,"body":5556,"cover":704,"coverFile":6287,"coverLines":6288,"coverStat":6297,"date":719,"description":5560,"excerpt":704,"extension":720,"faq":6298,"featured":734,"key":6311,"meta":6312,"navigation":737,"path":1855,"readingTime":739,"seo":6313,"seoDescription":6314,"seoTitle":6315,"slug":6316,"stem":6317,"tags":6318,"updated":3579,"__hash__":6319},"blog_en\u002Fen\u002Fblog\u002Fmanual-vs-automated-testing.md","Manual vs Automated Testing: What a Small Business Needs",{"type":136,"value":5557,"toc":6266},[5558,5561,5564,5568,5574,5580,5670,5673,5677,5680,5712,5719,5727,5731,5738,5741,5764,5773,6029,6035,6038,6042,6045,6058,6061,6071,6075,6078,6082,6089,6093,6099,6103,6113,6117,6125,6129,6132,6158,6164,6168,6171,6175,6178,6182,6185,6189,6192,6196,6199,6202,6205,6209,6241,6247,6249,6252,6263],[139,5559,5560],{},"A small business usually does not have to choose between manual and automated testing: it needs the right sequence. First, manual testing of new and unstable functionality; then automated tests for the few critical flows that repeat before every release: registration, cart, checkout, payment. That combination covers the main risks for moderate money.",[139,5562,5563],{},"Below we break down the difference, what each approach catches, what it costs in orders of magnitude and how to start without unnecessary spending. We work with both approaches, so we will not claim that \"automated tests solve everything\" or that \"manual is enough\".",[152,5565,5567],{"id":5566},"the-difference-between-manual-and-automated-testing","The difference between manual and automated testing",[139,5569,5570,5573],{},[166,5571,5572],{},"Manual testing"," is when a person opens the site and walks through scenarios through the user's eyes: adds a product to the cart, places an order, checks on different devices, looks for unusual situations. The result is bugs found, with descriptions, screenshots or video.",[139,5575,5576,5579],{},[166,5577,5578],{},"Automated testing"," is when the same scenarios are written as code and a program runs them itself. For example, a Playwright script opens a browser, adds a product, goes through checkout and verifies that the order was created. It runs in a few minutes and can be repeated as many times as you like.",[243,5581,5582,5594],{},[246,5583,5584],{},[249,5585,5586,5588,5591],{},[252,5587,853],{},[252,5589,5590],{},"Manual",[252,5592,5593],{},"Automated",[262,5595,5596,5606,5617,5628,5639,5648,5659],{},[249,5597,5598,5600,5603],{},[267,5599,872],{},[267,5601,5602],{},"Fast, no preparation",[267,5604,5605],{},"Needs time to write and set up",[249,5607,5608,5611,5614],{},[267,5609,5610],{},"Cost of one run",[267,5612,5613],{},"Hours are paid for every time",[267,5615,5616],{},"Almost zero once created",[249,5618,5619,5622,5625],{},[267,5620,5621],{},"Finding new, unexpected problems",[267,5623,5624],{},"Strong side: a person notices the odd",[267,5626,5627],{},"Weak: it checks only what was built in",[249,5629,5630,5633,5636],{},[267,5631,5632],{},"Repeated scenarios (regression)",[267,5634,5635],{},"Boring and expensive, people make mistakes",[267,5637,5638],{},"Strong side: stable and fast",[249,5640,5641,5644,5646],{},[267,5642,5643],{},"Assessing usability, design, content",[267,5645,924],{},[267,5647,1007],{},[249,5649,5650,5653,5656],{},[267,5651,5652],{},"Cross-browser and devices",[267,5654,5655],{},"Real devices, but slow",[267,5657,5658],{},"Fast in several browsers, but emulation only",[249,5660,5661,5664,5667],{},[267,5662,5663],{},"Maintenance",[267,5665,5666],{},"Not needed",[267,5668,5669],{},"Tests break when the interface changes",[139,5671,5672],{},"Hence the simple logic: a person looks for the new and odd, a machine protects against the repetition of the old.",[152,5674,5676],{"id":5675},"what-exactly-manual-testing-catches","What exactly manual testing catches",[139,5678,5679],{},"A person can see what nobody foresaw in the requirements. In practice manual testing reveals:",[160,5681,5682,5688,5694,5700,5706],{},[163,5683,5684,5687],{},[166,5685,5686],{},"Logic errors:"," a discount applied twice, delivery calculated incorrectly for a particular city.",[163,5689,5690,5693],{},[166,5691,5692],{},"Usability problems:"," a button under the keyboard on mobile, an unclear error message, a form that clears after failed validation.",[163,5695,5696,5699],{},[166,5697,5698],{},"Visual defects:"," overlapping text, stretched images, the wrong font.",[163,5701,5702,5705],{},[166,5703,5704],{},"Unusual paths:"," going \"back\" after payment, opening the cart in two tabs, changing the quantity to zero.",[163,5707,5708,5711],{},[166,5709,5710],{},"Real devices:"," an old Android phone, Safari on iPhone, a slow network.",[139,5713,5714,5715,5718],{},"A typical manual check is a checklist of dozens of items. We showed what it looks like for a store in the ",[146,5716,5717],{"href":583},"ecommerce testing checklist for pre-launch QA",". If you do not know where to start, the best first step is to run a full manual test once and record the result as test cases. They can be automated later.",[139,5720,5721,5722,5724,5725,2659],{},"Manual testing is needed before launching a new site, after big changes, and when adding complex functionality (payment, delivery integration, a customer account). We offer this as ",[146,5723,1338],{"href":1337}," from $20 per hour; for a store there is a separate ",[146,5726,30],{"href":1125},[152,5728,5730],{"id":5729},"what-exactly-automated-testing-catches","What exactly automated testing catches",[139,5732,5733,5734,5737],{},"Automated tests work best where a scenario is stable and repeats often. This is classic ",[166,5735,5736],{},"regression",": after every code change you need to be sure that the old things did not break.",[139,5739,5740],{},"Candidates for automation in a store:",[532,5742,5743,5746,5749,5752,5755,5758,5761],{},[163,5744,5745],{},"User registration and login.",[163,5747,5748],{},"Adding a product to the cart, changing the quantity, removing it.",[163,5750,5751],{},"Checkout as a guest and as a registered customer.",[163,5753,5754],{},"Applying a promo code.",[163,5756,5757],{},"Payment in the payment provider's test mode.",[163,5759,5760],{},"Catalog search and filters (basic cases).",[163,5762,5763],{},"Lead and contact forms.",[139,5765,5766,5767,5772],{},"An example of a simple ",[146,5768,5771],{"href":5769,"rel":5770},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Fintro",[2924],"Playwright"," test:",[2139,5774,5778],{"className":5775,"code":5776,"language":5777,"meta":14,"style":14},"language-ts shiki shiki-themes github-dark-default","import { test, expect } from '@playwright\u002Ftest'\n\ntest('a guest can place an order', async ({ page }) => {\n  await page.goto('\u002Fcatalog\u002Frunning-sneakers')\n  await page.getByRole('button', { name: 'Add to cart' }).click()\n  await page.goto('\u002Fcart')\n  await page.getByRole('link', { name: 'Checkout' }).click()\n\n  await page.getByLabel('Phone').fill('+380501234567')\n  await page.getByLabel('Name').fill('Test Tester')\n  await page.getByRole('button', { name: 'Confirm' }).click()\n\n  await expect(page.getByText('Thank you for your order')).toBeVisible()\n})\n","ts",[1529,5779,5780,5794,5798,5829,5847,5876,5891,5915,5919,5945,5969,5992,5996,6023],{"__ignoreMap":14},[2147,5781,5782,5785,5788,5791],{"class":2149,"line":2150},[2147,5783,5784],{"class":2163},"import",[2147,5786,5787],{"class":2159}," { test, expect } ",[2147,5789,5790],{"class":2163},"from",[2147,5792,5793],{"class":2186}," '@playwright\u002Ftest'\n",[2147,5795,5796],{"class":2149,"line":688},[2147,5797,2225],{"emptyLinePlaceholder":737},[2147,5799,5800,5803,5805,5808,5810,5813,5816,5820,5823,5826],{"class":2149,"line":689},[2147,5801,5802],{"class":2174},"test",[2147,5804,2202],{"class":2159},[2147,5806,5807],{"class":2186},"'a guest can place an order'",[2147,5809,2641],{"class":2159},[2147,5811,5812],{"class":2163},"async",[2147,5814,5815],{"class":2159}," ({ ",[2147,5817,5819],{"class":5818},"sQhOw","page",[2147,5821,5822],{"class":2159}," }) ",[2147,5824,5825],{"class":2163},"=>",[2147,5827,5828],{"class":2159}," {\n",[2147,5830,5831,5834,5836,5839,5841,5844],{"class":2149,"line":2210},[2147,5832,5833],{"class":2163},"  await",[2147,5835,366],{"class":2159},[2147,5837,5838],{"class":2174},"goto",[2147,5840,2202],{"class":2159},[2147,5842,5843],{"class":2186},"'\u002Fcatalog\u002Frunning-sneakers'",[2147,5845,5846],{"class":2159},")\n",[2147,5848,5849,5851,5853,5856,5858,5861,5864,5867,5870,5873],{"class":2149,"line":2216},[2147,5850,5833],{"class":2163},[2147,5852,366],{"class":2159},[2147,5854,5855],{"class":2174},"getByRole",[2147,5857,2202],{"class":2159},[2147,5859,5860],{"class":2186},"'button'",[2147,5862,5863],{"class":2159},", { name: ",[2147,5865,5866],{"class":2186},"'Add to cart'",[2147,5868,5869],{"class":2159}," }).",[2147,5871,5872],{"class":2174},"click",[2147,5874,5875],{"class":2159},"()\n",[2147,5877,5878,5880,5882,5884,5886,5889],{"class":2149,"line":2222},[2147,5879,5833],{"class":2163},[2147,5881,366],{"class":2159},[2147,5883,5838],{"class":2174},[2147,5885,2202],{"class":2159},[2147,5887,5888],{"class":2186},"'\u002Fcart'",[2147,5890,5846],{"class":2159},[2147,5892,5893,5895,5897,5899,5901,5904,5906,5909,5911,5913],{"class":2149,"line":2228},[2147,5894,5833],{"class":2163},[2147,5896,366],{"class":2159},[2147,5898,5855],{"class":2174},[2147,5900,2202],{"class":2159},[2147,5902,5903],{"class":2186},"'link'",[2147,5905,5863],{"class":2159},[2147,5907,5908],{"class":2186},"'Checkout'",[2147,5910,5869],{"class":2159},[2147,5912,5872],{"class":2174},[2147,5914,5875],{"class":2159},[2147,5916,5917],{"class":2149,"line":2248},[2147,5918,2225],{"emptyLinePlaceholder":737},[2147,5920,5921,5923,5925,5928,5930,5933,5935,5938,5940,5943],{"class":2149,"line":2283},[2147,5922,5833],{"class":2163},[2147,5924,366],{"class":2159},[2147,5926,5927],{"class":2174},"getByLabel",[2147,5929,2202],{"class":2159},[2147,5931,5932],{"class":2186},"'Phone'",[2147,5934,2436],{"class":2159},[2147,5936,5937],{"class":2174},"fill",[2147,5939,2202],{"class":2159},[2147,5941,5942],{"class":2186},"'+380501234567'",[2147,5944,5846],{"class":2159},[2147,5946,5947,5949,5951,5953,5955,5958,5960,5962,5964,5967],{"class":2149,"line":1283},[2147,5948,5833],{"class":2163},[2147,5950,366],{"class":2159},[2147,5952,5927],{"class":2174},[2147,5954,2202],{"class":2159},[2147,5956,5957],{"class":2186},"'Name'",[2147,5959,2436],{"class":2159},[2147,5961,5937],{"class":2174},[2147,5963,2202],{"class":2159},[2147,5965,5966],{"class":2186},"'Test Tester'",[2147,5968,5846],{"class":2159},[2147,5970,5971,5973,5975,5977,5979,5981,5983,5986,5988,5990],{"class":2149,"line":739},[2147,5972,5833],{"class":2163},[2147,5974,366],{"class":2159},[2147,5976,5855],{"class":2174},[2147,5978,2202],{"class":2159},[2147,5980,5860],{"class":2186},[2147,5982,5863],{"class":2159},[2147,5984,5985],{"class":2186},"'Confirm'",[2147,5987,5869],{"class":2159},[2147,5989,5872],{"class":2174},[2147,5991,5875],{"class":2159},[2147,5993,5994],{"class":2149,"line":2809},[2147,5995,2225],{"emptyLinePlaceholder":737},[2147,5997,5999,6001,6004,6007,6010,6012,6015,6018,6021],{"class":2149,"line":5998},13,[2147,6000,5833],{"class":2163},[2147,6002,6003],{"class":2174}," expect",[2147,6005,6006],{"class":2159},"(page.",[2147,6008,6009],{"class":2174},"getByText",[2147,6011,2202],{"class":2159},[2147,6013,6014],{"class":2186},"'Thank you for your order'",[2147,6016,6017],{"class":2159},")).",[2147,6019,6020],{"class":2174},"toBeVisible",[2147,6022,5875],{"class":2159},[2147,6024,6026],{"class":2149,"line":6025},14,[2147,6027,6028],{"class":2159},"})\n",[139,6030,6031,6032,6034],{},"Such a test runs in a few seconds and starts automatically on every code update. If someone breaks the \"Confirm\" button, you find out before the release, not from a buyer's complaint. We have a separate ",[146,6033,2705],{"href":2704}," service: Playwright scenarios for key paths and a CI run, priced after an audit.",[139,6036,6037],{},"What automated tests do not do: they do not evaluate design, they do not \"feel\" usability, and they do not find problems nobody thought of. A test checks only what is built into it.",[152,6039,6041],{"id":6040},"what-it-costs-a-comparison-by-order-of-magnitude","What it costs: a comparison by order of magnitude",[139,6043,6044],{},"Exact sums depend on the project, so here are benchmarks from our price list and the logic of the costs.",[160,6046,6047,6052],{},[163,6048,6049,6051],{},[166,6050,5572],{}," is paid hourly: a QA engineer's rate is $20–25 per hour. The cost repeats with every release but scales flexibly: nothing needs to be built in advance. For a store, a one-off QA audit with a checklist of 150+ items starts from UAH 6,000.",[163,6053,6054,6057],{},[166,6055,6056],{},"Automation"," is a one-time investment in writing and setting up, plus a small amount of maintenance. The first scenarios are ready in about 1–2 weeks. After that, every run is almost free.",[139,6059,6060],{},"The payback rule for a small business looks like this: if you release updates rarely (once every few months), it is cheaper to test manually. If updates come weekly and each requires full regression, automated tests pay off quickly, because they save hours of manual work every time. Do not estimate the break-even point by eye: count how many hours your manual regression takes and multiply by the number of releases per year.",[457,6062,6063],{},[139,6064,6065,6068,6069,366],{},[166,6066,6067],{},"Not sure where to start?"," We will run a free express check of your site within 48 hours and tell you where manual checking is enough and where it is time for automated tests. Details are on the ",[146,6070,1338],{"href":1337},[152,6072,6074],{"id":6073},"how-to-decide-what-you-need","How to decide what you need",[139,6076,6077],{},"Instead of abstract advice, use a simple decision scheme by situation.",[346,6079,6081],{"id":6080},"you-are-launching-a-new-site-or-store","You are launching a new site or store",[139,6083,6084,6085,6088],{},"You need ",[166,6086,6087],{},"manual testing"," before launch. This stage has the most new code and the most surprises. Check the full checkout, the mobile version, forms, emails, integrations. Automated tests are premature at this stage: the interface is still changing and the tests would have to be rewritten.",[346,6090,6092],{"id":6091},"the-site-works-and-updates-are-infrequent","The site works and updates are infrequent",[139,6094,6095,6098],{},[166,6096,6097],{},"Manual regression"," before every release against a ready checklist. Pick the 20–30 most important items (checkout, payment, forms) and run them after changes. It is cheap and reliable.",[346,6100,6102],{"id":6101},"updates-are-frequent-and-checkout-errors-have-already-cost-orders","Updates are frequent and checkout errors have already cost orders",[139,6104,6105,6106,6109,6110,220],{},"Time for ",[166,6107,6108],{},"automated tests"," of the critical paths. Start with three to five scenarios that directly bring in money: cart, checkout, payment. We covered the typical bugs in these places in ",[146,6111,6112],{"href":1970},"Why Orders Get Lost at Checkout",[346,6114,6116],{"id":6115},"development-is-continuous-and-there-is-a-team","Development is continuous and there is a team",[139,6118,6119,6120,6124],{},"Combine both approaches: automated tests in CI on every pull request, manual testing of new features before release. A dedicated QA on the team (part-time or full-time) covers this without hiring a staff employee; details are in the ",[146,6121,6123],{"href":6122},"\u002Fen\u002Fservices\u002Fdedicated-developer","dedicated developer"," format.",[152,6126,6128],{"id":6127},"where-to-start-a-one-month-plan","Where to start: a one-month plan",[139,6130,6131],{},"A realistic scenario for a small store with no testing at all:",[532,6133,6134,6140,6146,6152],{},[163,6135,6136,6139],{},[166,6137,6138],{},"Week 1."," A manual QA audit: we go through the whole site against the checklist and record bugs with video and priorities. The output is a list of problems and a set of test cases.",[163,6141,6142,6145],{},[166,6143,6144],{},"Week 2."," Fixing critical and serious bugs. Re-checking the fixes.",[163,6147,6148,6151],{},[166,6149,6150],{},"Week 3."," We pick the 3–5 most important test cases to automate: the ones that touch money. We write the Playwright scenarios.",[163,6153,6154,6157],{},[166,6155,6156],{},"Week 4."," We connect the automated test run to the deployment process (CI). We agree on a rule: a release does not go out if the automated tests are red.",[139,6159,6160,6161,6163],{},"After that, manual testing handles new functionality and odd cases, and automated tests guard what has been verified. Monthly regression can be handed to ",[146,6162,508],{"href":507}," from UAH 8,000 per month, where regression before releases is part of the plan.",[152,6165,6167],{"id":6166},"what-it-takes-for-automated-tests-to-really-work","What it takes for automated tests to really work",[139,6169,6170],{},"Automated tests are not \"install and forget\". They need three things, without which they quickly turn into a source of noise.",[346,6172,6174],{"id":6173},"a-stable-test-environment","A stable test environment",[139,6176,6177],{},"Tests must run on a separate staging environment with test data, not on the live store. For payment the provider's test mode is used, for Nova Poshta a test key or a stub. This lets you create orders safely and not confuse them with real ones. The database is reset to a known state before a run: otherwise a test that passed yesterday fails today because of someone else's data.",[346,6179,6181],{"id":6180},"resilient-selectors-and-scenarios","Resilient selectors and scenarios",[139,6183,6184],{},"A test tied to the markup (\"the third div in the second block\") breaks with any design tweak. It is more reliable to find elements by role and label (the \"Confirm\" button, the \"Phone\" field), as in the example above, or by dedicated test attributes. Then you can change a color or spacing without repairing tests.",[346,6186,6188],{"id":6187},"a-ci-run-and-a-reaction-rule","A CI run and a reaction rule",[139,6190,6191],{},"An automated test only makes sense when it runs automatically and someone reacts to a red result. A typical setup: tests start on every pull request and before a release, and a report with video and a failure trace is available to the whole team. Agree on a simple rule: if the tests are red, the release does not go out until the cause is found. A failure can be a real bug or an outdated test, but it cannot be ignored.",[152,6193,6195],{"id":6194},"an-example-payback-calculation","An example payback calculation",[139,6197,6198],{},"Take a hypothetical example to show the logic, not concrete figures for your business. Suppose a full manual regression of key scenarios takes 3 hours, and updates go out once a week. Over a year that is about 50 runs of 3 hours, or 150 hours. At a QA engineer's rate of $20–25 per hour, that is thousands of dollars a year spent only on repeating the same check.",[139,6200,6201],{},"Automating the same scenarios is a one-time cost of 1–2 weeks of work and a small annual maintenance of the tests. An automated run takes minutes, and a person is needed only to analyze failures. In such an example automation pays off within the first or second quarter. If, however, releases are quarterly, four runs a year will not be enough to justify the cost, and it is cheaper to keep manual regression.",[139,6203,6204],{},"An important caveat: this is an approximate model. Real figures depend on the complexity of scenarios, the stability of the interface and the number of integrations. That is why we always start with an estimate on the specific project, not with universal promises.",[152,6206,6208],{"id":6207},"typical-mistakes-of-small-businesses","Typical mistakes of small businesses",[160,6210,6211,6217,6223,6229,6235],{},[163,6212,6213,6216],{},[166,6214,6215],{},"Automating everything at once."," A big set of brittle tests is a burden: they fail over trifles and people stop listening to them. Better few but stable.",[163,6218,6219,6222],{},[166,6220,6221],{},"Automating an unstable interface."," If the design changes weekly, the tests constantly break.",[163,6224,6225,6228],{},[166,6226,6227],{},"Relying only on the developer."," The code's author tests the way they designed it, not the way a buyer will behave.",[163,6230,6231,6234],{},[166,6232,6233],{},"Not recording results."," Without a report you cannot compare a release with the previous one or prove that what was fixed is really fixed.",[163,6236,6237,6240],{},[166,6238,6239],{},"Thinking testing is an expense, not insurance."," One missed bug in payment usually costs more than a month of regular regression. Calculate the exact sum for your store from the number of orders per day and the share that would complete checkout.",[139,6242,6243,6244,6246],{},"If you already see that the problems lie in the code itself, not in the scenarios, you may need a ",[146,6245,5439],{"href":3154}," rather than new tests: automated tests do not cure bad architecture, they only expose it faster.",[152,6248,1975],{"id":1974},[139,6250,6251],{},"Manual testing finds the new, the odd and the inconvenient; automated testing protects against the repetition of old mistakes. A small business is usually fine starting with manual testing and a checklist, then automating a few critical scenarios as releases become frequent. The main point is not to choose between the approaches but to split the roles: a person explores, a machine stands guard.",[457,6253,6254],{},[139,6255,6256,6259,6260,6262],{},[166,6257,6258],{},"Need a release without nasty surprises?"," Describe your project in the ",[146,6261,355],{"href":354}," and we will propose a testing scheme: what to check manually, what to automate and what it will cost.",[2768,6264,6265],{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":14,"searchDepth":688,"depth":689,"links":6267},[6268,6269,6270,6271,6272,6278,6279,6284,6285,6286],{"id":5566,"depth":688,"text":5567},{"id":5675,"depth":688,"text":5676},{"id":5729,"depth":688,"text":5730},{"id":6040,"depth":688,"text":6041},{"id":6073,"depth":688,"text":6074,"children":6273},[6274,6275,6276,6277],{"id":6080,"depth":689,"text":6081},{"id":6091,"depth":689,"text":6092},{"id":6101,"depth":689,"text":6102},{"id":6115,"depth":689,"text":6116},{"id":6127,"depth":688,"text":6128},{"id":6166,"depth":688,"text":6167,"children":6280},[6281,6282,6283],{"id":6173,"depth":689,"text":6174},{"id":6180,"depth":689,"text":6181},{"id":6187,"depth":689,"text":6188},{"id":6194,"depth":688,"text":6195},{"id":6207,"depth":688,"text":6208},{"id":1974,"depth":688,"text":1975},"checkout.spec.ts",[6289,6291,6293,6295],{"marker":716,"text":6290},"add product to cart",{"marker":716,"text":6292},"guest checkout",{"marker":713,"text":6294},"manual check: new feature",{"marker":708,"text":6296},"automated tests run on every release","1 min 48 s",[6299,6302,6305,6308],{"q":6300,"a":6301},"Which is cheaper: manual or automated testing?","Manual is cheaper at the start: you can order it as a one-off and by the hour, and our QA engineer rate is $20–25 per hour. Automated tests require investment in writing and maintenance, but pay off on repeated scenarios. A small business should usually start manual and automate only the stable core.",{"q":6303,"a":6304},"When does a small business need automated tests?","When the same checks are repeated after every release and manual regression takes too much time. A typical candidate is the purchase path in a store. If the product changes often, automated tests will keep breaking, so automate only stable scenarios. Otherwise you spend money on fixing the tests themselves rather than on business value.",{"q":6306,"a":6307},"Where should I start testing a site that has never been tested?","With a manual QA audit of critical scenarios: ordering, payment, forms and the mobile version. For a store it takes 3–5 days and starts from UAH 6,000. The audit shows which checks are worth automating and which to leave to a person. It gives you a base to build on.",{"q":6309,"a":6310},"Can automated tests fully replace a manual tester?","No. Automated tests catch regressions in known scenarios well, but they do not judge usability and do not find unexpected problems in new functionality. Manual and automated testing complement each other, and the right mix depends on the product and the budget.","ruchne-chy-avtomatyzovane-testuvannia",{},{"title":5555,"description":5560},"Manual vs automated testing for websites: the difference, what a small business should choose, where to start and when automated tests pay off.","Manual vs Automated Testing: What to Choose — Tochno.","manual-vs-automated-testing","en\u002Fblog\u002Fmanual-vs-automated-testing",[2054],"bZ--_-4Mu1TuUDSO1ezsa_QZbTRWHjIaoOc9o64om9g",{"id":6321,"title":452,"author":134,"body":6322,"cover":704,"coverFile":7513,"coverLines":7514,"coverStat":7523,"date":719,"description":6326,"excerpt":704,"extension":720,"faq":7524,"featured":734,"key":7537,"meta":7538,"navigation":737,"path":451,"readingTime":2809,"seo":7539,"seoDescription":7540,"seoTitle":7541,"slug":7542,"stem":7543,"tags":7544,"updated":747,"__hash__":7545},"blog_en\u002Fen\u002Fblog\u002Fnova-poshta-and-payment-integration.md",{"type":136,"value":6323,"toc":7491},[6324,6327,6330,6334,6337,6340,6346,6350,6354,6357,6360,6379,6383,6386,6389,6392,6396,6407,6410,6414,6418,6425,6428,6445,6448,6823,6830,6834,6837,6848,6851,6855,6858,6861,6876,6879,6883,6887,6894,6901,6905,6908,6911,7263,7270,7274,7281,7284,7329,7332,7336,7339,7342,7354,7358,7361,7405,7414,7424,7428,7431,7445,7448,7452,7455,7472,7478,7488],[139,6325,6326],{},"Nova Poshta and online payment integrations break not where you expect. The API call itself usually works the first time. Problems start at the boundaries: when a branch disappears from the directory, a payment callback arrives twice, and a waybill is created for the wrong weight. This article is for store owners and Laravel developers: the 10 most common places where things break, and how we check them.",[139,6328,6329],{},"Everything below is based on the practice of Laravel projects with Nova Poshta delivery and payment through LiqPay or similar services. Check the exact field names against the provider's current documentation: APIs change, principles do not.",[152,6331,6333],{"id":6332},"why-these-integrations-cause-the-most-trouble","Why these integrations cause the most trouble",[139,6335,6336],{},"Delivery and payment live outside your code. You do not control their availability, response format or reaction time. On top of that, both integrations concern money: an error in how a banner is displayed costs nothing, while an error in payment costs an order and the customer's trust.",[139,6338,6339],{},"There is another reason. These integrations are rarely tested along the full path. A developer checks the \"happy path\" in the sandbox, the client clicks once in test mode, and everything looks fine. Real customers do something else: they close the tab during 3-D Secure, choose a parcel locker for a 30 kg parcel, type a phone number with spaces.",[139,6341,6342,6343,6345],{},"If you are ordering ",[146,6344,230],{"href":229},", ask the contractor exactly how they check these scenarios. The answer \"we tested in the sandbox\" is not enough.",[152,6347,6349],{"id":6348},"nova-poshta-directories-and-branch-selection","Nova Poshta: directories and branch selection",[346,6351,6353],{"id":6352},"_1-outdated-city-and-branch-directories","1. Outdated city and branch directories",[139,6355,6356],{},"A typical mistake: a developer loads the list of branches into the database once and forgets about it. Branches open, close, move and temporarily stop working. A customer chooses a branch that no longer exists, and the waybill is not created.",[139,6358,6359],{},"What to do:",[160,6361,6362,6369,6376],{},[163,6363,6364,6365,6368],{},"Refresh the directory on a schedule (",[1529,6366,6367],{},"php artisan schedule","), not by hand. For a small store once a day at night is enough.",[163,6370,6371,6372,6375],{},"Store in the order not only ",[1529,6373,6374],{},"warehouse_ref"," but also a text snapshot: city, branch number, address. A month later the branch may change, and the manager needs to see what the customer chose.",[163,6377,6378],{},"Mark branches that disappeared from the directory instead of deleting them. Otherwise old orders lose their link.",[346,6380,6382],{"id":6381},"_2-weight-and-dimension-limits","2. Weight and dimension limits",[139,6384,6385],{},"Not every branch accepts cargo of any weight. Parcel lockers have strict limits on parcel size and weight, and some branches do not accept oversized cargo. The branch directory has fields with maximum weight and dimensions, and they must be used.",[139,6387,6388],{},"A classic bug: there is a heavy product in the cart, checkout offers all branches, the customer picks a parcel locker, and the manager then calls and asks them to change the branch. That is lost time and some orders the customer will simply cancel.",[139,6390,6391],{},"Correct behavior: calculate the total cart weight, filter branches by the limits, and if none fits, show a clear message. For this, products must have weight and dimensions filled in. That is a requirement for content, not only for code.",[346,6393,6395],{"id":6394},"_3-city-search-and-settlements-with-the-same-name","3. City search and settlements with the same name",[139,6397,6398,6399,6402,6403,6406],{},"Ukraine has many settlements with identical names in different regions. A text search without specifying the region returns the wrong village. Use the settlement search method (",[1529,6400,6401],{},"Address\u002FsearchSettlements",") and store the ",[1529,6404,6405],{},"Ref",", not the name. In the dropdown show the region and district so the customer can see which \"Lisove\" they are choosing.",[139,6408,6409],{},"Separately check input with mistakes: \"Dnepr\" instead of \"Dnipro\", extra spaces, the apostrophe in \"Kam'ianske\" (there are three different apostrophe characters). Autocomplete has to survive this.",[152,6411,6413],{"id":6412},"creating-a-waybill-ttn","Creating a waybill (TTN)",[346,6415,6417],{"id":6416},"_4-validation-errors-when-creating-a-waybill","4. Validation errors when creating a waybill",[139,6419,6420,6421,6424],{},"A waybill is created with the ",[1529,6422,6423],{},"InternetDocument\u002Fsave"," method, and this is where the most \"small\" failures happen: the recipient's phone in the wrong format, a missing surname, a declared value of zero, a weight below the allowed minimum. If your code creates the waybill right at the moment of checkout, the customer sees an error they do not understand.",[139,6426,6427],{},"Our practice is this:",[532,6429,6430,6436,6439,6442],{},[163,6431,6432,6433,6124],{},"Normalize the phone on input: remove spaces, parentheses and hyphens, convert it to the ",[1529,6434,6435],{},"380XXXXXXXXX",[163,6437,6438],{},"Create the waybill not in the customer's HTTP request but in a background job (queue job).",[163,6440,6441],{},"If the API returned an error, store the error text in the order and show it to the manager in the admin panel. Do not swallow the exception.",[163,6443,6444],{},"Add a \"Create waybill again\" button for the manager.",[139,6446,6447],{},"An example of a job skeleton:",[2139,6449,6451],{"className":2141,"code":6450,"language":2143,"meta":14,"style":14},"class CreateShipmentJob implements ShouldQueue\n{\n    public int $tries = 5;\n    public array $backoff = [30, 120, 600, 1800];\n\n    public function __construct(public int $orderId) {}\n\n    public function handle(NovaPoshtaClient $np): void\n    {\n        $order = Order::findOrFail($this->orderId);\n\n        \u002F\u002F Idempotency: do not create a second waybill for the same order\n        if ($order->ttn_number) {\n            return;\n        }\n\n        $response = $np->createWaybill($order);\n\n        if (! $response['success']) {\n            $order->update(['shipment_error' => json_encode($response['errors'], JSON_UNESCAPED_UNICODE)]);\n            throw new ShipmentFailed($response['errors']);\n        }\n\n        $order->update([\n            'ttn_number' => $response['data'][0]['IntDocNumber'],\n            'shipment_error' => null,\n        ]);\n    }\n}\n",[1529,6452,6453,6467,6472,6491,6527,6531,6551,6555,6577,6582,6606,6610,6615,6628,6635,6641,6646,6665,6670,6687,6723,6742,6747,6752,6765,6792,6806,6812,6818],{"__ignoreMap":14},[2147,6454,6455,6458,6461,6464],{"class":2149,"line":2150},[2147,6456,6457],{"class":2163},"class",[2147,6459,6460],{"class":5818}," CreateShipmentJob",[2147,6462,6463],{"class":2163}," implements",[2147,6465,6466],{"class":2167}," ShouldQueue\n",[2147,6468,6469],{"class":2149,"line":688},[2147,6470,6471],{"class":2159},"{\n",[2147,6473,6474,6477,6480,6483,6485,6488],{"class":2149,"line":689},[2147,6475,6476],{"class":2163},"    public",[2147,6478,6479],{"class":2163}," int",[2147,6481,6482],{"class":2159}," $tries ",[2147,6484,2164],{"class":2163},[2147,6486,6487],{"class":2167}," 5",[2147,6489,6490],{"class":2159},";\n",[2147,6492,6493,6495,6498,6501,6503,6506,6509,6511,6514,6516,6519,6521,6524],{"class":2149,"line":2210},[2147,6494,6476],{"class":2163},[2147,6496,6497],{"class":2163}," array",[2147,6499,6500],{"class":2159}," $backoff ",[2147,6502,2164],{"class":2163},[2147,6504,6505],{"class":2159}," [",[2147,6507,6508],{"class":2167},"30",[2147,6510,2641],{"class":2159},[2147,6512,6513],{"class":2167},"120",[2147,6515,2641],{"class":2159},[2147,6517,6518],{"class":2167},"600",[2147,6520,2641],{"class":2159},[2147,6522,6523],{"class":2167},"1800",[2147,6525,6526],{"class":2159},"];\n",[2147,6528,6529],{"class":2149,"line":2216},[2147,6530,2225],{"emptyLinePlaceholder":737},[2147,6532,6533,6535,6538,6541,6543,6546,6548],{"class":2149,"line":2222},[2147,6534,6476],{"class":2163},[2147,6536,6537],{"class":2163}," function",[2147,6539,6540],{"class":2167}," __construct",[2147,6542,2202],{"class":2159},[2147,6544,6545],{"class":2163},"public",[2147,6547,6479],{"class":2163},[2147,6549,6550],{"class":2159}," $orderId) {}\n",[2147,6552,6553],{"class":2149,"line":2228},[2147,6554,2225],{"emptyLinePlaceholder":737},[2147,6556,6557,6559,6561,6564,6566,6569,6572,6574],{"class":2149,"line":2248},[2147,6558,6476],{"class":2163},[2147,6560,6537],{"class":2163},[2147,6562,6563],{"class":2174}," handle",[2147,6565,2202],{"class":2159},[2147,6567,6568],{"class":2167},"NovaPoshtaClient",[2147,6570,6571],{"class":2159}," $np)",[2147,6573,3269],{"class":2163},[2147,6575,6576],{"class":2163}," void\n",[2147,6578,6579],{"class":2149,"line":2283},[2147,6580,6581],{"class":2159},"    {\n",[2147,6583,6584,6587,6589,6591,6593,6596,6598,6601,6603],{"class":2149,"line":1283},[2147,6585,6586],{"class":2159},"        $order ",[2147,6588,2164],{"class":2163},[2147,6590,2168],{"class":2167},[2147,6592,2171],{"class":2163},[2147,6594,6595],{"class":2174},"findOrFail",[2147,6597,2202],{"class":2159},[2147,6599,6600],{"class":2167},"$this",[2147,6602,2196],{"class":2163},[2147,6604,6605],{"class":2159},"orderId);\n",[2147,6607,6608],{"class":2149,"line":739},[2147,6609,2225],{"emptyLinePlaceholder":737},[2147,6611,6612],{"class":2149,"line":2809},[2147,6613,6614],{"class":2153},"        \u002F\u002F Idempotency: do not create a second waybill for the same order\n",[2147,6616,6617,6620,6623,6625],{"class":2149,"line":5998},[2147,6618,6619],{"class":2163},"        if",[2147,6621,6622],{"class":2159}," ($order",[2147,6624,2196],{"class":2163},[2147,6626,6627],{"class":2159},"ttn_number) {\n",[2147,6629,6630,6633],{"class":2149,"line":6025},[2147,6631,6632],{"class":2163},"            return",[2147,6634,6490],{"class":2159},[2147,6636,6638],{"class":2149,"line":6637},15,[2147,6639,6640],{"class":2159},"        }\n",[2147,6642,6644],{"class":2149,"line":6643},16,[2147,6645,2225],{"emptyLinePlaceholder":737},[2147,6647,6649,6652,6654,6657,6659,6662],{"class":2149,"line":6648},17,[2147,6650,6651],{"class":2159},"        $response ",[2147,6653,2164],{"class":2163},[2147,6655,6656],{"class":2159}," $np",[2147,6658,2196],{"class":2163},[2147,6660,6661],{"class":2174},"createWaybill",[2147,6663,6664],{"class":2159},"($order);\n",[2147,6666,6668],{"class":2149,"line":6667},18,[2147,6669,2225],{"emptyLinePlaceholder":737},[2147,6671,6672,6674,6676,6678,6681,6684],{"class":2149,"line":4107},[2147,6673,6619],{"class":2163},[2147,6675,2234],{"class":2159},[2147,6677,2237],{"class":2163},[2147,6679,6680],{"class":2159}," $response[",[2147,6682,6683],{"class":2186},"'success'",[2147,6685,6686],{"class":2159},"]) {\n",[2147,6688,6690,6693,6695,6698,6700,6703,6705,6708,6711,6714,6717,6720],{"class":2149,"line":6689},20,[2147,6691,6692],{"class":2159},"            $order",[2147,6694,2196],{"class":2163},[2147,6696,6697],{"class":2174},"update",[2147,6699,2265],{"class":2159},[2147,6701,6702],{"class":2186},"'shipment_error'",[2147,6704,2190],{"class":2163},[2147,6706,6707],{"class":2167}," json_encode",[2147,6709,6710],{"class":2159},"($response[",[2147,6712,6713],{"class":2186},"'errors'",[2147,6715,6716],{"class":2159},"], ",[2147,6718,6719],{"class":2167},"JSON_UNESCAPED_UNICODE",[2147,6721,6722],{"class":2159},")]);\n",[2147,6724,6726,6729,6732,6735,6737,6739],{"class":2149,"line":6725},21,[2147,6727,6728],{"class":2163},"            throw",[2147,6730,6731],{"class":2163}," new",[2147,6733,6734],{"class":2167}," ShipmentFailed",[2147,6736,6710],{"class":2159},[2147,6738,6713],{"class":2186},[2147,6740,6741],{"class":2159},"]);\n",[2147,6743,6745],{"class":2149,"line":6744},22,[2147,6746,6640],{"class":2159},[2147,6748,6750],{"class":2149,"line":6749},23,[2147,6751,2225],{"emptyLinePlaceholder":737},[2147,6753,6755,6758,6760,6762],{"class":2149,"line":6754},24,[2147,6756,6757],{"class":2159},"        $order",[2147,6759,2196],{"class":2163},[2147,6761,6697],{"class":2174},[2147,6763,6764],{"class":2159},"([\n",[2147,6766,6768,6771,6773,6775,6778,6781,6784,6786,6789],{"class":2149,"line":6767},25,[2147,6769,6770],{"class":2186},"            'ttn_number'",[2147,6772,2190],{"class":2163},[2147,6774,6680],{"class":2159},[2147,6776,6777],{"class":2186},"'data'",[2147,6779,6780],{"class":2159},"][",[2147,6782,6783],{"class":2167},"0",[2147,6785,6780],{"class":2159},[2147,6787,6788],{"class":2186},"'IntDocNumber'",[2147,6790,6791],{"class":2159},"],\n",[2147,6793,6795,6798,6800,6803],{"class":2149,"line":6794},26,[2147,6796,6797],{"class":2186},"            'shipment_error'",[2147,6799,2190],{"class":2163},[2147,6801,6802],{"class":2167}," null",[2147,6804,6805],{"class":2159},",\n",[2147,6807,6809],{"class":2149,"line":6808},27,[2147,6810,6811],{"class":2159},"        ]);\n",[2147,6813,6815],{"class":2149,"line":6814},28,[2147,6816,6817],{"class":2159},"    }\n",[2147,6819,6821],{"class":2149,"line":6820},29,[2147,6822,2286],{"class":2159},[139,6824,6825,6826,6829],{},"The key here is not the syntax but the ",[1529,6827,6828],{},"ttn_number"," check at the start. Without it, a retry of the job will create a second waybill and you will pay for two.",[346,6831,6833],{"id":6832},"_5-cash-on-delivery-and-money-transfer-back","5. Cash on delivery and money transfer back",[139,6835,6836],{},"Cash on delivery adds complexity: the amount of the return money transfer must match the order amount minus the part already paid. Typical bugs:",[160,6838,6839,6842,6845],{},[163,6840,6841],{},"the customer paid partially (discount, promo code, bonuses), but the waybill was created with the full cash-on-delivery amount;",[163,6843,6844],{},"the amount is rounded differently in the store and in the waybill, and a discrepancy of cents appears that accounting then hunts for weeks;",[163,6846,6847],{},"for orders paid by card, cash on delivery should not be created at all, but because of a condition in the code it is created anyway.",[139,6849,6850],{},"Test combinations: full prepayment, partial prepayment, cash on delivery, promo code plus cash on delivery. This is ten minutes of manual checking that saves you from complaints.",[346,6852,6854],{"id":6853},"_6-api-unavailability-and-timeouts","6. API unavailability and timeouts",[139,6856,6857],{},"The Nova Poshta API can be slow, sometimes unavailable, and sometimes returns an error only for certain requests. If branch search at checkout goes straight to the API without a cache and without a wait-time limit, a slow response freezes the whole form.",[139,6859,6860],{},"The minimum worth having:",[160,6862,6863,6870,6873],{},[163,6864,6865,6866,6869],{},"an explicit HTTP client timeout (in Laravel ",[1529,6867,6868],{},"Http::timeout(8)->retry(2, 200)",");",[163,6871,6872],{},"a directory cache, so checkout does not depend on the API during search;",[163,6874,6875],{},"a fallback: if the directory is unavailable, allow entering the address manually and pass the order to a manager for clarification.",[139,6877,6878],{},"This is a business decision, not only a code one: what matters more, not losing the order or getting perfect data. For a store the order almost always comes first.",[152,6880,6882],{"id":6881},"online-payment-callbacks-statuses-duplicates","Online payment: callbacks, statuses, duplicates",[346,6884,6886],{"id":6885},"_7-trusting-the-return-page-instead-of-the-server-callback","7. Trusting the return page instead of the server callback",[139,6888,6889,6890,6893],{},"The most dangerous mistake in payment. After payment the customer returns to the \"Thank you for your order\" page (",[1529,6891,6892],{},"result_url","). If you mark the order as paid right there, you take a double risk: the customer can close the tab before returning and the payment will not be recorded, while an attacker can open this address manually and pretend to have paid.",[139,6895,6896,6897,6900],{},"The correct scheme: the order status is changed only by the payment system's server callback (",[1529,6898,6899],{},"server_url"," in LiqPay), and the return page only shows the current status and polls it if needed.",[346,6902,6904],{"id":6903},"_8-unverified-signature-and-repeated-callbacks","8. Unverified signature and repeated callbacks",[139,6906,6907],{},"The payment system sends a callback with a signature. If you do not verify it, anyone can send a POST request and mark an order as paid. In LiqPay the signature is calculated from the private key and the request data; the formula is in the documentation, and it must be implemented and covered by a test.",[139,6909,6910],{},"Payment systems also repeat callbacks if your server responded with anything other than 200 or did not respond in time. So the handler must be idempotent: a repeated callback with the same status must neither create a second order, nor deduct stock twice, nor send a second email.",[2139,6912,6914],{"className":2141,"code":6913,"language":2143,"meta":14,"style":14},"public function handle(Request $request): Response\n{\n    $data = $request->input('data');\n    $signature = $request->input('signature');\n\n    if (! hash_equals($this->sign($data), $signature)) {\n        abort(403);\n    }\n\n    $payload = json_decode(base64_decode($data), true);\n\n    DB::transaction(function () use ($payload) {\n        $order = Order::where('public_id', $payload['order_id'])\n            ->lockForUpdate()\n            ->firstOrFail();\n\n        \u002F\u002F Terminal state reached already: ignore repeated callbacks\n        if ($order->payment_status === 'paid') {\n            return;\n        }\n\n        if ($payload['status'] === 'success' && (float) $payload['amount'] === (float) $order->total) {\n            $order->markPaid($payload['payment_id']);\n        }\n    });\n\n    return response('OK');\n}\n",[1529,6915,6916,6937,6941,6960,6980,6984,7008,7020,7024,7028,7051,7055,7079,7105,7115,7125,7129,7134,7154,7160,7164,7168,7216,7233,7237,7242,7246,7259],{"__ignoreMap":14},[2147,6917,6918,6920,6922,6924,6926,6929,6932,6934],{"class":2149,"line":2150},[2147,6919,6545],{"class":2163},[2147,6921,6537],{"class":2163},[2147,6923,6563],{"class":2174},[2147,6925,2202],{"class":2159},[2147,6927,6928],{"class":2167},"Request",[2147,6930,6931],{"class":2159}," $request)",[2147,6933,3269],{"class":2163},[2147,6935,6936],{"class":2167}," Response\n",[2147,6938,6939],{"class":2149,"line":688},[2147,6940,6471],{"class":2159},[2147,6942,6943,6946,6948,6950,6952,6954,6956,6958],{"class":2149,"line":689},[2147,6944,6945],{"class":2159},"    $data ",[2147,6947,2164],{"class":2163},[2147,6949,2193],{"class":2159},[2147,6951,2196],{"class":2163},[2147,6953,2199],{"class":2174},[2147,6955,2202],{"class":2159},[2147,6957,6777],{"class":2186},[2147,6959,2219],{"class":2159},[2147,6961,6962,6965,6967,6969,6971,6973,6975,6978],{"class":2149,"line":2210},[2147,6963,6964],{"class":2159},"    $signature ",[2147,6966,2164],{"class":2163},[2147,6968,2193],{"class":2159},[2147,6970,2196],{"class":2163},[2147,6972,2199],{"class":2174},[2147,6974,2202],{"class":2159},[2147,6976,6977],{"class":2186},"'signature'",[2147,6979,2219],{"class":2159},[2147,6981,6982],{"class":2149,"line":2216},[2147,6983,2225],{"emptyLinePlaceholder":737},[2147,6985,6986,6989,6991,6993,6996,6998,7000,7002,7005],{"class":2149,"line":2222},[2147,6987,6988],{"class":2163},"    if",[2147,6990,2234],{"class":2159},[2147,6992,2237],{"class":2163},[2147,6994,6995],{"class":2167}," hash_equals",[2147,6997,2202],{"class":2159},[2147,6999,6600],{"class":2167},[2147,7001,2196],{"class":2163},[2147,7003,7004],{"class":2174},"sign",[2147,7006,7007],{"class":2159},"($data), $signature)) {\n",[2147,7009,7010,7013,7015,7018],{"class":2149,"line":2228},[2147,7011,7012],{"class":2174},"        abort",[2147,7014,2202],{"class":2159},[2147,7016,7017],{"class":2167},"403",[2147,7019,2219],{"class":2159},[2147,7021,7022],{"class":2149,"line":2248},[2147,7023,6817],{"class":2159},[2147,7025,7026],{"class":2149,"line":2283},[2147,7027,2225],{"emptyLinePlaceholder":737},[2147,7029,7030,7033,7035,7038,7040,7043,7046,7049],{"class":2149,"line":1283},[2147,7031,7032],{"class":2159},"    $payload ",[2147,7034,2164],{"class":2163},[2147,7036,7037],{"class":2167}," json_decode",[2147,7039,2202],{"class":2159},[2147,7041,7042],{"class":2167},"base64_decode",[2147,7044,7045],{"class":2159},"($data), ",[2147,7047,7048],{"class":2167},"true",[2147,7050,2219],{"class":2159},[2147,7052,7053],{"class":2149,"line":739},[2147,7054,2225],{"emptyLinePlaceholder":737},[2147,7056,7057,7060,7062,7065,7067,7070,7073,7076],{"class":2149,"line":2809},[2147,7058,7059],{"class":2167},"    DB",[2147,7061,2171],{"class":2163},[2147,7063,7064],{"class":2174},"transaction",[2147,7066,2202],{"class":2159},[2147,7068,7069],{"class":2163},"function",[2147,7071,7072],{"class":2159}," () ",[2147,7074,7075],{"class":2163},"use",[2147,7077,7078],{"class":2159}," ($payload) {\n",[2147,7080,7081,7083,7085,7087,7089,7092,7094,7097,7100,7102],{"class":2149,"line":5998},[2147,7082,6586],{"class":2159},[2147,7084,2164],{"class":2163},[2147,7086,2168],{"class":2167},[2147,7088,2171],{"class":2163},[2147,7090,7091],{"class":2174},"where",[2147,7093,2202],{"class":2159},[2147,7095,7096],{"class":2186},"'public_id'",[2147,7098,7099],{"class":2159},", $payload[",[2147,7101,2268],{"class":2186},[2147,7103,7104],{"class":2159},"])\n",[2147,7106,7107,7110,7113],{"class":2149,"line":6025},[2147,7108,7109],{"class":2163},"            ->",[2147,7111,7112],{"class":2174},"lockForUpdate",[2147,7114,5875],{"class":2159},[2147,7116,7117,7119,7122],{"class":2149,"line":6637},[2147,7118,7109],{"class":2163},[2147,7120,7121],{"class":2174},"firstOrFail",[2147,7123,7124],{"class":2159},"();\n",[2147,7126,7127],{"class":2149,"line":6643},[2147,7128,2225],{"emptyLinePlaceholder":737},[2147,7130,7131],{"class":2149,"line":6648},[2147,7132,7133],{"class":2153},"        \u002F\u002F Terminal state reached already: ignore repeated callbacks\n",[2147,7135,7136,7138,7140,7142,7145,7148,7151],{"class":2149,"line":6667},[2147,7137,6619],{"class":2163},[2147,7139,6622],{"class":2159},[2147,7141,2196],{"class":2163},[2147,7143,7144],{"class":2159},"payment_status ",[2147,7146,7147],{"class":2163},"===",[2147,7149,7150],{"class":2186}," 'paid'",[2147,7152,7153],{"class":2159},") {\n",[2147,7155,7156,7158],{"class":2149,"line":4107},[2147,7157,6632],{"class":2163},[2147,7159,6490],{"class":2159},[2147,7161,7162],{"class":2149,"line":6689},[2147,7163,6640],{"class":2159},[2147,7165,7166],{"class":2149,"line":6725},[2147,7167,2225],{"emptyLinePlaceholder":737},[2147,7169,7170,7172,7175,7178,7181,7183,7186,7189,7191,7194,7197,7200,7202,7204,7206,7208,7211,7213],{"class":2149,"line":6744},[2147,7171,6619],{"class":2163},[2147,7173,7174],{"class":2159}," ($payload[",[2147,7176,7177],{"class":2186},"'status'",[2147,7179,7180],{"class":2159},"] ",[2147,7182,7147],{"class":2163},[2147,7184,7185],{"class":2186}," 'success'",[2147,7187,7188],{"class":2163}," &&",[2147,7190,2234],{"class":2159},[2147,7192,7193],{"class":2163},"float",[2147,7195,7196],{"class":2159},") $payload[",[2147,7198,7199],{"class":2186},"'amount'",[2147,7201,7180],{"class":2159},[2147,7203,7147],{"class":2163},[2147,7205,2234],{"class":2159},[2147,7207,7193],{"class":2163},[2147,7209,7210],{"class":2159},") $order",[2147,7212,2196],{"class":2163},[2147,7214,7215],{"class":2159},"total) {\n",[2147,7217,7218,7220,7222,7225,7228,7231],{"class":2149,"line":6749},[2147,7219,6692],{"class":2159},[2147,7221,2196],{"class":2163},[2147,7223,7224],{"class":2174},"markPaid",[2147,7226,7227],{"class":2159},"($payload[",[2147,7229,7230],{"class":2186},"'payment_id'",[2147,7232,6741],{"class":2159},[2147,7234,7235],{"class":2149,"line":6754},[2147,7236,6640],{"class":2159},[2147,7238,7239],{"class":2149,"line":6767},[2147,7240,7241],{"class":2159},"    });\n",[2147,7243,7244],{"class":2149,"line":6794},[2147,7245,2225],{"emptyLinePlaceholder":737},[2147,7247,7248,7250,7252,7254,7257],{"class":2149,"line":6808},[2147,7249,2251],{"class":2163},[2147,7251,2254],{"class":2174},[2147,7253,2202],{"class":2159},[2147,7255,7256],{"class":2186},"'OK'",[2147,7258,2219],{"class":2159},[2147,7260,7261],{"class":2149,"line":6814},[2147,7262,2286],{"class":2159},[139,7264,7265,7266,7269],{},"Pay attention to two things: ",[1529,7267,7268],{},"lockForUpdate()"," stops two simultaneous callbacks from processing the order in parallel, and the amount comparison protects against tampering. The amount is rarely mentioned, and that is a mistake: a customer who changed parameters on the browser side should be refused.",[346,7271,7273],{"id":7272},"_9-unhandled-intermediate-and-failed-statuses","9. Unhandled intermediate and failed statuses",[139,7275,7276,7277,7280],{},"A payment has more states than \"success\" and \"error\". There are awaiting confirmation, 3-D Secure, processing, refund and bank decline. If the code only knows ",[1529,7278,7279],{},"success",", everything else hangs in limbo: the customer paid but the order is \"awaiting payment\", or the reverse.",[139,7282,7283],{},"For every status, make a decision and record it in a test:",[243,7285,7286,7296],{},[246,7287,7288],{},[249,7289,7290,7293],{},[252,7291,7292],{},"Payment status",[252,7294,7295],{},"What we do with the order",[262,7297,7298,7305,7313,7321],{},[249,7299,7300,7302],{},[267,7301,7279],{},[267,7303,7304],{},"mark as paid, reserve the product, send a confirmation",[249,7306,7307,7310],{},[267,7308,7309],{},"awaiting \u002F processing",[267,7311,7312],{},"keep \"in processing\", do not cancel on a timer too early",[249,7314,7315,7318],{},[267,7316,7317],{},"declined \u002F error",[267,7319,7320],{},"let the customer retry payment on the same order",[249,7322,7323,7326],{},[267,7324,7325],{},"refund",[267,7327,7328],{},"return stock, update the status, notify the manager",[139,7330,7331],{},"A special case: the customer paid but the callback never arrived. Let a job run every few minutes that checks the payment system for the status of \"hanging\" orders. It is cheaper than sorting things out by hand from bank statements.",[346,7333,7335],{"id":7334},"_10-mismatch-between-the-order-and-the-payment","10. Mismatch between the order and the payment",[139,7337,7338],{},"When the customer changes the cart after opening the payment page, or a manager edits the order after the payment was created, the amount in the payment system and in the store diverges. The result: 1,200 paid, an order for 1,450, and accounting in a panic.",[139,7340,7341],{},"The solution: after the payment is created, the order is locked against amount changes. If a change is needed, create a new payment and void the old one. Also add reconciliation: every day compare paid orders with the provider's payment register.",[457,7343,7344],{},[139,7345,7346,7349,7350,7353],{},[166,7347,7348],{},"Want your integrations checked before launch?"," We run checkout with delivery and payment through real scenarios and return bug reports with video. ",[146,7351,7352],{"href":1125},"Order an ecommerce QA audit",", from UAH 6,000.",[152,7355,7357],{"id":7356},"how-to-test-such-integrations","How to test such integrations",[139,7359,7360],{},"A separate list we use as a minimum before launching a store:",[532,7362,7363,7369,7375,7381,7387,7393,7399],{},[163,7364,7365,7368],{},[166,7366,7367],{},"Sandbox mode for all providers",": test cards, test waybills. Check that test mode did not stay on in production (it happens more often than we would like).",[163,7370,7371,7374],{},[166,7372,7373],{},"Negative scenarios",": bank decline, a closed tab on 3-D Secure, insufficient funds, an expired card.",[163,7376,7377,7380],{},[166,7378,7379],{},"Repeated callbacks",": send the same callback twice by hand (curl or Postman) and make sure nothing doubled.",[163,7382,7383,7386],{},[166,7384,7385],{},"A forged callback",": change the amount or the signature and make sure you get a refusal.",[163,7388,7389,7392],{},[166,7390,7391],{},"An unavailable API",": cut the network to the Nova Poshta API in a test and see what the customer sees.",[163,7394,7395,7398],{},[166,7396,7397],{},"Different devices",": payment and branch selection on iOS Safari and Android Chrome. In-app browsers (Instagram, Telegram) often behave differently.",[163,7400,7401,7404],{},[166,7402,7403],{},"Automated tests",": callback signature, idempotency and cash-on-delivery calculation are easy to cover with Pest\u002FPHPUnit tests, and the full checkout path can be automated with Playwright.",[139,7406,7407,7408,7411,7412,366],{},"In Laravel it is convenient to fix external API responses through ",[1529,7409,7410],{},"Http::fake()",": the test becomes fast and independent of the network, and you check your own logic, not someone else's server. More on automating key paths is on the ",[146,7413,2705],{"href":2704},[139,7415,7416,7417,7420,7421,220],{},"We wrote separately about why orders get lost even before payment: ",[146,7418,7419],{"href":1970},"7 common checkout bugs",". And the full list of pre-release checks is collected in the ",[146,7422,7423],{"href":583},"ecommerce testing checklist",[152,7425,7427],{"id":7426},"monitoring-after-launch","Monitoring after launch",[139,7429,7430],{},"Integration does not end with the release. External APIs change, keys expire, request limits run out. The minimum worth setting up:",[160,7432,7433,7436,7439,7442],{},[163,7434,7435],{},"a Telegram or email alert when the waybill creation job fails after all retries;",[163,7437,7438],{},"a daily report: the number of orders without a waybill and the number of payments without a confirmed callback;",[163,7440,7441],{},"logging of requests and responses to the payment provider and Nova Poshta (without full card numbers and private keys) linked to the order number;",[163,7443,7444],{},"reminders about the expiry of keys and tokens.",[139,7446,7447],{},"These are small things, but they are exactly what turns \"we found out from a customer\" into \"we fixed it before the customer noticed\".",[152,7449,7451],{"id":7450},"summary-what-to-check-today","Summary: what to check today",[139,7453,7454],{},"If the store already works, there are five quick checks:",[160,7456,7457,7460,7463,7466,7469],{},[163,7458,7459],{},"Is the branch directory updated automatically, and when was the last time?",[163,7461,7462],{},"Is the payment status changed only by the server callback, and is the signature verified?",[163,7464,7465],{},"What happens if the same callback arrives twice?",[163,7467,7468],{},"Can the manager see the waybill creation error, or is it only in the logs?",[163,7470,7471],{},"Does the cash-on-delivery amount match the remainder to pay when there are discounts?",[139,7473,7474,7475,7477],{},"If the answer to at least one question is \"we don't know\", that is a reason to check. These are the places we go through in a ",[146,7476,5439],{"href":5438}," when the store is already live and you need to understand where money is being lost.",[457,7479,7480],{},[139,7481,7482,7485,7486,220],{},[166,7483,7484],{},"The store works, but orders get lost?"," Describe the problem in the brief and we will tell you where to start checking, free and with no obligation. ",[146,7487,1080],{"href":354},[2768,7489,7490],{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":14,"searchDepth":688,"depth":689,"links":7492},[7493,7494,7499,7504,7510,7511,7512],{"id":6332,"depth":688,"text":6333},{"id":6348,"depth":688,"text":6349,"children":7495},[7496,7497,7498],{"id":6352,"depth":689,"text":6353},{"id":6381,"depth":689,"text":6382},{"id":6394,"depth":689,"text":6395},{"id":6412,"depth":688,"text":6413,"children":7500},[7501,7502,7503],{"id":6416,"depth":689,"text":6417},{"id":6832,"depth":689,"text":6833},{"id":6853,"depth":689,"text":6854},{"id":6881,"depth":688,"text":6882,"children":7505},[7506,7507,7508,7509],{"id":6885,"depth":689,"text":6886},{"id":6903,"depth":689,"text":6904},{"id":7272,"depth":689,"text":7273},{"id":7334,"depth":689,"text":7335},{"id":7356,"depth":688,"text":7357},{"id":7426,"depth":688,"text":7427},{"id":7450,"depth":688,"text":7451},"integrations.log",[7515,7517,7519,7521],{"marker":716,"text":7516},"callback signature verified",{"marker":713,"text":7518},"branch closed, address in the order",{"marker":2023,"text":7520},"duplicate order after repeated callback",{"marker":708,"text":7522},"API timeout 8 s, retry queue","10 points",[7525,7528,7531,7534],{"q":7526,"a":7527},"Why are Nova Poshta branches not loading on my site?","Most often the branch directory is not being refreshed, or the request to the Nova Poshta API fails without error handling. Keep a local copy of the directory and update it on a schedule, and show a fallback way to choose a branch when the API is down. Also check that your API key is still valid.",{"q":7529,"a":7530},"Why did the payment go through but the order stayed unpaid?","The payment system reports the result in a separate callback request, and if it is not accepted or the signature is not verified, the status will not update. The callback must be idempotent: a repeated call should not create a duplicate. Regularly reconcile orders with the payment provider statements.",{"q":7532,"a":7533},"How do I avoid duplicate Nova Poshta waybills (TTN)?","Before creating a waybill, check whether one already exists for the order, and block repeated requests on the server. Save the TTN number and status immediately after the API responds. That way a double click or a re-run of a queued job will not create a second waybill.",{"q":7535,"a":7536},"How can I test a payment integration without real money?","Use the payment provider's test mode: most providers offer test cards and a sandbox for successful and declined payments. Separately check a failure, a delay and a repeated callback. Before launch, make one real payment for a minimal amount to see how the site behaves in real conditions.","intehratsiia-novoi-poshty-ta-oplaty",{},{"title":452,"description":6326},"Nova Poshta and online payment integration in a Laravel store: 10 places where it breaks, with code examples, tests and a pre-launch checklist.","Nova Poshta and Payment Integration Pitfalls — Tochno.","nova-poshta-and-payment-integration","en\u002Fblog\u002Fnova-poshta-and-payment-integration",[746,5551],"SSwTlX4JAD7GKyqdmRV5FJqtv2NcXLCDxIny3F6JmrQ",{"id":7547,"title":7548,"author":134,"body":7549,"cover":704,"coverFile":8284,"coverLines":8285,"coverStat":8294,"date":719,"description":7553,"excerpt":704,"extension":720,"faq":8295,"featured":734,"key":8308,"meta":8309,"navigation":737,"path":8310,"readingTime":739,"seo":8311,"seoDescription":8312,"seoTitle":8313,"slug":8314,"stem":8315,"tags":8316,"updated":3579,"__hash__":8317},"blog_en\u002Fen\u002Fblog\u002Fnuxt-for-marketing-website.md","Nuxt for a Marketing Website: SEO, Speed and Multilingual Setup",{"type":136,"value":7550,"toc":8260},[7551,7554,7557,7561,7574,7577,7627,7630,7718,7721,7725,7729,7739,7793,7805,7809,7819,7856,7859,7863,7878,7882,7885,7889,7892,7896,7905,7923,7927,7938,7942,7945,7965,7968,7972,7983,7996,8000,8003,8007,8021,8025,8036,8040,8043,8046,8060,8064,8067,8087,8090,8098,8102,8105,8131,8136,8140,8194,8198,8241,8247,8257],[139,7552,7553],{},"Nuxt development for a marketing website makes sense when you need fast pages with full HTML for search engines, tight control over SEO and the ability to add interactivity (a calculator, a form, an account area) without rewriting the site. For a site of five static pages and an editor who \"just wants to tweak something\", you will not find a simpler tool. Below we break down what exactly Nuxt gives you, how to set it up and where it is overkill.",[139,7555,7556],{},"We build our own Tochno site on Nuxt with static page generation, so we describe the setup we deal with every day. Specific versions and module names change, so check the documentation before adopting anything.",[152,7558,7560],{"id":7559},"why-a-marketing-site-needs-ssg-or-ssr","Why a marketing site needs SSG or SSR",[139,7562,7563,7564,7567,7568,7573],{},"A classic SPA on Vue or React serves an empty ",[1529,7565,7566],{},"\u003Cdiv id=\"app\">"," and JavaScript draws the content in the browser. ",[146,7569,7572],{"href":7570,"rel":7571},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fjavascript\u002Fjavascript-seo-basics",[2924],"Google can execute JS",", but it does so in a second wave of indexing, with a delay and not always flawlessly. Other search engines and link preview services (messengers, social networks) do not execute JS at all.",[139,7575,7576],{},"For a marketing site this is unacceptable: its main goal is to be found and shown correctly in search and on social networks. So you need HTML with the full text already in the server response. Nuxt gives you this out of the box in several modes:",[243,7578,7579,7592],{},[246,7580,7581],{},[249,7582,7583,7586,7589],{},[252,7584,7585],{},"Mode",[252,7587,7588],{},"How it works",[252,7590,7591],{},"When to choose",[262,7593,7594,7605,7616],{},[249,7595,7596,7599,7602],{},[267,7597,7598],{},"SSG (prerender)",[267,7600,7601],{},"all pages are built into HTML at build time",[267,7603,7604],{},"landing pages, corporate sites, blogs, documentation",[249,7606,7607,7610,7613],{},[267,7608,7609],{},"SSR",[267,7611,7612],{},"HTML is generated on the server for each request",[267,7614,7615],{},"personalized pages, catalogs that change often",[249,7617,7618,7621,7624],{},[267,7619,7620],{},"Hybrid (routeRules)",[267,7622,7623],{},"some routes are prerendered, some rendered on request",[267,7625,7626],{},"a site with a blog plus an account area or API",[139,7628,7629],{},"A hybrid is configured declaratively in Nuxt:",[2139,7631,7633],{"className":5775,"code":7632,"language":5777,"meta":14,"style":14},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002F**': { prerender: true },       \u002F\u002F marketing pages: static HTML\n    '\u002Fapi\u002F**': { prerender: false },  \u002F\u002F API stays dynamic\n    '\u002Faccount\u002F**': { ssr: false },    \u002F\u002F private area: SPA, no SEO needed\n  },\n})\n",[1529,7634,7635,7640,7654,7659,7675,7691,7707,7714],{"__ignoreMap":14},[2147,7636,7637],{"class":2149,"line":2150},[2147,7638,7639],{"class":2153},"\u002F\u002F nuxt.config.ts\n",[2147,7641,7642,7645,7648,7651],{"class":2149,"line":688},[2147,7643,7644],{"class":2163},"export",[2147,7646,7647],{"class":2163}," default",[2147,7649,7650],{"class":2174}," defineNuxtConfig",[2147,7652,7653],{"class":5818},"({\n",[2147,7655,7656],{"class":2149,"line":689},[2147,7657,7658],{"class":2159},"  routeRules: {\n",[2147,7660,7661,7664,7667,7669,7672],{"class":2149,"line":2210},[2147,7662,7663],{"class":2186},"    '\u002F**'",[2147,7665,7666],{"class":2159},": { prerender: ",[2147,7668,7048],{"class":2167},[2147,7670,7671],{"class":2159}," },       ",[2147,7673,7674],{"class":2153},"\u002F\u002F marketing pages: static HTML\n",[2147,7676,7677,7680,7682,7685,7688],{"class":2149,"line":2216},[2147,7678,7679],{"class":2186},"    '\u002Fapi\u002F**'",[2147,7681,7666],{"class":2159},[2147,7683,7684],{"class":2167},"false",[2147,7686,7687],{"class":2159}," },  ",[2147,7689,7690],{"class":2153},"\u002F\u002F API stays dynamic\n",[2147,7692,7693,7696,7699,7701,7704],{"class":2149,"line":2222},[2147,7694,7695],{"class":2186},"    '\u002Faccount\u002F**'",[2147,7697,7698],{"class":2159},": { ssr: ",[2147,7700,7684],{"class":2167},[2147,7702,7703],{"class":2159}," },    ",[2147,7705,7706],{"class":2153},"\u002F\u002F private area: SPA, no SEO needed\n",[2147,7708,7709,7712],{"class":2149,"line":2228},[2147,7710,7711],{"class":2159},"  }",[2147,7713,6805],{"class":5818},[2147,7715,7716],{"class":2149,"line":2248},[2147,7717,6028],{"class":5818},[139,7719,7720],{},"For a site that changes once a week, SSG is almost always the best choice: pages are served from a CDN or plain nginx with no computation, and the response is instant.",[152,7722,7724],{"id":7723},"seo-in-nuxt-what-to-set-up-and-what-is-already-there","SEO in Nuxt: what to set up and what is already there",[346,7726,7728],{"id":7727},"meta-tags-and-canonical-urls","Meta tags and canonical URLs",[139,7730,7731,7732,3068,7735,7738],{},"Nuxt has built-in ",[1529,7733,7734],{},"useSeoMeta",[1529,7736,7737],{},"useHead",". Everything we describe in the page component ends up in the finished HTML:",[2139,7740,7742],{"className":5775,"code":7741,"language":5777,"meta":14,"style":14},"useSeoMeta({\n  title: 'Turnkey Nuxt Website Development',\n  description: 'Landing pages and corporate sites: design, development, SEO.',\n  ogTitle: 'Turnkey Nuxt Website Development',\n  ogImage: '\u002Fog\u002Fwebsite-development.png',\n})\n",[1529,7743,7744,7750,7760,7770,7779,7789],{"__ignoreMap":14},[2147,7745,7746,7748],{"class":2149,"line":2150},[2147,7747,7734],{"class":2174},[2147,7749,7653],{"class":2159},[2147,7751,7752,7755,7758],{"class":2149,"line":688},[2147,7753,7754],{"class":2159},"  title: ",[2147,7756,7757],{"class":2186},"'Turnkey Nuxt Website Development'",[2147,7759,6805],{"class":2159},[2147,7761,7762,7765,7768],{"class":2149,"line":689},[2147,7763,7764],{"class":2159},"  description: ",[2147,7766,7767],{"class":2186},"'Landing pages and corporate sites: design, development, SEO.'",[2147,7769,6805],{"class":2159},[2147,7771,7772,7775,7777],{"class":2149,"line":2210},[2147,7773,7774],{"class":2159},"  ogTitle: ",[2147,7776,7757],{"class":2186},[2147,7778,6805],{"class":2159},[2147,7780,7781,7784,7787],{"class":2149,"line":2216},[2147,7782,7783],{"class":2159},"  ogImage: ",[2147,7785,7786],{"class":2186},"'\u002Fog\u002Fwebsite-development.png'",[2147,7788,6805],{"class":2159},[2147,7790,7791],{"class":2149,"line":2222},[2147,7792,6028],{"class":2159},[139,7794,7795,7796,7798,7799,7801,7802,7804],{},"The rules we follow: a unique ",[1529,7797,1349],{}," up to 60 characters and ",[1529,7800,4286],{}," up to 155 for every page, a single ",[1529,7803,4290],{},", a canonical URL in a single format (with no trailing slash, or the opposite, but always the same). Duplicates and inconsistent canonical URLs are among the most common causes of \"strange\" problems in Search Console.",[346,7806,7808],{"id":7807},"sitemap-robots-structured-data","Sitemap, robots, structured data",[139,7810,7811,7812,2641,7815,7818],{},"There are ready-made modules for sitemap and robots (",[1529,7813,7814],{},"@nuxtjs\u002Fsitemap",[1529,7816,7817],{},"@nuxtjs\u002Frobots","), and for schema.org there are separate modules or your own composable that adds JSON-LD. For a marketing site we usually add:",[160,7820,7821,7830,7835,7844,7850],{},[163,7822,7823,3068,7826,7829],{},[1529,7824,7825],{},"Organization",[1529,7827,7828],{},"WebSite"," on all pages;",[163,7831,7832,7834],{},[1529,7833,4316],{}," on inner pages;",[163,7836,7837,7840,7841,7843],{},[1529,7838,7839],{},"Service"," or ",[1529,7842,4313],{}," on service pages;",[163,7845,7846,7849],{},[1529,7847,7848],{},"FAQPage"," where the page really has a question block;",[163,7851,7852,7855],{},[1529,7853,7854],{},"BlogPosting"," with an author for articles.",[139,7857,7858],{},"It is important that the markup matches the visible content. Markup for questions that are not on the page violates search engine rules and does more harm than good.",[346,7860,7862],{"id":7861},"internal-links-and-breadcrumbs","Internal links and breadcrumbs",[139,7864,7865,7866,7869,7870,7873,7874,7877],{},"The Nuxt ",[1529,7867,7868],{},"NuxtLink"," component renders an ordinary ",[1529,7871,7872],{},"\u003Ca href>",", so search engines see the links. It is easier to go wrong with buttons that navigate through ",[1529,7875,7876],{},"router.push"," without a real address: such transitions are invisible to a crawler. All indexable transitions must be real links.",[346,7879,7881],{"id":7880},"checks-before-release","Checks before release",[139,7883,7884],{},"After the build it is useful to have an automatic check: does every HTML file have a unique title and description, a single h1, a canonical URL, and no broken internal links. We do this with a script that runs in CI, and we block the release on violations. That is cheaper than hunting for duplicates in Search Console a month after launch.",[152,7886,7888],{"id":7887},"speed-where-nuxt-wins-and-where-you-can-lose","Speed: where Nuxt wins and where you can lose",[139,7890,7891],{},"Nuxt does not make a site fast automatically. It gives you tools, and the result depends on how you use them.",[346,7893,7895],{"id":7894},"images","Images",[139,7897,3079,7898,7901,7902,7904],{},[1529,7899,7900],{},"@nuxt\u002Fimage"," module converts pictures to modern formats (WebP, AVIF), picks sizes for the screen and adds ",[1529,7903,3082],{},". Two rules that give the most:",[532,7906,7907,7913],{},[163,7908,7909,7910,2436],{},"Do not lazy-load the first-screen image (the LCP element); on the contrary, prioritize it (",[1529,7911,7912],{},"fetchpriority=\"high\"",[163,7914,7915,7916,3068,7918,7920,7921,220],{},"Always specify ",[1529,7917,3067],{},[1529,7919,3071],{}," to avoid layout shifts (CLS). For more on the metrics, see ",[146,7922,4278],{"href":1050},[346,7924,7926],{"id":7925},"fonts","Fonts",[139,7928,7929,7930,7933,7934,7937],{},"Fonts most often spoil both LCP and CLS. In Nuxt the ",[1529,7931,7932],{},"@nuxt\u002Ffonts"," module downloads fonts from third-party services at build time and serves them from your own domain, substitutes fallback metrics to reduce shifts and adds ",[1529,7935,7936],{},"preload",". The benefit is obvious: fewer requests to third-party domains and no blocking CSS from Google Fonts. Advice: choose no more than two or three weights and subsets only for the languages you need.",[346,7939,7941],{"id":7940},"javascript-size","JavaScript size",[139,7943,7944],{},"This is where Nuxt loses to plain HTML. Even a static page loads Vue, the router and the Nuxt core to hydrate the page. As a benchmark, the core eats tens of kilobytes of compressed JS before your own code. What we do to keep the budget:",[160,7946,7947,7956,7959,7962],{},[163,7948,7949,7950,3068,7953,3058],{},"lazily hydrate heavy interactive blocks (calculator, form, cookie banner) with ",[1529,7951,7952],{},"hydrate-on-visible",[1529,7954,7955],{},"hydrate-on-idle",[163,7957,7958],{},"load heavy libraries (for example form validation) only when needed, with a dynamic import;",[163,7960,7961],{},"do not pull in a whole UI kit, only the components we use;",[163,7963,7964],{},"track the bundle size in CI so regressions are visible immediately.",[139,7966,7967],{},"To be honest: if your site needs one screen of text and a form, vanilla HTML or Astro will give less JS. Nuxt wins when the site has many components, interactivity and shared logic.",[346,7969,7971],{"id":7970},"caching-and-cdn","Caching and CDN",[139,7973,7974,7975,7978,7979,7982],{},"For SSG it is enough to serve static files with a long ",[1529,7976,7977],{},"Cache-Control"," for hashed assets (",[1529,7980,7981],{},"\u002F_nuxt\u002F*",") and a short one for HTML. This can be done on nginx, Vercel or Cloudflare. Separately check that compression (gzip or brotli) is turned on: the difference in transfer size is noticeable.",[457,7984,7985],{},[139,7986,7987,7990,7991,7995],{},[166,7988,7989],{},"Need a site that opens fast and brings in leads?"," We build landing pages and corporate sites on Nuxt: design, development, an SEO foundation and a speed check before launch. ",[146,7992,7994],{"href":7993},"\u002Fen\u002Fservices\u002Fwebsite-development","Turnkey Nuxt website development",", from UAH 25,000.",[152,7997,7999],{"id":7998},"multilingual-setup-without-duplicates-or-confusion","Multilingual setup without duplicates or confusion",[139,8001,8002],{},"For a two-language site (Ukrainian and English) three things matter: URL structure, hreflang and translations.",[346,8004,8006],{"id":8005},"url-structure","URL structure",[139,8008,8009,8010,8012,8013,8016,8017,8020],{},"The most practical scheme for the Ukrainian market: the main language without a prefix, others with a prefix. That is ",[1529,8011,71],{}," for Ukrainian and ",[1529,8014,8015],{},"\u002Fen\u002Fservices"," for English. In Nuxt i18n modules this is the ",[1529,8018,8019],{},"prefix_except_default"," mode. We transliterate Ukrainian slugs into Latin using the official transliteration and write English ones in English. That keeps URLs readable and useful for search.",[346,8022,8024],{"id":8023},"hreflang-and-canonical","hreflang and canonical",[139,8026,8027,8028,8031,8032,8035],{},"Each language version links to all the others and to itself (",[1529,8029,8030],{},"hreflang","), while the canonical URL points to the page itself. An ",[1529,8033,8034],{},"x-default"," is added for the main language. An important nuance: if the English version is not ready yet, do not publish empty pages and do not serve hreflang to non-existent URLs. It is better to have a language switched off by a flag until the content is ready.",[346,8037,8039],{"id":8038},"translations-and-content","Translations and content",[139,8041,8042],{},"Non-static texts (buttons, navigation, form labels) are convenient to keep in locale JSON files. Page and article content is better placed in separate Markdown or YAML files per language so the editor can change text without touching components. Then adding a new language comes down to adding files, not changing templates.",[139,8044,8045],{},"Typical multilingual mistakes:",[160,8047,8048,8051,8054,8057],{},[163,8049,8050],{},"an automatic redirect by browser language, which prevents the crawler from seeing all versions (better to offer a switcher than to force redirects);",[163,8052,8053],{},"translating only the text, not the meta tags and alt attributes;",[163,8055,8056],{},"a language switcher that leads to the home page rather than the same page in another language;",[163,8058,8059],{},"different date and currency formats without regard to locale.",[152,8061,8063],{"id":8062},"content-where-to-store-it","Content: where to store it",[139,8065,8066],{},"For a marketing site there are three approaches:",[532,8068,8069,8075,8081],{},[163,8070,8071,8074],{},[166,8072,8073],{},"Files in the repository"," (Markdown\u002FYAML via Nuxt Content). Pros: versioning, speed, no external dependencies, full control. Con: edits go through a developer or Git.",[163,8076,8077,8080],{},[166,8078,8079],{},"A headless CMS"," (Strapi, Directus, Sanity and others). Pros: a convenient editor for a non-technical person. Cons: one more system to maintain and extra costs.",[163,8082,8083,8086],{},[166,8084,8085],{},"An admin panel on Laravel."," Justified when content is closely tied to business logic (catalog, orders). Then Nuxt takes data from the Laravel API.",[139,8088,8089],{},"For a site where content changes a few times a month, the first option is the most economical: every change is a commit and an automatic rebuild. If several people edit the site every day, you cannot do without a CMS.",[139,8091,8092,8093,8097],{},"The link between Nuxt and Laravel is what our ",[146,8094,8096],{"href":8095},"\u002Fen\u002Fservices\u002Ffull-stack-laravel-nuxt","full-stack Laravel + Nuxt development"," is about, when you need more than a static site: an account area, orders, integrations.",[152,8099,8101],{"id":8100},"when-nuxt-is-not-needed","When Nuxt is not needed",[139,8103,8104],{},"It is better to say directly when we do not recommend Nuxt:",[160,8106,8107,8113,8119,8125],{},[163,8108,8109,8112],{},[166,8110,8111],{},"A site of 3–5 pages with no interactivity and no growth plans."," Static HTML or a simple generator is enough.",[163,8114,8115,8118],{},[166,8116,8117],{},"An editor who changes content and design every day on their own."," WordPress or a builder will be better here, because the benefit of full code control will not justify the maintenance cost.",[163,8120,8121,8124],{},[166,8122,8123],{},"A very small budget and a need \"yesterday\"."," A builder will launch the site faster, and you can move to custom code later, when limits appear.",[163,8126,8127,8130],{},[166,8128,8129],{},"A team with no Vue experience at all."," The cost of maintenance will rise. If you plan to develop the site yourself, take this factor into account.",[139,8132,8133,8134,220],{},"In the remaining cases, where you need speed, your own design, SEO control and room to grow, Nuxt pays off. We compared it with other approaches for stores in the article on ",[146,8135,799],{"href":218},[152,8137,8139],{"id":8138},"pitfalls-we-have-run-into","Pitfalls we have run into",[160,8141,8142,8163,8169,8180,8185],{},[163,8143,8144,8147,8148,8151,8152,8155,8156,7840,8159,8162],{},[166,8145,8146],{},"Hydration errors."," If the HTML from the server does not match what the client renders (random values, ",[1529,8149,8150],{},"Date.now()",", dependencies on ",[1529,8153,8154],{},"window","), Vue issues a warning and may repaint the page. We move browser-dependent data into ",[1529,8157,8158],{},"onMounted",[1529,8160,8161],{},"\u003CClientOnly>",", but only for non-indexed content.",[163,8164,8165,8168],{},[166,8166,8167],{},"Content only on the client."," Everything that must be indexed has to be in the HTML. The check is simple: open \"View page source\" (not the inspector) and find your text.",[163,8170,8171,1778,8174,3068,8176,8179],{},[166,8172,8173],{},"Trailing slashes and duplicate URLs.",[1529,8175,71],{},[1529,8177,8178],{},"\u002Fposluhy\u002F"," are different addresses for a search engine. Choose one format, and set up a redirect at the server level and a canonical URL.",[163,8181,8182,8184],{},[166,8183,3368],{}," Analytics, chats and widgets can eat the entire speed gain. Load them after user consent and deferred.",[163,8186,8187,8190,8191,8193],{},[166,8188,8189],{},"Preview environments in the index."," Close test and preview deployments from indexing (",[1529,8192,4307],{},"), otherwise the search engine will find duplicates of your site.",[152,8195,8197],{"id":8196},"checklist-before-launching-a-nuxt-site","Checklist before launching a Nuxt site",[532,8199,8200,8205,8213,8216,8223,8226,8229,8232,8235,8238],{},[163,8201,8202,8203,220],{},"The page source contains the full text and a single ",[1529,8204,4290],{},[163,8206,8207,8208,3068,8210,8212],{},"Unique ",[1529,8209,1349],{},[1529,8211,4286],{}," on every page.",[163,8214,8215],{},"Canonical URLs in a single format, redirect from trailing slashes set up.",[163,8217,8218,3068,8220,8222],{},[1529,8219,4303],{},[1529,8221,4300],{}," work in production and close off test environments.",[163,8224,8225],{},"Structured data passes the Rich Results Test.",[163,8227,8228],{},"Images in modern formats, the first-screen image not lazy-loaded, dimensions set.",[163,8230,8231],{},"Fonts served from your own domain with fallback metrics.",[163,8233,8234],{},"Lighthouse on mobile, not on desktop; real CWV data is added after launch from Search Console.",[163,8236,8237],{},"Forms and buttons work without JS or have a fallback path (a messenger link, mailto).",[163,8239,8240],{},"Language versions link to each other, and there are no empty languages.",[139,8242,8243,8244,8246],{},"If you already have a Nuxt site and have doubts about it, the first step is a ",[146,8245,5439],{"href":5438}," or an express check of speed and indexing.",[457,8248,8249],{},[139,8250,8251,8254,8255,220],{},[166,8252,8253],{},"Planning a site or a rewrite on Nuxt?"," Describe the task in the brief: we will tell you whether you really need Nuxt and give an estimate within 1 business day. ",[146,8256,1080],{"href":354},[2768,8258,8259],{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":14,"searchDepth":688,"depth":689,"links":8261},[8262,8263,8269,8275,8280,8281,8282,8283],{"id":7559,"depth":688,"text":7560},{"id":7723,"depth":688,"text":7724,"children":8264},[8265,8266,8267,8268],{"id":7727,"depth":689,"text":7728},{"id":7807,"depth":689,"text":7808},{"id":7861,"depth":689,"text":7862},{"id":7880,"depth":689,"text":7881},{"id":7887,"depth":688,"text":7888,"children":8270},[8271,8272,8273,8274],{"id":7894,"depth":689,"text":7895},{"id":7925,"depth":689,"text":7926},{"id":7940,"depth":689,"text":7941},{"id":7970,"depth":689,"text":7971},{"id":7998,"depth":688,"text":7999,"children":8276},[8277,8278,8279],{"id":8005,"depth":689,"text":8006},{"id":8023,"depth":689,"text":8024},{"id":8038,"depth":689,"text":8039},{"id":8062,"depth":688,"text":8063},{"id":8100,"depth":688,"text":8101},{"id":8138,"depth":688,"text":8139},{"id":8196,"depth":688,"text":8197},"nuxt.config.ts",[8286,8288,8290,8292],{"marker":716,"text":8287},"prerender: all content in HTML",{"marker":716,"text":8289},"canonical, hreflang, sitemap",{"marker":713,"text":8291},"JS budget: we watch the size",{"marker":708,"text":8293},"i18n: language prefix, no duplicates","SSG",[8296,8299,8302,8305],{"q":8297,"a":8298},"Is Nuxt good for SEO?","Yes: Nuxt serves ready-made HTML through SSG or SSR, so search engines and AI bots see all the content right away. Meta tags, canonical URLs, a sitemap and hreflang still need to be set up, but the tools exist. Without that setup, any framework gives mediocre results.",{"q":8300,"a":8301},"How much does a Nuxt website cost?","A Nuxt landing page from us starts from UAH 25,000 and takes 2–3 weeks. A corporate website starts from UAH 60,000 and usually takes 5–7 weeks. The final price depends on the number of pages, languages, integrations and design complexity. We give an exact figure after a short brief.",{"q":8303,"a":8304},"What is the difference between SSG and SSR in Nuxt?","SSG generates pages ahead of time during the build, while SSR builds them on the server for every request. For a marketing site that rarely changes, SSG is simpler, faster and cheaper to host. SSR makes sense when content changes often or depends on the user.",{"q":8306,"a":8307},"When is Nuxt not needed?","If you need a simple business card site, or a site that is edited daily without a developer, WordPress or a site builder will be more practical. Nuxt is justified when speed, SEO, multiple languages and control over the code matter. Choose by real needs, not by technology fashion.","nuxt-dlia-marketynhovoho-saitu",{},"\u002Fen\u002Fblog\u002Fnuxt-for-marketing-website",{"title":7548,"description":7553},"Nuxt development for a marketing website: how SSG delivers SEO and speed, how to set up multilingual routes and where Nuxt is overkill. Config examples.","Nuxt for a Marketing Website: SEO and Speed — Tochno.","nuxt-for-marketing-website","en\u002Fblog\u002Fnuxt-for-marketing-website",[359],"R80tkmtuZJSoKF-zEgNWNPMs53wYxK9kRAJ-kv01znQ",{"id":8319,"title":3155,"author":134,"body":8320,"cover":704,"coverFile":9259,"coverLines":9260,"coverStat":9269,"date":719,"description":8324,"excerpt":704,"extension":720,"faq":9270,"featured":734,"key":9283,"meta":9284,"navigation":737,"path":3154,"readingTime":739,"seo":9285,"seoDescription":9286,"seoTitle":9287,"slug":9288,"stem":9289,"tags":9290,"updated":747,"__hash__":9291},"blog_en\u002Fen\u002Fblog\u002Fwhat-is-code-audit.md",{"type":136,"value":8321,"toc":9241},[8322,8325,8328,8332,8335,8338,8367,8370,8374,8377,8421,8424,8428,8432,8435,8438,8455,8458,8677,8680,8682,8685,8731,8734,8854,8857,8861,8864,8900,8903,8940,8943,8947,8957,8961,8964,8968,8985,8988,8992,8995,9057,9060,9063,9073,9077,9083,9086,9100,9107,9111,9114,9134,9137,9141,9144,9167,9172,9176,9179,9214,9217,9219,9222,9238],[139,8323,8324],{},"A code audit is an independent review of a project's source code, architecture and infrastructure that ends in a report: what works well, what is unsafe or slow, and in which order to fix it. You need one when you inherit a project, change contractors, prepare to scale or sell the business, or simply do not understand why every new feature costs more than the last.",[139,8326,8327],{},"Below is what we check in Laravel projects, what the report looks like, which signs show it is time to order an audit, and what you should not expect from it.",[152,8329,8331],{"id":8330},"what-a-code-audit-is-and-what-it-is-not","What a code audit is and what it is not",[139,8333,8334],{},"An audit is a diagnosis, not treatment. An expert reads the code, runs the project, looks at the database, dependencies, configuration and deployment process, and writes up a document with conclusions. The fixes are a separate stage that you can give to any team, not necessarily the one that performed the audit.",[139,8336,8337],{},"A code audit differs from neighboring services:",[160,8339,8340,8349,8355,8361],{},[163,8341,8342,8345,8346,8348],{},[166,8343,8344],{},"QA testing"," checks the product's behavior through the user's eyes: does the cart, the form, the payment work. That is an ",[146,8347,1984],{"href":1125},". The code is not read.",[163,8350,8351,8354],{},[166,8352,8353],{},"Code review"," evaluates specific changes (a pull request) during development.",[163,8356,8357,8360],{},[166,8358,8359],{},"A pentest"," is a targeted attempt to break into the system. We do a security review at the code and configuration level, but we do not claim a full pentest.",[163,8362,8363,8366],{},[166,8364,8365],{},"A code audit"," covers the project as a whole: structure, quality, security, performance, processes.",[139,8368,8369],{},"The best result comes from both approaches together: QA finds the symptoms, the code audit finds the causes.",[152,8371,8373],{"id":8372},"signs-that-you-need-an-audit","Signs that you need an audit",[139,8375,8376],{},"Not every project needs an audit every year. There are situations where it is especially justified.",[532,8378,8379,8385,8391,8397,8403,8409,8415],{},[163,8380,8381,8384],{},[166,8382,8383],{},"The team changes."," A new contractor or tech lead needs to quickly understand what exactly they are getting before taking responsibility.",[163,8386,8387,8390],{},[166,8388,8389],{},"New features get more expensive and take long."," Developers say \"everything is tangled up there\", a classic symptom of technical debt.",[163,8392,8393,8396],{},[166,8394,8395],{},"The site is slow or goes down under load."," Especially in seasonal peaks: promotions, sales.",[163,8398,8399,8402],{},[166,8400,8401],{},"After an incident."," A data leak, an admin panel compromise, lost orders.",[163,8404,8405,8408],{},[166,8406,8407],{},"Before major changes."," Migrating to a new framework version, scaling, integrating with a CRM or 1C.",[163,8410,8411,8414],{},[166,8412,8413],{},"Before a sale or investment."," A buyer or investor wants to see the state of the asset.",[163,8416,8417,8420],{},[166,8418,8419],{},"You do not trust the contractor's estimates."," An independent opinion will help you understand whether \"this really needs to be rewritten\".",[139,8422,8423],{},"If at least two of these sound familiar, an audit will pay off faster than you would spend money on another round of fixes made \"blindly\".",[152,8425,8427],{"id":8426},"what-we-check-in-a-laravel-project","What we check in a Laravel project",[346,8429,8431],{"id":8430},"architecture-and-code-quality","Architecture and code quality",[139,8433,8434],{},"The first thing we look at is where the business logic lives. In a healthy project the controller is thin: validation, a service call, a response. In an unhealthy one, the controller runs to 600 lines with discount calculation, email sending and payment provider handling.",[139,8436,8437],{},"We check:",[160,8439,8440,8443,8446,8449,8452],{},[163,8441,8442],{},"separation of responsibilities: controllers, services, Form Requests, Policies, Jobs;",[163,8444,8445],{},"code duplication and \"god\" classes;",[163,8447,8448],{},"dependencies between modules and circular relations;",[163,8450,8451],{},"alignment with the framework: whether its mechanisms are bypassed with home-grown ones;",[163,8453,8454],{},"test coverage and reproducibility: whether tests pass and whether there is CI.",[139,8456,8457],{},"A typical example of a problem we describe in the report:",[2139,8459,8461],{"className":2141,"code":8460,"language":2143,"meta":14,"style":14},"\u002F\u002F OrderController.php — logic in the controller\npublic function store(Request $request)\n{\n    $total = 0;\n    foreach ($request->items as $item) {\n        $product = Product::find($item['id']);      \u002F\u002F query in a loop\n        $total += $product->price * $item['qty'];\n    }\n    if ($request->promo === 'SALE10') {            \u002F\u002F \"magic\" value\n        $total *= 0.9;\n    }\n    $order = Order::create([...]);\n    Mail::to($request->email)->send(new OrderMail($order)); \u002F\u002F synchronous sending\n}\n",[1529,8462,8463,8468,8484,8488,8500,8519,8546,8573,8577,8599,8611,8615,8636,8673],{"__ignoreMap":14},[2147,8464,8465],{"class":2149,"line":2150},[2147,8466,8467],{"class":2153},"\u002F\u002F OrderController.php — logic in the controller\n",[2147,8469,8470,8472,8474,8477,8479,8481],{"class":2149,"line":688},[2147,8471,6545],{"class":2163},[2147,8473,6537],{"class":2163},[2147,8475,8476],{"class":2174}," store",[2147,8478,2202],{"class":2159},[2147,8480,6928],{"class":2167},[2147,8482,8483],{"class":2159}," $request)\n",[2147,8485,8486],{"class":2149,"line":689},[2147,8487,6471],{"class":2159},[2147,8489,8490,8493,8495,8498],{"class":2149,"line":2210},[2147,8491,8492],{"class":2159},"    $total ",[2147,8494,2164],{"class":2163},[2147,8496,8497],{"class":2167}," 0",[2147,8499,6490],{"class":2159},[2147,8501,8502,8505,8508,8510,8513,8516],{"class":2149,"line":2216},[2147,8503,8504],{"class":2163},"    foreach",[2147,8506,8507],{"class":2159}," ($request",[2147,8509,2196],{"class":2163},[2147,8511,8512],{"class":2159},"items ",[2147,8514,8515],{"class":2163},"as",[2147,8517,8518],{"class":2159}," $item) {\n",[2147,8520,8521,8524,8526,8529,8531,8534,8537,8540,8543],{"class":2149,"line":2222},[2147,8522,8523],{"class":2159},"        $product ",[2147,8525,2164],{"class":2163},[2147,8527,8528],{"class":2167}," Product",[2147,8530,2171],{"class":2163},[2147,8532,8533],{"class":2174},"find",[2147,8535,8536],{"class":2159},"($item[",[2147,8538,8539],{"class":2186},"'id'",[2147,8541,8542],{"class":2159},"]);      ",[2147,8544,8545],{"class":2153},"\u002F\u002F query in a loop\n",[2147,8547,8548,8551,8554,8557,8559,8562,8565,8568,8571],{"class":2149,"line":2228},[2147,8549,8550],{"class":2159},"        $total ",[2147,8552,8553],{"class":2163},"+=",[2147,8555,8556],{"class":2159}," $product",[2147,8558,2196],{"class":2163},[2147,8560,8561],{"class":2159},"price ",[2147,8563,8564],{"class":2163},"*",[2147,8566,8567],{"class":2159}," $item[",[2147,8569,8570],{"class":2186},"'qty'",[2147,8572,6526],{"class":2159},[2147,8574,8575],{"class":2149,"line":2248},[2147,8576,6817],{"class":2159},[2147,8578,8579,8581,8583,8585,8588,8590,8593,8596],{"class":2149,"line":2283},[2147,8580,6988],{"class":2163},[2147,8582,8507],{"class":2159},[2147,8584,2196],{"class":2163},[2147,8586,8587],{"class":2159},"promo ",[2147,8589,7147],{"class":2163},[2147,8591,8592],{"class":2186}," 'SALE10'",[2147,8594,8595],{"class":2159},") {            ",[2147,8597,8598],{"class":2153},"\u002F\u002F \"magic\" value\n",[2147,8600,8601,8603,8606,8609],{"class":2149,"line":1283},[2147,8602,8550],{"class":2159},[2147,8604,8605],{"class":2163},"*=",[2147,8607,8608],{"class":2167}," 0.9",[2147,8610,6490],{"class":2159},[2147,8612,8613],{"class":2149,"line":739},[2147,8614,6817],{"class":2159},[2147,8616,8617,8620,8622,8624,8626,8629,8631,8634],{"class":2149,"line":2809},[2147,8618,8619],{"class":2159},"    $order ",[2147,8621,2164],{"class":2163},[2147,8623,2168],{"class":2167},[2147,8625,2171],{"class":2163},[2147,8627,8628],{"class":2174},"create",[2147,8630,2265],{"class":2159},[2147,8632,8633],{"class":2163},"...",[2147,8635,6741],{"class":2159},[2147,8637,8638,8641,8643,8646,8649,8651,8654,8656,8659,8661,8664,8667,8670],{"class":2149,"line":5998},[2147,8639,8640],{"class":2167},"    Mail",[2147,8642,2171],{"class":2163},[2147,8644,8645],{"class":2174},"to",[2147,8647,8648],{"class":2159},"($request",[2147,8650,2196],{"class":2163},[2147,8652,8653],{"class":2159},"email)",[2147,8655,2196],{"class":2163},[2147,8657,8658],{"class":2174},"send",[2147,8660,2202],{"class":2159},[2147,8662,8663],{"class":2163},"new",[2147,8665,8666],{"class":2167}," OrderMail",[2147,8668,8669],{"class":2159},"($order)); ",[2147,8671,8672],{"class":2153},"\u002F\u002F synchronous sending\n",[2147,8674,8675],{"class":2149,"line":6025},[2147,8676,2286],{"class":2159},[139,8678,8679],{},"In the report this turns into concrete items: move the calculation into a service, load products in one query, store promo codes in the database, send the email through a queue.",[346,8681,5114],{"id":5230},[139,8683,8684],{},"For Laravel we check basic but critical things that are often missed:",[160,8686,8687,8693,8700,8710,8716,8719,8722,8728],{},[163,8688,8689,8692],{},[1529,8690,8691],{},"APP_DEBUG=true"," in production and leaked environment variables;",[163,8694,8695,8696,8699],{},"mass assignment (",[1529,8697,8698],{},"$guarded = []",") and missing access checks in controllers;",[163,8701,8702,8703,2641,8706,8709],{},"SQL injection through raw queries (",[1529,8704,8705],{},"DB::raw",[1529,8707,8708],{},"whereRaw"," with concatenation);",[163,8711,8712,8713,6869],{},"XSS through output of unescaped data (",[1529,8714,8715],{},"{!! !!}",[163,8717,8718],{},"file uploads without type and size checks;",[163,8720,8721],{},"CSRF, cookie settings, security headers;",[163,8723,8724,8725,6869],{},"outdated packages with known vulnerabilities (",[1529,8726,8727],{},"composer audit",[163,8729,8730],{},"secrets stored in the repository and in git history.",[139,8732,8733],{},"A dangerous example that appears more often than we would like:",[2139,8735,8737],{"className":2141,"code":8736,"language":2143,"meta":14,"style":14},"\u002F\u002F admin search: injection through concatenation\n$orders = DB::select(\"SELECT * FROM orders WHERE email = '\" . $request->email . \"'\");\n\n\u002F\u002F correct: parameter binding\n$orders = DB::select('SELECT * FROM orders WHERE email = ?', [$request->email]);\n",[1529,8738,8739,8744,8803,8807,8812],{"__ignoreMap":14},[2147,8740,8741],{"class":2149,"line":2150},[2147,8742,8743],{"class":2153},"\u002F\u002F admin search: injection through concatenation\n",[2147,8745,8746,8749,8751,8754,8756,8759,8761,8764,8767,8769,8772,8775,8778,8781,8783,8786,8789,8791,8793,8796,8798,8801],{"class":2149,"line":688},[2147,8747,8748],{"class":2159},"$orders ",[2147,8750,2164],{"class":2163},[2147,8752,8753],{"class":2167}," DB",[2147,8755,2171],{"class":2163},[2147,8757,8758],{"class":2174},"select",[2147,8760,2202],{"class":2159},[2147,8762,8763],{"class":2186},"\"",[2147,8765,8766],{"class":2163},"SELECT",[2147,8768,2560],{"class":2163},[2147,8770,8771],{"class":2163}," FROM",[2147,8773,8774],{"class":2186}," orders ",[2147,8776,8777],{"class":2163},"WHERE",[2147,8779,8780],{"class":2186}," email ",[2147,8782,2164],{"class":2163},[2147,8784,8785],{"class":2186}," '\"",[2147,8787,8788],{"class":2163}," .",[2147,8790,2193],{"class":2159},[2147,8792,2196],{"class":2163},[2147,8794,8795],{"class":2159},"email ",[2147,8797,220],{"class":2163},[2147,8799,8800],{"class":2186}," \"'\"",[2147,8802,2219],{"class":2159},[2147,8804,8805],{"class":2149,"line":689},[2147,8806,2225],{"emptyLinePlaceholder":737},[2147,8808,8809],{"class":2149,"line":2210},[2147,8810,8811],{"class":2153},"\u002F\u002F correct: parameter binding\n",[2147,8813,8814,8816,8818,8820,8822,8824,8826,8829,8831,8833,8835,8837,8839,8841,8843,8846,8849,8851],{"class":2149,"line":2216},[2147,8815,8748],{"class":2159},[2147,8817,2164],{"class":2163},[2147,8819,8753],{"class":2167},[2147,8821,2171],{"class":2163},[2147,8823,8758],{"class":2174},[2147,8825,2202],{"class":2159},[2147,8827,8828],{"class":2186},"'",[2147,8830,8766],{"class":2163},[2147,8832,2560],{"class":2163},[2147,8834,8771],{"class":2163},[2147,8836,8774],{"class":2186},[2147,8838,8777],{"class":2163},[2147,8840,8780],{"class":2186},[2147,8842,2164],{"class":2163},[2147,8844,8845],{"class":2186}," ?'",[2147,8847,8848],{"class":2159},", [$request",[2147,8850,2196],{"class":2163},[2147,8852,8853],{"class":2159},"email]);\n",[139,8855,8856],{},"Separately we look at customers' personal data: what is stored, who has access, whether there are backups and logging. This is not a legal opinion but a technical risk assessment.",[346,8858,8860],{"id":8859},"performance-and-the-database","Performance and the database",[139,8862,8863],{},"Most slow Laravel sites suffer from a few recurring problems:",[160,8865,8866,8876,8882,8888,8894],{},[163,8867,8868,8871,8872,8875],{},[166,8869,8870],{},"N+1 queries."," A list of 50 products produces 150 database queries because relations are loaded one by one. It is cured with ",[1529,8873,8874],{},"with()",", but first it has to be found.",[163,8877,8878,8881],{},[166,8879,8880],{},"Missing indexes"," on columns used for filtering and sorting.",[163,8883,8884,8887],{},[166,8885,8886],{},"Heavy queries on every page request:"," counters and totals that could be cached.",[163,8889,8890,8893],{},[166,8891,8892],{},"Synchronous work with external APIs"," in the main flow: Nova Poshta, the payment provider, emails.",[163,8895,8896,8899],{},[166,8897,8898],{},"No caching"," of configuration, routes and results of expensive computations.",[139,8901,8902],{},"We turn on the query log in the test environment, walk through the key flows (catalog, product page, cart, checkout) and record the number of queries and the response time. To detect N+1, it helps a great deal to forbid lazy loading in non-production environments:",[2139,8904,8906],{"className":2141,"code":8905,"language":2143,"meta":14,"style":14},"\u002F\u002F AppServiceProvider::boot()\nModel::preventLazyLoading(! app()->isProduction());\n",[1529,8907,8908,8913],{"__ignoreMap":14},[2147,8909,8910],{"class":2149,"line":2150},[2147,8911,8912],{"class":2153},"\u002F\u002F AppServiceProvider::boot()\n",[2147,8914,8915,8918,8920,8923,8925,8927,8930,8932,8934,8937],{"class":2149,"line":688},[2147,8916,8917],{"class":2167},"Model",[2147,8919,2171],{"class":2163},[2147,8921,8922],{"class":2174},"preventLazyLoading",[2147,8924,2202],{"class":2159},[2147,8926,2237],{"class":2163},[2147,8928,8929],{"class":2174}," app",[2147,8931,2257],{"class":2159},[2147,8933,2196],{"class":2163},[2147,8935,8936],{"class":2174},"isProduction",[2147,8938,8939],{"class":2159},"());\n",[139,8941,8942],{},"After that, any suboptimal query fails with an error already in development. In the report we show before and after in numbers, not general words about \"optimization\".",[346,8944,8946],{"id":8945},"database-and-data","Database and data",[139,8948,8949,8950,8953,8954,8956],{},"We check the schema: whether it can be rebuilt from scratch by migrations, whether there are foreign keys and unique constraints, how money is stored (integers in minor units or ",[1529,8951,8952],{},"decimal",", but not ",[1529,8955,7193],{},"), and whether there are \"junk\" tables and duplicates. For a store, order statuses, stock and prices matter especially: errors here cost money.",[346,8958,8960],{"id":8959},"dependencies-and-maintainability","Dependencies and maintainability",[139,8962,8963],{},"PHP version, Laravel version, support status, packages with no updates for several years. We do not advise \"updating everything at once\"; we build a realistic plan: what is critical (vulnerabilities), what is desirable, what can wait.",[346,8965,8967],{"id":8966},"infrastructure-and-deployment-process","Infrastructure and deployment process",[160,8969,8970,8973,8976,8979,8982],{},[163,8971,8972],{},"How is the project deployed: manually over FTP or automatically with test checks?",[163,8974,8975],{},"Is there a staging environment distinct from production?",[163,8977,8978],{},"Backups: has anyone ever restored one?",[163,8980,8981],{},"Error and uptime monitoring: do you learn about an outage from a system or from a customer?",[163,8983,8984],{},"Logs and alerts.",[139,8986,8987],{},"A backup that has never been restored is a hypothesis, not a backup. We check this first.",[152,8989,8991],{"id":8990},"what-the-audit-result-looks-like","What the audit result looks like",[139,8993,8994],{},"A good report answers three questions: what is wrong, how critical it is and what to do first. We structure it like this:",[243,8996,8997,9007],{},[246,8998,8999],{},[249,9000,9001,9004],{},[252,9002,9003],{},"Section",[252,9005,9006],{},"Content",[262,9008,9009,9017,9025,9033,9041,9049],{},[249,9010,9011,9014],{},[267,9012,9013],{},"Executive summary",[267,9015,9016],{},"1 page for the owner: overall state and main risks",[249,9018,9019,9022],{},[267,9020,9021],{},"Findings",[267,9023,9024],{},"A description of where exactly in the code, why it is a problem, how to reproduce",[249,9026,9027,9030],{},[267,9028,9029],{},"Priorities",[267,9031,9032],{},"Critical \u002F serious \u002F desirable, an assessment of impact on money and risk",[249,9034,9035,9038],{},[267,9036,9037],{},"Fix plan",[267,9039,9040],{},"Sequence of work, approximate effort, dependencies",[249,9042,9043,9046],{},[267,9044,9045],{},"Quick wins",[267,9047,9048],{},"What can be done in 1–2 days with a visible effect",[249,9050,9051,9054],{},[267,9052,9053],{},"Process recommendations",[267,9055,9056],{},"Tests, CI, review, monitoring",[139,9058,9059],{},"We do not write \"the code is bad\" or \"the code is good\". Every finding is tied to a file or scenario, and the priority to its consequences: \"this vulnerability lets someone view other people's orders\", \"this query slows the catalog down fivefold\".",[139,9061,9062],{},"It is prioritization that distinguishes a useful audit from an 80-page list of remarks. A business owner is not obliged to fix everything but has to know what is on fire and what can be left.",[457,9064,9065],{},[139,9066,9067,9070,9071,366],{},[166,9068,9069],{},"Need an audit of your Laravel project?"," We review architecture, security and performance and deliver a report with a fix plan in 5–7 days. The price is from UAH 12,000; details are on the ",[146,9072,5439],{"href":5438},[152,9074,9076],{"id":9075},"how-much-it-costs-and-how-long-it-takes","How much it costs and how long it takes",[139,9078,9079,9080,9082],{},"The cost depends on the size of the codebase, the number of integrations and the depth of review. On the ",[146,9081,149],{"href":148}," a code audit of a Laravel project costs UAH 12,000–25,000 depending on scope and takes 5–7 days. We name the exact sum after a short look at the project: repository size, number of models and routes, presence of tests.",[139,9084,9085],{},"Factors that increase the scope:",[160,9087,9088,9091,9094,9097],{},[163,9089,9090],{},"a large number of integrations (payment, delivery, CRM, 1C);",[163,9092,9093],{},"no documentation or tests, so we have to figure things out \"from scratch\";",[163,9095,9096],{},"several applications (backend, Nuxt frontend, mobile client);",[163,9098,9099],{},"an old framework version with non-standard patches.",[139,9101,9102,9103,9106],{},"For a consultation without a full audit there is the ",[146,9104,9105],{"href":4669},"Tech Lead by the hour"," format: 90 minutes of conversation, an architecture review and answers to specific questions.",[152,9108,9110],{"id":9109},"what-to-prepare-for-an-audit","What to prepare for an audit",[139,9112,9113],{},"To keep the audit quick and free of unnecessary pauses, provide:",[532,9115,9116,9119,9122,9125,9128,9131],{},[163,9117,9118],{},"Read-only access to the repository (or a code archive).",[163,9120,9121],{},"A copy of the database without personal data, or access to a test environment.",[163,9123,9124],{},"A project description: what it does, who the users are, which scenarios matter most to the business.",[163,9126,9127],{},"A list of integrations and external services.",[163,9129,9130],{},"Known problems: what is slow, what breaks, what scares the developers.",[163,9132,9133],{},"Infrastructure information: hosting, deployment method, monitoring.",[139,9135,9136],{},"Secrets (API keys, passwords) are not needed: we work with anonymized or test values. A non-disclosure agreement can be signed before we receive the code.",[152,9138,9140],{"id":9139},"what-to-do-with-the-report-next","What to do with the report next",[139,9142,9143],{},"After the audit you have three paths:",[160,9145,9146,9152,9158],{},[163,9147,9148,9151],{},[166,9149,9150],{},"Fix it yourself or with your current team."," The report serves as the requirements.",[163,9153,9154,9157],{},[166,9155,9156],{},"Hand the fixes to us."," A fix package is calculated at rates of $20–35 per hour according to the volume of findings.",[163,9159,9160,9163,9164,220],{},[166,9161,9162],{},"Use the report as an argument in a conversation with a contractor"," or when choosing a new one. Read about that in ",[146,9165,9166],{"href":661},"How to Choose a Web Development Contractor",[139,9168,9169,9170,220],{},"A common mistake is trying to fix everything at once. It is better to follow priorities: first security and loss of money, then performance, then code structure. And accompany every fix with a test so as not to break neighboring functionality. If the project is critical to the business, it is useful to add automated checks: more in the article on ",[146,9171,1856],{"href":1855},[152,9173,9175],{"id":9174},"how-to-tell-a-quality-audit-from-a-formal-one","How to tell a quality audit from a formal one",[139,9177,9178],{},"The audit market is uneven: from an automatic static analyzer report to a deep architecture review. A few signs will help you assess an offer.",[160,9180,9181,9190,9196,9202,9208],{},[163,9182,9183,9186,9187,9189],{},[166,9184,9185],{},"Manual review, not just tools."," Static analyzers (PHPStan, Larastan) and ",[1529,9188,8727],{}," are useful, but they do not see business logic. If the whole audit is a printout of tool output, you are paying for something you could have run yourself.",[163,9191,9192,9195],{},[166,9193,9194],{},"Findings with examples."," Every remark has a file, a line and an explanation of consequences. \"We recommend improving security\" is not a finding.",[163,9197,9198,9201],{},[166,9199,9200],{},"An effort estimate."," A fix plan without approximate hours will not help you plan a budget.",[163,9203,9204,9207],{},[166,9205,9206],{},"A conversation with you, not just with the code."," The auditor should ask what matters most to the business: catalog speed, payment reliability or ease of change. The priorities depend on it.",[163,9209,9210,9213],{},[166,9211,9212],{},"Honest limits."," A good auditor says directly what was not checked: for example load testing or the legal compliance of personal data processing.",[139,9215,9216],{},"If an audit is offered for free \"as a gift\" with development, remember the conflict of interest: the one who writes the code rarely assesses their own work objectively.",[152,9218,1975],{"id":1974},[139,9220,9221],{},"A code audit is not about \"finding who is to blame\". It is a way to replace the feeling that \"something is off\" with a concrete list of problems, risks and actions. You need it when a project changes owner or team, slows down, gets more expensive to develop or prepares for growth. The result is a report that can be handed to any team and carried out in stages.",[457,9223,9224],{},[139,9225,9226,9229,9230,9232,9233,9237],{},[166,9227,9228],{},"Want to understand the real state of your project?"," Leave a ",[146,9231,355],{"href":354}," or write to us on the ",[146,9234,9236],{"href":9235},"\u002Fen\u002Fcontacts","contacts"," page: we will look at the project and tell you whether you need a full audit or a consultation will do.",[2768,9239,9240],{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":14,"searchDepth":688,"depth":689,"links":9242},[9243,9244,9245,9253,9254,9255,9256,9257,9258],{"id":8330,"depth":688,"text":8331},{"id":8372,"depth":688,"text":8373},{"id":8426,"depth":688,"text":8427,"children":9246},[9247,9248,9249,9250,9251,9252],{"id":8430,"depth":689,"text":8431},{"id":5230,"depth":689,"text":5114},{"id":8859,"depth":689,"text":8860},{"id":8945,"depth":689,"text":8946},{"id":8959,"depth":689,"text":8960},{"id":8966,"depth":689,"text":8967},{"id":8990,"depth":688,"text":8991},{"id":9075,"depth":688,"text":9076},{"id":9109,"depth":688,"text":9110},{"id":9139,"depth":688,"text":9140},{"id":9174,"depth":688,"text":9175},{"id":1974,"depth":688,"text":1975},"audit.report.md",[9261,9263,9265,9267],{"marker":2023,"text":9262},"SQL query in a loop: 1,200 queries",{"marker":713,"text":9264},"business logic in the controller",{"marker":713,"text":9266},"dependencies not updated",{"marker":716,"text":9268},"migrations reproduce the DB schema","5 days",[9271,9274,9277,9280],{"q":9272,"a":9273},"What is checked during a technical code audit?","Architecture, security, performance, code quality, dependencies, tests and the way the project is deployed. In a Laravel project we look at database queries, validation, access rights and error handling. The result is a report with priorities, not just a list of remarks, so the owner sees what is urgent and what can safely wait.",{"q":9275,"a":9276},"How much does a Laravel code audit cost?","A technical audit of a Laravel project costs UAH 12,000–25,000, depending on the size of the codebase, the number of integrations and the depth of the review. We give an exact figure after a short look at the project. Fixing the findings is paid separately at $20–35 per hour, and we agree the final cost with you before work starts.",{"q":9278,"a":9279},"How long does a code audit take?","Usually 5–7 days until the report with a fix plan is delivered. The timeline grows with many integrations, missing documentation and tests, several applications or an outdated framework version. We give an estimate after reviewing the project. The report orders findings by business impact, so you can decide on fixes right away.",{"q":9281,"a":9282},"What do I need to prepare for a code audit?","Read-only access to the repository, an anonymized copy of the database or a test environment, a description of the project and its integrations, and a list of known problems. Passwords and API keys are not needed: we work with test values. A non-disclosure agreement can be signed before you hand over any code.","shcho-take-tekhnichnyi-audyt-kodu",{},{"title":3155,"description":8324},"What a code audit covers (architecture, security, performance), what the report looks like, when to order one and what it costs: from UAH 12,000.","What Is a Code Audit and When Do You Need One — Tochno.","what-is-code-audit","en\u002Fblog\u002Fwhat-is-code-audit",[5551],"BAvRnkf9KwEQO4-W1D6Wgpnzm9ns8xB3FtxtSRHXzNk",{"id":9293,"title":9294,"author":134,"body":9295,"cover":704,"coverFile":9819,"coverLines":9820,"coverStat":9829,"date":719,"description":9299,"excerpt":704,"extension":720,"faq":9830,"featured":734,"key":9843,"meta":9844,"navigation":737,"path":9845,"readingTime":739,"seo":9846,"seoDescription":9847,"seoTitle":9848,"slug":9849,"stem":9850,"tags":9851,"updated":747,"__hash__":9852},"blog_en\u002Fen\u002Fblog\u002Fwhen-you-need-a-tech-lead.md","When You Need a Tech Lead and When a Developer Is Enough",{"type":136,"value":9296,"toc":9801},[9297,9300,9303,9307,9310,9381,9384,9387,9391,9394,9437,9441,9444,9494,9497,9501,9504,9521,9524,9528,9531,9535,9538,9542,9550,9554,9560,9564,9567,9571,9574,9578,9581,9642,9654,9657,9668,9672,9675,9695,9698,9702,9705,9743,9746,9750,9782,9784,9787,9790],[139,9298,9299],{},"You need a tech lead when the project's problem is no longer in individual tasks but in decisions: which architecture to use, who is responsible for code quality, how to estimate and plan, what to do about technical debt. If the task is clear, the stack is known and the team works without conflict, a good developer is enough. Below we break down the difference in practice and give a list of signs that tell you whether you need a tech lead rather than another pair of hands.",[139,9301,9302],{},"This is written by someone who has written PHP and Laravel for 11 years and knows both roles from the inside, so many examples below are typical of Laravel projects, but the logic applies to any stack.",[152,9304,9306],{"id":9305},"who-a-tech-lead-is-and-how-they-differ-from-a-developer","Who a tech lead is and how they differ from a developer",[139,9308,9309],{},"A developer solves a task: builds a feature, fixes a bug, integrates an API. A tech lead is responsible for making a set of such tasks add up to a working, maintainable product.",[243,9311,9312,9324],{},[246,9313,9314],{},[249,9315,9316,9318,9321],{},[252,9317],{},[252,9319,9320],{},"Developer",[252,9322,9323],{},"Tech lead",[262,9325,9326,9337,9348,9359,9370],{},[249,9327,9328,9331,9334],{},[267,9329,9330],{},"Focus",[267,9332,9333],{},"a task or feature",[267,9335,9336],{},"the system and the team",[249,9338,9339,9342,9345],{},[267,9340,9341],{},"Question",[267,9343,9344],{},"\"how do I build this?\"",[267,9346,9347],{},"\"should we build this, and how will it fit?\"",[249,9349,9350,9353,9356],{},[267,9351,9352],{},"Output",[267,9354,9355],{},"working code",[267,9357,9358],{},"architecture decisions, standards, predictability",[249,9360,9361,9364,9367],{},[267,9362,9363],{},"Responsibility",[267,9365,9366],{},"the quality of their own code",[267,9368,9369],{},"the quality of the final product and team alignment",[249,9371,9372,9375,9378],{},[267,9373,9374],{},"Communication",[267,9376,9377],{},"with the team lead and colleagues",[267,9379,9380],{},"with the client, product owner, business, contractors",[139,9382,9383],{},"Do not confuse the role with a senior developer. A senior writes complex code quickly and reliably. A tech lead also writes code but spends a significant part of their time on review, decisions, risk assessment and explaining to the client what can be done for the money and time. A strong senior does not necessarily want or know how to be a tech lead, and that is fine.",[139,9385,9386],{},"A tech lead is also not the same as a team lead or a project manager. A team lead is responsible for people and processes, a PM for deadlines and budget, a tech lead for technical decisions. On small teams these roles are often combined in one person, but the responsibilities differ.",[152,9388,9390],{"id":9389},"what-a-tech-lead-actually-does","What a tech lead actually does",[139,9392,9393],{},"Concrete tasks, not general words:",[160,9395,9396,9401,9407,9413,9419,9425,9431],{},[163,9397,9398,9400],{},[166,9399,3395],{}," Choosing the application structure, module boundaries, data storage, caching and queues. Recording decisions in short documents (ADRs): what was decided, why, and what the alternatives were.",[163,9402,9403,9406],{},[166,9404,9405],{},"Code review."," Checking changes before merging: correctness, security, performance, compliance with agreements. Review also teaches the team.",[163,9408,9409,9412],{},[166,9410,9411],{},"Standards and automation."," Linters, static analysis (for example PHPStan or Larastan), branch rules, CI with tests. This is done once and then works without reminders.",[163,9414,9415,9418],{},[166,9416,9417],{},"Estimation and planning."," Breaking functionality into tasks, spotting risks and dependencies, an honest estimate of timelines with uncertainty stated.",[163,9420,9421,9424],{},[166,9422,9423],{},"Technical debt."," Deciding what to fix now and what to leave, with the cost of postponing calculated.",[163,9426,9427,9430],{},[166,9428,9429],{},"Selecting and vetting contractors and technologies."," Assessing other people's proposals and code, spotting inflated estimates and unreliable solutions.",[163,9432,9433,9436],{},[166,9434,9435],{},"A bridge to the business."," Translating from the language of requirements to the language of technical trade-offs and back.",[152,9438,9440],{"id":9439},"signs-you-need-a-tech-lead","Signs you need a tech lead",[139,9442,9443],{},"This list is not academic; we see these symptoms in real projects. If three or more match, it is worth thinking about.",[532,9445,9446,9452,9458,9464,9470,9476,9482,9488],{},[163,9447,9448,9451],{},[166,9449,9450],{},"Every release breaks something nearby."," This signals that there are no tests and no boundaries between modules, and nobody is responsible for integrity.",[163,9453,9454,9457],{},[166,9455,9456],{},"Estimates constantly slip."," Tasks \"for two days\" take a week. The cause is often not laziness but hidden complexity that nobody examined in advance.",[163,9459,9460,9463],{},[166,9461,9462],{},"A new person takes months to get into the project."," There is no documentation, no standards, no clear structure, and one person knows everything.",[163,9465,9466,9469],{},[166,9467,9468],{},"Dependence on one developer."," If they leave or fall ill, the project stops. This is a business risk, not only a technical one.",[163,9471,9472,9475],{},[166,9473,9474],{},"A slow site or crashes under load, and the causes are not found."," Someone puts out fires, and nobody deals with systemic causes.",[163,9477,9478,9481],{},[166,9479,9480],{},"It is unclear how much this should cost."," The contractor names a sum and you cannot judge whether it is adequate.",[163,9483,9484,9487],{},[166,9485,9486],{},"Decisions are made \"as it goes\"."," Different parts of the system are written in different styles, and there are three ways to do the same thing.",[163,9489,9490,9493],{},[166,9491,9492],{},"Strategic questions without answers."," Rewrite or patch, move to another stack, build microservices or keep the monolith.",[139,9495,9496],{},"By the way, the first seven items are usually not cured by hiring one more developer. An extra person added to an unmanaged process adds chaos.",[152,9498,9500],{"id":9499},"when-a-developer-is-enough","When a developer is enough",[139,9502,9503],{},"A tech lead is not always needed, and you should not overpay for one. A developer is enough if:",[160,9505,9506,9509,9512,9515,9518],{},[163,9507,9508],{},"the project is small (a landing page, a corporate site, a typical online store on a ready base) and the stack is standard;",[163,9510,9511],{},"tasks are clearly formulated and estimated without much uncertainty;",[163,9513,9514],{},"one or two trusted people work on the project;",[163,9516,9517],{},"requirements are stable and there are almost no architectural decisions;",[163,9519,9520],{},"working processes already exist: tests, review, CI, and you can see that they work.",[139,9522,9523],{},"Example: you want to add a new delivery method or a recommendations block to an existing store. The task is local, and a developer solves it in a few days. A tech lead is worth it here only for a short consultation.",[152,9525,9527],{"id":9526},"typical-scenarios-when-a-tech-lead-saves-a-project","Typical scenarios when a tech lead saves a project",[139,9529,9530],{},"Below are generalized situations we run into. These are not specific clients but patterns.",[346,9532,9534],{"id":9533},"scenario-1-starting-a-new-project","Scenario 1. Starting a new project",[139,9536,9537],{},"At the start the most expensive mistakes are the choice of stack, the data structure and the boundaries between modules. Fixing them a year later costs many times more. A tech lead for 5–10 hours a week is enough here: they draw up a technical plan, make the key decisions and set up processes, while developers implement.",[346,9539,9541],{"id":9540},"scenario-2-inherited-code","Scenario 2. \"Inherited\" code",[139,9543,9544,9545,9547,9548,220],{},"The project was left over from a previous contractor. The code works, but nobody knows how reliable it is. Before investing money in development, it is worth doing a ",[146,9546,5439],{"href":5438},": get an assessment of architecture, security and performance and a fix plan ordered by priority. More on this in ",[146,9549,3155],{"href":3154},[346,9551,9553],{"id":9552},"scenario-3-choosing-and-controlling-a-contractor","Scenario 3. Choosing and controlling a contractor",[139,9555,9556,9557,220],{},"You hire a studio or a freelancer but cannot verify their estimates and code. A tech lead on your side writes the requirements, evaluates proposals and reviews the results at milestones. This insures you against overpaying and against a project that is \"supposedly ready\" but unmaintainable. We collected the questions to ask a contractor separately: ",[146,9558,9559],{"href":661},"12 Questions to Ask Before Choosing a Contractor",[346,9561,9563],{"id":9562},"scenario-4-there-is-a-team-but-results-are-unpredictable","Scenario 4. There is a team, but results are unpredictable",[139,9565,9566],{},"The developers are good, but deadlines slip and quality is uneven. Often there is no need to change people; what is needed are standards, review and a testing strategy. A tech lead introduces these practices within a few weeks and then only maintains them.",[346,9568,9570],{"id":9569},"scenario-5-scaling-and-slowness","Scenario 5. Scaling and slowness",[139,9572,9573],{},"The site slows down, the database grows, locks and slow queries appear. A typical cause in Laravel projects: N+1 queries, missing indexes, heavy synchronous operations that should be moved to queues. A tech lead quickly finds bottlenecks by profiling and sets tasks in order of impact.",[152,9575,9577],{"id":9576},"ways-to-work-with-a-tech-lead","Ways to work with a tech lead",[139,9579,9580],{},"A tech lead does not have to be a full-time staff position. For small and medium businesses the fractional format works best.",[243,9582,9583,9596],{},[246,9584,9585],{},[249,9586,9587,9590,9593],{},[252,9588,9589],{},"Format",[252,9591,9592],{},"What it is for",[252,9594,9595],{},"Price benchmark",[262,9597,9598,9609,9620,9631],{},[249,9599,9600,9603,9606],{},[267,9601,9602],{},"One-off consultation (90 min)",[267,9604,9605],{},"assess the architecture, stack, a contractor's proposal",[267,9607,9608],{},"UAH 3,000–4,000",[249,9610,9611,9614,9617],{},[267,9612,9613],{},"Code audit",[267,9615,9616],{},"understand the state of the project and get a fix plan",[267,9618,9619],{},"from UAH 12,000",[249,9621,9622,9625,9628],{},[267,9623,9624],{},"Tech lead for a project",[267,9626,9627],{},"plan, decisions, review for the development period",[267,9629,9630],{},"hourly rate, $32–35 \u002F h",[249,9632,9633,9636,9639],{},[267,9634,9635],{},"Dedicated specialist on your team",[267,9637,9638],{},"ongoing participation part-time or full-time",[267,9640,9641],{},"from $1,800 \u002F month",[139,9643,9644,9645,9648,9649,9651,9652,366],{},"These are our benchmarks from the pricing page; we name the exact cost depending on scope after the brief. The details of the ",[146,9646,9647],{"href":4669},"outsourced tech lead"," format and the terms of a ",[146,9650,6123],{"href":6122}," are described on separate pages, and general rates are collected on the ",[146,9653,467],{"href":148},[139,9655,9656],{},"A fractional tech lead is effective because most decisions do not need daily presence: it is enough to plan, do the review, resolve a hard question and explain it to the team. The rest of the time the team works by the agreed rules.",[457,9658,9659],{},[139,9660,9661,9663,9664,9667],{},[166,9662,5354],{}," Start with a 90-minute tech lead consultation: we will go through the project and the risks and tell you whether you need a tech lead permanently or a developer is enough. ",[146,9665,9666],{"href":4669},"Outsourced tech lead",", from UAH 3,000.",[152,9669,9671],{"id":9670},"how-much-time-a-tech-lead-needs-on-a-project","How much time a tech lead needs on a project",[139,9673,9674],{},"The most frequent question: how many hours a week are needed? There is no single answer, but practical benchmarks are as follows (estimates, depending on the project):",[160,9676,9677,9683,9689],{},[163,9678,9679,9682],{},[166,9680,9681],{},"Project start, the first 2–4 weeks:"," 8–15 hours a week. This is the most intensive period: architecture, CI setup, the first reviews, breaking work into tasks.",[163,9684,9685,9688],{},[166,9686,9687],{},"Steady development by a team of 2–3 people:"," 4–8 hours a week. Review, answers to technical questions, planning the next iterations.",[163,9690,9691,9694],{},[166,9692,9693],{},"Support of an already launched product:"," 2–4 hours a week or on request: complex incidents, estimating new features, controlling technical debt.",[139,9696,9697],{},"If someone offers a \"tech lead for 40 hours\" for a project with one person on the team, it is most likely a developer with a louder title. Check what exactly is included in the work: decisions and review, or just writing code.",[152,9699,9701],{"id":9700},"how-to-tell-that-a-tech-lead-is-doing-well","How to tell that a tech lead is doing well",[139,9703,9704],{},"If you hired a tech lead, here are signs of results that you can check without deep technical knowledge:",[160,9706,9707,9713,9719,9725,9731,9737],{},[163,9708,9709,9712],{},[166,9710,9711],{},"written decisions have appeared."," Even short notes: what was chosen and why;",[163,9714,9715,9718],{},[166,9716,9717],{},"release has become routine."," Deployment does not require heroics and does not break neighboring features;",[163,9720,9721,9724],{},[166,9722,9723],{},"estimates have become more accurate."," Deviations shrink and risks are voiced in advance;",[163,9726,9727,9730],{},[166,9728,9729],{},"all changes go through code review."," And it is not a formality: meaningful remarks are visible in the comments;",[163,9732,9733,9736],{},[166,9734,9735],{},"a new person gets into the project in days, not months."," There is a README, a setup guide and style agreements;",[163,9738,9739,9742],{},[166,9740,9741],{},"you understand what is going on."," The tech lead explains technical things in business language: what, why, how much.",[139,9744,9745],{},"If after several months none of this has changed and the tech lead just \"writes a lot of code\", you probably just need a senior developer, which is fine, you just should not pay for a different role.",[152,9747,9749],{"id":9748},"mistakes-when-hiring-a-tech-lead","Mistakes when hiring a tech lead",[160,9751,9752,9758,9764,9770,9776],{},[163,9753,9754,9757],{},[166,9755,9756],{},"Looking for a \"hero\" who will do everything alone."," A tech lead sets up the system, not replaces it.",[163,9759,9760,9763],{},[166,9761,9762],{},"Not giving authority."," If a tech lead can only advise while decisions are made elsewhere, the benefit is minimal.",[163,9765,9766,9769],{},[166,9767,9768],{},"Confusing with a theoretical architect."," A good tech lead regularly writes code or at least reads it, otherwise decisions drift away from reality.",[163,9771,9772,9775],{},[166,9773,9774],{},"Hiring full-time when part-time is enough."," For a project of 2–3 developers a few hours a week is usually enough.",[163,9777,9778,9781],{},[166,9779,9780],{},"Not checking experience on real projects."," Ask them to describe specific decisions, their consequences and mistakes. An honest story about failures says more than a list of technologies.",[152,9783,1975],{"id":1974},[139,9785,9786],{},"In short: a developer is needed to complete a task; a tech lead is needed to choose the right tasks and decisions and to keep the system from falling apart as it grows. If several signs from the list above match your project (missed deadlines, dependence on one person, fragile releases, unclear code quality), start with a small step: a consultation or an audit. It is inexpensive and quickly shows whether you need a tech lead on a permanent basis.",[139,9788,9789],{},"If the project is simple and the developer works without fires, there is no need to overpay for a tech lead. We will say so honestly, because it is better to lose an order than to sell something unnecessary.",[457,9791,9792],{},[139,9793,9794,9797,9798,220],{},[166,9795,9796],{},"Want to figure out which role your project needs?"," Tell us about the task and we will advise whether a developer is enough or you need a tech lead, with no obligation. ",[146,9799,9800],{"href":9235},"Contact us",{"title":14,"searchDepth":688,"depth":689,"links":9802},[9803,9804,9805,9806,9807,9814,9815,9816,9817,9818],{"id":9305,"depth":688,"text":9306},{"id":9389,"depth":688,"text":9390},{"id":9439,"depth":688,"text":9440},{"id":9499,"depth":688,"text":9500},{"id":9526,"depth":688,"text":9527,"children":9808},[9809,9810,9811,9812,9813],{"id":9533,"depth":689,"text":9534},{"id":9540,"depth":689,"text":9541},{"id":9552,"depth":689,"text":9553},{"id":9562,"depth":689,"text":9563},{"id":9569,"depth":689,"text":9570},{"id":9576,"depth":688,"text":9577},{"id":9670,"depth":688,"text":9671},{"id":9700,"depth":688,"text":9701},{"id":9748,"depth":688,"text":9749},{"id":1974,"depth":688,"text":1975},"tech-lead.decision",[9821,9823,9825,9827],{"marker":713,"text":9822},"every release breaks a neighboring module",{"marker":713,"text":9824},"estimates slip, nobody knows why",{"marker":708,"text":9826},"solution: a fractional tech lead",{"marker":716,"text":9828},"ADRs, code review, technical plan","5 signs",[9831,9834,9837,9840],{"q":9832,"a":9833},"How is a tech lead different from a senior developer?","A developer is responsible for the quality of their own code, while a tech lead owns the technical decisions of the whole project: architecture, standards, estimates and risks. A senior developer can fill this role only if they have the time and authority. These are different responsibilities, not just levels of experience.",{"q":9835,"a":9836},"How much does a tech lead consultation cost?","Our 90-minute tech lead consultation costs UAH 3,000–4,000. In that time we review your architecture or work plan and give concrete recommendations. Longer project support is discussed separately, because the scope depends on your team and tasks.",{"q":9838,"a":9839},"Does a small project need a tech lead?","Usually not, if the project is simple, the tasks are clear and the developer is experienced. You need a tech lead when releases break neighboring modules, estimates keep slipping, or several developers work without shared standards. Then even a few hours a month can save you from costly mistakes.",{"q":9841,"a":9842},"Can a tech lead work part time?","Yes, a fractional format suits small companies: the tech lead works on your project a few hours a week or month. They make architecture decisions, run code reviews and prepare the technical plan. Full-time is mostly justified for a large team. It costs less than a staff hire and still gives you control over quality.","koly-potriben-tekh-lid",{},"\u002Fen\u002Fblog\u002Fwhen-you-need-a-tech-lead",{"title":9294,"description":9299},"Hire a tech lead or is a developer enough? Signs your project needs a tech lead, how it differs from a senior, engagement formats and price benchmarks.","When to Hire a Tech Lead Instead of a Developer — Tochno.","when-you-need-a-tech-lead","en\u002Fblog\u002Fwhen-you-need-a-tech-lead",[5551],"zetjRs-9z7qcNdSvqzmZ5O2O_BK79gt66w-MMzvrcXM",1794139964597]