Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ac27acd
perf(iot-hub): remove dead astro:page-load listeners
rusikv Jul 1, 2026
985e63b
perf(3p): defer MailerLite embed to interaction; idle-schedule UTM li…
rusikv Jul 1, 2026
704974e
perf(assets): recompress blog release screenshot 843KB->147KB
rusikv Jul 1, 2026
eff01aa
perf(iot-hub): gate hero/gallery timers on visibility; idle FAQ hash-…
rusikv Jul 1, 2026
ffa359c
perf(3p): raise GTM idle-load fallback from 2.5s to 5s
rusikv Jul 1, 2026
8e2a4e6
perf(iot-hub): code-split install dialog behind a lazy boot module
rusikv Jul 2, 2026
42b264f
perf(3p): keep GitHub star-count fetch behind GTM's 5s fallback
rusikv Jul 2, 2026
0058d9d
perf(assets): recompress 19 oversized blog and marketing images
rusikv Jul 2, 2026
cdf1c79
perf(blog): inject image dimensions + lazy loading via rehype plugin
rusikv Jul 2, 2026
3a5a3a7
perf(nav): serve mega-menu icons from an svg sprite
rusikv Jul 2, 2026
f48271c
perf(docs): eager-load the first content image per page
rusikv Jul 2, 2026
19fe962
perf(assets): recompress blog index carousel cover
rusikv Jul 2, 2026
b3f4a6c
perf(pricing): draw FAQ icons via shared CSS masks
rusikv Jul 2, 2026
672a6d3
perf: remove remaining dead astro:page-load listeners
rusikv Jul 2, 2026
0a8d4c8
fix(3p): gate MailerLite form wiring on readyState, not DOMContentLoaded
rusikv Jul 2, 2026
e207098
fix(iot-hub): guard install dialog against double-open and failed import
rusikv Jul 2, 2026
aa4a437
fix(blog): tolerate malformed image URLs in the rehype plugin
rusikv Jul 2, 2026
473d967
refactor(nav): emit sprite manifest and regenerate sprite on every build
rusikv Jul 2, 2026
9a7b913
perf: document defer-tier schedule; bound idle work; harden masked icons
rusikv Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'],
Expand Down
97 changes: 97 additions & 0 deletions config/plugins/rehype-blog-images.ts
Original file line number Diff line number Diff line change
@@ -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 `<img>` 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<string, Dims | null>();

async function readDims(absPath: string): Promise<Dims | null> {
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<Root> = 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<string, unknown> }[] = [];
visit(tree, 'element', (node) => {
if (node.tagName !== 'img') return;
node.properties ??= {};
images.push(node as { properties: Record<string, unknown> });
});

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;
};
}
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/blog/thingsboard-3-9-0-release/debug.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/blog/thingsboard-3-9-0-release/image-7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/blog/thingsboard-3-9-0-release/image-8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/blog/thingsboard-3-9-0-release/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/blog/thingsboard-cli/cover.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/development-services/hero-1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/partners/mclimate-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading