Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions skills/context/progress-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -100,6 +101,7 @@ export default function RootLayout({
/>
</head>
<body className="min-h-full flex flex-col">
<OrganizationJsonLd />
<TooltipProvider>
<HashScrollSync />
<SiteHeader />
Expand Down
15 changes: 15 additions & 0 deletions src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -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`,
};
}
24 changes: 24 additions & 0 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -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,
},
];
}
32 changes: 32 additions & 0 deletions src/components/seo/organization-jsonld.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<script
type="application/ld+json"
// Serialized from trusted constants only — no user input is interpolated.
dangerouslySetInnerHTML={{
__html: JSON.stringify(organizationSchema).replace(/</g, "\\u003c"),
}}
/>
);
}
15 changes: 15 additions & 0 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = 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 =
Expand Down
Loading