pagegraph 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +163 -124
  2. package/dist/audit.d.ts +1 -1
  3. package/dist/audit.js +1 -1
  4. package/dist/build-only/audit.d.ts +1 -0
  5. package/dist/build-only/audit.js +6 -0
  6. package/dist/build-only/audit.js.map +1 -0
  7. package/dist/build-only/config.d.ts +1 -0
  8. package/dist/build-only/config.js +6 -0
  9. package/dist/build-only/config.js.map +1 -0
  10. package/dist/build-only/tanstack-start.d.ts +1 -0
  11. package/dist/build-only/tanstack-start.js +6 -0
  12. package/dist/build-only/tanstack-start.js.map +1 -0
  13. package/dist/build-only/vite.d.ts +1 -0
  14. package/dist/build-only/vite.js +6 -0
  15. package/dist/build-only/vite.js.map +1 -0
  16. package/dist/checks-uZk5Em9A.js +352 -0
  17. package/dist/checks-uZk5Em9A.js.map +1 -0
  18. package/dist/cli.js +3814 -3017
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +10 -45
  21. package/dist/config.js +10 -98
  22. package/dist/config.js.map +1 -1
  23. package/dist/{content-DVL2iyGU.d.ts → content-B1cPtL8b.d.ts} +4 -128
  24. package/dist/{content-BE9KCTgb.js → content-kcEnQdfi.js} +4 -489
  25. package/dist/content-kcEnQdfi.js.map +1 -0
  26. package/dist/declare-D5o7hwmv.js +9 -0
  27. package/dist/declare-D5o7hwmv.js.map +1 -0
  28. package/dist/declare-DVgdEdOL.d.ts +12127 -0
  29. package/dist/graph-Bcsourlv.js +162 -0
  30. package/dist/graph-Bcsourlv.js.map +1 -0
  31. package/dist/graph-BxCY90vq.d.ts +104 -0
  32. package/dist/graph-CBIY71w6.d.ts +57 -0
  33. package/dist/index.d.ts +37 -73
  34. package/dist/index.js +23 -133
  35. package/dist/index.js.map +1 -1
  36. package/dist/message-DT_CjUdt.js +12 -0
  37. package/dist/message-DT_CjUdt.js.map +1 -0
  38. package/dist/page-heads-6y-CARRQ.js +25 -0
  39. package/dist/page-heads-6y-CARRQ.js.map +1 -0
  40. package/dist/page-heads-DNkjMUEN.d.ts +21 -0
  41. package/dist/projections-D0oOFKiY.js +142 -0
  42. package/dist/projections-D0oOFKiY.js.map +1 -0
  43. package/dist/projections-DeEFe7XT.d.ts +78 -0
  44. package/dist/react.d.ts +26 -12051
  45. package/dist/react.js +12 -3
  46. package/dist/react.js.map +1 -1
  47. package/dist/route-config-35ZEzkNC.js +308 -0
  48. package/dist/route-config-35ZEzkNC.js.map +1 -0
  49. package/dist/route-config-wneg6ktn.d.ts +29 -0
  50. package/dist/tanstack-start/index.d.ts +40 -0
  51. package/dist/tanstack-start/index.js +368 -0
  52. package/dist/tanstack-start/index.js.map +1 -0
  53. package/dist/tanstack-start/server.d.ts +14 -0
  54. package/dist/tanstack-start/server.js +53 -0
  55. package/dist/tanstack-start/server.js.map +1 -0
  56. package/dist/vite-graph-loader-01p1WxEq.js +104 -0
  57. package/dist/vite-graph-loader-01p1WxEq.js.map +1 -0
  58. package/dist/vite-graph-loader-Btgjz5RH.d.ts +56 -0
  59. package/dist/vite.d.ts +2 -28
  60. package/dist/vite.js +1 -307
  61. package/dist/wire-B4RQMc4e.js +26 -0
  62. package/dist/wire-B4RQMc4e.js.map +1 -0
  63. package/package.json +35 -13
  64. package/dist/content-BE9KCTgb.js.map +0 -1
  65. package/dist/vite.js.map +0 -1
package/dist/react.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as applyTitleTemplate } from "./declare-D5o7hwmv.js";
1
2
  import { Link, useMatches } from "@tanstack/react-router";
2
3
  import { Fragment } from "react";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -273,13 +274,14 @@ function createSeo(config) {
273
274
  const jsonLd = createJsonLd(config);
274
275
  const seoHead = (ctx, instance) => {
275
276
  const canonical = `${origin}${instance.canonicalPath ?? normalizePathname(ctx.match.pathname)}`;
276
- const resolvedOgTitle = instance.ogTitle ?? instance.title;
277
+ const title = applyTitleTemplate(ctx.match.staticData.seo?.titleTemplate, instance.title);
278
+ const resolvedOgTitle = instance.ogTitle ?? title;
277
279
  const resolvedOgDescription = instance.ogDescription ?? instance.description;
278
280
  const routeSeo = ctx.match.staticData.seo;
279
281
  const { article } = instance;
280
282
  const jsonLdEntries = [];
281
283
  const meta = [
282
- { title: instance.title },
284
+ { title },
283
285
  {
284
286
  name: "description",
285
287
  content: instance.description
@@ -392,16 +394,23 @@ function createSeo(config) {
392
394
  document
393
395
  })));
394
396
  for (const document of jsonLd.composeJsonLd(jsonLdEntries, { canonical })) meta.push({ "script:ld+json": document });
395
- return {
397
+ const head = {
396
398
  meta,
397
399
  links: [{
398
400
  rel: "canonical",
399
401
  href: canonical
400
402
  }]
401
403
  };
404
+ return config.transformHead === void 0 ? head : config.transformHead(head);
405
+ };
406
+ const head = (ctx) => {
407
+ const declared = ctx.match.staticData.seo?.head;
408
+ if (declared === void 0) throw new Error(`seo.head needs staticData.seo.head on route ${ctx.match.routeId}`);
409
+ return seoHead(ctx, declared);
402
410
  };
403
411
  return {
404
412
  seoHead,
413
+ head,
405
414
  ...jsonLd
406
415
  };
407
416
  }
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","names":["composeJsonLd","composeEntries"],"sources":["../src/react/breadcrumbs.tsx","../src/react/site.ts","../src/react/json-ld-composition.ts","../src/react/json-ld.ts","../src/react/create-seo.ts"],"sourcesContent":["import type { AnyRouteMatch } from \"@tanstack/react-router\";\nimport { Link, useMatches } from \"@tanstack/react-router\";\nimport { Fragment } from \"react\";\n\nexport interface CrumbTrailItem {\n name: string;\n path: string;\n}\n\n/**\n * Walk the match chain in order, keeping matches that declare `staticData.seo.crumb`,\n * resolving string | fn form. Shared by <Breadcrumbs/> and seoHead's BreadcrumbList.\n */\nexport function resolveCrumbTrail(matches: ReadonlyArray<AnyRouteMatch>): Array<CrumbTrailItem> {\n const trail: Array<CrumbTrailItem> = [];\n for (const match of matches) {\n const crumb = match.staticData.seo?.crumb;\n if (crumb === undefined) continue;\n const name = typeof crumb === \"function\" ? crumb(match) : crumb;\n trail.push({ name, path: match.pathname });\n }\n return trail;\n}\n\nexport function Breadcrumbs({ className }: { className?: string | undefined }) {\n const trail = resolveCrumbTrail(useMatches());\n if (trail.length < 2) return null;\n\n const lastIndex = trail.length - 1;\n return (\n <nav aria-label=\"Breadcrumb\" className={className}>\n <ol className=\"text-muted-foreground flex items-center gap-2 font-mono text-[11px] tracking-wide uppercase\">\n {trail.map((item, index) => {\n const isLast = index === lastIndex;\n return (\n <Fragment key={item.path}>\n {index > 0 ? (\n <li aria-hidden=\"true\" className=\"text-muted-foreground/50\">\n /\n </li>\n ) : null}\n <li\n aria-current={isLast ? \"page\" : undefined}\n className={isLast ? \"text-foreground truncate\" : undefined}\n >\n {isLast ? (\n item.name\n ) : (\n <Link to={item.path} className=\"hover:text-foreground transition-colors\">\n {item.name}\n </Link>\n )}\n </li>\n </Fragment>\n );\n })}\n </ol>\n </nav>\n );\n}\n","/**\n * Site identity — the values a package cannot know about its host.\n *\n * Bound once through {@link createSeo}, so the ~30 route call sites that render\n * a head never thread an origin or a brand name through their arguments.\n * Everything here is a *value*; the schema construction it feeds lives in\n * `json-ld.ts`.\n */\n\nimport type { SeoJsonLdConfig } from \"./json-ld-composition\";\n\n/** Absolute URL for an origin-relative path; an already-absolute URL passes through. */\nexport function absoluteUrl(origin: string, pathOrUrl: string): string {\n return pathOrUrl.startsWith(\"http\") ? pathOrUrl : `${origin}${pathOrUrl}`;\n}\n\nexport interface SeoSite {\n /** Brand name: Organization/WebSite `name`, Article publisher, Service provider default. */\n name: string;\n /** Organization `logo`. Origin-relative path or absolute URL. */\n logo: string;\n /** Article `publisher.logo` (Google wants an ImageObject distinct from the brand mark). */\n publisherLogo: string;\n /** Article `image` when the instance declares none. */\n defaultImage: string;\n /** Article `author` when the instance declares none. */\n defaultAuthor: { name: string; url?: string | undefined };\n}\n\n/** Public mailing address rendered as a schema.org PostalAddress. */\nexport interface SeoPostalAddress {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string;\n postalCode: string;\n addressCountry: string;\n}\n\n/** The site-wide Organization entity (Knowledge Graph). */\nexport interface SeoOrganization {\n legalName?: string | undefined;\n description: string;\n sameAs: ReadonlyArray<string>;\n contactPoint: { contactType: string; email: string };\n address?: SeoPostalAddress | undefined;\n}\n\n/** The site-wide WebSite entity (sitelinks search box). */\nexport interface SeoWebsite {\n /** Origin-relative search target carrying the `{search_term_string}` placeholder. */\n searchPath: string;\n}\n\nexport interface SeoConfig {\n /** Canonical origin, no trailing slash — e.g. `https://example.com`. */\n origin: string;\n site: SeoSite;\n organization: SeoOrganization;\n website: SeoWebsite;\n jsonLd?: SeoJsonLdConfig | undefined;\n}\n","import type {\n Article,\n BreadcrumbList,\n FAQPage,\n Graph,\n IdReference,\n ItemList,\n Organization,\n Service,\n Thing,\n WebSite,\n WithContext,\n} from \"schema-dts\";\n\n/** Any schema.org entity accepted by `schema-dts`. */\nexport type JsonLdNode = Extract<Thing, object>;\n\n/** A top-level JSON-LD entity or graph ready to serialize into a script element. */\nexport type JsonLdDocument = WithContext<Thing> | Graph;\n\nexport interface JsonLdEntityIds {\n organization: string;\n website: string;\n}\n\nexport type JsonLdEntry =\n | {\n kind: \"organization\";\n source: \"generated\";\n document: WithContext<Organization>;\n }\n | { kind: \"website\"; source: \"generated\"; document: WithContext<WebSite> }\n | { kind: \"article\"; source: \"generated\"; document: WithContext<Article> }\n | { kind: \"service\"; source: \"generated\"; document: WithContext<Service> }\n | { kind: \"faq\"; source: \"generated\"; document: WithContext<FAQPage> }\n | {\n kind: \"breadcrumb\";\n source: \"generated\";\n document: WithContext<BreadcrumbList>;\n }\n | { kind: \"item-list\"; source: \"generated\"; document: WithContext<ItemList> }\n | { kind: \"custom\"; source: \"site\" | \"page\"; document: JsonLdDocument };\n\nexport interface JsonLdTransformContext {\n /** Canonical site origin configured through `createSeo`. */\n origin: string;\n /** Stable identifiers for the plugin's generated site entities. */\n entityIds: JsonLdEntityIds;\n /** Canonical page URL when composition happens for a route. */\n canonical?: string | undefined;\n}\n\n/** Return no document to suppress an entry, one to replace it, or an array to expand it. */\nexport type JsonLdTransformResult =\n | JsonLdDocument\n | ReadonlyArray<JsonLdDocument>\n | false\n | null\n | undefined;\n\nexport type JsonLdTransform = (\n entry: JsonLdEntry,\n context: JsonLdTransformContext,\n) => JsonLdTransformResult;\n\nexport interface SeoJsonLdConfig {\n /** Custom entities included by `generateSiteGraphSchema`. */\n site?:\n | ReadonlyArray<JsonLdDocument>\n | ((entityIds: JsonLdEntityIds) => ReadonlyArray<JsonLdDocument>)\n | undefined;\n /** Open composition hook for generated and custom entries. */\n transform?: JsonLdTransform | undefined;\n}\n\n/** Add the schema.org context to an entity while preserving its concrete type. */\nexport function defineJsonLd<const Node extends JsonLdNode & object>(\n node: Node,\n): Node & { \"@context\": \"https://schema.org\" } {\n return { \"@context\": \"https://schema.org\", ...node };\n}\n\n/** Reference a canonical entity declared elsewhere in the same graph or page. */\nexport function jsonLdRef(id: string): IdReference {\n return { \"@id\": id };\n}\n\n/** Extend a generated or custom document without rebuilding its existing properties. */\nexport function extendJsonLd<\n const Document extends JsonLdDocument,\n const Extension extends object,\n>(document: Document, extension: Extension): Document & Extension {\n return { ...(document as unknown as object), ...extension } as Document &\n Extension;\n}\n\nconst documentNodes = (document: JsonLdDocument): ReadonlyArray<JsonLdNode> => {\n if (\"@graph\" in document) {\n return document[\"@graph\"] as ReadonlyArray<JsonLdNode>;\n }\n const { \"@context\": _context, ...node } = document as unknown as Record<\n string,\n unknown\n >;\n return [node as JsonLdNode];\n};\n\n/** Compose related schema.org documents into one graph without nested `@context` values. */\nexport function jsonLdGraph(documents: ReadonlyArray<JsonLdDocument>): Graph {\n return {\n \"@context\": \"https://schema.org\",\n \"@graph\": documents.flatMap(documentNodes),\n };\n}\n\n/** Apply an optional zero/one/many transform while preserving entry order. */\nexport function composeJsonLd(\n entries: ReadonlyArray<JsonLdEntry>,\n context: JsonLdTransformContext,\n transform?: JsonLdTransform,\n): Array<JsonLdDocument> {\n if (transform === undefined) {\n return entries.map((entry) => entry.document as JsonLdDocument);\n }\n\n return entries.flatMap((entry) => {\n const result = transform(entry, context);\n if (result === undefined || result === null || result === false) return [];\n return Array.isArray(result) ? [...result] : [result as JsonLdDocument];\n });\n}\n","/**\n * JSON-LD structured-data generators.\n *\n * schema.org payloads typed against `schema-dts`, which is a types-only devDep —\n * it erases at build, so this module keeps the entry's zero-runtime-dep rule.\n * The brand values (name, logo, sameAs, the Organization description) are the\n * host's, and arrive through {@link SeoConfig}; the schema construction is ours.\n *\n * @see https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data\n * @see https://schema.org/\n */\n\nimport type {\n Article,\n BreadcrumbList,\n FAQPage,\n Graph,\n ItemList,\n Organization,\n Person,\n Question,\n Service,\n WebSite,\n WithContext,\n} from \"schema-dts\";\n\nimport {\n composeJsonLd as composeEntries,\n jsonLdGraph,\n jsonLdRef,\n type JsonLdDocument,\n type JsonLdEntityIds,\n type JsonLdEntry,\n} from \"./json-ld-composition\";\nimport { absoluteUrl, type SeoConfig } from \"./site\";\n\nexport interface ServiceParams {\n name: string;\n description: string;\n serviceType: string;\n provider?: string | undefined;\n areaServed?: string | undefined;\n}\n\nexport interface FAQItem {\n question: string;\n answer: string;\n category?: string | undefined;\n isHighlighted?: boolean | undefined;\n}\n\nexport interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\nexport interface ItemListEntry {\n name: string;\n url: string;\n}\n\nexport interface ArticleParams {\n headline: string;\n description: string;\n image?: string | undefined;\n datePublished: string;\n dateModified?: string | undefined;\n author: {\n name: string;\n url?: string | undefined;\n };\n url: string;\n}\n\n/** The JSON-LD generators, bound to one site identity. */\nexport interface JsonLd {\n readonly entityIds: JsonLdEntityIds;\n /** Brand entity for the Knowledge Graph. Render site-wide (root layout). */\n generateOrganizationSchema: () => WithContext<Organization>;\n /** WebSite entity with a sitelinks search box. Render on the homepage. */\n generateWebsiteSchema: () => WithContext<WebSite>;\n generateServiceSchema: (params: ServiceParams) => WithContext<Service>;\n generateFAQPageSchema: (faqs: FAQItem[]) => WithContext<FAQPage>;\n generateBreadcrumbSchema: (\n items: BreadcrumbItem[],\n ) => WithContext<BreadcrumbList>;\n /** Describe a visibly rendered, ordered collection of canonical pages. */\n generateItemListSchema: (\n name: string,\n items: ReadonlyArray<ItemListEntry>,\n ) => WithContext<ItemList>;\n generateArticleSchema: (params: ArticleParams) => WithContext<Article>;\n /** Compose the Organization, WebSite, and configured site entities into one graph. */\n generateSiteGraphSchema: () => Graph | undefined;\n /** Apply the configured transform to generated or custom entries. */\n composeJsonLd: (\n entries: ReadonlyArray<JsonLdEntry>,\n context?: { canonical?: string | undefined },\n ) => Array<JsonLdDocument>;\n}\n\nexport function createJsonLd(config: SeoConfig): JsonLd {\n const { origin, site, organization, website } = config;\n const organizationId = `${origin}/#organization`;\n const websiteId = `${origin}/#website`;\n const entityIds = { organization: organizationId, website: websiteId };\n\n const generateOrganizationSchema = (): WithContext<Organization> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n \"@id\": organizationId,\n name: site.name,\n ...(organization.legalName === undefined\n ? {}\n : { legalName: organization.legalName }),\n url: origin,\n logo: absoluteUrl(origin, site.logo),\n description: organization.description,\n sameAs: [...organization.sameAs],\n contactPoint: {\n \"@type\": \"ContactPoint\",\n contactType: organization.contactPoint.contactType,\n email: organization.contactPoint.email,\n },\n ...(organization.address === undefined\n ? {}\n : {\n address: {\n \"@type\": \"PostalAddress\" as const,\n ...organization.address,\n },\n }),\n });\n\n const generateWebsiteSchema = (): WithContext<WebSite> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n \"@id\": websiteId,\n name: site.name,\n url: origin,\n publisher: jsonLdRef(organizationId),\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: {\n \"@type\": \"EntryPoint\",\n urlTemplate: absoluteUrl(origin, website.searchPath),\n },\n query: \"required name=search_term_string\",\n },\n });\n\n const generateServiceSchema = (\n params: ServiceParams,\n ): WithContext<Service> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"Service\",\n name: params.name,\n description: params.description,\n serviceType: params.serviceType,\n provider: {\n \"@type\": \"Organization\",\n ...(params.provider === undefined\n ? { \"@id\": organizationId, url: origin }\n : {}),\n name: params.provider ?? site.name,\n },\n areaServed: params.areaServed ?? \"Worldwide\",\n });\n\n const generateFAQPageSchema = (faqs: FAQItem[]): WithContext<FAQPage> => {\n const questions: Question[] = faqs.map((faq) => ({\n \"@type\": \"Question\",\n name: faq.question,\n acceptedAnswer: {\n \"@type\": \"Answer\",\n text: faq.answer,\n },\n }));\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: questions,\n };\n };\n\n const generateBreadcrumbSchema = (\n items: BreadcrumbItem[],\n ): WithContext<BreadcrumbList> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((item, index) => ({\n \"@type\": \"ListItem\",\n position: index + 1,\n name: item.name,\n item: `${origin}${item.url}`,\n })),\n });\n\n const generateItemListSchema = (\n name: string,\n items: ReadonlyArray<ItemListEntry>,\n ): WithContext<ItemList> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n name,\n numberOfItems: items.length,\n itemListElement: items.map((item, index) => ({\n \"@type\": \"ListItem\",\n position: index + 1,\n name: item.name,\n url: `${origin}${item.url}`,\n })),\n });\n\n const generateArticleSchema = (\n params: ArticleParams,\n ): WithContext<Article> => {\n const authorSchema: Person = {\n \"@type\": \"Person\",\n name: params.author.name,\n ...(params.author.url && { url: params.author.url }),\n };\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: params.headline,\n description: params.description,\n // Deliberately NOT absolutized: a declared instance image lands verbatim\n // (og:image is where the absolute form is required, and seoHead does that).\n image: params.image ?? absoluteUrl(origin, site.defaultImage),\n datePublished: params.datePublished,\n dateModified: params.dateModified ?? params.datePublished,\n author: authorSchema,\n publisher: {\n \"@type\": \"Organization\",\n \"@id\": organizationId,\n name: site.name,\n logo: {\n \"@type\": \"ImageObject\",\n url: absoluteUrl(origin, site.publisherLogo),\n },\n },\n mainEntityOfPage: {\n \"@type\": \"WebPage\",\n \"@id\": params.url,\n },\n };\n };\n\n const composeJsonLd: JsonLd[\"composeJsonLd\"] = (entries, context = {}) =>\n composeEntries(\n entries,\n {\n origin,\n entityIds,\n ...(context.canonical === undefined\n ? {}\n : { canonical: context.canonical }),\n },\n config.jsonLd?.transform,\n );\n\n const generateSiteGraphSchema = (): Graph | undefined => {\n const configuredSite =\n typeof config.jsonLd?.site === \"function\"\n ? config.jsonLd.site(entityIds)\n : (config.jsonLd?.site ?? []);\n const entries: Array<JsonLdEntry> = [\n {\n kind: \"organization\",\n source: \"generated\",\n document: generateOrganizationSchema(),\n },\n {\n kind: \"website\",\n source: \"generated\",\n document: generateWebsiteSchema(),\n },\n ...configuredSite.map(\n (document): JsonLdEntry => ({\n kind: \"custom\",\n source: \"site\",\n document,\n }),\n ),\n ];\n const documents = composeJsonLd(entries);\n return documents.length === 0 ? undefined : jsonLdGraph(documents);\n };\n\n return {\n entityIds,\n generateOrganizationSchema,\n generateWebsiteSchema,\n generateServiceSchema,\n generateFAQPageSchema,\n generateBreadcrumbSchema,\n generateItemListSchema,\n generateArticleSchema,\n generateSiteGraphSchema,\n composeJsonLd,\n };\n}\n","/**\n * The render layer: one route's SEO declaration plus its per-instance content,\n * composed into the meta/link descriptors TanStack Router renders into <head>,\n * with the JSON-LD that page warrants.\n *\n * {@link createSeo} binds the site identity once (see `site.ts`) and returns\n * the bound API, so a route file calls `seoHead(ctx, instance)` and never sees\n * an origin or a brand name.\n */\n\nimport type { AnyRouteMatch, MetaDescriptor } from \"@tanstack/react-router\";\n\nimport { resolveCrumbTrail } from \"./breadcrumbs\";\nimport type { JsonLdDocument, JsonLdEntry } from \"./json-ld-composition\";\nimport { absoluteUrl, type SeoConfig } from \"./site\";\nimport { createJsonLd, type FAQItem, type JsonLd } from \"./json-ld\";\n\n/** Structural subset of TanStack's head() ctx — the leaf match plus the full chain. */\nexport interface SeoHeadCtx {\n matches: ReadonlyArray<AnyRouteMatch>;\n match: AnyRouteMatch;\n}\n\nexport interface PageSeoInstance {\n /** FULL title — no suffix is appended. */\n title: string;\n description: string;\n ogTitle?: string | undefined;\n ogDescription?: string | undefined;\n /** Instance override of the route's robots policy. */\n robots?: string | undefined;\n article?:\n | {\n publishedAt: string;\n modifiedAt?: string | undefined;\n author?: { name: string; url?: string | undefined } | undefined;\n image?: string | undefined;\n tags?: ReadonlyArray<string> | undefined;\n }\n | undefined;\n faqs?: ReadonlyArray<FAQItem> | undefined;\n service?:\n | { name: string; description?: string | undefined; serviceType: string }\n | undefined;\n /** Standalone keywords meta for non-article pages; article pages derive it from tags. */\n keywords?: ReadonlyArray<string> | undefined;\n /** Visible canonical pages rendered as an ordered collection on this page. */\n itemList?:\n | {\n name: string;\n items: ReadonlyArray<{ name: string; url: string }>;\n }\n | undefined;\n /** Additional schema.org documents for this route. */\n jsonLd?: ReadonlyArray<JsonLdDocument> | undefined;\n /**\n * Explicit canonical path (used for canonical + og:url instead of the match pathname).\n * For routes whose canonical is computed independently of the URL, e.g. a docs splat\n * that derives it from the resolved slug segments in its loader.\n */\n canonicalPath?: string | undefined;\n /**\n * Explicit breadcrumb trail for the BreadcrumbList JSON-LD, overriding the match-chain\n * trail. For routes whose hierarchy lives outside the route tree — a docs splat whose\n * ancestry is a content page tree. Each item's `path` is a URL path.\n */\n breadcrumbs?: ReadonlyArray<{ name: string; path: string }> | undefined;\n}\n\nexport interface SeoHead {\n meta: NonNullable<AnyRouteMatch[\"meta\"]>;\n links: Array<{ rel: \"canonical\"; href: string }>;\n}\n\n/** The render API, bound to one site identity. */\nexport interface Seo extends JsonLd {\n seoHead: (ctx: SeoHeadCtx, instance: PageSeoInstance) => SeoHead;\n}\n\n/** Strip a trailing slash from a pathname, keeping the root \"/\" intact. */\nfunction normalizePathname(pathname: string): string {\n return pathname.length > 1 && pathname.endsWith(\"/\")\n ? pathname.slice(0, -1)\n : pathname;\n}\n\nexport function createSeo(config: SeoConfig): Seo {\n const { origin, site } = config;\n const jsonLd = createJsonLd(config);\n\n const seoHead = (ctx: SeoHeadCtx, instance: PageSeoInstance): SeoHead => {\n const canonical = `${origin}${instance.canonicalPath ?? normalizePathname(ctx.match.pathname)}`;\n const resolvedOgTitle = instance.ogTitle ?? instance.title;\n const resolvedOgDescription =\n instance.ogDescription ?? instance.description;\n const routeSeo = ctx.match.staticData.seo;\n const { article } = instance;\n const jsonLdEntries: Array<JsonLdEntry> = [];\n\n const meta: Array<MetaDescriptor> = [\n { title: instance.title },\n { name: \"description\", content: instance.description },\n { property: \"og:title\", content: resolvedOgTitle },\n { property: \"og:description\", content: resolvedOgDescription },\n { property: \"og:type\", content: article ? \"article\" : \"website\" },\n { property: \"og:url\", content: canonical },\n { name: \"twitter:card\", content: \"summary_large_image\" },\n { name: \"twitter:title\", content: resolvedOgTitle },\n { name: \"twitter:description\", content: resolvedOgDescription },\n ];\n\n const robots = instance.robots ?? routeSeo?.robots;\n if (robots !== undefined) {\n meta.push({ name: \"robots\", content: robots });\n }\n\n if (article) {\n if (article.tags?.length) {\n meta.push({ name: \"keywords\", content: article.tags.join(\", \") });\n }\n meta.push(\n { property: \"article:published_time\", content: article.publishedAt },\n {\n property: \"article:modified_time\",\n content: article.modifiedAt ?? article.publishedAt,\n },\n );\n if (article.image !== undefined) {\n const imageUrl = absoluteUrl(origin, article.image);\n meta.push(\n { property: \"og:image\", content: imageUrl },\n { name: \"twitter:image\", content: imageUrl },\n );\n }\n }\n\n if (article) {\n jsonLdEntries.push({\n kind: \"article\",\n source: \"generated\",\n document: jsonLd.generateArticleSchema({\n headline: resolvedOgTitle,\n description: instance.description,\n image: article.image,\n datePublished: article.publishedAt,\n dateModified: article.modifiedAt ?? article.publishedAt,\n author: article.author ?? site.defaultAuthor,\n url: canonical,\n }),\n });\n }\n\n if (!article && instance.keywords) {\n meta.push({ name: \"keywords\", content: instance.keywords.join(\", \") });\n }\n\n if (instance.service) {\n jsonLdEntries.push({\n kind: \"service\",\n source: \"generated\",\n document: jsonLd.generateServiceSchema({\n name: instance.service.name,\n description: instance.service.description ?? instance.description,\n serviceType: instance.service.serviceType,\n }),\n });\n }\n\n if (instance.faqs) {\n jsonLdEntries.push({\n kind: \"faq\",\n source: \"generated\",\n document: jsonLd.generateFAQPageSchema([...instance.faqs]),\n });\n }\n\n if (instance.itemList) {\n jsonLdEntries.push({\n kind: \"item-list\",\n source: \"generated\",\n document: jsonLd.generateItemListSchema(\n instance.itemList.name,\n instance.itemList.items,\n ),\n });\n }\n\n // TanStack accumulates every matched route's head output. Only the current leaf\n // owns the BreadcrumbList; otherwise each parent repeats a progressively stale\n // trail alongside the leaf's canonical trail on nested pages.\n const isLeafMatch = ctx.matches.at(-1)?.id === ctx.match.id;\n const trail = isLeafMatch\n ? (instance.breadcrumbs ?? resolveCrumbTrail(ctx.matches))\n : [];\n if (trail.length >= 2) {\n jsonLdEntries.push({\n kind: \"breadcrumb\",\n source: \"generated\",\n document: jsonLd.generateBreadcrumbSchema(\n trail.map((item) => ({ name: item.name, url: item.path })),\n ),\n });\n }\n\n if (instance.jsonLd) {\n jsonLdEntries.push(\n ...instance.jsonLd.map(\n (document): JsonLdEntry => ({\n kind: \"custom\",\n source: \"page\",\n document,\n }),\n ),\n );\n }\n\n for (const document of jsonLd.composeJsonLd(jsonLdEntries, { canonical })) {\n meta.push({ \"script:ld+json\": document });\n }\n\n // React's head types (JSX.IntrinsicElements['meta']) model only <meta> attributes;\n // TanStack renders `script:ld+json` entries as JSON-LD <script> tags at runtime. The\n // installed @tanstack/react-router augments leaf head meta to those JSX attributes, so\n // the JSON-LD entries are asserted to it (same pattern as an inline root JSON-LD).\n return {\n meta: meta as NonNullable<AnyRouteMatch[\"meta\"]>,\n links: [{ rel: \"canonical\", href: canonical }],\n };\n };\n\n return { seoHead, ...jsonLd };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","names":["composeJsonLd","composeEntries"],"sources":["../src/react/breadcrumbs.tsx","../src/react/site.ts","../src/react/json-ld-composition.ts","../src/react/json-ld.ts","../src/react/create-seo.ts"],"sourcesContent":["import type { AnyRouteMatch } from \"@tanstack/react-router\";\nimport { Link, useMatches } from \"@tanstack/react-router\";\nimport { Fragment } from \"react\";\n\nexport interface CrumbTrailItem {\n name: string;\n path: string;\n}\n\n/**\n * Walk the match chain in order, keeping matches that declare `staticData.seo.crumb`,\n * resolving string | fn form. Shared by <Breadcrumbs/> and seoHead's BreadcrumbList.\n */\nexport function resolveCrumbTrail(matches: ReadonlyArray<AnyRouteMatch>): Array<CrumbTrailItem> {\n const trail: Array<CrumbTrailItem> = [];\n for (const match of matches) {\n const crumb = match.staticData.seo?.crumb;\n if (crumb === undefined) continue;\n const name = typeof crumb === \"function\" ? crumb(match) : crumb;\n trail.push({ name, path: match.pathname });\n }\n return trail;\n}\n\nexport function Breadcrumbs({ className }: { className?: string | undefined }) {\n const trail = resolveCrumbTrail(useMatches());\n if (trail.length < 2) return null;\n\n const lastIndex = trail.length - 1;\n return (\n <nav aria-label=\"Breadcrumb\" className={className}>\n <ol className=\"text-muted-foreground flex items-center gap-2 font-mono text-[11px] tracking-wide uppercase\">\n {trail.map((item, index) => {\n const isLast = index === lastIndex;\n return (\n <Fragment key={item.path}>\n {index > 0 ? (\n <li aria-hidden=\"true\" className=\"text-muted-foreground/50\">\n /\n </li>\n ) : null}\n <li\n aria-current={isLast ? \"page\" : undefined}\n className={isLast ? \"text-foreground truncate\" : undefined}\n >\n {isLast ? (\n item.name\n ) : (\n <Link to={item.path} className=\"hover:text-foreground transition-colors\">\n {item.name}\n </Link>\n )}\n </li>\n </Fragment>\n );\n })}\n </ol>\n </nav>\n );\n}\n","/**\n * Site identity — the values a package cannot know about its host.\n *\n * Bound once through {@link createSeo}, so the ~30 route call sites that render\n * a head never thread an origin or a brand name through their arguments.\n * Everything here is a *value*; the schema construction it feeds lives in\n * `json-ld.ts`.\n */\n\nimport type { SeoHead } from \"./create-seo\";\nimport type { SeoJsonLdConfig } from \"./json-ld-composition\";\n\n/** Absolute URL for an origin-relative path; an already-absolute URL passes through. */\nexport function absoluteUrl(origin: string, pathOrUrl: string): string {\n return pathOrUrl.startsWith(\"http\") ? pathOrUrl : `${origin}${pathOrUrl}`;\n}\n\nexport interface SeoSite {\n /** Brand name: Organization/WebSite `name`, Article publisher, Service provider default. */\n name: string;\n /** Organization `logo`. Origin-relative path or absolute URL. */\n logo: string;\n /** Article `publisher.logo` (Google wants an ImageObject distinct from the brand mark). */\n publisherLogo: string;\n /** Article `image` when the instance declares none. */\n defaultImage: string;\n /** Article `author` when the instance declares none. */\n defaultAuthor: { name: string; url?: string | undefined };\n}\n\n/** Public mailing address rendered as a schema.org PostalAddress. */\nexport interface SeoPostalAddress {\n streetAddress: string;\n addressLocality: string;\n addressRegion: string;\n postalCode: string;\n addressCountry: string;\n}\n\n/** The site-wide Organization entity (Knowledge Graph). */\nexport interface SeoOrganization {\n legalName?: string | undefined;\n description: string;\n sameAs: ReadonlyArray<string>;\n contactPoint: { contactType: string; email: string };\n address?: SeoPostalAddress | undefined;\n}\n\n/** The site-wide WebSite entity (sitelinks search box). */\nexport interface SeoWebsite {\n /** Origin-relative search target carrying the `{search_term_string}` placeholder. */\n searchPath: string;\n}\n\nexport interface SeoConfig {\n /** Canonical origin, no trailing slash — e.g. `https://example.com`. */\n origin: string;\n site: SeoSite;\n organization: SeoOrganization;\n website: SeoWebsite;\n jsonLd?: SeoJsonLdConfig | undefined;\n /**\n * Last-mile override for every head `seoHead` renders, e.g. to append\n * site-specific meta. Runs after the canonical, title template, and JSON-LD.\n */\n transformHead?: ((head: SeoHead) => SeoHead) | undefined;\n}\n","import type {\n Article,\n BreadcrumbList,\n FAQPage,\n Graph,\n IdReference,\n ItemList,\n Organization,\n Service,\n Thing,\n WebSite,\n WithContext,\n} from \"schema-dts\";\n\n/** Any schema.org entity accepted by `schema-dts`. */\nexport type JsonLdNode = Extract<Thing, object>;\n\nimport type { JsonLdDocument } from \"../core/declare\";\n\nexport type { JsonLdDocument };\n\nexport interface JsonLdEntityIds {\n organization: string;\n website: string;\n}\n\nexport type JsonLdEntry =\n | {\n kind: \"organization\";\n source: \"generated\";\n document: WithContext<Organization>;\n }\n | { kind: \"website\"; source: \"generated\"; document: WithContext<WebSite> }\n | { kind: \"article\"; source: \"generated\"; document: WithContext<Article> }\n | { kind: \"service\"; source: \"generated\"; document: WithContext<Service> }\n | { kind: \"faq\"; source: \"generated\"; document: WithContext<FAQPage> }\n | {\n kind: \"breadcrumb\";\n source: \"generated\";\n document: WithContext<BreadcrumbList>;\n }\n | { kind: \"item-list\"; source: \"generated\"; document: WithContext<ItemList> }\n | { kind: \"custom\"; source: \"site\" | \"page\"; document: JsonLdDocument };\n\nexport interface JsonLdTransformContext {\n /** Canonical site origin configured through `createSeo`. */\n origin: string;\n /** Stable identifiers for the plugin's generated site entities. */\n entityIds: JsonLdEntityIds;\n /** Canonical page URL when composition happens for a route. */\n canonical?: string | undefined;\n}\n\n/** Return no document to suppress an entry, one to replace it, or an array to expand it. */\nexport type JsonLdTransformResult =\n | JsonLdDocument\n | ReadonlyArray<JsonLdDocument>\n | false\n | null\n | undefined;\n\nexport type JsonLdTransform = (\n entry: JsonLdEntry,\n context: JsonLdTransformContext,\n) => JsonLdTransformResult;\n\nexport interface SeoJsonLdConfig {\n /** Custom entities included by `generateSiteGraphSchema`. */\n site?:\n | ReadonlyArray<JsonLdDocument>\n | ((entityIds: JsonLdEntityIds) => ReadonlyArray<JsonLdDocument>)\n | undefined;\n /** Open composition hook for generated and custom entries. */\n transform?: JsonLdTransform | undefined;\n}\n\n/** Add the schema.org context to an entity while preserving its concrete type. */\nexport function defineJsonLd<const Node extends JsonLdNode & object>(\n node: Node,\n): Node & { \"@context\": \"https://schema.org\" } {\n return { \"@context\": \"https://schema.org\", ...node };\n}\n\n/** Reference a canonical entity declared elsewhere in the same graph or page. */\nexport function jsonLdRef(id: string): IdReference {\n return { \"@id\": id };\n}\n\n/** Extend a generated or custom document without rebuilding its existing properties. */\nexport function extendJsonLd<\n const Document extends JsonLdDocument,\n const Extension extends object,\n>(document: Document, extension: Extension): Document & Extension {\n return { ...(document as unknown as object), ...extension } as Document &\n Extension;\n}\n\nconst documentNodes = (document: JsonLdDocument): ReadonlyArray<JsonLdNode> => {\n if (\"@graph\" in document) {\n return document[\"@graph\"] as ReadonlyArray<JsonLdNode>;\n }\n const { \"@context\": _context, ...node } = document as unknown as Record<\n string,\n unknown\n >;\n return [node as JsonLdNode];\n};\n\n/** Compose related schema.org documents into one graph without nested `@context` values. */\nexport function jsonLdGraph(documents: ReadonlyArray<JsonLdDocument>): Graph {\n return {\n \"@context\": \"https://schema.org\",\n \"@graph\": documents.flatMap(documentNodes),\n };\n}\n\n/** Apply an optional zero/one/many transform while preserving entry order. */\nexport function composeJsonLd(\n entries: ReadonlyArray<JsonLdEntry>,\n context: JsonLdTransformContext,\n transform?: JsonLdTransform,\n): Array<JsonLdDocument> {\n if (transform === undefined) {\n return entries.map((entry) => entry.document as JsonLdDocument);\n }\n\n return entries.flatMap((entry) => {\n const result = transform(entry, context);\n if (result === undefined || result === null || result === false) return [];\n return Array.isArray(result) ? [...result] : [result as JsonLdDocument];\n });\n}\n","/**\n * JSON-LD structured-data generators.\n *\n * schema.org payloads typed against `schema-dts`, which is a types-only devDep —\n * it erases at build, so this module keeps the entry's zero-runtime-dep rule.\n * The brand values (name, logo, sameAs, the Organization description) are the\n * host's, and arrive through {@link SeoConfig}; the schema construction is ours.\n *\n * @see https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data\n * @see https://schema.org/\n */\n\nimport type {\n Article,\n BreadcrumbList,\n FAQPage,\n Graph,\n ItemList,\n Organization,\n Person,\n Question,\n Service,\n WebSite,\n WithContext,\n} from \"schema-dts\";\n\nimport {\n composeJsonLd as composeEntries,\n jsonLdGraph,\n jsonLdRef,\n type JsonLdDocument,\n type JsonLdEntityIds,\n type JsonLdEntry,\n} from \"./json-ld-composition\";\nimport type { SeoFaq } from \"../core/declare\";\nimport { absoluteUrl, type SeoConfig } from \"./site\";\n\nexport interface ServiceParams {\n name: string;\n description: string;\n serviceType: string;\n provider?: string | undefined;\n areaServed?: string | undefined;\n}\n\n\nexport interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\nexport interface ItemListEntry {\n name: string;\n url: string;\n}\n\nexport interface ArticleParams {\n headline: string;\n description: string;\n image?: string | undefined;\n datePublished: string;\n dateModified?: string | undefined;\n author: {\n name: string;\n url?: string | undefined;\n };\n url: string;\n}\n\n/** The JSON-LD generators, bound to one site identity. */\nexport interface JsonLd {\n readonly entityIds: JsonLdEntityIds;\n /** Brand entity for the Knowledge Graph. Render site-wide (root layout). */\n generateOrganizationSchema: () => WithContext<Organization>;\n /** WebSite entity with a sitelinks search box. Render on the homepage. */\n generateWebsiteSchema: () => WithContext<WebSite>;\n generateServiceSchema: (params: ServiceParams) => WithContext<Service>;\n generateFAQPageSchema: (faqs: SeoFaq[]) => WithContext<FAQPage>;\n generateBreadcrumbSchema: (\n items: BreadcrumbItem[],\n ) => WithContext<BreadcrumbList>;\n /** Describe a visibly rendered, ordered collection of canonical pages. */\n generateItemListSchema: (\n name: string,\n items: ReadonlyArray<ItemListEntry>,\n ) => WithContext<ItemList>;\n generateArticleSchema: (params: ArticleParams) => WithContext<Article>;\n /** Compose the Organization, WebSite, and configured site entities into one graph. */\n generateSiteGraphSchema: () => Graph | undefined;\n /** Apply the configured transform to generated or custom entries. */\n composeJsonLd: (\n entries: ReadonlyArray<JsonLdEntry>,\n context?: { canonical?: string | undefined },\n ) => Array<JsonLdDocument>;\n}\n\nexport function createJsonLd(config: SeoConfig): JsonLd {\n const { origin, site, organization, website } = config;\n const organizationId = `${origin}/#organization`;\n const websiteId = `${origin}/#website`;\n const entityIds = { organization: organizationId, website: websiteId };\n\n const generateOrganizationSchema = (): WithContext<Organization> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n \"@id\": organizationId,\n name: site.name,\n ...(organization.legalName === undefined\n ? {}\n : { legalName: organization.legalName }),\n url: origin,\n logo: absoluteUrl(origin, site.logo),\n description: organization.description,\n sameAs: [...organization.sameAs],\n contactPoint: {\n \"@type\": \"ContactPoint\",\n contactType: organization.contactPoint.contactType,\n email: organization.contactPoint.email,\n },\n ...(organization.address === undefined\n ? {}\n : {\n address: {\n \"@type\": \"PostalAddress\" as const,\n ...organization.address,\n },\n }),\n });\n\n const generateWebsiteSchema = (): WithContext<WebSite> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n \"@id\": websiteId,\n name: site.name,\n url: origin,\n publisher: jsonLdRef(organizationId),\n potentialAction: {\n \"@type\": \"SearchAction\",\n target: {\n \"@type\": \"EntryPoint\",\n urlTemplate: absoluteUrl(origin, website.searchPath),\n },\n query: \"required name=search_term_string\",\n },\n });\n\n const generateServiceSchema = (\n params: ServiceParams,\n ): WithContext<Service> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"Service\",\n name: params.name,\n description: params.description,\n serviceType: params.serviceType,\n provider: {\n \"@type\": \"Organization\",\n ...(params.provider === undefined\n ? { \"@id\": organizationId, url: origin }\n : {}),\n name: params.provider ?? site.name,\n },\n areaServed: params.areaServed ?? \"Worldwide\",\n });\n\n const generateFAQPageSchema = (faqs: SeoFaq[]): WithContext<FAQPage> => {\n const questions: Question[] = faqs.map((faq) => ({\n \"@type\": \"Question\",\n name: faq.question,\n acceptedAnswer: {\n \"@type\": \"Answer\",\n text: faq.answer,\n },\n }));\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: questions,\n };\n };\n\n const generateBreadcrumbSchema = (\n items: BreadcrumbItem[],\n ): WithContext<BreadcrumbList> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: items.map((item, index) => ({\n \"@type\": \"ListItem\",\n position: index + 1,\n name: item.name,\n item: `${origin}${item.url}`,\n })),\n });\n\n const generateItemListSchema = (\n name: string,\n items: ReadonlyArray<ItemListEntry>,\n ): WithContext<ItemList> => ({\n \"@context\": \"https://schema.org\",\n \"@type\": \"ItemList\",\n name,\n numberOfItems: items.length,\n itemListElement: items.map((item, index) => ({\n \"@type\": \"ListItem\",\n position: index + 1,\n name: item.name,\n url: `${origin}${item.url}`,\n })),\n });\n\n const generateArticleSchema = (\n params: ArticleParams,\n ): WithContext<Article> => {\n const authorSchema: Person = {\n \"@type\": \"Person\",\n name: params.author.name,\n ...(params.author.url && { url: params.author.url }),\n };\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Article\",\n headline: params.headline,\n description: params.description,\n // Deliberately NOT absolutized: a declared instance image lands verbatim\n // (og:image is where the absolute form is required, and seoHead does that).\n image: params.image ?? absoluteUrl(origin, site.defaultImage),\n datePublished: params.datePublished,\n dateModified: params.dateModified ?? params.datePublished,\n author: authorSchema,\n publisher: {\n \"@type\": \"Organization\",\n \"@id\": organizationId,\n name: site.name,\n logo: {\n \"@type\": \"ImageObject\",\n url: absoluteUrl(origin, site.publisherLogo),\n },\n },\n mainEntityOfPage: {\n \"@type\": \"WebPage\",\n \"@id\": params.url,\n },\n };\n };\n\n const composeJsonLd: JsonLd[\"composeJsonLd\"] = (entries, context = {}) =>\n composeEntries(\n entries,\n {\n origin,\n entityIds,\n ...(context.canonical === undefined\n ? {}\n : { canonical: context.canonical }),\n },\n config.jsonLd?.transform,\n );\n\n const generateSiteGraphSchema = (): Graph | undefined => {\n const configuredSite =\n typeof config.jsonLd?.site === \"function\"\n ? config.jsonLd.site(entityIds)\n : (config.jsonLd?.site ?? []);\n const entries: Array<JsonLdEntry> = [\n {\n kind: \"organization\",\n source: \"generated\",\n document: generateOrganizationSchema(),\n },\n {\n kind: \"website\",\n source: \"generated\",\n document: generateWebsiteSchema(),\n },\n ...configuredSite.map(\n (document): JsonLdEntry => ({\n kind: \"custom\",\n source: \"site\",\n document,\n }),\n ),\n ];\n const documents = composeJsonLd(entries);\n return documents.length === 0 ? undefined : jsonLdGraph(documents);\n };\n\n return {\n entityIds,\n generateOrganizationSchema,\n generateWebsiteSchema,\n generateServiceSchema,\n generateFAQPageSchema,\n generateBreadcrumbSchema,\n generateItemListSchema,\n generateArticleSchema,\n generateSiteGraphSchema,\n composeJsonLd,\n };\n}\n","/**\n * The render layer: one route's SEO declaration plus its per-instance content,\n * composed into the meta/link descriptors TanStack Router renders into <head>,\n * with the JSON-LD that page warrants.\n *\n * {@link createSeo} binds the site identity once (see `site.ts`) and returns\n * the bound API, so a route file calls `seoHead(ctx, instance)` and never sees\n * an origin or a brand name.\n */\n\nimport type { AnyRouteMatch, MetaDescriptor } from \"@tanstack/react-router\";\n\nimport { applyTitleTemplate, type SeoPageHead } from \"../core/declare\";\n\nimport { resolveCrumbTrail } from \"./breadcrumbs\";\nimport type { JsonLdDocument, JsonLdEntry } from \"./json-ld-composition\";\nimport { absoluteUrl, type SeoConfig } from \"./site\";\nimport { createJsonLd, type JsonLd } from \"./json-ld\";\n\n/** Structural subset of TanStack's head() ctx — the leaf match plus the full chain. */\nexport interface SeoHeadCtx {\n matches: ReadonlyArray<AnyRouteMatch>;\n match: AnyRouteMatch;\n}\n\nexport interface SeoHead {\n meta: NonNullable<AnyRouteMatch[\"meta\"]>;\n links: Array<{ rel: \"canonical\"; href: string }>;\n}\n\n/** The render API, bound to one site identity. */\nexport interface Seo extends JsonLd {\n seoHead: (ctx: SeoHeadCtx, instance: SeoPageHead) => SeoHead;\n /**\n * `head()` for a static route that declares its head in `staticData.seo.head`:\n *\n * ```ts\n * createFileRoute(\"/pricing\")({\n * staticData: { seo: { kind: \"page\", sitemap, head: { title, description, faqs } } },\n * head: seo.head,\n * })\n * ```\n *\n * The graph, `pageHeads`, and the checks read the same declaration.\n *\n * Generic so TanStack infers nothing from it: a `head` typed with\n * `AnyRouteMatch` would make the route's search and loader types `any`, and\n * that spreads through the route tree.\n */\n head: <Ctx extends SeoHeadCtx>(ctx: Ctx) => SeoHead;\n}\n\n/** Strip a trailing slash from a pathname, keeping the root \"/\" intact. */\nfunction normalizePathname(pathname: string): string {\n return pathname.length > 1 && pathname.endsWith(\"/\")\n ? pathname.slice(0, -1)\n : pathname;\n}\n\nexport function createSeo(config: SeoConfig): Seo {\n const { origin, site } = config;\n const jsonLd = createJsonLd(config);\n\n const seoHead = (ctx: SeoHeadCtx, instance: SeoPageHead): SeoHead => {\n const canonical = `${origin}${instance.canonicalPath ?? normalizePathname(ctx.match.pathname)}`;\n const title = applyTitleTemplate(ctx.match.staticData.seo?.titleTemplate, instance.title);\n const resolvedOgTitle = instance.ogTitle ?? title;\n const resolvedOgDescription =\n instance.ogDescription ?? instance.description;\n const routeSeo = ctx.match.staticData.seo;\n const { article } = instance;\n const jsonLdEntries: Array<JsonLdEntry> = [];\n\n const meta: Array<MetaDescriptor> = [\n { title },\n { name: \"description\", content: instance.description },\n { property: \"og:title\", content: resolvedOgTitle },\n { property: \"og:description\", content: resolvedOgDescription },\n { property: \"og:type\", content: article ? \"article\" : \"website\" },\n { property: \"og:url\", content: canonical },\n { name: \"twitter:card\", content: \"summary_large_image\" },\n { name: \"twitter:title\", content: resolvedOgTitle },\n { name: \"twitter:description\", content: resolvedOgDescription },\n ];\n\n const robots = instance.robots ?? routeSeo?.robots;\n if (robots !== undefined) {\n meta.push({ name: \"robots\", content: robots });\n }\n\n if (article) {\n if (article.tags?.length) {\n meta.push({ name: \"keywords\", content: article.tags.join(\", \") });\n }\n meta.push(\n { property: \"article:published_time\", content: article.publishedAt },\n {\n property: \"article:modified_time\",\n content: article.modifiedAt ?? article.publishedAt,\n },\n );\n if (article.image !== undefined) {\n const imageUrl = absoluteUrl(origin, article.image);\n meta.push(\n { property: \"og:image\", content: imageUrl },\n { name: \"twitter:image\", content: imageUrl },\n );\n }\n }\n\n if (article) {\n jsonLdEntries.push({\n kind: \"article\",\n source: \"generated\",\n document: jsonLd.generateArticleSchema({\n headline: resolvedOgTitle,\n description: instance.description,\n image: article.image,\n datePublished: article.publishedAt,\n dateModified: article.modifiedAt ?? article.publishedAt,\n author: article.author ?? site.defaultAuthor,\n url: canonical,\n }),\n });\n }\n\n if (!article && instance.keywords) {\n meta.push({ name: \"keywords\", content: instance.keywords.join(\", \") });\n }\n\n if (instance.service) {\n jsonLdEntries.push({\n kind: \"service\",\n source: \"generated\",\n document: jsonLd.generateServiceSchema({\n name: instance.service.name,\n description: instance.service.description ?? instance.description,\n serviceType: instance.service.serviceType,\n }),\n });\n }\n\n if (instance.faqs) {\n jsonLdEntries.push({\n kind: \"faq\",\n source: \"generated\",\n document: jsonLd.generateFAQPageSchema([...instance.faqs]),\n });\n }\n\n if (instance.itemList) {\n jsonLdEntries.push({\n kind: \"item-list\",\n source: \"generated\",\n document: jsonLd.generateItemListSchema(\n instance.itemList.name,\n instance.itemList.items,\n ),\n });\n }\n\n // TanStack accumulates every matched route's head output. Only the current leaf\n // owns the BreadcrumbList; otherwise each parent repeats a progressively stale\n // trail alongside the leaf's canonical trail on nested pages.\n const isLeafMatch = ctx.matches.at(-1)?.id === ctx.match.id;\n const trail = isLeafMatch\n ? (instance.breadcrumbs ?? resolveCrumbTrail(ctx.matches))\n : [];\n if (trail.length >= 2) {\n jsonLdEntries.push({\n kind: \"breadcrumb\",\n source: \"generated\",\n document: jsonLd.generateBreadcrumbSchema(\n trail.map((item) => ({ name: item.name, url: item.path })),\n ),\n });\n }\n\n if (instance.jsonLd) {\n jsonLdEntries.push(\n ...instance.jsonLd.map(\n (document): JsonLdEntry => ({\n kind: \"custom\",\n source: \"page\",\n document,\n }),\n ),\n );\n }\n\n for (const document of jsonLd.composeJsonLd(jsonLdEntries, { canonical })) {\n meta.push({ \"script:ld+json\": document });\n }\n\n // React's head types (JSX.IntrinsicElements['meta']) model only <meta> attributes;\n // TanStack renders `script:ld+json` entries as JSON-LD <script> tags at runtime. The\n // installed @tanstack/react-router augments leaf head meta to those JSX attributes, so\n // the JSON-LD entries are asserted to it (same pattern as an inline root JSON-LD).\n const head: SeoHead = {\n meta: meta as NonNullable<AnyRouteMatch[\"meta\"]>,\n links: [{ rel: \"canonical\", href: canonical }],\n };\n return config.transformHead === undefined ? head : config.transformHead(head);\n };\n\n const head = <Ctx extends SeoHeadCtx>(ctx: Ctx): SeoHead => {\n const declared = ctx.match.staticData.seo?.head;\n if (declared === undefined) {\n throw new Error(`seo.head needs staticData.seo.head on route ${ctx.match.routeId}`);\n }\n return seoHead(ctx, declared);\n };\n\n return { seoHead, head, ...jsonLd };\n}\n"],"mappings":"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"}
@@ -0,0 +1,308 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { open, stat } from "node:fs/promises";
3
+ import { basename, normalize, resolve } from "node:path";
4
+ import { Generator, getConfig } from "@tanstack/router-generator";
5
+ //#region src/vite/route-config.ts
6
+ /**
7
+ * Derives a route-config module from the parsed TanStack route tree.
8
+ *
9
+ * The route tree is the single source of truth for which paths exist; this
10
+ * plugin projects it into the small config the host consumes at runtime:
11
+ * - `robotsExclusions` — path prefixes disallowed in robots.txt (feed them to
12
+ * `renderRobots`).
13
+ * - `reservedSegments` — top-level path segments the app must not hand out (an
14
+ * org slug, a tenant name). Derived from the full public route surface — the
15
+ * `publicGroups` route groups plus the group-less `extraPublicRoutes` — which
16
+ * is parsed here but not emitted: "which pages are public" already has a
17
+ * canonical runtime answer in the SEO graph.
18
+ *
19
+ * It also enforces that every page in `enforceCoverageIn` declares its SEO: a
20
+ * page leaf missing both `staticData` and `head` fails dev startup and build.
21
+ *
22
+ * **Why we run our own Generator.** The parse must come from
23
+ * `@tanstack/router-generator` itself — a second, hand-rolled scanner would
24
+ * drift from TanStack's group/param/layout conventions. But routing our plugin
25
+ * through TanStack Start does not work: `start-plugin-core` builds its own
26
+ * generator-plugin array and drops `router.plugins` on the floor
27
+ * (tanstack/router#7768 — `plugins` is not public API there), and even when
28
+ * forced through, the router unplugin wraps `generator.run()` in
29
+ * `catch (e) { console.error(e) }`, so a plugin can never fail a build.
30
+ *
31
+ * So we instantiate `Generator` ourselves — `config.plugins` is natively
32
+ * honored on our own instance, and because we own the `.run()` call site a
33
+ * coverage violation throws straight out of `buildStart`. Cost: route files are
34
+ * parsed twice at startup (~40 files, milliseconds) — the host's router plugin
35
+ * still owns the real `routeTree.gen.ts`; ours is a shadow parse whose route
36
+ * tree is thrown away (see {@link inMemoryWriteFs}).
37
+ *
38
+ * `onRouteTreeChanged` fires on every generator run, but the config is written
39
+ * only when its content changes, and the output path lives outside the routes
40
+ * directory, so regeneration never feeds back into the watcher.
41
+ */
42
+ const PLUGIN_NAME = "seo-route-config";
43
+ /** Match a route's group folder (`filePath` is relative to the routes directory). */
44
+ function groupOf(filePath, groups) {
45
+ return groups.find((group) => filePath.startsWith(`${group}/`));
46
+ }
47
+ /**
48
+ * A page leaf is a `.tsx` route file that is not a `route.tsx` layout. This
49
+ * excludes layout wrappers, `.ts` server routes (robots.txt / sitemap.xml /
50
+ * markdown mirrors), and any other non-page file — matching the leaves that
51
+ * contribute a crawlable URL.
52
+ */
53
+ function isPageLeaf(node) {
54
+ return node.filePath.endsWith(".tsx") && basename(node.filePath) !== "route.tsx";
55
+ }
56
+ /**
57
+ * Convert a group-prefixed routePath (e.g. "/(marketing)/blog/$slug") into the
58
+ * public URL form used by route-config ("/blog/*"): drop the group segment,
59
+ * collapse the first dynamic/splat segment and everything after it to "*", and
60
+ * trim the index trailing slash.
61
+ */
62
+ function routePathToPublicUrl(routePath) {
63
+ const stripped = routePath.replace(/^\/\([^)]+\)/, "");
64
+ if (stripped === "" || stripped === "/") return "/";
65
+ const segments = stripped.split("/");
66
+ const dynamicIndex = segments.findIndex((segment) => segment.startsWith("$"));
67
+ if (dynamicIndex !== -1) return `${segments.slice(0, dynamicIndex).join("/")}/*`;
68
+ return stripped.endsWith("/") ? stripped.slice(0, -1) : stripped;
69
+ }
70
+ /**
71
+ * First path segment of each route, reserved so it can't be claimed as an org
72
+ * slug. Strip the leading slash and a trailing "/*", then take the first
73
+ * segment ("/features/email" -> "features").
74
+ */
75
+ function extractReservedSegments(routes) {
76
+ const segments = /* @__PURE__ */ new Set();
77
+ for (const route of routes) {
78
+ const first = route.replace(/^\//, "").replace(/\/\*$/, "").split("/")[0];
79
+ if (first) segments.add(first);
80
+ }
81
+ return Array.from(segments).sort();
82
+ }
83
+ /**
84
+ * robots.txt disallow prefixes are directory prefixes, so a splat route
85
+ * ("/reset-password/*") disallows the whole subtree ("/reset-password").
86
+ */
87
+ function normalizeRoutePrefix(route) {
88
+ const cleaned = route.replace(/\/\*$/, "");
89
+ return cleaned === "" ? "/" : cleaned;
90
+ }
91
+ function deriveRouteConfig(routeNodes, options) {
92
+ const publicUrls = new Set(options.extraPublicRoutes);
93
+ const disallowedUrls = /* @__PURE__ */ new Set();
94
+ for (const node of routeNodes) {
95
+ const group = groupOf(node.filePath, options.publicGroups);
96
+ if (group === void 0 || !isPageLeaf(node) || node.routePath === void 0) continue;
97
+ const url = routePathToPublicUrl(node.routePath);
98
+ publicUrls.add(url);
99
+ if (options.disallowGroups.includes(group)) disallowedUrls.add(url);
100
+ }
101
+ const reservedSegments = [...extractReservedSegments([...publicUrls].sort()), ...options.reservedExtras].sort();
102
+ return {
103
+ robotsExclusions: [.../* @__PURE__ */ new Set([...options.alwaysDisallow, ...[...disallowedUrls].map(normalizeRoutePrefix)])].sort(),
104
+ reservedSegments
105
+ };
106
+ }
107
+ /**
108
+ * Every page in an enforced group must declare its search presence. A leaf that
109
+ * passes neither `staticData` (from which the SEO graph reads `staticData.seo`)
110
+ * nor `head` has no path to correct metadata.
111
+ */
112
+ function findCoverageViolations(routeNodes, options) {
113
+ const violations = [];
114
+ for (const node of routeNodes) {
115
+ const group = groupOf(node.filePath, options.enforceCoverageIn);
116
+ if (group === void 0 || !isPageLeaf(node)) continue;
117
+ const props = node.createFileRouteProps;
118
+ if (props?.has("staticData") || props?.has("head")) continue;
119
+ violations.push(`${node.filePath} (${group} page)`);
120
+ }
121
+ return violations;
122
+ }
123
+ function coverageMessage(violations) {
124
+ return `[${PLUGIN_NAME}] ${violations.length} page(s) declare neither staticData nor head. Every page in an SEO-enforced route group must declare its SEO — add staticData: { seo: ... } or a head() to each route:\n - ${violations.join("\n - ")}`;
125
+ }
126
+ const HEADER = `// Auto-generated by the ${PLUGIN_NAME} Vite plugin.\n// DO NOT EDIT MANUALLY — regenerated from the route tree on every dev/build.
127
+ `;
128
+ function renderRouteConfig(config) {
129
+ return `${HEADER}
130
+ export const routeConfig = ${JSON.stringify(config, null, 2)} as const;
131
+
132
+ export type RouteConfig = typeof routeConfig;
133
+ `;
134
+ }
135
+ function routeConfigGeneratorPlugin(options) {
136
+ const { outputPath } = options;
137
+ return {
138
+ name: PLUGIN_NAME,
139
+ init() {
140
+ if (!existsSync(resolve(outputPath, ".."))) throw new Error(`[${PLUGIN_NAME}] output directory does not exist: ${resolve(outputPath, "..")}`);
141
+ },
142
+ onRouteTreeChanged({ routeNodes }) {
143
+ const violations = findCoverageViolations(routeNodes, options);
144
+ if (violations.length > 0) throw new Error(coverageMessage(violations));
145
+ const content = renderRouteConfig(deriveRouteConfig(routeNodes, options));
146
+ if ((existsSync(outputPath) ? readFileSync(outputPath, "utf8") : void 0) !== content) writeFileSync(outputPath, content);
147
+ }
148
+ };
149
+ }
150
+ /**
151
+ * A `Generator` filesystem that reads real files but keeps every write in
152
+ * memory, so our shadow parse cannot touch the repo.
153
+ *
154
+ * The Generator writes through `safeFileWrite`: `writeFile(<tmp>)`, then
155
+ * `rename(<tmp>, <dest>)`. Two destinations exist, and neither may land:
156
+ * - the route tree — we point it at a throwaway path and hold it in memory, so
157
+ * the only route tree on disk stays the one the host's router plugin writes;
158
+ * - route source files — the Generator normalizes them (`createFileRoute` ids,
159
+ * code-split transforms). The host's generator already did that with its own
160
+ * config; ours is not the host's, so such a write is dropped rather than
161
+ * allowed to fight it.
162
+ *
163
+ * Reads and stats of real files go to disk, which keeps the Generator's mtime
164
+ * cache honest: a route edited mid-watch is always seen.
165
+ */
166
+ function inMemoryWriteFs(routeTreePath) {
167
+ const written = /* @__PURE__ */ new Map();
168
+ let clock = 0n;
169
+ const tick = () => clock += 1n;
170
+ return {
171
+ /**
172
+ * The in-memory route tree is invisible to the watcher, so nothing else can
173
+ * mark it stale. Bumping its mtime before each run is what makes the
174
+ * Generator rebuild the tree — and therefore re-fire `onRouteTreeChanged`
175
+ * — on every run, not only when the set of routes changes. Coverage and the
176
+ * derived config depend on route *contents* (`staticData` / `head`), which
177
+ * leave the route tree itself untouched.
178
+ */
179
+ touchRouteTree() {
180
+ const file = written.get(routeTreePath);
181
+ if (file) written.set(routeTreePath, {
182
+ content: file.content,
183
+ mtimeMs: tick()
184
+ });
185
+ },
186
+ fs: {
187
+ async stat(filePath) {
188
+ const file = written.get(filePath);
189
+ if (file) return {
190
+ mtimeMs: file.mtimeMs,
191
+ mode: 33188,
192
+ uid: 0,
193
+ gid: 0
194
+ };
195
+ const real = await stat(filePath, { bigint: true });
196
+ return {
197
+ mtimeMs: real.mtimeMs,
198
+ mode: Number(real.mode),
199
+ uid: Number(real.uid),
200
+ gid: Number(real.gid)
201
+ };
202
+ },
203
+ async readFile(filePath) {
204
+ const file = written.get(filePath);
205
+ if (file) return {
206
+ stat: { mtimeMs: file.mtimeMs },
207
+ fileContent: file.content
208
+ };
209
+ if (!existsSync(filePath)) return "file-not-existing";
210
+ const handle = await open(filePath, "r");
211
+ const fileStat = await handle.stat({ bigint: true });
212
+ const fileContent = (await handle.readFile()).toString();
213
+ await handle.close();
214
+ return {
215
+ stat: { mtimeMs: fileStat.mtimeMs },
216
+ fileContent
217
+ };
218
+ },
219
+ async writeFile(filePath, content) {
220
+ written.set(filePath, {
221
+ content,
222
+ mtimeMs: tick()
223
+ });
224
+ },
225
+ async rename(oldPath, newPath) {
226
+ const file = written.get(oldPath);
227
+ written.delete(oldPath);
228
+ if (file && newPath === routeTreePath) written.set(newPath, {
229
+ content: file.content,
230
+ mtimeMs: tick()
231
+ });
232
+ },
233
+ async chmod() {},
234
+ async chown() {}
235
+ }
236
+ };
237
+ }
238
+ function resolveOptions(options) {
239
+ return {
240
+ outputPath: options.outputPath,
241
+ publicGroups: options.publicGroups,
242
+ enforceCoverageIn: options.enforceCoverageIn,
243
+ disallowGroups: options.disallowGroups ?? [],
244
+ extraPublicRoutes: options.extraPublicRoutes ?? [],
245
+ reservedExtras: options.reservedExtras ?? [],
246
+ alwaysDisallow: options.alwaysDisallow ?? [],
247
+ routesDirectory: options.routesDirectory ?? "src/routes"
248
+ };
249
+ }
250
+ /**
251
+ * Vite plugin that runs its own `@tanstack/router-generator` instance over the
252
+ * routes directory and derives the route-config module from the result.
253
+ */
254
+ function seoRouteConfig(options) {
255
+ const resolved = resolveOptions(options);
256
+ let generator;
257
+ let touchRouteTree;
258
+ let routesDirectory;
259
+ /**
260
+ * A throw here propagates: it fails `vite build` and dev startup, which is
261
+ * the reason we own this call site instead of registering through Start.
262
+ */
263
+ const run = async (event) => {
264
+ if (generator === void 0 || touchRouteTree === void 0) throw new Error(`[${PLUGIN_NAME}] generator was not initialized by configResolved`);
265
+ touchRouteTree();
266
+ await generator.run(event);
267
+ };
268
+ return {
269
+ name: PLUGIN_NAME,
270
+ enforce: "pre",
271
+ configResolved(config) {
272
+ const root = config.root;
273
+ const cacheDirectory = resolve(root, `node_modules/.cache/${PLUGIN_NAME}`);
274
+ routesDirectory = resolve(root, resolved.routesDirectory);
275
+ const generatorConfig = getConfig({
276
+ routesDirectory,
277
+ generatedRouteTree: resolve(cacheDirectory, "route-tree.gen.ts"),
278
+ tmpDir: resolve(cacheDirectory, "tmp"),
279
+ plugins: [routeConfigGeneratorPlugin(resolved)],
280
+ disableTypes: true,
281
+ enableRouteTreeFormatting: false,
282
+ disableLogging: true
283
+ }, root);
284
+ const memory = inMemoryWriteFs(generatorConfig.generatedRouteTree);
285
+ touchRouteTree = memory.touchRouteTree;
286
+ generator = new Generator({
287
+ config: generatorConfig,
288
+ root,
289
+ fs: memory.fs
290
+ });
291
+ },
292
+ async buildStart() {
293
+ await run();
294
+ },
295
+ async watchChange(id, { event }) {
296
+ const file = normalize(id);
297
+ if (routesDirectory === void 0 || !file.startsWith(routesDirectory)) return;
298
+ await run({
299
+ path: file,
300
+ type: event
301
+ });
302
+ }
303
+ };
304
+ }
305
+ //#endregion
306
+ export { seoRouteConfig as i, inMemoryWriteFs as n, resolveOptions as r, deriveRouteConfig as t };
307
+
308
+ //# sourceMappingURL=route-config-35ZEzkNC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-config-35ZEzkNC.js","names":["statFile","openFile"],"sources":["../src/vite/route-config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { open as openFile, stat as statFile } from \"node:fs/promises\";\nimport { basename, normalize, resolve } from \"node:path\";\n\nimport {\n Generator,\n getConfig,\n type GeneratorEvent,\n type GeneratorPlugin,\n type RouteNode,\n} from \"@tanstack/router-generator\";\nimport type { Plugin } from \"vite\";\n\n/**\n * Derives a route-config module from the parsed TanStack route tree.\n *\n * The route tree is the single source of truth for which paths exist; this\n * plugin projects it into the small config the host consumes at runtime:\n * - `robotsExclusions` — path prefixes disallowed in robots.txt (feed them to\n * `renderRobots`).\n * - `reservedSegments` — top-level path segments the app must not hand out (an\n * org slug, a tenant name). Derived from the full public route surface — the\n * `publicGroups` route groups plus the group-less `extraPublicRoutes` — which\n * is parsed here but not emitted: \"which pages are public\" already has a\n * canonical runtime answer in the SEO graph.\n *\n * It also enforces that every page in `enforceCoverageIn` declares its SEO: a\n * page leaf missing both `staticData` and `head` fails dev startup and build.\n *\n * **Why we run our own Generator.** The parse must come from\n * `@tanstack/router-generator` itself — a second, hand-rolled scanner would\n * drift from TanStack's group/param/layout conventions. But routing our plugin\n * through TanStack Start does not work: `start-plugin-core` builds its own\n * generator-plugin array and drops `router.plugins` on the floor\n * (tanstack/router#7768 — `plugins` is not public API there), and even when\n * forced through, the router unplugin wraps `generator.run()` in\n * `catch (e) { console.error(e) }`, so a plugin can never fail a build.\n *\n * So we instantiate `Generator` ourselves — `config.plugins` is natively\n * honored on our own instance, and because we own the `.run()` call site a\n * coverage violation throws straight out of `buildStart`. Cost: route files are\n * parsed twice at startup (~40 files, milliseconds) — the host's router plugin\n * still owns the real `routeTree.gen.ts`; ours is a shadow parse whose route\n * tree is thrown away (see {@link inMemoryWriteFs}).\n *\n * `onRouteTreeChanged` fires on every generator run, but the config is written\n * only when its content changes, and the output path lives outside the routes\n * directory, so regeneration never feeds back into the watcher.\n */\n\nconst PLUGIN_NAME = \"seo-route-config\";\n\n/** Match a route's group folder (`filePath` is relative to the routes directory). */\nfunction groupOf(filePath: string, groups: ReadonlyArray<string>): string | undefined {\n return groups.find((group) => filePath.startsWith(`${group}/`));\n}\n\n/**\n * A page leaf is a `.tsx` route file that is not a `route.tsx` layout. This\n * excludes layout wrappers, `.ts` server routes (robots.txt / sitemap.xml /\n * markdown mirrors), and any other non-page file — matching the leaves that\n * contribute a crawlable URL.\n */\nfunction isPageLeaf(node: RouteNode): boolean {\n return node.filePath.endsWith(\".tsx\") && basename(node.filePath) !== \"route.tsx\";\n}\n\n/**\n * Convert a group-prefixed routePath (e.g. \"/(marketing)/blog/$slug\") into the\n * public URL form used by route-config (\"/blog/*\"): drop the group segment,\n * collapse the first dynamic/splat segment and everything after it to \"*\", and\n * trim the index trailing slash.\n */\nfunction routePathToPublicUrl(routePath: string): string {\n const stripped = routePath.replace(/^\\/\\([^)]+\\)/, \"\");\n if (stripped === \"\" || stripped === \"/\") return \"/\";\n\n const segments = stripped.split(\"/\");\n const dynamicIndex = segments.findIndex((segment) => segment.startsWith(\"$\"));\n if (dynamicIndex !== -1) {\n return `${segments.slice(0, dynamicIndex).join(\"/\")}/*`;\n }\n\n return stripped.endsWith(\"/\") ? stripped.slice(0, -1) : stripped;\n}\n\n/**\n * First path segment of each route, reserved so it can't be claimed as an org\n * slug. Strip the leading slash and a trailing \"/*\", then take the first\n * segment (\"/features/email\" -> \"features\").\n */\nfunction extractReservedSegments(routes: ReadonlyArray<string>): Array<string> {\n const segments = new Set<string>();\n for (const route of routes) {\n const first = route.replace(/^\\//, \"\").replace(/\\/\\*$/, \"\").split(\"/\")[0];\n if (first) segments.add(first);\n }\n return Array.from(segments).sort();\n}\n\n/**\n * robots.txt disallow prefixes are directory prefixes, so a splat route\n * (\"/reset-password/*\") disallows the whole subtree (\"/reset-password\").\n */\nfunction normalizeRoutePrefix(route: string): string {\n const cleaned = route.replace(/\\/\\*$/, \"\");\n return cleaned === \"\" ? \"/\" : cleaned;\n}\n\nexport interface RouteConfigShape {\n robotsExclusions: Array<string>;\n reservedSegments: Array<string>;\n}\n\nexport function deriveRouteConfig(\n routeNodes: ReadonlyArray<RouteNode>,\n options: ResolvedOptions,\n): RouteConfigShape {\n const publicUrls = new Set<string>(options.extraPublicRoutes);\n const disallowedUrls = new Set<string>();\n\n for (const node of routeNodes) {\n const group = groupOf(node.filePath, options.publicGroups);\n if (group === undefined || !isPageLeaf(node) || node.routePath === undefined) continue;\n const url = routePathToPublicUrl(node.routePath);\n publicUrls.add(url);\n if (options.disallowGroups.includes(group)) disallowedUrls.add(url);\n }\n\n const reservedSegments = [\n ...extractReservedSegments([...publicUrls].sort()),\n ...options.reservedExtras,\n ].sort();\n\n const robotsExclusions = [\n ...new Set([...options.alwaysDisallow, ...[...disallowedUrls].map(normalizeRoutePrefix)]),\n ].sort();\n\n return { robotsExclusions, reservedSegments };\n}\n\n/**\n * Every page in an enforced group must declare its search presence. A leaf that\n * passes neither `staticData` (from which the SEO graph reads `staticData.seo`)\n * nor `head` has no path to correct metadata.\n */\nexport function findCoverageViolations(\n routeNodes: ReadonlyArray<RouteNode>,\n options: ResolvedOptions,\n): Array<string> {\n const violations: Array<string> = [];\n for (const node of routeNodes) {\n const group = groupOf(node.filePath, options.enforceCoverageIn);\n if (group === undefined || !isPageLeaf(node)) continue;\n\n const props = node.createFileRouteProps;\n if (props?.has(\"staticData\") || props?.has(\"head\")) continue;\n\n violations.push(`${node.filePath} (${group} page)`);\n }\n return violations;\n}\n\nexport function coverageMessage(violations: ReadonlyArray<string>): string {\n return (\n `[${PLUGIN_NAME}] ${violations.length} page(s) declare neither staticData nor ` +\n `head. Every page in an SEO-enforced route group must declare its SEO — add ` +\n `staticData: { seo: ... } or a head() to each route:\\n - ${violations.join(\"\\n - \")}`\n );\n}\n\nconst HEADER =\n `// Auto-generated by the ${PLUGIN_NAME} Vite plugin.\\n` +\n \"// DO NOT EDIT MANUALLY — regenerated from the route tree on every dev/build.\\n\";\n\nfunction renderRouteConfig(config: RouteConfigShape): string {\n return `${HEADER}\nexport const routeConfig = ${JSON.stringify(config, null, 2)} as const;\n\nexport type RouteConfig = typeof routeConfig;\n`;\n}\n\nfunction routeConfigGeneratorPlugin(options: ResolvedOptions): GeneratorPlugin {\n const { outputPath } = options;\n return {\n name: PLUGIN_NAME,\n init() {\n // The caller passes an explicit absolute output path, so a relocated routes\n // directory can never silently retarget the write. Guard the parent directory\n // here so a bad path fails at startup, not mid-watch.\n if (!existsSync(resolve(outputPath, \"..\"))) {\n throw new Error(\n `[${PLUGIN_NAME}] output directory does not exist: ${resolve(outputPath, \"..\")}`,\n );\n }\n },\n onRouteTreeChanged({ routeNodes }) {\n const violations = findCoverageViolations(routeNodes, options);\n if (violations.length > 0) throw new Error(coverageMessage(violations));\n\n const content = renderRouteConfig(deriveRouteConfig(routeNodes, options));\n const existing = existsSync(outputPath) ? readFileSync(outputPath, \"utf8\") : undefined;\n if (existing !== content) writeFileSync(outputPath, content);\n },\n };\n}\n\n/**\n * A `Generator` filesystem that reads real files but keeps every write in\n * memory, so our shadow parse cannot touch the repo.\n *\n * The Generator writes through `safeFileWrite`: `writeFile(<tmp>)`, then\n * `rename(<tmp>, <dest>)`. Two destinations exist, and neither may land:\n * - the route tree — we point it at a throwaway path and hold it in memory, so\n * the only route tree on disk stays the one the host's router plugin writes;\n * - route source files — the Generator normalizes them (`createFileRoute` ids,\n * code-split transforms). The host's generator already did that with its own\n * config; ours is not the host's, so such a write is dropped rather than\n * allowed to fight it.\n *\n * Reads and stats of real files go to disk, which keeps the Generator's mtime\n * cache honest: a route edited mid-watch is always seen.\n */\nexport function inMemoryWriteFs(routeTreePath: string) {\n const written = new Map<string, { content: string; mtimeMs: bigint }>();\n let clock = 0n;\n const tick = () => (clock += 1n);\n\n return {\n /**\n * The in-memory route tree is invisible to the watcher, so nothing else can\n * mark it stale. Bumping its mtime before each run is what makes the\n * Generator rebuild the tree — and therefore re-fire `onRouteTreeChanged`\n * — on every run, not only when the set of routes changes. Coverage and the\n * derived config depend on route *contents* (`staticData` / `head`), which\n * leave the route tree itself untouched.\n */\n touchRouteTree() {\n const file = written.get(routeTreePath);\n if (file) written.set(routeTreePath, { content: file.content, mtimeMs: tick() });\n },\n fs: {\n async stat(filePath: string) {\n const file = written.get(filePath);\n if (file) return { mtimeMs: file.mtimeMs, mode: 0o100644, uid: 0, gid: 0 };\n const real = await statFile(filePath, { bigint: true });\n return {\n mtimeMs: real.mtimeMs,\n mode: Number(real.mode),\n uid: Number(real.uid),\n gid: Number(real.gid),\n };\n },\n async readFile(filePath: string) {\n const file = written.get(filePath);\n if (file) return { stat: { mtimeMs: file.mtimeMs }, fileContent: file.content };\n if (!existsSync(filePath)) return \"file-not-existing\" as const;\n const handle = await openFile(filePath, \"r\");\n const fileStat = await handle.stat({ bigint: true });\n const fileContent = (await handle.readFile()).toString();\n await handle.close();\n return { stat: { mtimeMs: fileStat.mtimeMs }, fileContent };\n },\n async writeFile(filePath: string, content: string) {\n written.set(filePath, { content, mtimeMs: tick() });\n },\n async rename(oldPath: string, newPath: string) {\n const file = written.get(oldPath);\n written.delete(oldPath);\n // Anything but the throwaway route tree — i.e. a route source file — is\n // dropped here, which is the whole point: this generator never writes.\n if (file && newPath === routeTreePath) {\n written.set(newPath, { content: file.content, mtimeMs: tick() });\n }\n },\n async chmod() {},\n async chown() {},\n },\n };\n}\n\nexport interface SeoRouteConfigOptions {\n /** Absolute path the generated route-config module is written to. */\n outputPath: string;\n /** Route groups whose page leaves form the public surface. Include the parens. */\n publicGroups: ReadonlyArray<string>;\n /** Route groups where every page leaf must declare `staticData` or `head`. */\n enforceCoverageIn: ReadonlyArray<string>;\n /** Route groups whose page paths are disallowed in robots.txt (e.g. auth). */\n disallowGroups?: ReadonlyArray<string> | undefined;\n /** Public routes that live outside any group folder, so nothing discovers them. */\n extraPublicRoutes?: ReadonlyArray<string> | undefined;\n /** Reserved top-level segments that are not route-tree paths (e.g. an api mount). */\n reservedExtras?: ReadonlyArray<string> | undefined;\n /** Prefixes always disallowed in robots.txt, route tree or not. */\n alwaysDisallow?: ReadonlyArray<string> | undefined;\n /** Routes directory, relative to the Vite root. Defaults to `src/routes`. */\n routesDirectory?: string | undefined;\n}\n\nexport interface ResolvedOptions {\n outputPath: string;\n publicGroups: ReadonlyArray<string>;\n enforceCoverageIn: ReadonlyArray<string>;\n disallowGroups: ReadonlyArray<string>;\n extraPublicRoutes: ReadonlyArray<string>;\n reservedExtras: ReadonlyArray<string>;\n alwaysDisallow: ReadonlyArray<string>;\n routesDirectory: string;\n}\n\nexport function resolveOptions(options: SeoRouteConfigOptions): ResolvedOptions {\n return {\n outputPath: options.outputPath,\n publicGroups: options.publicGroups,\n enforceCoverageIn: options.enforceCoverageIn,\n disallowGroups: options.disallowGroups ?? [],\n extraPublicRoutes: options.extraPublicRoutes ?? [],\n reservedExtras: options.reservedExtras ?? [],\n alwaysDisallow: options.alwaysDisallow ?? [],\n routesDirectory: options.routesDirectory ?? \"src/routes\",\n };\n}\n\n/**\n * Vite plugin that runs its own `@tanstack/router-generator` instance over the\n * routes directory and derives the route-config module from the result.\n */\nexport function seoRouteConfig(options: SeoRouteConfigOptions): Plugin {\n const resolved = resolveOptions(options);\n\n let generator: Generator | undefined;\n let touchRouteTree: (() => void) | undefined;\n let routesDirectory: string | undefined;\n\n /**\n * A throw here propagates: it fails `vite build` and dev startup, which is\n * the reason we own this call site instead of registering through Start.\n */\n const run = async (event?: GeneratorEvent) => {\n if (generator === undefined || touchRouteTree === undefined) {\n throw new Error(`[${PLUGIN_NAME}] generator was not initialized by configResolved`);\n }\n touchRouteTree();\n await generator.run(event);\n };\n\n return {\n name: PLUGIN_NAME,\n enforce: \"pre\",\n configResolved(config) {\n const root = config.root;\n // The route tree we generate is a byproduct we never want on disk; park it\n // (and the generator's temp files) inside node_modules so a stray write\n // could never show up as a repo change.\n const cacheDirectory = resolve(root, `node_modules/.cache/${PLUGIN_NAME}`);\n routesDirectory = resolve(root, resolved.routesDirectory);\n const generatorConfig = getConfig(\n {\n routesDirectory,\n generatedRouteTree: resolve(cacheDirectory, \"route-tree.gen.ts\"),\n tmpDir: resolve(cacheDirectory, \"tmp\"),\n plugins: [routeConfigGeneratorPlugin(resolved)],\n // The route tree is thrown away — skip its type emission and its\n // prettier pass.\n disableTypes: true,\n enableRouteTreeFormatting: false,\n // The host's generator is the one that reports on the route tree; this\n // second pass over the same files would only echo it.\n disableLogging: true,\n },\n root,\n );\n\n const memory = inMemoryWriteFs(generatorConfig.generatedRouteTree);\n touchRouteTree = memory.touchRouteTree;\n generator = new Generator({ config: generatorConfig, root, fs: memory.fs });\n },\n async buildStart() {\n await run();\n },\n // Vite watches the whole project, but only a route file can change the tree\n // or a route's declared options. Without this guard every CSS/component edit\n // would re-run the generator and its coverage pass (`touchRouteTree` forces\n // a rebuild, so the Generator's own short-circuit can't absorb them).\n async watchChange(id, { event }) {\n const file = normalize(id);\n if (routesDirectory === undefined || !file.startsWith(routesDirectory)) return;\n await run({ path: file, type: event });\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import "@tanstack/router-generator";
2
+ import { Plugin } from "vite";
3
+ //#region src/vite/route-config.d.ts
4
+ interface SeoRouteConfigOptions {
5
+ /** Absolute path the generated route-config module is written to. */
6
+ outputPath: string;
7
+ /** Route groups whose page leaves form the public surface. Include the parens. */
8
+ publicGroups: ReadonlyArray<string>;
9
+ /** Route groups where every page leaf must declare `staticData` or `head`. */
10
+ enforceCoverageIn: ReadonlyArray<string>;
11
+ /** Route groups whose page paths are disallowed in robots.txt (e.g. auth). */
12
+ disallowGroups?: ReadonlyArray<string> | undefined;
13
+ /** Public routes that live outside any group folder, so nothing discovers them. */
14
+ extraPublicRoutes?: ReadonlyArray<string> | undefined;
15
+ /** Reserved top-level segments that are not route-tree paths (e.g. an api mount). */
16
+ reservedExtras?: ReadonlyArray<string> | undefined;
17
+ /** Prefixes always disallowed in robots.txt, route tree or not. */
18
+ alwaysDisallow?: ReadonlyArray<string> | undefined;
19
+ /** Routes directory, relative to the Vite root. Defaults to `src/routes`. */
20
+ routesDirectory?: string | undefined;
21
+ }
22
+ /**
23
+ * Vite plugin that runs its own `@tanstack/router-generator` instance over the
24
+ * routes directory and derives the route-config module from the result.
25
+ */
26
+ declare function seoRouteConfig(options: SeoRouteConfigOptions): Plugin;
27
+ //#endregion
28
+ export { seoRouteConfig as n, SeoRouteConfigOptions as t };
29
+ //# sourceMappingURL=route-config-wneg6ktn.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { r as SeoGraphLoader } from "../vite-graph-loader-Btgjz5RH.js";
2
+ import { i as SiteRuntime, n as PagegraphOptions, r as RobotsPolicy, t as AppGraph } from "../graph-CBIY71w6.js";
3
+ import { Plugin } from "vite";
4
+ //#region src/tanstack-start/plugin.d.ts
5
+ declare function pagegraph(options: PagegraphOptions): Array<Plugin>;
6
+ //#endregion
7
+ //#region src/tanstack-start/load.d.ts
8
+ interface EvaluateAppGraphOptions {
9
+ /** The app's Vite root. */
10
+ readonly root: string;
11
+ /** The app's Vite config file. Defaults to Vite's lookup from `root`. */
12
+ readonly configFile?: string | undefined;
13
+ /**
14
+ * Vite mode the config is evaluated in. Origins and other build constants come
15
+ * from it, so the CLI defaults to `production` to describe the deployed site.
16
+ */
17
+ readonly mode?: string | undefined;
18
+ }
19
+ /**
20
+ * Resolve the app's Vite config in serve mode, evaluate the graph in its
21
+ * `pagegraph` environment, and release it — the dev server's evaluation path
22
+ * without a server. A config that branches on `command` sees `serve`; pass
23
+ * `mode` for the deployment the graph should describe.
24
+ */
25
+ declare function evaluateAppGraph(options: EvaluateAppGraphOptions): Promise<AppGraph>;
26
+ /**
27
+ * A `seo.config.ts` graph loader for a TanStack Start app: the same graph the
28
+ * `pagegraph()` plugin ships to the runtime, with its robots policy.
29
+ *
30
+ * ```ts
31
+ * export default defineSeoConfig({
32
+ * origin: "https://example.com",
33
+ * loadGraph: tanstackStartGraph({ root: import.meta.dirname }),
34
+ * });
35
+ * ```
36
+ */
37
+ declare const tanstackStartGraph: (options: EvaluateAppGraphOptions) => SeoGraphLoader;
38
+ //#endregion
39
+ export { type AppGraph, type EvaluateAppGraphOptions, type PagegraphOptions, type RobotsPolicy, type SiteRuntime, evaluateAppGraph, pagegraph, tanstackStartGraph };
40
+ //# sourceMappingURL=index.d.ts.map