diff --git a/astro.config.ts b/astro.config.ts
index c7a240ef31..a5ceebb6a4 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -6,6 +6,7 @@ import { redirects } from './astro.redirects';
import { sidebar } from './astro.sidebar';
import { devServerFileWatcher } from './config/integrations/dev-server-file-watcher';
import { sitemap } from './config/integrations/sitemap';
+import { rehypeBlogImages } from './config/plugins/rehype-blog-images';
import { rehypeMdxIncludeHeadings } from './config/plugins/rehype-mdx-include-headings';
import { rehypeTasklistEnhancer } from './config/plugins/rehype-tasklist-enhancer';
import { PROD_ORIGIN } from './src/consts';
@@ -150,7 +151,7 @@ export default defineConfig({
// @ts-expect-error — `remark-smartypants` type is not matching Astro's for some reason even though they both use unified's `Plugin` type
[remarkSmartypants, { dashes: false }],
],
- rehypePlugins: [rehypeSlug, rehypeTasklistEnhancer(), rehypeMdxIncludeHeadings()],
+ rehypePlugins: [rehypeSlug, rehypeTasklistEnhancer(), rehypeMdxIncludeHeadings(), rehypeBlogImages()],
},
image: {
domains: ['avatars.githubusercontent.com'],
diff --git a/config/plugins/rehype-blog-images.ts b/config/plugins/rehype-blog-images.ts
new file mode 100644
index 0000000000..cab9d80abe
--- /dev/null
+++ b/config/plugins/rehype-blog-images.ts
@@ -0,0 +1,97 @@
+import { join } from 'node:path';
+import type { Root } from 'hast';
+import sharp from 'sharp';
+import type { Plugin, Transformer } from 'unified';
+import { visit } from 'unist-util-visit';
+
+/**
+ * Rehype plugin for blog-post body images (scoped to `src/content/blog/`).
+ *
+ * Markdown images render as bare `
` tags with no dimensions and eager
+ * loading. This plugin:
+ *
+ * 1. Injects intrinsic `width`/`height` (read from the file in `public/`) so
+ * the browser reserves layout space before the image downloads.
+ * 2. Keeps the first body image eager (it is the usual LCP candidate) and
+ * lazy-loads every following image; all get `decoding="async"`.
+ *
+ * Images that already declare `loading`, `width`, or `height` are left as
+ * authored. External URLs and unresolvable files are skipped gracefully.
+ */
+
+interface Dims {
+ width: number;
+ height: number;
+}
+
+// Module-level cache: many posts reuse images, and metadata reads repeat
+// across dozens of posts on every build. `null` marks a failed read so broken
+// paths aren't retried per occurrence. The cache is never invalidated, so in
+// `pnpm dev` an edited image serves stale width/height until a server restart.
+const dimsCache = new Map();
+
+async function readDims(absPath: string): Promise {
+ const cached = dimsCache.get(absPath);
+ if (cached !== undefined) return cached;
+ let dims: Dims | null = null;
+ try {
+ const meta = await sharp(absPath).metadata();
+ if (meta.width && meta.height) dims = { width: meta.width, height: meta.height };
+ } catch {
+ // Missing or unreadable file — leave dimensions off, keep the attrs.
+ }
+ dimsCache.set(absPath, dims);
+ return dims;
+}
+
+export function rehypeBlogImages(): Plugin<[], Root> {
+ const transformer: Transformer = async (tree, file) => {
+ const sourcePath = (file.path ?? file.history?.[0] ?? '').replaceAll('\\', '/');
+ if (!sourcePath.includes('/src/content/blog/')) return;
+
+ // Collect in document order first; async work inside a visitor is unsafe.
+ const images: { properties: Record }[] = [];
+ visit(tree, 'element', (node) => {
+ if (node.tagName !== 'img') return;
+ node.properties ??= {};
+ images.push(node as { properties: Record });
+ });
+
+ let isFirst = true;
+ for (const img of images) {
+ const props = img.properties;
+ const first = isFirst;
+ isFirst = false;
+
+ // Respect explicitly authored loading behavior.
+ if (props.loading == null) {
+ props.loading = first ? 'eager' : 'lazy';
+ props.decoding ??= 'async';
+ }
+
+ const src = typeof props.src === 'string' ? props.src : '';
+ // Only local, root-relative assets ('/images/…', not '//host/…').
+ if (!src.startsWith('/') || src.startsWith('//')) continue;
+ if (props.width != null || props.height != null) continue;
+
+ // Malformed percent-encoding (a literal `%` in a filename) makes
+ // decodeURIComponent throw — degrade to skipping dimensions, like
+ // every other failure path here.
+ let cleanPath: string;
+ try {
+ cleanPath = decodeURIComponent(src.split(/[?#]/)[0] ?? '');
+ } catch {
+ continue;
+ }
+ const dims = await readDims(join(process.cwd(), 'public', cleanPath));
+ if (dims) {
+ props.width = dims.width;
+ props.height = dims.height;
+ }
+ }
+ };
+
+ return function attacher() {
+ return transformer;
+ };
+}
diff --git a/package.json b/package.json
index 2284abda92..fe35c4b6ab 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,9 @@
"scripts": {
"dev": "astro dev",
"start": "astro dev",
- "build": "NODE_OPTIONS=--max-old-space-size=8192 astro build",
- "build:fast": "SKIP_OG=true NODE_OPTIONS=--max-old-space-size=8192 astro build",
- "build:linkcheck": "SKIP_OG=true SKIP_LLMS=true SKIP_IMG=true NODE_OPTIONS=--max-old-space-size=8192 astro build",
+ "build": "node ./scripts/generate-nav-sprite.mjs && NODE_OPTIONS=--max-old-space-size=8192 astro build",
+ "build:fast": "node ./scripts/generate-nav-sprite.mjs && SKIP_OG=true NODE_OPTIONS=--max-old-space-size=8192 astro build",
+ "build:linkcheck": "node ./scripts/generate-nav-sprite.mjs && SKIP_OG=true SKIP_LLMS=true SKIP_IMG=true NODE_OPTIONS=--max-old-space-size=8192 astro build",
"preview": "astro preview",
"check": "astro check",
"format": "pnpm run format:code",
@@ -23,7 +23,8 @@
"lint:redirects": "node --experimental-transform-types ./scripts/check-redirect-chains.ts",
"netlify:build": "pnpm ${NETLIFY_BUILD_SCRIPT:-build}",
"lunaria:build": "node --experimental-transform-types ./scripts/lunaria.mts",
- "generate:redirects": "node --experimental-transform-types ./scripts/generate-redirects.ts"
+ "generate:redirects": "node --experimental-transform-types ./scripts/generate-redirects.ts",
+ "generate:nav-sprite": "node ./scripts/generate-nav-sprite.mjs"
},
"devDependencies": {
"@actions/core": "^1.11.1",
diff --git a/public/images/blog/1-million-reasons-to-choose-tbmq-as-high-performance-mqtt-broker/migration_from_jedis_to_lettuce.webp b/public/images/blog/1-million-reasons-to-choose-tbmq-as-high-performance-mqtt-broker/migration_from_jedis_to_lettuce.webp
index e06a321eb8..b0205eb694 100644
Binary files a/public/images/blog/1-million-reasons-to-choose-tbmq-as-high-performance-mqtt-broker/migration_from_jedis_to_lettuce.webp and b/public/images/blog/1-million-reasons-to-choose-tbmq-as-high-performance-mqtt-broker/migration_from_jedis_to_lettuce.webp differ
diff --git a/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-1.webp b/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-1.webp
index b618171a6a..4905f4480d 100644
Binary files a/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-1.webp and b/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-1.webp differ
diff --git a/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-2.webp b/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-2.webp
index 0ce0f8665f..3282218854 100644
Binary files a/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-2.webp and b/public/images/blog/breakthrough-agricultural-solutions-with-thingsboard/dashboard-img-2.webp differ
diff --git a/public/images/blog/from-architecture-to-results-why-working-with-the-platform-team-makes-a-difference/cover.webp b/public/images/blog/from-architecture-to-results-why-working-with-the-platform-team-makes-a-difference/cover.webp
index 61bed1d0c2..0e7af731bd 100644
Binary files a/public/images/blog/from-architecture-to-results-why-working-with-the-platform-team-makes-a-difference/cover.webp and b/public/images/blog/from-architecture-to-results-why-working-with-the-platform-team-makes-a-difference/cover.webp differ
diff --git a/public/images/blog/improve-your-business-with-mobile-app/cover.webp b/public/images/blog/improve-your-business-with-mobile-app/cover.webp
index 013888e59e..cec025aa5b 100644
Binary files a/public/images/blog/improve-your-business-with-mobile-app/cover.webp and b/public/images/blog/improve-your-business-with-mobile-app/cover.webp differ
diff --git a/public/images/blog/improve-your-business-with-mobile-app/desktop-to-mobile.webp b/public/images/blog/improve-your-business-with-mobile-app/desktop-to-mobile.webp
index f53c908487..84aa604166 100644
Binary files a/public/images/blog/improve-your-business-with-mobile-app/desktop-to-mobile.webp and b/public/images/blog/improve-your-business-with-mobile-app/desktop-to-mobile.webp differ
diff --git a/public/images/blog/new-thingsboard-tbmq-pricing-modular-add-ons-top-ups-and-total-cost-clarity/public_cloud-2.webp b/public/images/blog/new-thingsboard-tbmq-pricing-modular-add-ons-top-ups-and-total-cost-clarity/public_cloud-2.webp
index 62ff14fa23..92406c49e0 100644
Binary files a/public/images/blog/new-thingsboard-tbmq-pricing-modular-add-ons-top-ups-and-total-cost-clarity/public_cloud-2.webp and b/public/images/blog/new-thingsboard-tbmq-pricing-modular-add-ons-top-ups-and-total-cost-clarity/public_cloud-2.webp differ
diff --git a/public/images/blog/tbmq-2-1-new-chapter-in-mqtt-messaging-with-embedded-integrations/helm-tbmq2.webp b/public/images/blog/tbmq-2-1-new-chapter-in-mqtt-messaging-with-embedded-integrations/helm-tbmq2.webp
index 9e408970e3..9194057a2d 100644
Binary files a/public/images/blog/tbmq-2-1-new-chapter-in-mqtt-messaging-with-embedded-integrations/helm-tbmq2.webp and b/public/images/blog/tbmq-2-1-new-chapter-in-mqtt-messaging-with-embedded-integrations/helm-tbmq2.webp differ
diff --git a/public/images/blog/thingsboard-3-9-0-release/debug.webp b/public/images/blog/thingsboard-3-9-0-release/debug.webp
index be19447bd3..607f26c4d8 100644
Binary files a/public/images/blog/thingsboard-3-9-0-release/debug.webp and b/public/images/blog/thingsboard-3-9-0-release/debug.webp differ
diff --git a/public/images/blog/thingsboard-3-9-0-release/image-7.png b/public/images/blog/thingsboard-3-9-0-release/image-7.png
index bef2645827..d1e45f8cc5 100644
Binary files a/public/images/blog/thingsboard-3-9-0-release/image-7.png and b/public/images/blog/thingsboard-3-9-0-release/image-7.png differ
diff --git a/public/images/blog/thingsboard-3-9-0-release/image-8.png b/public/images/blog/thingsboard-3-9-0-release/image-8.png
index bef2645827..d1e45f8cc5 100644
Binary files a/public/images/blog/thingsboard-3-9-0-release/image-8.png and b/public/images/blog/thingsboard-3-9-0-release/image-8.png differ
diff --git a/public/images/blog/thingsboard-3-9-0-release/image.png b/public/images/blog/thingsboard-3-9-0-release/image.png
index bef2645827..d1e45f8cc5 100644
Binary files a/public/images/blog/thingsboard-3-9-0-release/image.png and b/public/images/blog/thingsboard-3-9-0-release/image.png differ
diff --git a/public/images/blog/thingsboard-4-0-release/converters-2-0.webp b/public/images/blog/thingsboard-4-0-release/converters-2-0.webp
index e9663ac7ce..1fb8d2db04 100644
Binary files a/public/images/blog/thingsboard-4-0-release/converters-2-0.webp and b/public/images/blog/thingsboard-4-0-release/converters-2-0.webp differ
diff --git a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_12-1.webp b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_12-1.webp
index 3daa74a2ba..2d7e58858b 100644
Binary files a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_12-1.webp and b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_12-1.webp differ
diff --git a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_2.webp b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_2.webp
index 02d4382336..3bb1c552c7 100644
Binary files a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_2.webp and b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/reporting_2.webp differ
diff --git a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/secrets.webp b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/secrets.webp
index fc37eb3136..4bdb4b71a8 100644
Binary files a/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/secrets.webp and b/public/images/blog/thingsboard-4-2-release-reporting-2-0-ai-integration-secrets-management-and-more/secrets.webp differ
diff --git a/public/images/blog/thingsboard-cli/cover.webp b/public/images/blog/thingsboard-cli/cover.webp
index 576db6dd2c..0bf0ecab93 100644
Binary files a/public/images/blog/thingsboard-cli/cover.webp and b/public/images/blog/thingsboard-cli/cover.webp differ
diff --git a/public/images/blog/thingsboard-edge-4-0-1-release/edge-cluster.webp b/public/images/blog/thingsboard-edge-4-0-1-release/edge-cluster.webp
index 8c07e56b7e..01e1863fa4 100644
Binary files a/public/images/blog/thingsboard-edge-4-0-1-release/edge-cluster.webp and b/public/images/blog/thingsboard-edge-4-0-1-release/edge-cluster.webp differ
diff --git a/public/images/blog/thingsboard-edge-4-0-1-release/edge-rule-chains.webp b/public/images/blog/thingsboard-edge-4-0-1-release/edge-rule-chains.webp
index b4452c35ec..8fa15a8a8a 100644
Binary files a/public/images/blog/thingsboard-edge-4-0-1-release/edge-rule-chains.webp and b/public/images/blog/thingsboard-edge-4-0-1-release/edge-rule-chains.webp differ
diff --git a/public/images/development-services/hero-1.webp b/public/images/development-services/hero-1.webp
index 49f53db334..a5e5d5a82d 100644
Binary files a/public/images/development-services/hero-1.webp and b/public/images/development-services/hero-1.webp differ
diff --git a/public/images/partners/mclimate-logo.png b/public/images/partners/mclimate-logo.png
index e45481a6e3..bde5f81fc3 100644
Binary files a/public/images/partners/mclimate-logo.png and b/public/images/partners/mclimate-logo.png differ
diff --git a/public/nav-sprite.svg b/public/nav-sprite.svg
new file mode 100644
index 0000000000..871a0e5e60
--- /dev/null
+++ b/public/nav-sprite.svg
@@ -0,0 +1,240 @@
+
+
diff --git a/scripts/generate-nav-sprite.mjs b/scripts/generate-nav-sprite.mjs
new file mode 100644
index 0000000000..6906b973d1
--- /dev/null
+++ b/scripts/generate-nav-sprite.mjs
@@ -0,0 +1,76 @@
+import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
+import { basename, join, resolve } from 'node:path';
+
+// Builds the navigation icon sprite + its manifest from
+// src/assets/images/landings/nav/*.svg:
+//
+// public/nav-sprite.svg — per icon
+// src/assets/images/landings/nav-sprite.manifest.json — name → { id, viewBox }
+//
+// The mega-menu references icons via