Skip to content
prod e051e98
Browse

1 · SEO (9A · do first)

Objective — make the live product discoverable: a valid sitemap + robots, per-page meta/schema/titles, then Search Console verification and a PageSpeed pass — done first because every other SEO signal needs a reachable URL.

SEO needs a reachable URL, so it leads — work the three steps in order, each building on the last. Discoverability signals (sitemap, schema, indexing) only count once the domain is live, which is why this is the first stream of Phase 9.

SEO is a short pipeline — each signal only counts once the one before it is in place. Work it in order:

flowchart LR
A[Sitemap + robots] --> B[Audit vendor defaults]
B --> C[Meta + schema]
C --> D[Canonical URLs]
D --> E[Page titles]
E --> F[Search Console]
F --> G[PageSpeed / CWV]

Confirm /sitemap.xml and /robots.txt exist and reference each other, then generate or validate the sitemap as needed.

  1. Probe both files and confirm robots points at the sitemap.

    Terminal window
    curl -I https://<domain>/sitemap.xml
    curl https://<domain>/robots.txt | grep -i sitemap
    # Expected: a 200 for sitemap.xml, and a "Sitemap:" line from robots.txt
    • /sitemap.xml returns 200 and /robots.txt references it.

2. Audit the vendor’s SEO defaults first

Section titled “2. Audit the vendor’s SEO defaults first”

A CodeCanyon app ships its own meta tags, canonical link, and schema — often branded to the vendor. Find what already exists before you add anything, or you’ll duplicate tags and override the wrong ones.

  1. Grep the views and probe a live page for what the vendor already ships.

    Terminal window
    # What meta/canonical/OG already exist in the templates?
    grep -rE "canonical|og:url|meta.*description" resources/views/ --include="*.blade.php"
    # Vendor-branded defaults to replace (swap in your vendor's name)
    grep -riE "workdo|<vendor-name>" resources/views/ --include="*.blade.php"
    # What the live page actually renders
    curl -s https://<domain> | grep -E "<title>|<meta|canonical"
    • ✅ You know which tags exist and which carry vendor defaults. Use the decision guide — no meta → do the full meta setup; title/description only or missing canonical → jump to step 4 (canonical); missing OG/Twitter → add social cards in step 3.

Set per-page titles and descriptions, Open Graph / Twitter cards, and JSON-LD schema markup so listings and social previews render correctly. Prefer a single <x-meta> Blade component included in the layout <head> so every page inherits it.

  1. Wire per-page meta, social cards, and JSON-LD. Give each page a unique title + description, add Open Graph / Twitter card tags, and embed JSON-LD schema markup — via a reusable meta component or the vendor’s admin SEO settings.

    • ✅ Each indexed page has a unique title/description, social cards, and schema markup.
  2. Set the index/noindex robots meta per page type. Public marketing pages should be indexable; authenticated app surfaces and transactional pages must not be.

    Page typeRobots meta
    Public home/pricing/features/about/contact/blogindex, follow
    Dashboard / app homenoindex, nofollow
    Admin / super-adminnoindex, nofollow
    Settings / my-account / profilenoindex, nofollow
    Cart / checkout / payment / invoicenoindex, nofollow
    Search / filtered listingnoindex, follow
    API endpointsnoindex, nofollow
    {{-- Default to indexable; private/transactional views set $robots = 'noindex, nofollow'. --}}
    <meta name="robots" content="{{ $robots ?? 'index, follow' }}">
    • curl -s https://<DOMAIN>/dashboard | grep robots shows noindex, nofollow; public pricing shows index, follow.

Canonical tags stop duplicate-content penalties — and paginated and filtered pages are exactly where CodeCanyon apps get them wrong. Handle all three cases.

  1. Add the base canonical, then special-case pagination and filters.

    {{-- Compute exactly ONE canonical, then emit ONE tag. Stacking three
    separate <link rel="canonical"> snippets renders all three on a
    filtered page-2 URL — Google then picks one arbitrarily. --}}
    @php
    // Strip filters/sorts (the query string), keep only pagination.
    $canonical = strtok(url()->current(), '?');
    if (request('page') > 1) {
    $canonical .= '?page=' . (int) request('page');
    }
    @endphp
    <link rel="canonical" href="{{ $canonical }}">
    • curl -s https://<domain>/pricing | grep canonical returns exactly one canonical per page; paginated and filtered URLs resolve to the right canonical.

Unique, front-loaded titles (Page | App) improve click-through and browser-tab clarity. Set a base-layout fallback, then a title on every view.

  1. Discover every layout and route that renders a title.

    Terminal window
    for d in resources/views Modules packages; do
    [ -d "$d" ] && grep -rlE "(<title>|@yield\(['\"]title)" "$d" --include="*.blade.php"
    done
    php artisan route:list --method=GET --columns=uri,name
    # Expected: the inventory of layouts + routes that need a title
    • ✅ You have the layout + route inventory.
  2. Set a base-layout fallback, then a per-view title.

    <title>
    @hasSection('title')@yield('title') | {{ config('app.name') }}
    @else{{ config('app.name') }}@endif
    </title>

    Then add @section('title', 'Page Name') to each view — format Page | App, keep it ≤ 60 characters.

    • ✅ Every view that extends a layout defines a unique title (find stragglers: @extends views with no @section('title')).

Verify the property, then submit the sitemap so Google starts indexing.

  1. Verify the property and submit the sitemap in Google Search Console.

    • ✅ The property is verified and the sitemap is submitted.
  2. Use a Domain property and DNS TXT verification. In Search Console → Add property → Domain → enter <DOMAIN> without http or www. Add the google-site-verification=… TXT record at DNS, then confirm propagation before clicking Verify:

    Terminal window
    dig <DOMAIN> TXT +short
    # Expected: the google-site-verification=… string
    • ✅ The Domain property is verified via DNS TXT.
  3. Triage indexing after 48–72h under Search Console → Indexing → Pages.

    IssueCauseFix
    Crawled — not indexedThin / low-value pageImprove content
    Blocked by robots.txtRobots too restrictiveLoosen Disallow
    Not found (404)Broken pageFix or redirect
    • ✅ Indexing issues are classified and assigned.

Measure, fix the high-leverage wins (images, caching, Core Web Vitals), then re-measure. Do this last — it’s the polish, not the foundation.

  1. Measure the baseline at pagespeed.web.dev — record Mobile + Desktop scores and Core Web Vitals (LCP < 2.5s, INP < 200ms, CLS < 0.1).

    • ✅ Baseline scores + Core Web Vitals recorded for Mobile and Desktop.
  2. Fix the big wins, then re-measure. Convert heavy images to WebP, enable gzip + browser caching, cache Laravel’s routes/config/views, then address each failing vital (preload the LCP image; add width/height to images for CLS; defer non-critical JS for INP).

    Terminal window
    find public -type f \( -name "*.jpg" -o -name "*.png" \) -size +100k # heavy images
    command -v cwebp >/dev/null || { echo "install cwebp first"; exit 1; }
    cwebp -q 80 public/images/hero.jpg -o public/images/hero.webp # convert to WebP
    PHP_BIN=$(grep "set('bin/php'" deploy.php 2>/dev/null | grep -oE "/[^'\"]+/php[^'\"]*" | head -1)
    PHP_BIN="${PHP_BIN:-php}"
    $PHP_BIN artisan route:cache && $PHP_BIN artisan config:cache && $PHP_BIN artisan view:cache
    • ✅ Re-run PageSpeed: Desktop ≥ 90, Mobile ≥ 80, LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1.
  3. Enable compression + browser caching on Apache, then verify over the wire. On shared/Apache hosting these live in public/.htaccess.

    Terminal window
    curl -I -H "Accept-Encoding: gzip, deflate, br" https://<DOMAIN>
    # Look for: Content-Encoding: gzip (or br)

    If absent, add gzip and long-lived static caching:

    <IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/css
    AddOutputFilterByType DEFLATE application/javascript application/json image/svg+xml
    </IfModule>
    <IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
    ExpiresByType text/html "access plus 0 seconds"
    </IfModule>
    • curl -I -H "Accept-Encoding: gzip, deflate, br" https://<DOMAIN> returns a Content-Encoding header; static assets cache long-term, HTML does not.
  4. Serve WebP with a fallback and lazy-load below-fold images.

    <picture>
    <source srcset="{{ asset('images/hero.webp') }}" type="image/webp">
    <img src="{{ asset('images/hero.jpg') }}" alt="Hero image" width="1200" height="630">
    </picture>

    Add loading="lazy" to below-the-fold images only.

    • ✅ Heavy images use WebP with fallback; below-fold images lazy-load; the LCP/hero image is not lazy-loaded.

Do not mark this step done until every box below is checked.

  • 🤖 Sitemap + robots/sitemap.xml valid and referenced from /robots.txt.
  • 🤖 Vendor defaults audited — you know which meta/canonical/OG tags the app already ships before overriding.
  • 🤖 Meta + schema + social cards — per-page titles/descriptions, Open Graph/Twitter cards, and JSON-LD in place.
  • 🤖 Canonical URLs (Critical) — base, paginated, and filtered pages each resolve to one correct canonical.
  • 🤖 Page titles unique — base-layout fallback set; every view extending a layout defines a Page | App title.
  • 👤 Search Console verified — property verified and sitemap submitted.
  • 🤖 PageSpeed pass — Core Web Vitals hit targets (Desktop ≥ 90, Mobile ≥ 80, LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1).