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.
Background
Section titled “Background”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]1. Audit the sitemap and robots
Section titled “1. Audit the sitemap and robots”Confirm /sitemap.xml and /robots.txt exist and reference each other, then generate or validate the sitemap as needed.
-
Probe both files and confirm robots points at the sitemap.
Terminal window curl -I https://<domain>/sitemap.xmlcurl https://<domain>/robots.txt | grep -i sitemap# Expected: a 200 for sitemap.xml, and a "Sitemap:" line from robots.txt- ✅
/sitemap.xmlreturns 200 and/robots.txtreferences 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.
-
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 renderscurl -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.
3. Add meta, schema, and titles
Section titled “3. Add meta, schema, and titles”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.
-
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.
-
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 type Robots meta Public home/pricing/features/about/contact/blog index, followDashboard / app home noindex, nofollowAdmin / super-admin noindex, nofollowSettings / my-account / profile noindex, nofollowCart / checkout / payment / invoice noindex, nofollowSearch / filtered listing noindex, followAPI endpoints noindex, nofollow{{-- Default to indexable; private/transactional views set $robots = 'noindex, nofollow'. --}}<meta name="robots" content="{{ $robots ?? 'index, follow' }}">- ✅
curl -s https://<DOMAIN>/dashboard | grep robotsshowsnoindex, nofollow; public pricing showsindex, follow.
- ✅
4. Set canonical URLs (Critical)
Section titled “4. Set canonical URLs (Critical)”Canonical tags stop duplicate-content penalties — and paginated and filtered pages are exactly where CodeCanyon apps get them wrong. Handle all three cases.
-
Add the base canonical, then special-case pagination and filters.
{{-- Compute exactly ONE canonical, then emit ONE tag. Stacking threeseparate <link rel="canonical"> snippets renders all three on afiltered 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 canonicalreturns exactly one canonical per page; paginated and filtered URLs resolve to the right canonical.
- ✅
5. Optimize page titles
Section titled “5. Optimize page titles”Unique, front-loaded titles (Page | App) improve click-through and browser-tab clarity. Set a base-layout fallback, then a title on every view.
-
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"donephp artisan route:list --method=GET --columns=uri,name# Expected: the inventory of layouts + routes that need a title- ✅ You have the layout + route inventory.
-
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 — formatPage | App, keep it ≤ 60 characters.- ✅ Every view that extends a layout defines a unique title (find stragglers:
@extendsviews with no@section('title')).
- ✅ Every view that extends a layout defines a unique title (find stragglers:
6. Verify Search Console
Section titled “6. Verify Search Console”Verify the property, then submit the sitemap so Google starts indexing.
-
Verify the property and submit the sitemap in Google Search Console.
- ✅ The property is verified and the sitemap is submitted.
-
Use a Domain property and DNS TXT verification. In Search Console → Add property → Domain → enter
<DOMAIN>withouthttporwww. Add thegoogle-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.
-
Triage indexing after 48–72h under Search Console → Indexing → Pages.
Issue Cause Fix Crawled — not indexed Thin / low-value page Improve content Blocked by robots.txt Robots too restrictive Loosen DisallowNot found (404) Broken page Fix or redirect - ✅ Indexing issues are classified and assigned.
7. Run a PageSpeed / Core Web Vitals pass
Section titled “7. Run a PageSpeed / Core Web Vitals pass”Measure, fix the high-leverage wins (images, caching, Core Web Vitals), then re-measure. Do this last — it’s the polish, not the foundation.
-
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.
-
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/heightto images for CLS;defernon-critical JS for INP).Terminal window find public -type f \( -name "*.jpg" -o -name "*.png" \) -size +100k # heavy imagescommand -v cwebp >/dev/null || { echo "install cwebp first"; exit 1; }cwebp -q 80 public/images/hero.jpg -o public/images/hero.webp # convert to WebPPHP_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.
-
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/cssAddOutputFilterByType DEFLATE application/javascript application/json image/svg+xml</IfModule><IfModule mod_expires.c>ExpiresActive OnExpiresByType 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 aContent-Encodingheader; static assets cache long-term, HTML does not.
- ✅
-
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.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Sitemap + robots —
/sitemap.xmlvalid 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 | Apptitle. - 👤 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).