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 ( +