seo(landing): Organization/WebSite entity JSON-LD, AI-crawler robots allow-list, md-twin alternates, HSTS + cf-visitor https redirect - #415
Open
BSalaeddin wants to merge 2 commits into
Open
Conversation
…allow-list, md-twin alternates, HSTS + cf-visitor https redirect
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
…ts in the docs link gate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
The 2026-09-11 fleet SEO pull says the site is found but not understood:
Organizationand noWebSiteJSON-LD on any of the 73 pages, and the 64 docs pages — the bulk of the indexable surface — carried zero structured data of any kind.http://useupup.com/serves the full page: 200, byte-identical to https, no redirect, no HSTS. Plaintext is a second live origin for the whole site./sitemap-landing.xml404s outright,/documentation/sitemap.xmltakes two hops into a 404. GSC has been reporting "couldn't fetch" for both.llms.txtis linked from nowhere — 0<a href>to it in any page's HTML. Same for the 65/docs-md/markdown twins: reachable only from a client-sidefetchin the Copy button, and served with no canonical back-link, so if discovered they are duplicates of every docs page under a second URL.*rule, no GPTBot/ClaudeBot/PerplexityBot/Google-Extended anywhere in the repo.lastmodwas a single build timestamp on all 73 URLs — a fake "everything changed" signal Google's documentation says it learns to ignore./mobile-demo/is a 200 HTML page with the homepage's title, no canonical and no robots directive;Disallowin robots.txt blocks crawling, not indexing.The AI-crawler probe is the one thing that came back clean: 57/57 pass at Cloudflare — nothing is being blocked at the edge, so the only reason agents don't cite us is that we never gave them anything to cite.
Changes (all
apps/landingunless noted)Entity graph. New
src/components/StructuredData/EntityStructuredData.tsxemits anOrganization(@id/#organization,alternateNamefor the three spellings the brand is searched under,logo,parentOrganization: Devino) plus aWebSite(@id/#website,publisher→ the org). It is mounted from the root layout, so it reaches every page including all 64 docs pages.sameAscarries only URLs verified live on 2026-09-12: the GitHub repo, the npm package (@useupup/react, latest 3.3.3 on the registry), and the README's Discord invite (301s todiscord.com/invite/…; the invite API returns a live, non-expiring guild). The existingSoftwareApplicationgains@id/#softwareandauthor/publisherpointing at the org by@id. NoAggregateRating, noReview— pinned by a negative assertion. FAQPage is unchanged.Docs structured data + markdown-twin discoverability.
docs/[[...slug]]/page.tsxnow declaresalternates.types['text/markdown'], rendering<link rel="alternate" type="text/markdown">. NewDocsStructuredDataemitsBreadcrumbList(from the samefindTrail(tree, url)the visual breadcrumb uses, so they cannot disagree) andTechArticle(isPartOfthe WebSite,publisherthe Organization). No dates — content/docs has none and inventing one is worse than omitting it. A crawlable<a href="/docs-md/<slug>/">View as Markdown</a>sits next to the Copy-page button, anddocs-md/[[...slug]]/route.tsanswers withLink: <canonical html url>; rel="canonical"so the twin is an alternate representation, not a duplicate.llms.txt discoverability. Real
<a href="/llms.txt">links in the footer (every page) and on the docs hub.robots.txt AI allow-list. New
src/lib/seo/ai-crawlers.tsexports the exact 18 agent names;robots.tsadds a second rules entry for them alongside*, both sharing ONEdisallowconstant so a named agent can never get a laxer list than the wildcard (that would be a side door into/api/).Scheme, HSTS, stale paths (
next.config.mjs). Production now sendsStrict-Transport-Security: max-age=63072000; includeSubDomains; preload; non-prod keeps onlyX-Robots-Tagand gets no HSTS. An http→https redirect keyed only on Cloudflare'scf-visitorheader sits first inredirects()— neverx-forwarded-proto, which Traefik rewrites and which would loop. It is two rules, not one: Next strips the trailing slash before matching a source and does not re-append it to an absolute destination, so a single${SITE_BASE}/:path*would send/react/to…/reactand cost a second 308. The first rule matches extensionless paths and restores the slash; file paths and the bare root fall through to the second, which must not gain one. Added single-hop redirects for the two stale GSC sitemaps, and host-scoped/robots.txt+/sitemap.xmlrules for the docs alias (they previously chained into a 404).Sitemap.
lastModifiedremoved from all 73 entries. Priorities,changeFrequencyand the fumadocs enumeration are untouched./mobile-demo/noindex. Newmobile-demo/layout.tsxcarriesrobots: { index: false, follow: false }(the page is'use client'and cannot export metadata). The robots.txtDisallowstays — they cover different things.Verification
Production build (
turbo run build --filter=@useupup/landing --force, 4/4 tasks) served bynext start -p 4466:Gates, all from
rtk proxywith the raw exit code:turbo run lint typecheck test --filter=@useupup/landing --forcepnpm run test:qualitypnpm run vocab:checkprettier --checkon all 15 touched filesturbo run build --filter=@useupup/landing --force--project docsWhat CI covers
New vitest
apps/landing/src/__tests__/seo-surfaces.test.ts(14 tests, runs inmain.yml› Test):https://useupup.com/…with a trailing slash, none contains/mobile-demo,/api/,/docs-mdor/llms, and no entry carrieslastModified— the fake-freshness signal cannot come back silently.*rule allows/and disallows/api/+/mobile-demo/; a second rule lists exactlyAI_CRAWLER_USER_AGENTSwith the same disallow set; the sitemap URL is the production one. Non-prod (NEXT_PUBLIC_BASE_URLstubbed to the dev host withvi.stubEnv+vi.resetModules, becauseclientEnvis parsed once at module load) disallows/.EntityStructuredDataandStructuredDataare rendered withreact-dom/serverand everyapplication/ld+jsonblock is JSON-parsed: Organization has its@id, ≥2 httpssameAsURLs,alternateName, and Devino asparentOrganization; WebSite'spublisherand SoftwareApplication'sauthor/publisherboth resolve to the org@id;AggregateRatingandReviewappear nowhere.New Playwright cases in
apps/e2e-test/landing/docs.spec.ts(theDocs-E2Ejob, which runs on every landing PR): canonical +rel=alternatemarkdown link on/docs/getting-started/and the twin's 200 /text/markdown/rel="canonical"Linkheader; thecf-visitor308 tohttps://useupup.com/react/and the plain request still 200;/sitemap-landing.xml→ 308; the HSTS header on/;User-Agent: GPTBotin the robots body; the footer/llms.txtlink. The dev server the project boots leavesNEXT_PUBLIC_BASE_URLunset, sonext.config.mjscomputes the productionSITE_BASEand takes the production branch ofheaders()/redirects()— confirmed live, the HSTS assertion holds undernext dev.The existing
docs-llms.test.ts,docs-source.test.ts(64 pages),site-url.test.tsand thedocs.spec.tsredirect cases are unchanged and green.Owner-only items (not in this PR)
cf-visitorrule here is the in-repo braces; the zone toggle is the belt and stops the plaintext request before it reaches the container./sitemap-landing.xml,/documentation/sitemap.xml) and keep onlyhttps://useupup.com/sitemap.xml. They now 308 instead of 404, but the submissions themselves should go.app.useupup.com: the DNS record resolves through Cloudflare and lands on the BioFlow container (404text/plainwith BioFlow's CSP and HSTS). Delete theapprecord, or add a Traefik router that 308s it tohttps://useupup.com/.preloadin the value is a prerequisite for submitting, not the submission.