diff --git a/skills/context/progress-tracker.md b/skills/context/progress-tracker.md
index e706699..783c3c8 100644
--- a/skills/context/progress-tracker.md
+++ b/skills/context/progress-tracker.md
@@ -91,6 +91,9 @@ All seven homepage sections are built, wired, and animated (scroll-driven entran
## Latest Handoff
+- **SEO metadata route cleanup** — `src/app/robots.ts` now omits the optional `host` directive and keeps canonical `Allow` + `Sitemap` output for indexable production hosts while preview/staging hosts remain fully disallowed.
+- **Organization JSON-LD hardening** — `src/components/seo/organization-jsonld.tsx` now escapes `<` (`\u003c`) during script serialization per Next.js guidance to guard against markup injection vectors.
+- **Verified** — `bun run build` passes; static metadata routes still emit (`/robots.txt`, `/sitemap.xml`) and JSON-LD remains present in built HTML.
- **Hash navigation + ScrollTrigger hardening** — `HashScrollSync` waits for safe `ScrollTrigger.refresh(true)` and Mission pin readiness before restoring deep links; abandoned restores cancel so hash spy resumes after route changes. `hash-navigation.ts` kills competing scroll tweens, updates spy on focus-in, keeps `#footer` while visible/focused/at max scroll, and resolves active hash via viewport/header intersection with a Services boundary hold (`#services` won't demote to `#monad-links`).
- **History runtime crash fixed** — Replaced `ScrollTrigger.batch()` + separate summary trigger with one trigger per milestone (final milestone reveals summary). Fixes `Cannot read properties of undefined (reading 'end')` during `ScrollTrigger.create()`.
- **Mission click-scroll guard** — `mission-pin-stack.ts` returns `null` when pin trigger bounds are non-finite; `mission-cards-client.tsx` bails and requests refresh instead of tweening invalid state.
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 0ddb9eb..88f8334 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -6,6 +6,7 @@ import { HashScrollSync } from "@/components/hash-scroll-sync";
import { RouteLoadingShell } from "@/components/layout/route-loading-shell";
import { ScrollToTopButton } from "@/components/layout/scroll-to-top-button";
import { SiteHeader } from "@/components/layout/site-header";
+import { OrganizationJsonLd } from "@/components/seo/organization-jsonld";
import { TooltipProvider } from "@/components/ui/tooltip";
import {
SITE_HEADER_SCROLL_BOOTSTRAP_SCRIPT,
@@ -100,6 +101,7 @@ export default function RootLayout({
/>
+
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..acb0b85
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,15 @@
+import type { MetadataRoute } from "next";
+
+import { IS_INDEXABLE_HOST, SITE_URL } from "@/lib/site";
+
+export default function robots(): MetadataRoute.Robots {
+ // Preview/staging hosts (e.g. preview.cortexglobal.xyz) must not be indexed.
+ if (!IS_INDEXABLE_HOST) {
+ return { rules: { userAgent: "*", disallow: "/" } };
+ }
+
+ return {
+ rules: { userAgent: "*", allow: "/" },
+ sitemap: `${SITE_URL}/sitemap.xml`,
+ };
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..82a8819
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,24 @@
+import type { MetadataRoute } from "next";
+
+import { SITE_URL } from "@/lib/site";
+
+// Static marketing routes under app/(site). Keep in sync when pages are added.
+export default function sitemap(): MetadataRoute.Sitemap {
+ const lastModified = new Date();
+
+ return [
+ { url: SITE_URL, lastModified, changeFrequency: "weekly", priority: 1 },
+ {
+ url: `${SITE_URL}/privacy`,
+ lastModified,
+ changeFrequency: "yearly",
+ priority: 0.3,
+ },
+ {
+ url: `${SITE_URL}/terms`,
+ lastModified,
+ changeFrequency: "yearly",
+ priority: 0.3,
+ },
+ ];
+}
diff --git a/src/components/seo/organization-jsonld.tsx b/src/components/seo/organization-jsonld.tsx
new file mode 100644
index 0000000..a252db1
--- /dev/null
+++ b/src/components/seo/organization-jsonld.tsx
@@ -0,0 +1,32 @@
+import { cortexSocialLinkKeys, externalLinks } from "@/lib/content/links";
+import {
+ OG_IMAGE_PATH,
+ SITE_DESCRIPTION,
+ SITE_NAME,
+ SITE_URL,
+} from "@/lib/site";
+
+// Organization structured data (schema.org) for rich results / knowledge panel.
+// schema.org consumers require absolute URLs, so everything resolves against
+// SITE_URL (driven by NEXT_PUBLIC_SITE_URL).
+const organizationSchema = {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ name: SITE_NAME,
+ url: SITE_URL,
+ logo: `${SITE_URL}${OG_IMAGE_PATH}`,
+ description: SITE_DESCRIPTION,
+ sameAs: cortexSocialLinkKeys.map((key) => externalLinks[key].href),
+};
+
+export function OrganizationJsonLd() {
+ return (
+
+ );
+}
diff --git a/src/lib/site.ts b/src/lib/site.ts
index 0d19d66..540569d 100644
--- a/src/lib/site.ts
+++ b/src/lib/site.ts
@@ -3,6 +3,21 @@ export const SITE_NAME = "Cortex Global" as const;
export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.cortexglobal.xyz";
+export const SITE_HOST = new URL(SITE_URL).host;
+
+/**
+ * Hosts whose deployments search engines may index — the live production
+ * domain(s) only. Preview/staging hosts (preview.cortexglobal.xyz, *.vercel.app)
+ * must stay out of the index, so robots.ts disallows everything unless SITE_HOST
+ * is in this set. Driven by NEXT_PUBLIC_SITE_URL via SITE_URL.
+ */
+const INDEXABLE_HOSTS: ReadonlySet = new Set([
+ "cortexglobal.xyz",
+ "www.cortexglobal.xyz",
+]);
+
+export const IS_INDEXABLE_HOST = INDEXABLE_HOSTS.has(SITE_HOST);
+
export const SITE_TAGLINE = "Local Service. Global Impact." as const;
export const SITE_DESCRIPTION =