diff --git a/docs/superpowers/plans/2026-07-27-tbmq-io-link-migration.md b/docs/superpowers/plans/2026-07-27-tbmq-io-link-migration.md new file mode 100644 index 0000000000..03a5b151d3 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-tbmq-io-link-migration.md @@ -0,0 +1,1247 @@ +# TBMQ → tbmq.io Link Migration — Pass 1 (Entry Points) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Point four thingsboard.io entry points that lead to TBMQ — the Products menu, the Docs menu, the docs left-panel product selector, and the Try it now TBMQ panel — at tbmq.io, routed through a single URL module so no tbmq.io URL is ever spelled twice. + +**Architecture:** One new module, `src/data/external-sites.ts`, owns the tbmq.io origin and two URL builders. Every call site imports from it. Four surfaces change: the two mega-menu items (through the shared `SubMenuLink.astro`), the docs product selector's `FAMILIES` array, and the Try it now TBMQ panel's 18 links. TBMQ docs stay live and fully reachable on thingsboard.io — deletion and the cross-site edge redirects are later work, captured in a removal inventory document. + +**Explicitly out of scope** (deferred by the user on 2026-07-27, each awaiting its own go-ahead): + +| Deferred | Why it is not here | +|---|---| +| The pricing page TBMQ toggle | Left for a later decision. The page keeps its local TBMQ sections, sub-tabs, calculators and FAQ, and the 16 `?section=tbmq-options` deep links keep working. | +| Homepage + `/products/` ecosystem cards | Not among the four requested surfaces. | +| The `/docs/mqtt-broker/*` edge redirect | Would take TBMQ docs offline in production the moment it deploys. It also drags 27 chaining `SINGLE_REDIRECTS` and 4 `CATCH_ALL_REDIRECTS` groups with it — all documented in Task 7's inventory. | +| Blog posts, TBMQ docs content, `_includes` | Content edits; covered by the redirect when it lands. | + +Do not implement any deferred row, even if it looks like a natural extension of a task you are on. Flag it and move on. + +**Tech Stack:** Astro 6 + Starlight, TypeScript, SCSS, pnpm. + +**Spec:** `docs/superpowers/specs/2026-07-27-tbmq-io-link-migration-design.md` + +## Global Constraints + +- **No hardcoded tbmq.io URLs outside `src/data/external-sites.ts`.** Every other file imports `TBMQ_URLS`, `tbmqUrl` or `tbmqDocsUrl`. A literal `https://tbmq.io` anywhere else is a plan violation. `https://demo.tbmq.io/signup` is the one pre-existing exception and it moves *into* the module as `TBMQ_URLS.liveDemo`. +- **Path aliases only.** Use `@data/*`, `@components/*`, `@models/*`, `@util/*`, `@root/*` per `CLAUDE.md`. Never relative paths, never the legacy `~/*` for new code. (Existing `~/*` imports in a file you touch stay as they are — do not churn them.) +- **Tabs for indentation** in `.ts` / `.astro` files. Spaces in `.md`. +- **External links carry both attributes:** `target="_blank" rel="noopener noreferrer"`. Never `target="_blank"` alone. +- **Don't add narrating comments.** Comment only non-obvious *why* (a constraint, a gotcha), matching each file's existing comment density. +- **Format after editing:** run `pnpm exec prettier -w `. If a file was already non-format-clean in regions you didn't touch, leave those regions alone. +- **No TBMQ content deletion.** This plan deletes no `.mdx` doc, no asset, no pricing/installations data file. Everything removable is recorded in the inventory instead. +- **Never run `pnpm build` / `pnpm build:fast` without asking the user first** (project build policy in `CLAUDE.md`). + +## Testing note — read before Task 1 + +**This repo has no unit-test runner.** There is no `test` script, no `tests/` directory, no vitest/jest dependency. Do not invent one, and do not add a test framework — that would be a scope change nobody asked for. + +Verification per task is therefore: + +1. `pnpm check` — Astro + TypeScript type checking (catches wrong prop types, missing interface fields, bad imports) +2. `pnpm lint:eslint` — lint, unused imports, unused vars +3. **Grep assertions on source** — prove the old literal is gone and the helper call is present +4. One end-to-end task at the end asserts against **built HTML** and runs the link checker + +Steps below give you the exact commands and the exact expected output. A step that says "Expected: no output" means the grep must find nothing — `grep` exits 1, which is success for that step. + +--- + +### Task 1: The URL module + +**Files:** +- Create: `src/data/external-sites.ts` + +**Interfaces:** +- Consumes: nothing. +- Produces — every later task depends on these exact names and signatures: + - `TBMQ_ORIGIN: string` — e.g. `'https://tbmq.io'`, never a trailing slash + - `tbmqUrl(path?: string): string` — absolute URL for a tbmq.io path + - `tbmqDocsUrl(slug?: string): string` — absolute URL under tbmq.io's TBMQ docs root + - `TBMQ_URLS` — readonly object with keys `product`, `docs`, `liveDemo`, all `string` + +- [ ] **Step 1: Write the module** + +Create `src/data/external-sites.ts`: + +```ts +/** + * Sibling-site origins and URL builders. + * + * TBMQ moved to its own site (tbmq.io). Every thingsboard.io link that used to + * point at a local TBMQ page resolves through the helpers here, so the origin + * and the path shape live in exactly one place — when tbmq.io restructures, + * this file is the only edit. + * + * `TBMQ_SITE_URL` mirrors the `IOT_HUB_API_URL` pattern in `src/models/iot-hub.ts`: + * build-time `import.meta.env` with a literal fallback, no `PUBLIC_` prefix, + * because every URL produced here is baked into HTML at build time. + */ + +const normalizeOrigin = (origin: string): string => origin.replace(/\/+$/, ''); + +export const TBMQ_ORIGIN = normalizeOrigin(import.meta.env.TBMQ_SITE_URL ?? 'https://tbmq.io'); + +/** + * tbmq.io serves the TBMQ docs under the same tree thingsboard.io used, so a + * slug maps across 1:1. Their `/docs/` 301s here, which is why `TBMQ_URLS.docs` + * can stay on the shorter, restructure-proof entry point. + */ +const TBMQ_DOCS_ROOT = '/docs/mqtt-broker/'; + +/** Absolute tbmq.io URL for `path`. Leading and trailing slashes are normalized. */ +export function tbmqUrl(path = '/'): string { + const trimmed = path.replace(/^\/+/, ''); + const withSlash = trimmed === '' || trimmed.endsWith('/') ? trimmed : `${trimmed}/`; + return `${TBMQ_ORIGIN}/${withSlash}`; +} + +/** + * Absolute tbmq.io docs URL for a slug relative to the TBMQ docs root. + * tbmqDocsUrl('installation/') → https://tbmq.io/docs/mqtt-broker/installation/ + * tbmqDocsUrl('pe/installation/') → https://tbmq.io/docs/mqtt-broker/pe/installation/ + * + * Query strings survive: a slug ending in `?installationType=helm` is not given + * a trailing slash. + */ +export function tbmqDocsUrl(slug = ''): string { + const trimmed = slug.replace(/^\/+/, ''); + if (trimmed === '') return tbmqUrl(TBMQ_DOCS_ROOT); + const [pathPart, query] = trimmed.split('?'); + const path = pathPart!.endsWith('/') ? pathPart : `${pathPart}/`; + return `${TBMQ_ORIGIN}${TBMQ_DOCS_ROOT}${path}${query ? `?${query}` : ''}`; +} + +/** + * Named tbmq.io entry points. Prefer these over spelling a path at a call site. + * Add a key when a surface needs it — `/pricing/` and `/installations/` are + * deliberately absent until the pricing migration is approved. + */ +export const TBMQ_URLS = { + product: tbmqUrl('/product/'), + docs: tbmqUrl('/docs/'), + liveDemo: 'https://demo.tbmq.io/signup', +} as const; +``` + +- [ ] **Step 2: Type-check and lint** + +```bash +pnpm check && pnpm lint:eslint +``` + +Expected: both PASS. `pnpm check` reports 0 errors for `src/data/external-sites.ts`. + +If `import.meta.env.TBMQ_SITE_URL` produces a TS error about an unknown env key, that is the signal to check whether the repo has an `env.d.ts` declaring `ImportMetaEnv`. `src/models/iot-hub.ts` reads `import.meta.env.IOT_HUB_API_URL` with no declaration and passes, so this should pass too — do not add a declaration file unless `pnpm check` actually demands one. + +- [ ] **Step 3: Verify the produced values** + +The module can't be imported by bare `node` (`import.meta.env` is Vite-injected). Assert on the source instead — the shapes are simple enough that the type checker plus these greps are the real coverage: + +```bash +grep -n "TBMQ_ORIGIN\|TBMQ_DOCS_ROOT = \|product:\|docs:\|liveDemo:" src/data/external-sites.ts +``` + +Expected: `TBMQ_ORIGIN` defined once from `import.meta.env.TBMQ_SITE_URL`, `TBMQ_DOCS_ROOT = '/docs/mqtt-broker/'`, and all three `TBMQ_URLS` keys present. + +Task 9 asserts the actual resolved strings against built HTML. + +- [ ] **Step 4: Format and commit** + +```bash +pnpm exec prettier -w src/data/external-sites.ts +git add src/data/external-sites.ts +git commit -m "feat(tbmq): add central tbmq.io URL module" +``` + +--- + +### Task 2: Mega-menu external-link support + +**Files:** +- Modify: `src/data/navigation.ts` — `SubMenuItem` interface (~L7–13) +- Modify: `src/components/Landing/SubMenuLink.astro` + +**Interfaces:** +- Consumes: nothing from Task 1. +- Produces: `SubMenuItem.external?: boolean`, honored by `SubMenuLink.astro`. Task 3 sets it. + +This is separated from Task 3 because it is a reusable capability with its own review surface: a reviewer could accept the mechanism and reject the specific links, or vice versa. + +- [ ] **Step 1: Add the field to the interface** + +In `src/data/navigation.ts`, the interface currently reads: + +```ts +export interface SubMenuItem { + href: string; + icon?: string; + heading: string; + description?: string; + linkClass?: string; +} +``` + +Add `external`: + +```ts +export interface SubMenuItem { + href: string; + icon?: string; + heading: string; + description?: string; + linkClass?: string; + /** Opens in a new tab. Set for links that leave thingsboard.io. */ + external?: boolean; +} +``` + +- [ ] **Step 2: Honor it in the shared link component** + +`src/components/Landing/SubMenuLink.astro` currently renders: + +```astro + +``` + +Change that one line to: + +```astro + +``` + +Leave the rest of the file untouched — the `NavIcon`, `.sub-text`, heading and description markup all stay exactly as they are. + +- [ ] **Step 3: Type-check and lint** + +```bash +pnpm check && pnpm lint:eslint +``` + +Expected: both PASS. No new errors. + +- [ ] **Step 4: Verify nothing regressed for non-external items** + +```bash +grep -c "external" src/components/Landing/SubMenuLink.astro +``` + +Expected: `2` (the `target` and `rel` conditionals). Every existing submenu item leaves `external` unset, so both attributes render as `undefined` and Astro omits them — internal links are byte-identical to before. + +- [ ] **Step 5: Format and commit** + +```bash +pnpm exec prettier -w src/data/navigation.ts src/components/Landing/SubMenuLink.astro +git add src/data/navigation.ts src/components/Landing/SubMenuLink.astro +git commit -m "feat(nav): support external submenu links" +``` + +--- + +### Task 3: Products and Docs mega-menu TBMQ items (spec items 1 and 2) + +**Files:** +- Modify: `src/data/navigation.ts` — products submenu TBMQ item (~L100–106), docs submenu TBMQ item (~L405–410) + +**Interfaces:** +- Consumes: `TBMQ_URLS` from Task 1; `SubMenuItem.external` from Task 2. +- Produces: nothing new. + +- [ ] **Step 1: Add the import** + +At the top of `src/data/navigation.ts`, add: + +```ts +import { TBMQ_URLS } from '@data/external-sites'; +``` + +Place it with any other imports in the file. If the file currently has no imports, put it as the first line, followed by a blank line before the first `export interface`. + +- [ ] **Step 2: Repoint the Products submenu item** + +Find this entry in `productsSubmenu` (it is the item with `linkClass: 'mqtt-broker-lnk'` and `heading: 'TBMQ'`): + +```ts + { + href: '/products/mqtt-broker/', + icon: '/src/assets/images/landings/nav/tbmq-icon.svg', + heading: 'TBMQ', + description: 'Scalable MQTT broker', + linkClass: 'mqtt-broker-lnk', + }, +``` + +Replace with: + +```ts + { + href: TBMQ_URLS.product, + icon: '/src/assets/images/landings/nav/tbmq-icon.svg', + heading: 'TBMQ', + description: 'Scalable MQTT broker', + linkClass: 'mqtt-broker-lnk', + external: true, + }, +``` + +- [ ] **Step 3: Repoint the Docs submenu item** + +Find this entry in `docsSubmenu` (heading `TBMQ`, no `icon` key): + +```ts + { + href: '/docs/mqtt-broker/', + heading: 'TBMQ', + description: 'Scalable MQTT broker', + linkClass: 'mqtt-broker-lnk', + }, +``` + +Replace with: + +```ts + { + href: TBMQ_URLS.docs, + heading: 'TBMQ', + description: 'Scalable MQTT broker', + linkClass: 'mqtt-broker-lnk', + external: true, + }, +``` + +- [ ] **Step 4: Verify both literals are gone** + +```bash +grep -n "mqtt-broker" src/data/navigation.ts +``` + +Expected: only two lines, both `linkClass: 'mqtt-broker-lnk',`. No `href: '/products/mqtt-broker/'`, no `href: '/docs/mqtt-broker/'`. + +- [ ] **Step 5: Type-check and lint** + +```bash +pnpm check && pnpm lint:eslint +``` + +Expected: both PASS. + +- [ ] **Step 6: Format and commit** + +```bash +pnpm exec prettier -w src/data/navigation.ts +git add src/data/navigation.ts +git commit -m "feat(nav): point TBMQ menu items at tbmq.io" +``` + +--- + +### Task 4: Docs product selector (spec item 4) + +**Files:** +- Modify: `src/components/VersionSwitcher.astro` — `Family` interface (~L31–43), TBMQ family entry (~L88–101), product popover `` (~L250–283) + +**Interfaces:** +- Consumes: `TBMQ_URLS` from Task 1. +- Produces: `Family.externalUrl?: string`. + +**Critical constraint — do not remove the TBMQ `editions` array.** `currentFamily` is derived as `FAMILIES.find((f) => f.editions.some((e) => e.product === currentProduct))`. TBMQ docs are still present, so a visitor can be *on* a TBMQ docs page; emptying `editions` makes `currentFamily` `undefined` and the non-null assertion on the following line throws at build time for all 174 TBMQ pages. Only the popover link is overridden. + +- [ ] **Step 1: Add the import** + +In the frontmatter of `src/components/VersionSwitcher.astro`, alongside the existing imports: + +```ts +import { TBMQ_URLS } from '@data/external-sites'; +``` + +- [ ] **Step 2: Add `externalUrl` to the `Family` interface** + +The interface currently ends with: + +```ts + /** Edition to land on when this family is picked from the product dropdown. + * Defaults to `editions[editions.length - 1]` (Professional/Cloud first). */ + preferredEdition?: Products; +} +``` + +Add before the closing brace: + +```ts + /** When set, picking this family from the dropdown leaves thingsboard.io. + * `editions` still drives which pages belong to the family, so local docs + * for it keep resolving while they exist. */ + externalUrl?: string; +} +``` + +- [ ] **Step 3: Set it on the MQTT Broker family** + +The entry currently reads: + +```ts + { + id: Products.TBMQ, + group: 'ecosystem', + name: 'MQTT Broker', + tagline: 'Reliable messaging for massive fleets', + iconId: 'tbmq', + editions: [ + { product: Products.TBMQ, label: 'Community' }, + { product: Products.TBMQ_PE, label: 'Professional' }, + ], + preferredEdition: Products.TBMQ_PE, + }, +``` + +Add one line — change nothing else: + +```ts + { + id: Products.TBMQ, + group: 'ecosystem', + name: 'MQTT Broker', + tagline: 'Reliable messaging for massive fleets', + iconId: 'tbmq', + editions: [ + { product: Products.TBMQ, label: 'Community' }, + { product: Products.TBMQ_PE, label: 'Professional' }, + ], + preferredEdition: Products.TBMQ_PE, + externalUrl: TBMQ_URLS.docs, + }, +``` + +- [ ] **Step 4: Use it in the product popover** + +The popover maps families to `` elements. It currently opens: + +```astro + {FAMILIES.filter((f) => f.group === section.group).map((f) => ( + +``` + +Change the `href` and add the two external attributes: + +```astro + {FAMILIES.filter((f) => f.group === section.group).map((f) => ( + +``` + +- [ ] **Step 5: Add the outbound affordance** — **NOT IMPLEMENTED (deferred 2026-07-27)** + +> The user asked to leave the MQTT Broker row looking like every other row for +> now: same markup, no `↗` glyph, no `.ps-external` rule. The link still opens in +> a new tab — only the visual affordance is missing. Keep this step written out; +> it is the spec for adding the styling later if we decide the outbound jump +> needs to be signposted. Everything below in this step is unbuilt. + +Inside the same ``, the trailing check mark renders for the active family: + +```astro + {f.id === currentFamily.id && ( + + )} +``` + +Immediately **before** that block, add an outbound arrow for external families: + +```astro + {f.externalUrl && ( + + )} +``` + +Then add the style rule to the component's `