The state of x9elysium.com,
told straight.
A full SEO and AI-search audit of x9elysium.com — what's working, what's quietly broken, and the five fixes that unlock the next twenty points of health score before any new content ships.
Listen · Full SEO Audit · Kate · Crisp BBC
X9Elysium SEO Audit — Full Report
Site: https://x9elysium.com
Audit date: 2026-05-02
Business type: Agency (Shopify consulting) with strong local-service signals (Toronto, Calgary, Mississauga)
Hosting platform observed: Hostinger (response header platform: hostinger, x-powered-by: Next.js) — note: CLAUDE.md and netlify.toml describe a static export to Netlify; current deploy does not match.
Executive Summary
| Category | Weight | Score | Weighted |
|---|---|---|---|
| Technical SEO | 22% | 45 | 9.9 |
| Content Quality | 23% | 55 | 12.7 |
| On-Page SEO | 20% | 40 | 8.0 |
| Schema / Structured Data | 10% | 85 | 8.5 |
| Performance (CWV) | 10% | 55 | 5.5 |
| AI Search Readiness | 10% | 60 | 6.0 |
| Images | 5% | 65 | 3.3 |
| SEO Health Score | 53.8 / 100 |
Headline finding: Foundations are well thought-out (rich JSON-LD graph, AI-crawler allowlist, llms.txt, per-blog metadata), but two production-grade defects are dragging the site down hard:
- Every blog post detail page returns HTTP 500. All 18 URLs in the sitemap under
/blog/<slug>5xx. Confirmed onpost-1,post-6,best-shopify-plus-agencies-toronto-2026,launching-dtc-brand-toronto-90-day-playbook. RSS feed and/blogindex work, but the actual articles do not. This is the #1 ranking-blocker on the site. - Five primary pages share the exact same
<title>and meta description:/,/about,/services,/work,/contact. They all returnX9Elysium — Shopify Commerce Consultingand the same description, because each subpage is a"use client"component and Next 14 App Router silently disallowsmetadataexports from client components — so they fall through to the root-layout defaults.
Fix those two and the score jumps roughly 20 points before any other work.
Top 5 critical issues (do this week)
/blog/<slug>returns 500 — HTTP 500 on every post. Internal links from/blog,/locations/*, footer, and llms.txt all lead to error pages. Search engines will deindex.- Duplicate title/meta on 5 main pages — homepage, about, services, work, contact all share the same title tag. Google treats this as a single canonical entity and will demote the duplicates.
- No canonical on the 5 main pages — only
/blog,/locations/*,/careers, blog-post pages have<link rel="canonical">. Homepage and money pages have none. - 404 page returns 500 —
/this-page-does-not-exist-12345returns HTTP 500 instead of 404. Google's crawler treats 5xx as a temporary issue and will retry indefinitely; Search Console will fill with soft errors. output: "export"removed fromnext.config.js—CLAUDE.mddescribes a static export to Netlify; the currentnext.config.jshas nooutputfield andnetlify.tomlstill sayspublish = "out". The build is in an inconsistent state and the live site is being served from Hostinger.
Top 5 quick wins
- Convert
app/about/page.tsx,app/services/page.tsx,app/work/page.tsx,app/contact/page.tsxto server components withmetadataexports (split client-only sections into child components). One commit fixes finding #2. - Add
alternates: { canonical: "https://x9elysium.com" }to root layout and per-page exports. - Delete
content/posts/post-1.md…post-10.md. They are leftover Hugo template fluff (generic ROI / strategic-planning essays) that hurt E-E-A-T. Remove from sitemap, add 410 (or 301 to a relevant real post). - Create
/public/images/og-image.png(1200×630). Today the blog metadata fallback points to a 404. - Generate a real
/404page (Next App Router'sapp/not-found.tsx) so soft-404s become hard 404s.
Technical SEO
Crawlability
- ✅
robots.txtis well-built:User-agent: *allows site, blocks/api/, lists sitemap. Explicit allow-list for 12 AI crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, PerplexityBot, Perplexity-User, Google-Extended, CCBot, anthropic-ai, Amazonbot, Applebot-Extended). - ✅
sitemap.xmlresolves at root, lists 33 URLs with priority, lastmod 2026-05-02 on most entries. - 🛑 Sitemap lists URLs that 500. All 18 blog posts and the
/terms-policypage are in the sitemap; blog posts are broken. Either fix the routes or remove from sitemap until they're back. - ⚠️ Trailing-slash behaviour returns 308. Internal links use the non-trailing form, which is fine, but do not let any external system link with a trailing slash — every redirect costs link equity.
Indexability
- ✅
<meta name="robots" content="index, follow">on all main pages. - ⚠️ Five main pages have no canonical:
/,/about,/services,/work,/contact. Without an explicit canonical, Google may pick the wrong URL when query strings or UTM params are appended (e.g., from Tawk.to or Clarity). - 🛑 Blog post 500s — confirmed on:
https://x9elysium.com/blog/post-1→ 500https://x9elysium.com/blog/post-6→ 500https://x9elysium.com/blog/best-shopify-plus-agencies-toronto-2026→ 500https://x9elysium.com/blog/launching-dtc-brand-toronto-90-day-playbook→ 500- Trailing-slash variants 308 → 500.
- 🛑 Hard 404 missing — random URL returns 500, not 404.
app/not-found.tsxdoes not exist.
Build / hosting consistency
- 🛑
next.config.jsno longer setsoutput: "export". PerCLAUDE.md, this site is intended to deploy as a static export to Netlify (netlify.tomlstill declarespublish = "out"). The live response headers show the site running on Hostinger withx-powered-by: Next.js. Either:- re-enable static export and deploy
out/to Netlify; or - commit to a Node host, update
CLAUDE.mdandnetlify.toml, and figure out why blog/[slug] specifically 500s on Hostinger.
- re-enable static export and deploy
- ⚠️
next.config.jsline 6:images: { unoptimized: true }. This is required for static export but means every<Image>ships the original file at every viewport — no WebP, no responsive srcset.
Security / headers
- ✅ HTTPS enforced.
content-security-policy: upgrade-insecure-requestsis present. - ⚠️ No
Strict-Transport-Security, noX-Content-Type-Options, noReferrer-Policy. Add these vianetlify.toml(or whichever host's headers config is live). - ⚠️ Tawk.to and Microsoft Clarity load via
Script strategy="afterInteractive". Both ship third-party JS that can degrade INP. PreferlazyOnloadfor Tawk.
Core Web Vitals
- Could not pull live CrUX data without auth. Static analysis flags:
- LCP risk: 4.4 MB
hero-video.mp4+ an animated emerald-glow gradient hero. Heavy first paint, especially on mobile. - INP risk: Tawk.to widget mounts a chat iframe + listeners; Clarity mounts a session-recorder. Both run on
afterInteractive. Combined, this often pushes mobile INP over 200 ms. - CLS: Likely fine — Inter via
next/font/googlereserves font space, all<Image>components have explicitwidth/height.
- LCP risk: 4.4 MB
- Action: run a real Lighthouse on
/and/locations/toronto; a 4.4 MB hero video should bepreload="none",playsinline, and ideally below-the-fold or replaced with a poster image.
On-Page SEO
Title tags & meta descriptions
| URL | Title | Description | Canonical |
|---|---|---|---|
/ |
"X9Elysium — Shopify Commerce Consulting" | shared default | ❌ none |
/about |
"X9Elysium — Shopify Commerce Consulting" | shared default | ❌ none |
/services |
"X9Elysium — Shopify Commerce Consulting" | shared default | ❌ none |
/work |
"X9Elysium — Shopify Commerce Consulting" | shared default | ❌ none |
/contact |
"X9Elysium — Shopify Commerce Consulting" | shared default | ❌ none |
/blog |
"Insights | X9Elysium — Shopify Strategy for Toronto, Calgary & Vancouver" | unique | ✅ |
/locations/toronto |
"Shopify Plus Agency in Toronto | X9Elysium" | unique | ✅ |
/locations/calgary |
"Shopify Agency in Calgary | X9Elysium" | unique | ✅ |
/careers |
"Careers | X9Elysium — Build the Future of Canadian Commerce With Us" | unique | ✅ |
Root cause: app/about/page.tsx:1, app/services/page.tsx:1, app/work/page.tsx:1, app/contact/page.tsx:1 all start with "use client". Client components in the App Router cannot export metadata — Next silently ignores it and falls back to the root layout. The careers/page.tsx works because it is a server component.
Fix pattern: keep the page as a server component, export metadata, and move client-side state/animations into a child component:
// app/services/page.tsx (server component)
import type { Metadata } from "next";
import ServicesClient from "./ServicesClient";
export const metadata: Metadata = {
title: "Shopify Plus Services | X9Elysium",
description: "Shopify store audits, custom apps, platform migrations, and unified commerce strategy for Canadian retailers.",
alternates: { canonical: "https://x9elysium.com/services" },
};
export default function ServicesPage() {
return <ServicesClient />;
}
Heading structure
- ✅ Single
<h1>on every page audited. - ⚠️ Earlier WebFetch read of
/worksummarized three H1-style strings ("Our Work", "Client Stories", "Let's build something that lasts."). Raw HTML shows only one<h1>tag, but verify the visual hierarchy matches semantic levels — sometimes Tailwind classes paint H2/H3 to look like display text and confuse the reader.
Internal linking
- ✅ Solid: nav links, footer, breadcrumb JSON-LD on inner pages, llms.txt cross-references.
- 🛑 Internal links to
/blog/<slug>from/blogindex, location pages, and llms.txt all currently lead to 500-ing pages. Until #1 is fixed, every internal blog link is hostile to crawlers.
Schema / Structured Data
This is the strongest area of the site.
- ✅ Root layout ships an
@graphwith three nodes:Organization,WebSite,ProfessionalService(app/layout.tsx:58-115). Includes ContactPoint, PostalAddress, sameAs (Instagram, Facebook, LinkedIn). - ✅ Blog index emits
Blog+BreadcrumbList(app/blog/page.tsx:34-70). - ✅ Blog post detail emits
BlogPosting(withinLanguage: "en-CA",wordCount, author withsameAs),BreadcrumbList, andFAQPageonly when the post defines FAQs (app/blog/[slug]/page.tsx:77-138). Conditional FAQ emission is correct — meets the Aug 2023 Google guideline. - ✅ Locations page ships
ProfessionalService(LocalBusiness shape) +BreadcrumbListwithareaServedlisting Toronto, Mississauga, Brampton, Markham, Vaughan, Hamilton, GTA admin area (app/locations/toronto/page.tsx:32-77). - ✅ Career detail emits
JobPostingwithhiringOrganization,jobLocationType: TELECOMMUTE,applicantLocationRequirements: Country/Canada(app/careers/[slug]/page.tsx:58-83).
Improvements
- ⚠️
JobPostingis missingvalidThrough,baseSalary, anddirectApply: false. Google now requiresvalidThroughto keep job listings eligible for the rich result. - ⚠️ Add
WebPagenodes to inner pages (services, work, contact) once they have unique metadata. - ⚠️ Consider
Serviceschema for the six service offerings on/services(one per service). - ⚠️
Organization.addressis set to Mississauga only;llms.txtand copy claim a presence in Vancouver and Calgary. If Vancouver is a real office (not just a service area), addOfficeLocationor a secondLocalBusinessnode. - ⚠️ No
aggregateRating/Reviewschema on the agency. If client testimonials are real and verifiable, this is a high-leverage add for AI citations.
Content Quality (E-E-A-T)
What's working
- Real authored content on the 8 location-aware MDX posts (Toronto, Calgary, Canada-wide). 1.4k–2k words each. First-person, opinionated, dated.
- Strong author signal:
Darshan Patelwith avatar, LinkedInsameAs, bio attached to every post viaDEFAULT_AUTHOR(app/lib/blog.ts:23-29). - Locations pages have unique metadata, schema, and pull only category-tagged posts.
What's hurting E-E-A-T
- 🛑
content/posts/post-1.md…post-10.mdare leftover Hugo template fluff. Topics: "Maximizing ROI with E-commerce Consulting", "Maximizing ROI with Strategic Planning", etc. — generic, AI-flavoured prose that doesn't cite client work, doesn't mention Shopify specifically, doesn't reflect the agency's real expertise. They dilute the topical authority the 8 real posts are building.post-6.mdis only 350 words — thin content.post-2.mdeven contains a placeholder broken image:.- These 10 URLs are in the sitemap, indexed (when they don't 500), and pull the site's authorship signal toward generic blog spam.
- ⚠️ Real posts have ISO dates of
2023-04-04(post-1) up through 2026 (real ones). When the post-N files get fixed and indexed, mixed-era content with nodateModifiedupdates looks stale. - ⚠️ No author page (
/authors/darshan-patel). The author is referenced ashttps://x9elysium.com/aboutinstead — fine, but a dedicated author entity strengthens E-E-A-T more.
Recommendation
Treat post-1 … post-10 as a deletion candidate set. For each:
- If the topic is genuinely on-brand: rewrite from scratch with X9Elysium specifics, add a real
dateModified, link to a real case study, and rename the slug to something descriptive. - Otherwise: 410 (gone) via Netlify
_redirectsor 301 to the closest topical real post.
Performance
- ⚠️ Hero video 4.4 MB at
public/hero-video.mp4. Even withplaysInlineand lazy-loading semantics, a 4 MB asset on a hero section is an LCP and data-cap risk on mobile. Either:- serve a poster JPG (~80 KB) as the LCP element and load the video on
requestIdleCallback; or - re-encode at 1080p H.265 + WebM at ~1.5 MB; or
- drop the video on viewports < 768 px.
- serve a poster JPG (~80 KB) as the LCP element and load the video on
- ⚠️ Tawk.to + Clarity both load on
afterInteractive(app/layout.tsx:129-152). Combined that's ~150–200 KB of third-party JS plus runtime cost. Move Tawk tolazyOnload(or todelayedbased on user interaction). Consider gating Clarity to non-iframe sessions only. - ⚠️
images: { unoptimized: true }(next.config.js:6) — required for static export, but means no automatic WebP/AVIF, no responsive srcset. If you stay on Hostinger Node, drop this flag and let Next optimise. - ✅ Inter via
next/font/googlewithdisplay: swapand explicit weights — eliminates CLS from font loading.
Images
- ✅ All 5 homepage
<img>elements have non-emptyalttext. - ✅ Explicit
width/heighton every<Image>keeps CLS contained. - ⚠️ No
og-image.pnginpublic/images/. Blog metadata fallback (app/blog/[slug]/page.tsx:32) points tohttps://x9elysium.com/images/og-image.pngwhich 404s. Twitter / LinkedIn previews for posts without aheroImagewill be image-less. - ⚠️ Logo files duplicated:
logo-1.png(86k),logo-x9e.png(86k),logo.png(12k),logo-dark.svg(11k),logo.svg(11k),logoo.jpg(63k),x9-logo.png(200k). Pick one canonical logo per format and delete the rest — keeps the bundle small and avoids cache thrash. - ⚠️
flower.jpg(313k) andInstagram.htmlin repo root look like leftovers — confirm they aren't shipping.
AI Search Readiness (GEO)
- ✅ Excellent
llms.txt(public/llms.txt) — TL;DR, ARR range, services list, locations served, recent insights with one-sentence summaries, RSS + sitemap pointers. - ✅ All major AI crawlers explicitly allowed in
robots.txt. - ✅
inLanguage: "en-CA"set onBlogPostingschema — reinforces Canadian relevance for AI grounding. - 🛑 Citability collapses on blog 500s. Perplexity, ChatGPT, and Gemini crawl
/blog/<slug>to verify the claims inllms.txt. When those URLs 500, the model's confidence in citing X9Elysium drops — and the AI allowlist becomes meaningless. - ⚠️
llms.txtmentions Vancouver as a served location, but there is no/locations/vancouverpage. Either build one (high-leverage local SEO for an under-served BC market) or remove the Vancouver line from llms.txt and copy. - ⚠️ No "Frequently Asked Questions" block on
/services,/contact, or/about. AI grounding picks up self-contained Q/A passages well; the homepage FAQ is a good model — repeat it on/services.
Sitemap audit
- ✅ Sitemap is valid XML, generated dynamically from
app/sitemap.ts, includes 33 URLs. - ✅ Sensible priorities:
/1.0,/contact0.9,/locations/*0.85,/blog0.8,/careers0.75. - 🛑 Sitemap lists 18 blog post URLs that 500.
- ⚠️ All
lastModifiedvalues default tonew Date()for static routes (app/sitemap.ts:11-57) — every build "updates" every page. Better to read git mtime or hard-code per-page lastmod. - ⚠️ Sitemap doesn't include
/blog/post-1…/blog/post-10URLs explicitly becausegetAllPostsreads them — once those template files are deleted, they automatically drop from the sitemap. Good.
Hreflang / international
- ✅
<html lang="en">set (app/layout.tsx:118). - ✅
inLanguage: "en-CA"onBlogPosting. - ⚠️ Mixed signals: HTML lang is
en(generic), schema isen-CA. Pick one. Recommendlang="en-CA"since target audience is Canadian. - N/A: no other languages — no hreflang work needed.