From 430a9ccd1b30daf8ae6b949137b742a987d010f5 Mon Sep 17 00:00:00 2001 From: TypeComposer Bot Date: Fri, 12 Jun 2026 15:20:28 +0000 Subject: [PATCH 1/2] feat(seo): migrate hash router to browser history for clean indexable URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SEO P0 follow-up to PRs #29–#31. Problem: hash routing (/#/docs/skills) is not indexable by search engines because the fragment is never sent to the server. Direct deep-links also 404 without a server-side SPA fallback. Changes: - src/router/router.ts: history: "hash" → history: "browser" — enables clean URLs like /docs/skills, /docs/getting-started, /playground - vite.config.js: base: "./" → base: "/" — absolute asset paths prevent broken asset loads when navigating to nested routes (e.g. /docs/skills) - public/.htaccess: improve comments; SPA fallback rule was already correct (rewrite all non-file/non-dir requests to /index.html) and remains unchanged in logic — confirmed present before this PR - src/utils/seo.ts: add per-route canonical injection (setCanonical); now meaningful because each route has a real URL; updated Router description to remove "hash" reference; root canonical in index.html stays https://typecomposer.com/ - public/sitemap.xml: strip /#/ from all 29 URLs (e.g. /#/docs/skills → /docs/skills, /#/playground → /playground) - public/llms.txt: strip /#/ from all 29 doc links; update Router key concept description to reference browser history mode - index.html: update JSON-LD SearchAction target from /#/docs/… to /docs/… Hosting note (⚠ must verify after deploy): The .htaccess SPA fallback was already present in the repo (added before PR #31) and is confirmed to contain the correct RewriteRule. Direct deep-link indexability (e.g. https://typecomposer.com/docs/skills returning 200 not 404) requires the Apache server to honour this .htaccess. Test with: curl -I https://typecomposer.com/docs/skills Expected: HTTP/2 200, not 301/302/404. Build: npm run build — ✓ 2405 modules, no errors Reviewer: @zico15 --- index.html | 6 ++--- public/.htaccess | 10 +++++--- public/llms.txt | 60 ++++++++++++++++++++++---------------------- public/sitemap.xml | 58 +++++++++++++++++++++--------------------- src/router/router.ts | 2 +- src/utils/seo.ts | 31 ++++++++++++++++------- vite.config.js | 4 ++- 7 files changed, 94 insertions(+), 77 deletions(-) diff --git a/index.html b/index.html index adad602..949a1a5 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - + @@ -45,7 +45,7 @@ "description": "A zero-HTML TypeScript framework for building web and native user interfaces.", "potentialAction": { "@type": "SearchAction", - "target": "https://typecomposer.com/#/docs/{search_term_string}", + "target": "https://typecomposer.com/docs/{search_term_string}", "query-input": "required name=search_term_string" } } @@ -72,4 +72,4 @@ - \ No newline at end of file + diff --git a/public/.htaccess b/public/.htaccess index 978b4af..dbcd975 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1,9 +1,11 @@ -# Ativa o módulo de reescrita de URLs +# SPA fallback: rewrite all non-file, non-directory requests to index.html +# This enables browser-history (HTML5 pushState) routing for clean URLs like +# /docs/skills, /docs/getting-started, /playground, etc. RewriteEngine On -# Se o recurso solicitado não for um arquivo (-f) nem um diretório (-d) +# Allow direct access to real files (assets, robots.txt, sitemap.xml, etc.) RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d -# Redireciona todas as requisições para index.html -RewriteRule ^.*$ /index.html [L,QSA] \ No newline at end of file +# Rewrite everything else to the SPA entry point +RewriteRule ^.*$ /index.html [L,QSA] diff --git a/public/llms.txt b/public/llms.txt index 66bffc1..37b79cb 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -10,41 +10,41 @@ TypeComposer lets you compose UIs entirely in TypeScript — no JSX, no HTML tem - **Reactivity** — `ref`, `refBoolean`, `refNumber`, `refString`, `refList`, `refMap`, `refSet`, `computed` — reactive primitives that update the DOM automatically. - **Layout** — `VBox`, `HBox`, `BorderPanel` — flexbox-based layout components. - **Elements** — `DivElement`, `ButtonElement`, `InputElement`, `LabelElement`, `SpanElement`, `HeadingElement`, `TableElement`. -- **Router** — Hash-based SPA router with typed route definitions. `Router.create({ history: "hash", routes: [...] })`. +- **Router** — Browser-history SPA router with typed route definitions. `Router.create({ history: "browser", routes: [...] })`. - **RouterView** — Outlet component that renders the matched child route. - **Dependency Injection** — Built-in DI container; inject services into components via constructor. - **Agent Skills** — Integration guide for using TypeComposer apps as AI agent skills. ## Docs -- [Why TypeComposer?](https://typecomposer.com/#/docs/home) -- [Getting Started](https://typecomposer.com/#/docs/getting-started) -- [Component](https://typecomposer.com/#/docs/components/component) -- [Template](https://typecomposer.com/#/docs/components/template) -- [Lifecycle](https://typecomposer.com/#/docs/components/lifecycle-docs) -- [Dependency Injection](https://typecomposer.com/#/docs/dependency-injection) -- [ref](https://typecomposer.com/#/docs/reactivity/ref) -- [refBoolean](https://typecomposer.com/#/docs/reactivity/refboolean) -- [refNumber](https://typecomposer.com/#/docs/reactivity/refnumber) -- [refString](https://typecomposer.com/#/docs/reactivity/refstring) -- [refList](https://typecomposer.com/#/docs/reactivity/reflist) -- [refMap](https://typecomposer.com/#/docs/reactivity/refmap) -- [refSet](https://typecomposer.com/#/docs/reactivity/refset) -- [computed](https://typecomposer.com/#/docs/reactivity/computed) -- [Router](https://typecomposer.com/#/docs/router) -- [RouterView](https://typecomposer.com/#/docs/router-view) -- [BorderPanel](https://typecomposer.com/#/docs/layout/border-panel) -- [VBox](https://typecomposer.com/#/docs/layout/vbox) -- [HBox](https://typecomposer.com/#/docs/layout/hbox) -- [Div](https://typecomposer.com/#/docs/elements/div) -- [Button](https://typecomposer.com/#/docs/elements/button) -- [Input](https://typecomposer.com/#/docs/elements/input) -- [Label](https://typecomposer.com/#/docs/elements/label) -- [Table](https://typecomposer.com/#/docs/elements/table) -- [Span](https://typecomposer.com/#/docs/elements/span) -- [Heading](https://typecomposer.com/#/docs/elements/heading) -- [Agent Skills](https://typecomposer.com/#/docs/skills) -- [Playground](https://typecomposer.com/#/playground) +- [Why TypeComposer?](https://typecomposer.com/docs/home) +- [Getting Started](https://typecomposer.com/docs/getting-started) +- [Component](https://typecomposer.com/docs/components/component) +- [Template](https://typecomposer.com/docs/components/template) +- [Lifecycle](https://typecomposer.com/docs/components/lifecycle-docs) +- [Dependency Injection](https://typecomposer.com/docs/dependency-injection) +- [ref](https://typecomposer.com/docs/reactivity/ref) +- [refBoolean](https://typecomposer.com/docs/reactivity/refboolean) +- [refNumber](https://typecomposer.com/docs/reactivity/refnumber) +- [refString](https://typecomposer.com/docs/reactivity/refstring) +- [refList](https://typecomposer.com/docs/reactivity/reflist) +- [refMap](https://typecomposer.com/docs/reactivity/refmap) +- [refSet](https://typecomposer.com/docs/reactivity/refset) +- [computed](https://typecomposer.com/docs/reactivity/computed) +- [Router](https://typecomposer.com/docs/router) +- [RouterView](https://typecomposer.com/docs/router-view) +- [BorderPanel](https://typecomposer.com/docs/layout/border-panel) +- [VBox](https://typecomposer.com/docs/layout/vbox) +- [HBox](https://typecomposer.com/docs/layout/hbox) +- [Div](https://typecomposer.com/docs/elements/div) +- [Button](https://typecomposer.com/docs/elements/button) +- [Input](https://typecomposer.com/docs/elements/input) +- [Label](https://typecomposer.com/docs/elements/label) +- [Table](https://typecomposer.com/docs/elements/table) +- [Span](https://typecomposer.com/docs/elements/span) +- [Heading](https://typecomposer.com/docs/elements/heading) +- [Agent Skills](https://typecomposer.com/docs/skills) +- [Playground](https://typecomposer.com/playground) ## GitHub @@ -52,4 +52,4 @@ TypeComposer lets you compose UIs entirely in TypeScript — no JSX, no HTML tem - [typecomposer/docs](https://github.com/TypeComposer/docs) — documentation site - [typecomposer/plugin](https://github.com/TypeComposer/plugin) — Vite plugin - [typecomposer/create](https://github.com/TypeComposer/create) — CLI scaffolding -- [typecomposer/ssr](https://github.com/TypeComposer/ssr) — SSR / hydration \ No newline at end of file +- [typecomposer/ssr](https://github.com/TypeComposer/ssr) — SSR / hydration diff --git a/public/sitemap.xml b/public/sitemap.xml index 7a1443f..9adc528 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -9,157 +9,157 @@ - https://typecomposer.com/#/docs/home + https://typecomposer.com/docs/home weekly 0.9 - https://typecomposer.com/#/docs/getting-started + https://typecomposer.com/docs/getting-started weekly 0.9 - https://typecomposer.com/#/docs/skills + https://typecomposer.com/docs/skills monthly 0.6 - https://typecomposer.com/#/docs/components/component + https://typecomposer.com/docs/components/component weekly 0.8 - https://typecomposer.com/#/docs/components/template + https://typecomposer.com/docs/components/template weekly 0.8 - https://typecomposer.com/#/docs/components/lifecycle-docs + https://typecomposer.com/docs/components/lifecycle-docs weekly 0.7 - https://typecomposer.com/#/docs/dependency-injection + https://typecomposer.com/docs/dependency-injection weekly 0.8 - https://typecomposer.com/#/docs/reactivity/ref + https://typecomposer.com/docs/reactivity/ref weekly 0.8 - https://typecomposer.com/#/docs/reactivity/refboolean + https://typecomposer.com/docs/reactivity/refboolean weekly 0.7 - https://typecomposer.com/#/docs/reactivity/refnumber + https://typecomposer.com/docs/reactivity/refnumber weekly 0.7 - https://typecomposer.com/#/docs/reactivity/refstring + https://typecomposer.com/docs/reactivity/refstring weekly 0.7 - https://typecomposer.com/#/docs/reactivity/reflist + https://typecomposer.com/docs/reactivity/reflist weekly 0.7 - https://typecomposer.com/#/docs/reactivity/refmap + https://typecomposer.com/docs/reactivity/refmap weekly 0.7 - https://typecomposer.com/#/docs/reactivity/refset + https://typecomposer.com/docs/reactivity/refset weekly 0.7 - https://typecomposer.com/#/docs/reactivity/computed + https://typecomposer.com/docs/reactivity/computed weekly 0.7 - https://typecomposer.com/#/docs/router + https://typecomposer.com/docs/router weekly 0.8 - https://typecomposer.com/#/docs/router-view + https://typecomposer.com/docs/router-view weekly 0.7 - https://typecomposer.com/#/docs/layout/border-panel + https://typecomposer.com/docs/layout/border-panel weekly 0.7 - https://typecomposer.com/#/docs/layout/vbox + https://typecomposer.com/docs/layout/vbox weekly 0.7 - https://typecomposer.com/#/docs/layout/hbox + https://typecomposer.com/docs/layout/hbox weekly 0.7 - https://typecomposer.com/#/docs/elements/div + https://typecomposer.com/docs/elements/div weekly 0.6 - https://typecomposer.com/#/docs/elements/button + https://typecomposer.com/docs/elements/button weekly 0.6 - https://typecomposer.com/#/docs/elements/input + https://typecomposer.com/docs/elements/input weekly 0.6 - https://typecomposer.com/#/docs/elements/label + https://typecomposer.com/docs/elements/label weekly 0.6 - https://typecomposer.com/#/docs/elements/table + https://typecomposer.com/docs/elements/table weekly 0.6 - https://typecomposer.com/#/docs/elements/span + https://typecomposer.com/docs/elements/span weekly 0.6 - https://typecomposer.com/#/docs/elements/heading + https://typecomposer.com/docs/elements/heading weekly 0.6 - https://typecomposer.com/#/playground + https://typecomposer.com/playground monthly 0.5 - \ No newline at end of file + diff --git a/src/router/router.ts b/src/router/router.ts index 1a08868..7695d2e 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -7,7 +7,7 @@ import { HomePage } from "@/pages/Home"; loadDocs().finally(() => { Router.create({ - history: "hash", + history: "browser", routes: [ { path: "/", component: HomePage }, { diff --git a/src/utils/seo.ts b/src/utils/seo.ts index 83d49be..79fdc95 100644 --- a/src/utils/seo.ts +++ b/src/utils/seo.ts @@ -1,11 +1,10 @@ /** - * SEO utilities — update and <meta name="description"> on each route - * transition. Since this is a hash-router SPA with no SSR, these updates help - * browser history titles, social preview when JS runs, and AI crawlers that - * execute JS (e.g. Googlebot, GPTBot after rendering). + * SEO utilities — update <title>, <meta name="description">, and + * <link rel="canonical"> on each route transition. * - * Canonical stays fixed at https://typecomposer.com/ in index.html because - * hash fragments are not real URLs; there is no per-page canonical benefit. + * With browser-history routing every route is a real URL, so Googlebot and + * other crawlers can index /docs/skills, /docs/getting-started, etc. directly. + * Per-route canonicals are therefore meaningful and are set dynamically here. */ interface PageMeta { @@ -16,6 +15,7 @@ interface PageMeta { const BASE_TITLE = "TypeComposer"; const BASE_DESC = "A zero-HTML TypeScript framework for building web and native user interfaces. Compose UIs from pure TypeScript classes — no JSX, no templates."; +const CANONICAL_BASE = "https://typecomposer.com"; /** Per-route meta map keyed by Router.pathname value (e.g. "docs/getting-started") */ const PAGE_META: Record<string, PageMeta> = { @@ -84,7 +84,7 @@ const PAGE_META: Record<string, PageMeta> = { }, "docs/router": { title: "Router", - description: "TypeComposer's built-in hash router — define typed routes, nested routes, wildcards, and redirects in pure TypeScript.", + description: "TypeComposer's built-in router — define typed routes, nested routes, wildcards, and redirects in pure TypeScript.", }, "docs/router-view": { title: "RouterView", @@ -133,7 +133,8 @@ const PAGE_META: Record<string, PageMeta> = { }; /** - * Update <title> and <meta name="description"> for the given router pathname. + * Update <title>, <meta name="description">, and <link rel="canonical"> + * for the given router pathname (e.g. "docs/skills"). * Call this from BaseView constructor or onConnected whenever the route changes. */ export function updatePageMeta(pathname: string): void { @@ -145,6 +146,8 @@ export function updatePageMeta(pathname: string): void { document.title = BASE_TITLE; setMetaDescription(BASE_DESC); } + // Set per-route canonical now that we use real browser-history URLs + setCanonical(`${CANONICAL_BASE}/${pathname}`); } function setMetaDescription(content: string): void { @@ -155,4 +158,14 @@ function setMetaDescription(content: string): void { document.head.appendChild(tag); } tag.content = content; -} \ No newline at end of file +} + +function setCanonical(href: string): void { + let tag = document.querySelector<HTMLLinkElement>('link[rel="canonical"]'); + if (!tag) { + tag = document.createElement("link"); + tag.rel = "canonical"; + document.head.appendChild(tag); + } + tag.href = href; +} diff --git a/vite.config.js b/vite.config.js index fee80f9..d1fb2fd 100644 --- a/vite.config.js +++ b/vite.config.js @@ -34,7 +34,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, - base: "./", + // Use absolute base "/" so asset paths resolve correctly with browser-history + // routing (e.g. /docs/skills loads assets from /assets/*, not /docs/assets/*). + base: "/", css: { preprocessorOptions: { scss: { From 5ec4bede0297e2b2e955e055a340d3eae261fea1 Mon Sep 17 00:00:00 2001 From: lucas-spin <lucas-spin@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:34:47 +0000 Subject: [PATCH 2/2] refactor(seo): remove runtime setCanonical workaround from seo.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-route canonical tag was being injected at runtime via setCanonical() on every navigation. This is a JS-dependent workaround that only works for crawlers that execute JavaScript. The static root canonical in index.html covers the home route, and the sitemap.xml already lists clean per-route URLs for all 29 pages — giving crawlers the authoritative canonical signal without any JS required. Removed: - setCanonical() function - CANONICAL_BASE constant - setCanonical() call inside updatePageMeta() - Stale index.html comment referencing the workaround Kept unchanged: - hash -> browser history routing (src/router/router.ts) - vite.config.js base: '/' - sitemap.xml clean URLs (no #/) - llms.txt clean URLs - JSON-LD SearchAction clean URL target in index.html - Root canonical <link> in index.html - All per-route title/description meta (PAGE_META map) --- index.html | 2 +- src/utils/seo.ts | 26 +++++++------------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/index.html b/index.html index 949a1a5..f3d23a4 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ <link rel="icon" type="image/svg+xml" href="/typecomposer.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <!-- Canonical URL (root; per-route canonicals are set dynamically by seo.ts) --> + <!-- Canonical URL --> <link rel="canonical" href="https://typecomposer.com/" /> <!-- SEO Meta Tags --> diff --git a/src/utils/seo.ts b/src/utils/seo.ts index 79fdc95..56e125d 100644 --- a/src/utils/seo.ts +++ b/src/utils/seo.ts @@ -1,10 +1,11 @@ /** - * SEO utilities — update <title>, <meta name="description">, and - * <link rel="canonical"> on each route transition. + * SEO utilities — update <title> and <meta name="description"> on each route + * transition. * * With browser-history routing every route is a real URL, so Googlebot and * other crawlers can index /docs/skills, /docs/getting-started, etc. directly. - * Per-route canonicals are therefore meaningful and are set dynamically here. + * The static <link rel="canonical"> in index.html covers the root; per-route + * canonicals are declared in the sitemap rather than injected at runtime. */ interface PageMeta { @@ -15,7 +16,6 @@ interface PageMeta { const BASE_TITLE = "TypeComposer"; const BASE_DESC = "A zero-HTML TypeScript framework for building web and native user interfaces. Compose UIs from pure TypeScript classes — no JSX, no templates."; -const CANONICAL_BASE = "https://typecomposer.com"; /** Per-route meta map keyed by Router.pathname value (e.g. "docs/getting-started") */ const PAGE_META: Record<string, PageMeta> = { @@ -133,8 +133,8 @@ const PAGE_META: Record<string, PageMeta> = { }; /** - * Update <title>, <meta name="description">, and <link rel="canonical"> - * for the given router pathname (e.g. "docs/skills"). + * Update <title> and <meta name="description"> for the given router pathname + * (e.g. "docs/skills"). * Call this from BaseView constructor or onConnected whenever the route changes. */ export function updatePageMeta(pathname: string): void { @@ -146,8 +146,6 @@ export function updatePageMeta(pathname: string): void { document.title = BASE_TITLE; setMetaDescription(BASE_DESC); } - // Set per-route canonical now that we use real browser-history URLs - setCanonical(`${CANONICAL_BASE}/${pathname}`); } function setMetaDescription(content: string): void { @@ -158,14 +156,4 @@ function setMetaDescription(content: string): void { document.head.appendChild(tag); } tag.content = content; -} - -function setCanonical(href: string): void { - let tag = document.querySelector<HTMLLinkElement>('link[rel="canonical"]'); - if (!tag) { - tag = document.createElement("link"); - tag.rel = "canonical"; - document.head.appendChild(tag); - } - tag.href = href; -} +} \ No newline at end of file