This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the ThingsBoard documentation site, built with Astro + Starlight. It's a multi-language documentation site with 14 supported languages.
# Install dependencies (requires pnpm)
pnpm install
# Development
pnpm dev # Start dev server
pnpm build # Production build
pnpm build:fast # Fast build (skips OG image generation) — use this for verification
**Build policy:** Before running any build, always ask the user: "Run `pnpm build:fast` to verify, or skip?"
pnpm preview # Preview production build
# Quality checks
pnpm check # TypeScript/Astro type checking
pnpm lint:eslint # ESLint
pnpm lint:linkcheck # Link validation (runs build first)
pnpm lint:linkcheck:nobuild # Link validation (skip build)
pnpm lint:slugcheck # Validate slugs match across languages
pnpm format # Format with PrettierAll documentation lives in src/content/docs/{lang}/ as .mdx files with YAML frontmatter. Content uses Astro's Content Collections with type-safe Zod schemas defined in src/content.config.ts.
Schema types determine frontmatter shape: base, deploy, backend, cms, media, integration, migration, tutorial, recipe. The type frontmatter field selects the schema.
Sidebar is configured in astro.sidebar.ts with 5 top-level tabs (Start, Guides, Reference, Integrations, Third-Party). Labels are translated via src/content/nav/{lang}.ts files.
- 14 languages configured in
config/locales.ts - English (
en) is the default/fallback language - Each language has its own directory under
src/content/docs/ i18nReady: truefrontmatter marks pages ready for translation- Translation status tracked by Lunaria (
lunaria.config.ts) - Arabic (
ar) uses RTL
~/* maps to ./src/* (configured in tsconfig.json).
Custom component overrides live in src/components/starlight/ — these replace default Starlight components (Hero, Sidebar, Footer, Search, etc.).
Landing page components are in src/components/Landing/ (Card, ListCard, SplitCard, Discord).
Use the edit-doc skill for full props, usage examples, and authoring rules for these components:
- ImageGallery — responsive image grid with lightbox, product suffix resolution, dark theme variants
- MultiProductImageGallery — auto product-suffix wrapper around ImageGallery
- DocImage — single optimized image with width/alignment options
- Banner — product/info banners (peFeature, ce, pe, cloud, trendz variants)
- Badge — tb-badge accent badge for sidebar and page titles
- YouTubeVideo — responsive 16:9 YouTube embed
- ConditionalHeading — TOC-aware heading for use inside JSX conditionals in
_includes - InstallationCardGrid — installation option card grid
- RuleNodeCardGrid — rule node category card grid
- DocLink — product-aware internal links (always use instead of bare markdown links)
- Code blocks —
maxLines,collapsible,wrap,download='file.ext'meta options;<Code>component for dynamic code
All product identifiers live in src/models/site.models.ts as the Products enum:
| Enum value | URL prefix | Notes |
|---|---|---|
CE |
(empty) | Default/root product |
PE |
pe/ |
|
PAAS |
paas/ |
Sub-variant: PAAS_EU (paas/eu/) |
EDGE |
edge/ |
Sub-variant: EDGE_PE (edge/pe/) |
TRENDZ |
trendz/ |
|
GW |
iot-gateway/ |
|
TBMQ |
mqtt-broker/ |
Sub-variant: TBMQ_PE (mqtt-broker/pe/) |
MOBILE |
mobile/ |
Sub-variant: MOBILE_PE (mobile/pe/) |
LICENSE |
license-server/ |
URL pattern: /[lang/]docs/[product-prefix][page-slug]/
Content directories mirror the product prefixes under src/content/docs/docs/:
src/content/docs/docs/
├── user-guide/ ← CE pages
├── pe/user-guide/ ← PE pages
├── paas/ ← Cloud pages
├── edge/ ← Edge pages
├── trendz/ ← Trendz pages
├── iot-gateway/ ← IoT Gateway pages
├── mqtt-broker/ ← TBMQ pages
├── mobile/ ← Mobile pages
└── license-server/ ← License Server pages
Documentation pages are thin wrappers that import a shared include file and pass the current product as a prop. This avoids duplicating content across products.
src/content/_includes/docs/{path}/{page}.mdx ← actual content (shared)
src/content/docs/docs/{path}/{page}.mdx ← CE stub (passes Products.CE)
src/content/docs/docs/pe/{path}/{page}.mdx ← PE stub (passes Products.PE)
See the edit-doc skill for detailed _includes rules, conditional rendering patterns, and common pitfalls.
src/data/versions.ts — centralized product version strings. Never hardcode version strings in Docker image tags, download URLs, or code blocks. Import from ~/data/versions.
Available: CE_FULL_VER, PE_FULL_VER, TRENDZ_VER, EDGE_VER, EDGE_PE_VER, TBMQ_VER, TBMQ_PE_VER.
config/plugins/remark-fallback-lang.ts— marks untranslated contentconfig/plugins/rehype-tasklist-enhancer.ts— enhanced task listsconfig/plugins/rehype-mdx-include-headings.ts— extracts headings from_includesMDX files and injects them into the page TOC; supports<ConditionalHeading>for product-conditional headingsconfig/plugins/llms-txt.ts— generates llms.txtconfig/plugins/smoke-test.ts— build validation
src/content/docs/— documentation pages rendered by Starlightsrc/pages/— special routes: root redirect, language redirects, 404, OG image generation, use-cases, case-studiessrc/pages/[lang]/— dynamic per-language routes (index, install, tutorial redirects)
All non-doc pages (landing, use-cases, case-studies, standalone pages) share a unified typography system defined as SCSS mixins in src/styles/_variables.scss. Use the typography skill for the full reference: semantic mixins (page-title, section-title, text-m, etc.), spacing scale, breakpoints, and dark-theme color conventions.
Key rule: Never hardcode font values — use mixins. Never use compile-time SCSS color variables for theme-dependent colors — use CSS custom properties (var(--color-*)).
Data-driven pages at /use-cases/{slug}. Use the use-case-pages skill for data types, page composition, layout, and section components.
Key dirs: src/data/use-cases/, src/components/UseCase/, src/pages/use-cases/.
Data-driven pages at /case-studies/{slug}. Use the case-study-pages skill for data types, page composition, layout, and section components.
Key dirs: src/data/case-studies/, src/components/CaseStudy/, src/pages/case-studies/.
Data-driven page at /clients-feedback/. Key dirs: src/data/clients-feedback/, src/components/Feedback/, src/pages/clients-feedback/.
Catalog of supported hardware + integration guides at /device-library/ (index) and /device-library/{slug}/ (detail pages). Flat slug, not product-prefixed — one URL per device regardless of platform. Platform context rides on a ?platform= query parameter that the detail page reads client-side to activate the right CE/PE branch and swap hostname sentinels (YOUR_TB_HOST → actual platform host) in code snippets.
Content lives in the devices collection, not docs:
src/content/devices/en/{slug}.mdx ← device guides (EN-only; no i18n for this collection)
src/content.config.ts ← `deviceSchema` + `PLATFORM_VALUES` allow-list
src/util/device-platform.ts ← platform metadata, sentinel map, query → variant mapping
src/util/device-images.ts ← asset-pipeline resolver for catalog thumbnails + hero images
Presentation uses StarlightPage with template: 'doc' so guide bodies get the same Starlight typography as real docs pages, wrapped in the chrome components under src/components/DeviceLibrary/:
DeviceLibrary.astro ← index: search + filter sidebar + paginated grid (client-side)
DeviceCard.astro ← catalog thumbnail card
DeviceInfoCard.astro ← detail page hero (product image + spec sheet + CTA)
PlatformContent.astro ← wraps `<PlatformContent variant="ce|pe">…</PlatformContent>` blocks
PlatformToggle.astro ← CE/PE segmented control (role="group" + aria-pressed)
DeviceCTAFooter.astro ← detail page CTA footer
FilterSidebar.astro ← search + filter sidebar (reused)
Chrome components all carry .not-content so Starlight's markdown flow/typography rules don't apply to them, while the MDX body inside <Content /> does get full Starlight styling.
Assets:
src/assets/devices/{filename}— catalog thumbnails (referenced bydeviceImageFileName:frontmatter)src/assets/devices-library/**— body content images (screenshots, diagrams, galleries)- Both go through Astro's asset pipeline (content-hashed URLs, WebP re-encoding, intrinsic
width/height)
Redirects: scripts/device-library-redirects.json maps every legacy URL shape (/docs/devices-library/{slug}/, /docs/pe/devices-library/{slug}/, /device-library/{platform}/{slug}/, and the guides/ variants) to /device-library/{slug}/?platform={platform}. Imported at build time by astro.redirects.ts. Covers 983/983 inbound URLs from the legacy site.
Single source of truth: src/data/redirects.ts. Four exports, chosen by pattern shape:
| Export | Use for | Example |
|---|---|---|
SINGLE_REDIRECTS |
one-off /docs/* page rename |
{ oldPath: 'pe/user-guide/roadmap', target: '/docs/pe/releases/roadmap/' } |
CATCH_ALL_REDIRECTS |
/docs/* prefix rename (whole tree renamed 1:1) |
{ oldPrefix: 'pe/edge', entries: [] } → /docs/pe/edge/* → /docs/edge/pe/:splat |
DYNAMIC_REDIRECTS |
splat / :placeholder patterns that aren't a simple prefix rename |
/blog/category/:category/page/* → /blog/?category=:category |
NON_DOCS_REDIRECTS |
everything outside /docs/* (marketing, /products/*, /industries/*, external targets) |
/iot-use-cases/ → /use-cases/ |
Workflow to add a redirect:
- Edit
src/data/redirects.ts(pick the export that matches the pattern). - For new
CATCH_ALL_REDIRECTSprefixes with empty entries, populate thenewPrefixfield on the same entry — the generator reads it directly. - Run
pnpm generate:redirects— regeneratespublic/_redirectsandpublic/redirects.json. - Commit both the data change and the regenerated output.
Two places, two purposes:
public/_redirects— served by Cloudflare Pages. Gives real 301s at the edge. Cloudflare rule: "Redirects are always followed, regardless of whether or not an asset matches the incoming request." (docs) — so a matching rule here always wins, even if a static HTML file exists at the same path.astro.redirects.ts→redirects:— used by Astro inpnpm dev/pnpm previewso old URLs resolve locally instead of 404-ing. In static build mode these emit a200 + <meta refresh>HTML stub, which Cloudflare's edge rule then supersedes in production. The file spreadspublic/redirects.json(all/docs/*) +device-library-redirects.json+NON_DOCS_REDIRECTS, so a single run ofpnpm generate:redirectskeeps dev and prod in sync.
Why page-based .astro redirect stubs are deprecated: they only emit meta-refresh pages (no real 301), they pollute the sitemap, and they duplicate rules already present in _redirects. The generator in src/data/redirects.ts → public/_redirects covers them all.
Hard rules:
- Do NOT create new
.astrostub files undersrc/pages/docs/that only callAstro.redirect(). Put the entry insrc/data/redirects.tsinstead. - Do NOT hand-edit
public/_redirectsorpublic/redirects.jsonbelow the auto-generated markers — they're rewritten bypnpm generate:redirects. Editsrc/data/redirects.tsand regenerate. - Keep dynamic rules (splat /
:placeholder) under 100. Cloudflare Pages limit is 2,000 static + 100 dynamic = 2,100 total; the generator already quarantines dynamic rules to the tail block to keep the static zone uncapped.
Per-page OG cards (1200×630 PNG) are generated at build time by Satori + Resvg. Each content collection has its own static endpoint under src/pages/open-graph/. One JSX template (_shared/Card.tsx) is varied only by an "eyebrow" line and an optional bottom-left meta line.
Files:
src/pages/open-graph/_shared/Card.tsx— templatesrc/pages/open-graph/_shared/render.ts— Satori → Resvg pipeline + content-hash cachesrc/pages/open-graph/_shared/page-data.ts— collection enumeratorssrc/pages/open-graph/_shared/jsx-runtime.ts— minimal Satori-shaped JSX shim (no React)src/pages/open-graph/{collection}/[…].png.ts— six static endpoints (docs, blog, case-studies, use-cases, device-library, pages)src/util/ogContext.ts— eyebrow / label helpers +MARKETING_ALLOWLISTsrc/util/getOgImageUrl.ts— pathname → OG PNG URL aggregator
Key facts:
- Cache lives at
node_modules/.og-cache/(gitignored). BumpTEMPLATE_VERSIONinrender.tsto invalidate. SKIP_OG=true(used bypnpm build:fast) makesrenderCardreturn the global fallback instead of running Satori — endpoints still register paths.- Pages outside
MARKETING_ALLOWLIST(or otherwise unmapped) fall back to/thingsboard-og.pngviaSeoMeta.astro. - Roboto 400/500/700 (7 subsets each: latin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext, vietnamese) + Noto Sans Symbols 400 for arrows. CJK / Arabic / Hebrew not covered — render as
.notdef. Site CSS uses an unrelated system font stack; noFONT_CREDENTIALSenv var. - Astro dev quirk:
trailingSlash: 'always'makes dev-server 404 dynamic-route URLs that end in.png.SeoMeta.astroandrouteData.tsappend/toog:imageonly whenimport.meta.env.DEVso dev links resolve tolocalhost:.../foo.png/while production HTML keeps the clean.pngURL Cloudflare Pages serves directly.
To add a new marketing landing to OG generation: add its pathname to MARKETING_ALLOWLIST in src/util/ogContext.ts and rebuild.
Use the release skill for the full checklist. Key files:
src/data/versions.ts— version constantssrc/models/upgrade-instructions.ts—UPGRADE_VERSIONSarray (newest first)src/models/releases-table.ts—RELEASE_FAMILIESarray (newest first)
- Tabs for indentation in code files; spaces for JSON, Markdown, MDX, YAML, TOML
- Prettier with
prettier-plugin-astro, printWidth 100, single quotes, trailing commas - ESLint flat config with TypeScript and Astro plugins
PRs run: astro check, eslint, slugcheck. All must pass.