About Services Cases Blog Tools Reviews Partners Contacts

SEO for a Food Delivery Website: What I Did and Why — A Real Report Breakdown

SEO for a Food Delivery Website: A Real Audit Report

I’m a Google Ads specialist, but ads don’t live separately from the website. If a site is slow, with outdated delivery terms and errors, ads bring people nowhere, and organic traffic simply passes by.

Below I break down a real report on SEO and technical optimization of a food delivery website. An OpenCart online store (ocStore 3.0.2.0) with two language versions — Ukrainian and Russian. The work was completed on 29.10.2026. There are no ranking results yet, they’ll show in a few weeks, so this is only about what was done and why.

Contents
  1. How I made the changes
  2. Content: categories, home page, restaurants, contacts
  3. Blog
  4. Delivery terms: the same on every page
  5. Delivery pages by district
  6. Technical SEO
  7. Internal linking
  8. Structured data and link previews
  9. A new restaurant in Chuhuiv
  10. Google Business
  11. Speed
  12. Checkout
  13. What’s left for the developer
  14. Summary table of changes
  15. What’s next
  16. How much this kind of work costs
  17. FAQ
MetricBeforeAfter
Categories with texts and meta tagsoutdated or empty15 × 2 languages
Blog articlesoutdated49 rewritten + 4 new
Delivery pages by district011 × 2 languages
URLs in the sitemap620 with junk, UA sitemap with an error408 live in each sitemap
Home page CLS on mobile0,3830
301 redirects—64
First page of the SEO and technical optimization report for a food delivery website with key numbers
First page of the report with key numbers

How I made the changes

I made all changes to the database and files with scripts. Each script first shows a “before → after” preview, makes a backup and has a rollback button. That matters when you work on a live site that takes orders every day.

Running a script again doesn’t duplicate anything: if a block has already been added, the script skips it. After every run the template cache was cleared, and I checked the result on the site and in Search Console.

Script preview: category description and meta tags “before → after”
Script preview “before → after”

Content: categories, home page, restaurants, contacts

Categories. For 15 menu categories (pizza, burgers, fries, deals, WOK, pizzone, sushi, sushi sets, salads, soups, desserts, breakfasts, drinks, beer, alcohol) I wrote a “Details” block at the bottom of the page, meta title and meta description in two languages. Categories are the main landing pages for queries like “pizza delivery Brovary” or “sushi Troieshchyna”. Previously the texts were outdated or empty, and on top of that showed the wrong free delivery threshold. I built the new texts around real queries from Search Console.

“Details” block at the bottom of the Pizza category with delivery terms and links to blog articles
“Details” block in the Pizza category with links to articles

Home page and restaurant pages. New text and meta tags, up-to-date delivery terms. The pages of three restaurants (Troieshchyna and two in Brovary) got new texts. I marked closed locations as “closed” with an offer to order delivery from the open ones. Information about closed locations misled customers, and the traffic to these pages shouldn’t be lost. Local queries like “pizzeria Troieshchyna” lead exactly here. I also added styles to the restaurant page template, because the text there was hard to read.

Contacts. The H1, title and description of the contacts page didn’t mention Chuhuiv, where a new restaurant had opened. Now they do: “Pizzeria phone numbers and addresses — Kyiv, Brovary, Chuhuiv”. At the same time I removed an outdated “updated” note with a date from the title.

Script for the contacts page: title and description “before → after”, Chuhuiv added
Contacts: Chuhuiv in the heading, title and description

Product meta tags. Previously all products had the same template titles. For 100 priority records (top 50 products by impressions × 2 languages) I wrote individual titles and descriptions. I adjusted the product page code: if a product has its own meta tags, they are used, otherwise the general template applies, as before. I also fixed emoji in meta tags: the database in utf8 encoding can’t store 4-byte emoji, and they turned into “?”. There were 172 such records; I replaced the emoji with allowed characters.

“Hamburger” product page with a meta tag check: new title, description, language and canonical
Product page: new title, description and canonical

Blog

49 articles rewritten. I took the articles that already had impressions in Google: new H1 headings, texts, meta tags, internal links to categories and other articles, a “Read also” block at the end. I removed outdated deals, wrong amounts and addresses of closed locations. Both language versions. I also fixed the article template styles: headings overlapped the text.

Updated blog article with an H2 heading structure and internal links
Updated article with an H2 structure, links and delivery terms

4 new articles for informational queries, each in two languages and with a 1920×600 banner:

ArticleTarget queriesPublication
Pizza caloriespizza calories, kcal per slicepublished
Pizza: 30 or 40 cmwhich pizza to order, how many pizzas for a grouppublished
Chicken roll: what it is, ingredients and pricechicken roll what is it, ingredients, price30.10
How many rolls to order per personhow many rolls to order, sushi set for a group30.10
Banner of the new blog article “How many rolls per person” for a sushi delivery website
Banner of the new article “How many rolls per person”

The blog collects informational traffic and passes weight to categories through internal links. And up-to-date facts in articles mean trust and conversion.

Delivery terms: the same on every page

This is probably the most important part for the business. The “Delivery and payment” page was in Russian only, with outdated terms and without the right bank of Kyiv. Now it’s in two languages, with tables by zone:

ZoneDistrictsMin. orderOrders untilDelivery time
1Kyiv: Troieshchyna, Voskresenka, Lisovyi, Rusanivka, Livoberezhnyi, Berezniaky; Brovary400 UAH22:00up to 59 min
2Villages near Brovary: Pohreby, Kalynivka, Kvitneve, Trebukhiv, Kniazhychi800 UAH22:00up to 89 min
3Kyiv: DVRZ; near Brovary: Hoholiv, Bezuhlivka, Chubynske, Shchaslyve, Hora1,200 UAH21:00up to 89 min
4Kyiv: Pozniaky, Osokorky, Kharkivskyi, Darnytsia, Bortnychi + the right bank2,000 UAH21:00up to 89 min
Chuhuiv 1City center400 UAH21:00up to 59 min
Chuhuiv 2Aviator neighborhood and surroundings800 UAH20:30up to 89 min
Chuhuiv 3Eskhar, Malynivka, Kluhyno-Bashkyrivka1,300 UAH20:00up to 89 min
New delivery terms page with a table by zone: minimum amount, order cut-off time, delivery time
New delivery terms page with a table by zone
Map of delivery zones in Kyiv and Brovary with district boundaries
Delivery zone map: Kyiv and Brovary
Map of delivery zones in Chuhuiv: center, Aviator, Eskhar, Malynivka
Delivery zone map: Chuhuiv

But updating one page isn’t enough. The old terms had spread across category descriptions, articles and restaurant pages. A separate script fixed them everywhere. For example, for the far zone the texts said “from 400 UAH ($8.9), up to 59 minutes”, while in reality it’s “from 2,000 UAH ($44), up to 89 minutes, until 21:00”. For DVRZ and the villages I set their minimum amounts and added a link to the terms page everywhere.

What this meant in practice: customers from remote districts read “free from 400 UAH” and got turned down by the operator. The same terms on every page mean fewer refusals and complaints, and more trust from both people and Google.

Delivery pages by district

I created 11 landing pages for “pizza delivery + district” queries, each in two languages — 22 URLs: Pozniaky, Osokorky, Kharkivskyi, Bortnychi, DVRZ, Berezniaky, Rusanivka, Voskresenka, Lisovyi, Livoberezhnyi and the Right Bank.

  • each page has the terms of that specific zone, district landmarks, popular dishes, deals, a “Questions and answers” block, buttons to the menu and a phone number;
  • I generated 1920×600 banners in Gemini and converted them to WebP, separately for the UA and RU versions;
  • I added links to the district pages from the delivery terms page and from articles about the left bank;
  • publication on 30.10, and on the same day an indexing request in Search Console for the 6 main URLs.

Previously, for queries like “pizza Osokorky” or “pizza delivery Pozniaky” the site simply had no matching page. A separate page per district is the main way to get this kind of traffic.

Pizza delivery page for the Osokorky district with a banner, zone terms and menu buttons
“Pizza delivery to Osokorky” page with a banner

Technical SEO

Sitemaps. The old sitemap contained 620 URLs: deleted products, duplicates, addresses like index.php?route=…, pagination pages. The Ukrainian sitemap returned an error altogether and never reached Google. Now there are two sitemaps, /sitemap.xml (RU) and /sitemap-ua.xml (UA), with 408 URLs each: only products from active categories, categories, articles and restaurants. Status in Search Console — “Success”.

Old sitemap in Search Console: 620 URLs, Ukrainian sitemap with the status “Couldn’t fetch”
Before: 620 URLs and a “Couldn’t fetch” error
New RU and UA sitemaps in Search Console: status “Success”, 408 URLs each
After: both sitemaps “Success”, 408 URLs each

robots.txt. I added a line with the Ukrainian sitemap and closed service sections (login, admin, system folders), search and sorting.

New robots.txt of the food delivery website: closed service sections, search and sorting, two sitemaps
New robots.txt

“Broken” products and 301s. I found products without an active category: their pages returned a 500 error. I disabled 32 products and removed them from the sitemaps. I added 64 301 rules from old URLs of disabled products and outdated pages to the current menu sections. 500 errors hurt how Google rates the site, and redirects keep the accumulated link equity and the customers who come via old links.

301 redirect rules and sitemap addresses in .htaccess
Redirects in .htaccess

Canonical. Search Console showed restaurant pages as “Duplicate without user-selected canonical”: each one also opened at an address with the other language’s slug, and there was no canonical tag (the articles had one). I added canonical, as in the blog articles — one line in the controller.

Restaurant page code: canonical tag and hreflang for the UA and RU versions
Canonical on a restaurant page

Page language. It was lang="ua", now it’s lang="uk". There is no “ua” code in the standard, and Google could detect the language incorrectly.

Internal linking

  • in the descriptions of 13 categories (both languages, 26 records) I added a “Useful from the blog” block with 1–4 relevant articles;
  • under articles — 3 cards of similar articles “More from the blog”, picked by topic: pizza, sushi, burgers, deals, kids. They show only where there’s no own “Read also” block, and old deals and closed restaurants aren’t suggested;
  • added links to the district pages from the delivery terms page and from articles about the corresponding districts.
Internal linking script check: how many article links are added to each category
Script check before applying: links from categories to articles
Article with a manual “Read also” block and automatic “More from the blog” cards
Article with “Read also” and a “More from the blog” block

Internal links speed up indexing of new pages, pass weight to categories, and users stay on the site longer.

Structured data and link previews

Products (Product). The old markup had errors: the category name instead of the dish name, an error in the price field (lowprice instead of lowPrice), no photo or description, and for products without sizes the price was taken from the string “250 UAH”. Now there’s the dish name, photo, description, brand, price (a “from–to” range for 30/40 cm pizzas, a single price for the rest) and availability. Google’s “Product snippets” and “Merchant listings” checks show no errors. Correct markup lets search results show the price and photo, which stands out more than a plain link.

Google Rich Results Test for a product page: 3 items with no errors
Google rich results test for a product without errors

Restaurants (Restaurant). On the pages of the four open restaurants: name, address, phone, opening hours (10:00–22:00, in Chuhuiv until 21:00), cuisine, links to the menu and to the Google Maps profile. The markup confirms addresses and opening hours to Google for local search.

Google rich results test for a restaurant page: “Local businesses” and “Organization” without errors
Google test: “Local businesses” without errors

Organization — in the site header: name and alternate name, PNG logo, phone, customer support in Ukrainian and Russian, social networks (Facebook, Instagram, YouTube, Telegram), apps in the App Store and Google Play.

FAQPage — on district pages the “Questions and answers” block automatically turns into markup: 3 questions on Osokorky, 5 on Pozniaky.

Open Graph. Previously the preview image on every page was an SVG logo, which messengers don’t display. Links in Telegram, Viber and Facebook were sent without an image. Now a product shows the dish photo, an article shows its banner, and the other pages show a general 1200×630 image with the logo and phone numbers. The preview language (og:locale) depends on the page language: uk_UA or ru_UA. A good preview means more clicks from chats.

Applying the Open Graph and restaurant markup script: 7 edits and a new preview image
Applying the script: 7 edits, preview image created

A new restaurant in Chuhuiv

On July 1, 2026 the chain opened a restaurant in Chuhuiv, but it wasn’t on the site. I created a page in two languages: opening date, takeaway and seating, 3 delivery zones, menu, deals, map. I took the photo from the banner of the opening news.

  • Restaurant markup: address, phone, closing time 21:00;
  • Chuhuiv added to the H1, title and description of the contacts page;
  • Google’s “Local businesses” and “Organization” checks show no errors.
New restaurant page in Chuhuiv with the opening banner, opening hours and phone
Restaurant page in Chuhuiv
Google rich results test for the Chuhuiv restaurant: 4 items without errors
Google test: “Local businesses” and “Organization” without errors

Google Business

In the Restaurant markup of the four restaurants I added links to their Google Maps profiles (hasMap, sameAs). This way Google connects the site page with the restaurant’s profile on Maps.

During the check I found that one of the Google Business accounts still holds 3 old suspended profiles with old addresses. The open restaurants are in another account. That’s worth knowing before working on local search results.

Speed

Layout shift (CLS). On mobile the home page “jumped” while the slider loaded: CLS 0.383 against a threshold of 0.1. I reserved space for the slider and loaded the first slide right away with high priority. Result — CLS 0. On 29.10 I started the fix validation in Search Console; it takes up to 28 days.

Banners in WebP. The home page slider banners are now served in WebP: they’re lighter than JPG at the same quality. For that, the image processing module got a WebP parameter, and 6 calls were changed on the home page. Fonts got font-display: swap so text is visible immediately, without waiting for the font. I removed 4 “loader” images from the home page that were loaded from a test domain.

Home page of the food delivery website after converting slider banners to WebP
Home page after converting banners to WebP
Applying the speed script: WebP support, edits to the image module, home page, styles and header
Applying the script: WebP and font-display

Removed from every page:

WhatWhy it was removed
Open Sans font from Google FontsNot used (the site uses a different font), blocked rendering
select2 (JS + CSS)Not called in any template, but loaded on every page
swiper (JS 204 KB + CSS)Sliders run on a different library
Google Maps APINow loads only on the page with the map
4 “loader” imagesLoaded from a test domain
Applying the cleanup script: select2, swiper, Google Fonts and the extra Google Maps include removed
Applying the script and style cleanup

After applying it I checked: only jQuery and the main stylesheet remain in the header; the slider, cart and checkout work, and there are no console errors.

Checkout

Nobody filled in the Email field in the checkout form. I hid it, but the value is still passed, so order processing and the export to the iiko (Syrve) accounting system aren’t affected. A test order after the change went through.

Checkout form on the food delivery website, the Email field before the change
Checkout form, the Email field before the change
Test order on the mobile version after the form change: “Thank you” message
Test order after the change

Slow checkout. After clicking “Place order” the spinner kept spinning for 5–6 seconds. I measured each step and found the cause: on every order the site downloads the entire menu from iiko (Syrve) again, about 9 MB. That alone takes 4.35 seconds.

Timing of checkout steps: downloading the whole menu from the accounting system takes 4.35 seconds
Timing of checkout steps
Checkout stepTime
Syrve token0.05 s
Downloading the entire menu (9,072 KB)4.35 s
Parsing the menu (61 MB of memory)0.10 s
Terminal groups0.05 s
Email to the admin via SMTP0.02 s, error
Total without creating the order4.57 s
Order creation and status checkanother ~1–3 s

The fix is to keep the menu on the server for 10 minutes instead of pulling it every time. Details below, in the developer tasks.

A hungry person doesn’t want to wait 6 seconds after clicking a button. I wrote separately about how checkout kills conversion in food delivery.

CRO for Google Ads: It's Not the Ads Wasting Your Budget. It's Your Website Google Ads · MarketingCRO for Google Ads: It's Not the Ads Wasting Your Budget. It's Your WebsiteCRO for Google Ads: why ads can't save a broken website. Two real teardowns — a landing page and a product page — plus a first-screen checklist.Oct 6, 2026Read

What’s left for the developer

Some tasks shouldn’t be done without the developer who maintains the site. For each one I described the cause, where exactly the error is and a ready solution.

1. iiko (Syrve) menu cache — important

On every order the site downloads the entire menu from the accounting system — 9 MB and 4.35 seconds. The fix: keep the menu on the server for 10 minutes. If a product from the cart isn’t found in the cache (for example, it was just added in iiko), the site fetches a fresh menu and retries, so no orders get lost. I handed over a ready script with a backup and rollback — it can be applied as is or used as a template.

2. Orders in the log stay in “pending” status

Every order in the log ends with the entry “After three checks the order creation is still in progress”. The site checks the status in Syrve three times with a 0.2-second pause and doesn’t wait for confirmation, so the amount reconciliation isn’t performed. We need to make sure all orders reach iiko and increase the pause, for example to 1 second.

Order export entries in the log: after three checks the order stays in pending status
Latest order export entries in the log

3. Protection against the 500 error on products without a category — important

If a product has no category or it was deleted, opening the product page goes into infinite recursion in the breadcrumb building method, and the site returns a 500. I cleaned up the consequences manually (32 products disabled), but the cause remains: after any deletion or move of a category the error will come back. For the developer I described three steps: a safe method protected against an empty result and with a depth limit, a check in the product controller and, ideally, a 404 or 301 for products without an active category. Plus how to test it on a test product.

4. jQuery is loaded twice

One version in the header, another in the footer. The second overwrites the first, and plugins attached to the first are lost. Keep one version, check inline scripts in the templates, and move script loading to the end of the page or load them with defer.

5. The 232 KB main.css blocks rendering

Critical above-the-fold styles should go into the header, and the rest should load asynchronously. This is the main reserve for speeding up LCP on mobile.

6. Server cleanup and personal data

  • remove temporary scripts, backups and junk from the site root after all changes are verified;
  • iiko export files (~90 MB) with customer phone numbers and names sit in a public folder. I closed direct access, but they need to be moved outside the site or deleted;
  • clear the 17 MB error.log.

7. Order emails via SMTP

The email to the admin is sent synchronously via Gmail with a 5-second timeout, and during the measurement it returned an error. We need to check whether the emails arrive. If they aren’t needed, disable them — it will shorten checkout.

Summary table of changes

So that the developer or the next contractor understands what was changed, the report has a full list of files and database tables.

FileWhat changed
product/product.phpOwn product meta tags; photo for Open Graph; correct Product markup and price calculation
product/product.twigOld markup block replaced with a new one
posts/news.php“More from the blog”; article banner in the preview; FAQPage
posts/news_info.twigArticle text styles; “More from the blog” cards; FAQPage output
posts/restaurantsList.phpCanonical; Restaurant markup (4 restaurants, closing time, hasMap / sameAs)
restaurantsList_info.twigText styles; Restaurant markup output
common/header.phpImage and language for Open Graph; lang="uk"
common/header.twigog:image and og:locale; Organization markup; Google Fonts removed
common/footer.twigGoogle Maps API only on the page with the map
common/home.twigCLS: space for the slider, first slide without lazy loading, loader removed
common/home.php, tool/image.phpBanners in WebP
main.cssfont-display: swap
checkout/form.phpselect2 include disabled
module/banner.phpswiper include disabled (also carousel and slideshow)
checkout/form.twigEmail field hidden
feed/custom_sitemap.phpClean RU and UA sitemaps
robots.txtUA sitemap, service URLs closed
.htaccess64 301 redirects, sitemap addresses
image/og-default.jpgNew default preview image
Database tableWhat changed
oc_category_descriptionTexts, meta tags, “Useful from the blog”, delivery terms — 15 categories × 2 languages
oc_posts_description49 articles updated; 4 new articles, 11 district pages, a restaurant page × 2 languages; delivery terms; contacts; restaurant and home page texts
oc_product_descriptionMeta tags for 100 records, emoji fixed in 172 records
oc_product32 products without a category disabled

Database and file backups are saved; rollback is a button in the corresponding script.

What’s next

  • 30.10: the “Website” field in the Google Business profiles — a link to each restaurant’s own page with UTM tags;
  • 30.10: indexing request for the district pages and a PageSpeed measurement after the speed fixes;
  • in 2–3 weeks in Search Console: CLS, indexing of district pages and new articles, disappearance of restaurant duplicates, rich results (products, local businesses);
  • in 4–6 weeks: rankings and clicks for “pizza delivery + district” queries and for the new articles;
  • a “Pizza delivery in Chuhuiv” page, modeled on the Kyiv district pages;
  • filling in the Google Business profiles: menu, photos, posts;
  • next articles: “Thin-crust pizza”, “Pizza crusts”, “Baked or tempura rolls”, “What to order with beer”.

When the numbers come in, I’ll show them separately.

How much this kind of work costs

A rough estimate for this scope of work on the Ukrainian market is $3,500–6,500 if a freelancer does it. An agency would charge roughly $6,000–10,000 for the same scope. This is a guideline based on current market rates, not a price list.

BlockScopeEstimate
Audit: Search Console, indexing, codeanalysis and plan$150–300
Category content15 × 2 languages, texts and meta tags$300–450
Blog update49 articles × 2 languages$1 000–2 000
New articles4 × 2 languages$150–300
District pages and delivery terms11 × 2 languages + terms page$450–900
Home page, restaurants, contactstexts, meta tags, a new restaurant in Chuhuiv$200–350
Product meta tags100 items + emoji fix$100–200
Technical SEOsitemaps, 64 redirects, 500 error, canonical, lang$300–600
Structured data and Open GraphProduct, Restaurant, Organization, FAQ$200–400
SpeedCLS, WebP, script cleanup$250–500
Banners~25 items, generation and WebP$100–250
Checkout diagnosticsmeasurements, brief for the developer$100–200
Reports2 PDF$100–200
Total≈ $3 400–6 650

I do this kind of SEO as part of a package with Google Ads and ChatGPT advertising — $1,000 per month.

Food delivery Google Ads case study | Local business | CPL 42 UAH ($0.93) Google Ads case study · Food deliveryFood delivery Google Ads case study | Local business | CPL 42 UAH ($0.93)6+ years with a pizza delivery chain: CPL 41.26 UAH ($0.92) across all campaigns, 174 UAH ($3.9) without brand. How case study numbers get manipulated.Sep 14, 2026Read 16 — WebsiteWebsite CRO auditConversion analysis of your website: what stops visitors from ads from leaving a request or placing an order. A list of specific fixes with priorities.from $50Learn more
Is your site holding your ads back? Get in touch, I’ll see where orders get lost

FAQ on the topic

Frequently asked questions

Do you need SEO if Google Ads is already running?
Yes. Ads and SEO bring people to the same website. Up-to-date terms, speed and a convenient checkout raise conversion for both paid and organic traffic.
Why have separate pages for delivery districts?
People search for delivery in their own district. Without a separate page with that zone’s terms, the site has nothing to show for such a query.
Why shouldn’t a sitemap contain extra URLs?
Google spends crawl budget on junk and non-existent pages instead of useful ones. A clean sitemap with live pages helps index what really matters faster.
What is CLS and why does it matter?
CLS measures layout shift during loading. When a page “jumps”, people miss buttons and leave the site sooner. The threshold is 0.1.
How long until SEO shows results?
Google checks technical fixes over several weeks. The first changes in indexing show up after 2–3 weeks, in rankings and clicks usually after 1–2 months.



Contact

Let's discuss
your project

Leave a request — we'll assess your situation, discuss your goals and find the best solution for your business and budget.

🏆
As a Google Partner, I can help you get a Google Ads bonus of up to 55,000 UAH ($1.2K) for your first campaigns.
❌ I don't work with: gambling · betting · alcohol · tobacco · crypto · grey niches · payday loans
▸ new_project.init()
Discuss a project