All lessons
FrontendIntermediate13 min readIn my stack

SEO, performance & accessibility

Core Web Vitals, technical SEO and WCAG essentials, measured, not guessed.

  • Core Web Vitals
  • JSON-LD
  • hreflang
  • Sitemaps
  • Accessibility
Step 1/5
A search crawler visits a page that is just an empty JS shell: <div id=root> and a spinner. Nothing to read yet.

In 30 seconds

A website has to be found, load fast and work for everyone. Technical SEO helps search engines crawl and understand your pages; Core Web Vitals measure how fast and stable they feel to real users; accessibility makes sure people using keyboards, screen readers or zoom can use them. The three overlap heavily, and all of them are best handled by measuring real data and fixing the biggest problem first.

Why I use it

  • I care about this because a storefront that ranks poorly or loads slowly loses customers before they see a single product.
  • I build SEO into the CMS from the start: per-page metadata, sitemaps, redirects and structured data, so editors never need a developer to fix a title.
  • I work on multilingual sites, where hreflang and clean locale URLs decide whether each language version reaches the right audience.
  • I treat accessibility as part of quality, and it also helps SEO, because semantic, well-structured pages are easier for both people and crawlers to understand.

How it works

01Core Web Vitals: LCP, INP and CLS

Core Web Vitals are three metrics Google uses to describe real-user experience. Largest Contentful Paint (LCP) is when the main content, usually a hero image or heading, appears; good is 2.5 seconds or less. Interaction to Next Paint (INP) measures how quickly the page responds visually to clicks, taps and key presses across the whole visit; good is 200 milliseconds or less. INP replaced First Input Delay in March 2024. Cumulative Layout Shift (CLS) measures how much content jumps around unexpectedly; good is 0.1 or less. A page passes when 75% of real visits meet each threshold, measured separately for mobile and desktop. They are a ranking signal, but a modest one; their bigger value is conversions and user trust.

  • LCP ≤ 2.5 s: loading.
  • INP ≤ 200 ms: responsiveness.
  • CLS ≤ 0.1: visual stability.
  • Judged at the 75th percentile of real users.

02How to fix each metric

LCP has four parts: server response time (TTFB), the delay before the browser discovers the LCP resource, its download time, and render delay. Fix them in that order: cache HTML at the edge, make the hero image discoverable in the HTML (not set by JavaScript or CSS background), never lazy-load it, give it fetchpriority="high", and serve it in AVIF or WebP at the right size. INP is about main-thread work: ship less JavaScript, split long tasks so the browser can paint between them (scheduler.yield() or setTimeout), use startTransition for non-urgent React updates, and avoid heavy third-party scripts. CLS is about reserved space: always set width and height or aspect-ratio on images and embeds, reserve space for banners and ads, and use font fallbacks with matching metrics.

html
<!-- LCP image: discoverable, eager, high priority, right size -->
<img
  src="/img/hero-1200.avif"
  srcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w, /img/hero-1800.avif 1800w"
  sizes="(max-width: 768px) 100vw, 1200px"
  width="1200" height="675"
  fetchpriority="high"
  alt="Fresh coffee being poured into a cup" />
 
<!-- below-the-fold images -->
<img src="/img/team.avif" width="800" height="600" loading="lazy" decoding="async" alt="Our team" />

03Crawling, indexing and canonicals

Search engines work in three steps: crawl (fetch URLs), index (store and understand them) and rank. robots.txt controls crawling, not indexing: a blocked URL can still appear in results if other sites link to it. To keep a page out of the index, allow crawling and return a noindex meta tag or X-Robots-Tag header. A canonical link tells search engines which URL is the main version when the same content is reachable at several addresses, such as with tracking parameters or filters; it is a strong hint, not a command. Return real status codes: 301 for permanent moves, 404 or 410 for removed pages, and never a 200 'not found' page. Make sure important content is in the server-rendered HTML, not only loaded by client JavaScript.

text
# robots.txt
User-agent: *
Disallow: /cart
Disallow: /account
Disallow: /*?sort=
 
Sitemap: https://example.com/sitemap.xml

04Sitemaps, structured data and hreflang

A sitemap lists the URLs you want indexed, with an accurate lastmod date; Google ignores priority and changefreq. One file holds up to 50,000 URLs or 50 MB uncompressed, so large sites use a sitemap index. Structured data, usually JSON-LD in a script tag, describes the page in a vocabulary from schema.org: Organization, Product with Offer, BreadcrumbList, Article, Event. It can unlock rich results such as prices and ratings. For multilingual sites, hreflang tells search engines which URL is for which language or region. Every version must list all versions including itself, the links must be reciprocal, and x-default points at the fallback. Use language codes like en, hy, ru, optionally with a region like en-GB.

html
<link rel="canonical" href="https://example.com/en/products/espresso" />
<link rel="alternate" hreflang="en" href="https://example.com/en/products/espresso" />
<link rel="alternate" hreflang="hy" href="https://example.com/hy/products/espresso" />
<link rel="alternate" hreflang="ru" href="https://example.com/ru/products/espresso" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/espresso" />
 
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Product", "name": "Espresso beans 1 kg",
  "offers": { "@type": "Offer", "price": "24.90", "priceCurrency": "EUR",
              "availability": "https://schema.org/InStock" } }
</script>

05Image and font optimisation

Images are usually the heaviest part of a page. Serve modern formats (AVIF, then WebP), resize to the displayed size with srcset and sizes, compress, and lazy-load everything below the fold. Frameworks help: Next.js's Image component generates sizes and formats automatically; in Next.js 16 the old priority prop is replaced by preload, and the docs recommend loading="eager" or fetchPriority="high" for the LCP image. Fonts cause both slow text and layout shift. Self-host them, subset to the characters you need (separate files for Latin, Cyrillic or Armenian), preload only the one or two critical files, use font-display: swap, and use a fallback font with size-adjust so the swap does not move text. Variable fonts can replace several weight files with one.

css
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans-latin.woff2") format("woff2");
  font-display: swap;
  unicode-range: U+0000-00FF;
}
 
/* metric-matched fallback reduces layout shift on swap */
@font-face {
  font-family: "Brand Sans Fallback";
  src: local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
}

06Accessibility essentials (WCAG)

WCAG (Web Content Accessibility Guidelines) is the standard; version 2.2 at level AA is the usual target and is what laws like the European Accessibility Act, enforced since June 2025, point to. The essentials cover most real problems. Use semantic HTML: one h1, headings in order, landmarks (header, nav, main, footer), button for actions and a for links. Every image needs meaningful alt text, or alt="" if decorative. Text contrast must be at least 4.5:1, or 3:1 for large text and UI components. Everything must work with a keyboard, with a visible focus indicator. Forms need real labels and clear error messages. Targets should be at least 24 by 24 CSS pixels. Set the lang attribute per page, which matters for screen readers on multilingual sites.

  • Semantic elements first, ARIA only when HTML has no equivalent.
  • Keyboard: Tab order, visible focus, no traps, Escape closes dialogs.
  • Contrast 4.5:1 text, 3:1 large text and controls.
  • Test with a screen reader (VoiceOver, NVDA) and 200% zoom.

07Measuring: Lighthouse, PageSpeed and CrUX

There are two kinds of data. Lab data comes from a simulated load on a fixed device and network: Lighthouse in Chrome DevTools or in CI. It is repeatable and great for debugging, but it cannot measure INP directly; Total Blocking Time is its proxy. Field data comes from real Chrome users: the Chrome UX Report (CrUX), which aggregates the previous 28 days. PageSpeed Insights shows both for a URL, and Search Console groups field data across your whole site. For your own detailed numbers, add the web-vitals library and send metrics to your analytics, including which element was the LCP and which interaction was slow. Optimise based on field data, verify fixes in the lab, then wait for field data to confirm.

ts
import { onLCP, onINP, onCLS } from "web-vitals/attribution";
 
function send(metric: { name: string; value: number; rating: string }) {
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}
 
onLCP(send);
onINP(send);
onCLS(send);

Use it when

  • Every public website, from the first sprint, not as a launch-week fix.
  • E-commerce and content sites where organic search and conversion drive revenue.
  • Multilingual sites that need each language to rank in its own market.
  • Products sold in regions with accessibility regulation, such as the EU.

Skip it when

  • Internal tools behind login do not need SEO work, though they still need performance and accessibility.
  • Do not chase a perfect Lighthouse score at the expense of features users need; fix what field data shows is slow.

Alternatives

  • Next.js Metadata API

    Generates titles, canonicals, hreflang alternates, sitemaps and robots from code in the App Router.

  • WebPageTest

    Deep lab testing with filmstrips, waterfalls and real devices in many locations.

  • Real user monitoring tools

    Vercel Speed Insights, Cloudflare Web Analytics or Sentry collect field Web Vitals per route.

  • axe DevTools / Lighthouse accessibility

    Automated accessibility checks; they catch roughly a third of issues, so manual testing remains necessary.

Best practices

  1. Server-render important content and metadata.
  2. Make the LCP image discoverable, eager and high priority; lazy-load the rest.
  3. Reserve space for every image, embed and banner to prevent layout shift.
  4. Break up long JavaScript tasks and audit third-party scripts regularly.
  5. Keep one canonical URL per page, with reciprocal hreflang for every locale.
  6. Generate sitemaps and structured data from the CMS so they never go stale.
  7. Monitor field Web Vitals and accessibility in CI, not once a year.

Common mistakes

  • Using robots.txt to hide a page from search results; it only blocks crawling.
  • Lazy-loading the hero image, which delays LCP.
  • hreflang tags that are not reciprocal or point to redirected URLs, so search engines ignore them.
  • Relying on FAQ or HowTo markup for rich results; Google limited FAQ results to a few authoritative sites and removed HowTo results in 2023.
  • Removing focus outlines for looks, which makes the site unusable by keyboard.

Deep dive

08Inside INP: input delay, processing and presentation

INP reports roughly the worst interaction of a visit; on pages with many interactions, one outlier per 50 is ignored. Every interaction has three parts, and each has a different fix. Input delay is the time before your handler even starts, usually because another long task is running, such as hydration, a tag manager or an analytics batch. Processing time is your event handlers themselves. Presentation delay is the work after the handlers until the next frame is painted: style, layout and paint of whatever changed, which grows with DOM size and with large React re-renders. The Long Animation Frames API (LoAF) reports which scripts made a frame slow, down to source URL and function, and the web-vitals attribution build attaches those entries to INP. So: break up competing long tasks to cut input delay, paint the visual feedback first and defer the rest to cut processing, and keep the DOM small and updates local to cut presentation delay.

ts
const yieldToMain = () =>
  "scheduler" in globalThis && "yield" in scheduler
    ? scheduler.yield()
    : new Promise((resolve) => setTimeout(resolve, 0));
 
button.addEventListener("click", async () => {
  showAddedState();      // 1. visual feedback in this frame
  await yieldToMain();   // 2. let the browser paint
  await saveCart();      // 3. heavy work afterwards
  sendAnalytics();
});

09Instant navigations: bfcache and speculation rules

The fastest page load is the one that never happens. The back/forward cache (bfcache) keeps a complete snapshot of a page in memory, so Back and Forward restore it instantly with no network and no JavaScript re-run. A page loses that benefit if it registers an unload handler, holds certain open connections when the user leaves, or is excluded by headers such as Cache-Control: no-store. DevTools, under Application and Back/forward cache, tests a page and lists exactly why it is blocked. For forward navigations, the Speculation Rules API lets Chromium-based browsers prefetch or fully prerender likely next pages, either from a list or by matching links, with an eagerness setting such as moderate (start on hover). A prerendered page shows near-zero LCP when opened. Other browsers ignore the rules, so it is a safe progressive enhancement. Prerender only side-effect-free GET pages, exclude logout or add-to-cart links, and delay analytics until the page is actually shown, using document.prerendering.

html
<script type="speculationrules">
{
  "prerender": [{
    "where": { "and": [
      { "href_matches": "/*" },
      { "not": { "href_matches": "/logout" } },
      { "not": { "href_matches": "/cart/*" } },
      { "not": { "selector_matches": "[data-no-prerender]" } }
    ]},
    "eagerness": "moderate"
  }]
}
</script>

10Crawl budget and rendering on large sites

A site with a few hundred pages gets crawled completely. With hundreds of thousands of URLs, such as a faceted catalogue, crawl budget becomes real: Googlebot spends a limited number of fetches per site, shaped by how fast and healthy your server is and how much demand exists for your URLs. Waste comes from infinite URL spaces: filter combinations, sort orders, session or tracking parameters, endless calendars. Control it in layers. Link prominently only to the facet pages you want indexed. Keep other combinations canonicalised to the base page, or disallow parameters in robots.txt if they should not even be fetched; remember that a blocked URL cannot show its noindex. Keep sitemaps to canonical, indexable URLs with honest lastmod. Server logs filtered to verified Googlebot show where crawling really goes. Rendering matters too: Google executes JavaScript, but as a separate, more expensive step, so content and links that appear only after client-side fetching are found later and less reliably.

  • Fast, stable responses (low TTFB, few 5xx errors) let Googlebot crawl more.
  • Return 404 for empty filter combinations instead of a thin 200 page.
  • Search Console's Crawl stats report shows response codes, file types and host status.

11Accessibility beyond the checklist: names, roles and live updates

Automated tools catch only part of WCAG failures; the rest needs an understanding of the accessibility tree. The browser turns the DOM into a tree of objects, each with a role (button, link, dialog), an accessible name (from text content, a label, aria-label or aria-labelledby), states such as expanded or checked, and an optional description. Screen readers read this tree, not your pixels; inspect it in the DevTools Accessibility pane. Deeper issues show up there: icon buttons with no name; custom dropdowns that are not keyboard operable; single-page navigations that swap content without moving focus, so screen-reader users do not know the page changed (move focus to the new h1 or announce the title); status messages like 'added to cart' that appear visually but are never announced. For announcements, use a live region such as role='status' that already exists in the DOM before you change its text. The native dialog element with showModal() traps focus and handles Escape for you. Test key flows with VoiceOver or NVDA.

html
<button type="button" aria-label="Open cart" aria-expanded="false" aria-controls="cart">
  <svg aria-hidden="true" focusable="false"><!-- icon --></svg>
</button>
 
<!-- present on page load; changing its text is announced politely -->
<p role="status" class="visually-hidden" id="cart-status"></p>
 
<dialog id="cart" aria-labelledby="cart-title">
  <h2 id="cart-title">Your cart</h2>
  <!-- opened with cart.showModal(): focus is trapped, Escape closes -->
</dialog>

Real-world scenarios

01

Organic traffic dropped after a site migration

Problem

A 5,000-page site moved to a new framework with new URLs. Three weeks later, organic traffic is down about 40 percent and Search Console shows many 'Not found' and 'Duplicate without user-selected canonical' errors.

How to solve it

Rebuild the redirect map from every old URL source: the old sitemaps, analytics landing pages and backlink reports. Each old URL must 301 to its closest new equivalent in one hop, with no chains and no blanket redirects to the home page. Crawl the old URL list with a site crawler to verify the status codes. Then check for leftovers from staging: noindex tags, canonicals pointing at staging hosts, a restrictive robots.txt. Update hreflang and internal links to point directly at the new URLs. Submit the new sitemap and use URL Inspection on key pages to compare rendered HTML with the old site. Recovery usually takes weeks, so track it per section.

02

Lighthouse says 95, Search Console says Poor

Problem

Lab tests show excellent scores, yet Search Console reports poor LCP and INP for most mobile URLs. The team is confused about which number to trust.

How to solve it

Trust the field data, then explain the gap. Lab tests load one clean page on a simulated device; real users bring slow phones, consent banners, A/B testing scripts, logged-in personalisation and cold caches. Add the web-vitals attribution build and send the LCP element, INP interaction target and the script responsible to analytics. Often the LCP element in the field is the cookie banner or a different hero per country, and TTFB is slow because personalised pages bypass the edge cache. Reproduce the real conditions in the lab, with throttling, the consent banner and third-party tags enabled. Fix the top cause, then wait for CrUX's 28-day window to roll over before judging the result.

03

Faceted category pages flood the index

Problem

An e-commerce site has about 300 categories, but Search Console lists hundreds of thousands of discovered URLs, mostly filter and sort combinations. New products take weeks to appear in search.

How to solve it

Decide which facets have real search demand, such as category plus brand, and give them clean static URLs, unique titles and copy, internal links and sitemap entries. For all other combinations, keep the filters usable but canonicalise to the base category, and disallow pure sort, view and pagination-size parameters in robots.txt so Googlebot stops fetching them. Do not combine a robots.txt block with noindex on the same URL, because the noindex will never be seen. Return 404 for combinations with no products. Publish a separate product sitemap with accurate lastmod to speed up discovery, and watch Crawl stats until the share of parameter URLs falls.

Cheat sheet

Warm up a critical third-party origin
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
Yield to the main thread in long work
await scheduler.yield(); // fallback: await new Promise(r => setTimeout(r, 0))
Watch slow frames and their scripts
new PerformanceObserver((l) => console.log(l.getEntries())).observe({ type: "long-animation-frame", buffered: true });
Delay work until a prerendered page is shown
if (document.prerendering) document.addEventListener("prerenderingchange", init, { once: true }); else init();
Find what blocks the bfcache
DevTools > Application > Back/forward cache > Test back/forward cache
Keep a PDF or image out of the index
X-Robots-Tag: noindex (HTTP response header)
Run Lighthouse in CI
npx @lhci/cli autorun
See the page as Google rendered it
Search Console > URL Inspection > View crawled page
Announce a status change to screen readers
<p role="status">Added to cart</p>
Accessible modal with focus trapping
document.querySelector("dialog").showModal();

In my real projects

  • On a restaurant ordering platform and a pet-supplies store, the CMS I built includes per-model SEO fields and a generated sitemap.
  • On a coffee chain's website I set up the sitemap, robots, OG images and the app-link association files that let links open the mobile app.
  • A digital agency website I worked on has sitemap, robots and llms.txt generation plus SEO redirects after a full rewrite.
  • On a tour booking platform in 8 languages, my contributions included SEO schema markup, redirects and performance work.

Questions & answers

Q1What are the Core Web Vitals and their thresholds?

There are three. Largest Contentful Paint measures loading: when the largest visible element, usually the hero image or main heading, renders; good is 2.5 seconds or less. Interaction to Next Paint measures responsiveness across all interactions in a visit; good is 200 milliseconds or less. It replaced First Input Delay in March 2024. Cumulative Layout Shift measures unexpected movement of content; good is 0.1 or less. A page passes when the 75th percentile of real users meets each threshold, for mobile and desktop separately. The data comes from the Chrome UX Report over a rolling 28-day window, so improvements take a few weeks to show.

Q2Our LCP is 4 seconds. How would you approach it?

First I find the LCP element and break the time into its four parts: time to first byte, resource load delay, resource load duration and render delay. PageSpeed Insights and the DevTools Performance panel show this. If TTFB is high, I cache HTML at the CDN or use static or incremental rendering. If the delay is high, the image is probably discovered late, through JavaScript, a CSS background, or lazy-loading, so I put it in the HTML with fetchpriority high. If download is slow, I serve AVIF or WebP at the correct size. If render delay is high, I look for render-blocking CSS, fonts or hydration. Then I confirm in field data.

Q3How do you improve INP in a React app?

INP is slow when the main thread is busy at the moment of interaction, or when the update after it is expensive. I profile the slow interaction in DevTools and look for long tasks. Fixes, in order of impact: ship less JavaScript through code splitting and server components; remove or delay heavy third-party scripts; keep the immediate response small, for example updating the button state first and wrapping expensive work in startTransition; break long loops with scheduler.yield(); virtualise long lists; and avoid re-rendering huge trees by keeping state local. The web-vitals attribution build tells you which element and which phase is slow in production.

Q4How do you set up SEO for a multilingual site?

Each language gets its own URL, usually a path prefix like /en/ or /hy/, never just a cookie or automatic redirect based on browser language, because crawlers do not send those signals. Every page includes hreflang alternates for all language versions, including itself, plus an x-default, and these must be reciprocal and point to final, indexable URLs. Each version has a self-referencing canonical; never canonicalise translations to English. Titles, descriptions, slugs and structured data are translated, the html lang attribute matches, and the sitemap lists every localised URL. Offer a language switcher rather than forcing redirects, so both users and crawlers can reach every version.

Q5What is the difference between robots.txt, noindex and canonical?

They solve different problems. robots.txt controls crawling: it asks bots not to fetch certain paths, which saves crawl budget on things like cart, account or endless filter combinations. It does not remove pages from the index, and if you block a page, the bot cannot even see a noindex on it. noindex, as a meta tag or X-Robots-Tag header, controls indexing: the page can be crawled but will not appear in results. A canonical consolidates duplicates: it tells search engines which URL should represent content available at several URLs, and passes signals to that URL. It is a strong hint, not a guarantee.

Q6What are the accessibility basics every website must get right?

I start with the issues that block the most people. Semantic HTML: real buttons and links, headings in order, landmarks and one main. Keyboard access: every control reachable with Tab, a visible focus indicator, no keyboard traps, dialogs that move and return focus. Text alternatives: meaningful alt text for informative images and empty alt for decorative ones. Contrast of at least 4.5:1 for normal text. Labelled form fields with errors explained in text, not only colour. Correct lang attributes, respect for reduced motion, and layouts that work at 200% zoom. Then I run axe or Lighthouse, and test key flows with a screen reader, because automated tools miss most issues.

Senior-level questions

S1How do rendering choices (SSG, ISR, SSR, client rendering) affect SEO and Web Vitals?

Static generation and incremental regeneration give the best baseline: HTML is prebuilt and cached at the edge, so TTFB is low, LCP starts early and crawlers get complete content. The trade-off is freshness, handled by revalidation. Server rendering per request suits personalised or fast-changing pages, but TTFB now depends on your slowest data call, so I stream the shell first and cache data aggressively. Pure client rendering is the weakest for SEO and LCP: the content appears only after JavaScript downloads, runs and fetches, and search engines render it in a deferred step. Every server-rendered option also costs hydration, which hurts INP if large, so I keep interactive islands small with server components. In practice I mix per route: static for marketing and catalogue pages, server rendering for search and account pages, client-only for private dashboards.

S2How do you stop a large team from slowly regressing performance?

By making performance visible and owned, not by a one-off audit. I define budgets per page type: field LCP, INP and CLS at the 75th percentile, plus JavaScript and image weight. In CI, Lighthouse CI runs against preview deployments with assertions, and a bundle-size check comments on every pull request with the delta. In production, real-user monitoring with the web-vitals library feeds a dashboard segmented by page type, device and country, with alerts when the 75th percentile crosses a threshold. Third-party scripts need a review and an owner before they are added, because tags are the most common silent regression. The trade-off is some friction for feature teams; I keep it small by making budgets warnings first and blocking only on clear, measurable regressions.

S3How do you handle third-party scripts like tag managers, chat widgets and A/B testing?

I treat each one as a cost with an owner. First, inventory: what loads, from where, how many kilobytes and how much main-thread time, using the Performance panel and the network waterfall. Remove anything without a current owner. For the rest, choose a loading strategy. Analytics and tag managers load after the page is interactive. Chat widgets load on intent, with a lightweight placeholder button that loads the real widget on click. Client-side A/B testing is the hardest, because anti-flicker snippets hide the page and destroy LCP, so I prefer server-side or edge experiments. Consent management must be fast because it often becomes the LCP element. Some teams move scripts to a web worker with a tool like Partytown; it can help, but compatibility varies, so I test each tag.

Glossary

Input delay
Time between a user's interaction and the moment its event handler starts running.
Presentation delay
Time from the end of event handlers until the browser paints the next frame.
Long Animation Frame (LoAF)
A browser API reporting frames that took over 50 ms, with the scripts that caused them.
bfcache
Back/forward cache: an in-memory snapshot that makes Back and Forward navigations instant.
Speculation rules
A JSON script that lets Chromium browsers prefetch or prerender pages the user is likely to open.
Crawl budget
How many URLs a search engine is willing and able to fetch from a site in a given time.
Faceted navigation
Filter-based browsing (size, colour, brand) that can generate huge numbers of URL combinations.
Accessibility tree
The browser's model of the page for assistive technology: roles, names, states and relationships.
Accessible name
The text a screen reader announces for an element, computed from content, labels or ARIA.
Live region
An element whose text changes are announced automatically, such as role='status' or aria-live.

Have a product in mind?Let's build it.

Tell me about your idea, your product or your team. I usually reply within one working day.