diff --git a/dist/exports/rsc.d.ts b/dist/exports/rsc.d.ts
index 6aa81d0..ca8795f 100644
--- a/dist/exports/rsc.d.ts
+++ b/dist/exports/rsc.d.ts
@@ -7,3 +7,4 @@
*/
export { RenderBlocks } from '../modules/blocks/index.js';
export type { BlockComponentMap, BlockData, EnhanceProps, RenderBlocksProps, } from '../modules/blocks/index.js';
+export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js';
diff --git a/dist/exports/rsc.js b/dist/exports/rsc.js
index fbe1661..7be092a 100644
--- a/dist/exports/rsc.js
+++ b/dist/exports/rsc.js
@@ -5,5 +5,6 @@
* lives here rather than in the main package entry to keep React out of the
* server-config bundle.
*/ export { RenderBlocks } from '../modules/blocks/index.js';
+export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js';
//# sourceMappingURL=rsc.js.map
\ No newline at end of file
diff --git a/dist/exports/rsc.js.map b/dist/exports/rsc.js.map
index 6f4f24d..0665d19 100644
--- a/dist/exports/rsc.js.map
+++ b/dist/exports/rsc.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["/**\n * Entry point: ipal-kit/rsc\n *\n * Server-component exports. RenderBlocks is a React Server Component, so it\n * lives here rather than in the main package entry to keep React out of the\n * server-config bundle.\n */\nexport { RenderBlocks } from '../modules/blocks/index.js'\nexport type {\n BlockComponentMap,\n BlockData,\n EnhanceProps,\n RenderBlocksProps,\n} from '../modules/blocks/index.js'\n"],"names":["RenderBlocks"],"mappings":"AAAA;;;;;;CAMC,GACD,SAASA,YAAY,QAAQ,6BAA4B"}
\ No newline at end of file
+{"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["/**\n * Entry point: ipal-kit/rsc\n *\n * Server-component exports. RenderBlocks is a React Server Component, so it\n * lives here rather than in the main package entry to keep React out of the\n * server-config bundle.\n */\nexport { RenderBlocks } from '../modules/blocks/index.js'\nexport type {\n BlockComponentMap,\n BlockData,\n EnhanceProps,\n RenderBlocksProps,\n} from '../modules/blocks/index.js'\nexport { MediaPreconnect } from '../modules/storage/MediaPreconnect.js'\n"],"names":["RenderBlocks","MediaPreconnect"],"mappings":"AAAA;;;;;;CAMC,GACD,SAASA,YAAY,QAAQ,6BAA4B;AAOzD,SAASC,eAAe,QAAQ,wCAAuC"}
\ No newline at end of file
diff --git a/dist/modules/storage/MediaPreconnect.d.ts b/dist/modules/storage/MediaPreconnect.d.ts
new file mode 100644
index 0000000..497e502
--- /dev/null
+++ b/dist/modules/storage/MediaPreconnect.d.ts
@@ -0,0 +1,19 @@
+/**
+ * Emits + for the media CDN
+ * domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
+ * media host early — before it hits the first . Saves ~150–300ms on the
+ * first image load.
+ *
+ * Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
+ * so there's ONE source of truth — no per-project hardcoded domain. Renders
+ * nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
+ * preconnect).
+ *
+ * Server Component — drop it in the
of your locale layout:
+ *
+ * import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
+ * // in (or top of — Next hoists link tags):
+ *
+ */
+export declare function MediaPreconnect(): import("react/jsx-runtime").JSX.Element | null;
+export default MediaPreconnect;
diff --git a/dist/modules/storage/MediaPreconnect.js b/dist/modules/storage/MediaPreconnect.js
new file mode 100644
index 0000000..f85c61e
--- /dev/null
+++ b/dist/modules/storage/MediaPreconnect.js
@@ -0,0 +1,47 @@
+import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
+/**
+ * Emits + for the media CDN
+ * domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
+ * media host early — before it hits the first . Saves ~150–300ms on the
+ * first image load.
+ *
+ * Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
+ * so there's ONE source of truth — no per-project hardcoded domain. Renders
+ * nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
+ * preconnect).
+ *
+ * Server Component — drop it in the of your locale layout:
+ *
+ * import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
+ * // in (or top of — Next hoists link tags):
+ *
+ */ export function MediaPreconnect() {
+ const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '');
+ if (!publicUrl) {
+ return null;
+ }
+ // Origin only (scheme + host) — preconnect targets an origin, not a path.
+ let origin;
+ try {
+ origin = new URL(publicUrl).origin;
+ } catch {
+ return null // malformed URL → skip rather than emit a broken tag
+ ;
+ }
+ return /*#__PURE__*/ _jsxs(_Fragment, {
+ children: [
+ /*#__PURE__*/ _jsx("link", {
+ crossOrigin: "anonymous",
+ href: origin,
+ rel: "preconnect"
+ }),
+ /*#__PURE__*/ _jsx("link", {
+ href: origin,
+ rel: "dns-prefetch"
+ })
+ ]
+ });
+}
+export default MediaPreconnect;
+
+//# sourceMappingURL=MediaPreconnect.js.map
\ No newline at end of file
diff --git a/dist/modules/storage/MediaPreconnect.js.map b/dist/modules/storage/MediaPreconnect.js.map
new file mode 100644
index 0000000..6c89a80
--- /dev/null
+++ b/dist/modules/storage/MediaPreconnect.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/modules/storage/MediaPreconnect.tsx"],"sourcesContent":["/**\n * Emits + for the media CDN\n * domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the\n * media host early — before it hits the first . Saves ~150–300ms on the\n * first image load.\n *\n * Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),\n * so there's ONE source of truth — no per-project hardcoded domain. Renders\n * nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to\n * preconnect).\n *\n * Server Component — drop it in the of your locale layout:\n *\n * import { MediaPreconnect } from '@intecion/ipal-kit/rsc'\n * // in (or top of — Next hoists link tags):\n * \n */\nexport function MediaPreconnect() {\n const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\\/$/, '')\n if (!publicUrl) {return null}\n\n // Origin only (scheme + host) — preconnect targets an origin, not a path.\n let origin: string\n try {\n origin = new URL(publicUrl).origin\n } catch {\n return null // malformed URL → skip rather than emit a broken tag\n }\n\n return (\n <>\n \n \n >\n )\n}\n\nexport default MediaPreconnect\n"],"names":["MediaPreconnect","publicUrl","process","env","R2_PUBLIC_URL","replace","origin","URL","link","crossOrigin","href","rel"],"mappings":";AAAA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASA;IACd,MAAMC,YAAYC,QAAQC,GAAG,CAACC,aAAa,EAAEC,QAAQ,OAAO;IAC5D,IAAI,CAACJ,WAAW;QAAC,OAAO;IAAI;IAE5B,0EAA0E;IAC1E,IAAIK;IACJ,IAAI;QACFA,SAAS,IAAIC,IAAIN,WAAWK,MAAM;IACpC,EAAE,OAAM;QACN,OAAO,KAAK,qDAAqD;;IACnE;IAEA,qBACE;;0BACE,KAACE;gBAAKC,aAAY;gBAAYC,MAAMJ;gBAAQK,KAAI;;0BAChD,KAACH;gBAAKE,MAAMJ;gBAAQK,KAAI;;;;AAG9B;AAEA,eAAeX,gBAAe"}
\ No newline at end of file
diff --git a/docs/storage.md b/docs/storage.md
index 6333a28..7856aa0 100644
--- a/docs/storage.md
+++ b/docs/storage.md
@@ -183,4 +183,29 @@ czysta nazwa trafia i do bazy, i do bucketa). Dlaczego to ważne:
ścieżki i cache.
Sama funkcja `normalizeFilename(name)` też jest wyeksportowana, gdybyś potrzebował
-jej poza hookiem.
\ No newline at end of file
+jej poza hookiem.
+
+## Preconnect do domeny mediów (wydajność)
+
+Komponent `MediaPreconnect` generuje `` + `dns-prefetch`
+dla domeny mediów (R2_PUBLIC_URL) — przeglądarka nawiązuje połączenie TLS/DNS
+z CDN zawczasu, zanim napotka pierwszy ``. Zysk ~150–300 ms na pierwszym
+obrazie.
+
+```tsx
+// layout.tsx — w (albo górze , Next hoistuje link tagi)
+import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
+
+
+
+
+```
+
+Czyta domenę z **R2_PUBLIC_URL** (to samo źródło co buildR2Storage) — zero
+hardkodu, jedno źródło prawdy. Gdy R2_PUBLIC_URL nie ustawione (lokalny dysk,
+brak CDN) → nie renderuje nic. Zmiana domeny mediów = zmiana jednej zmiennej
+env, komponent podąża automatycznie.
+
+> NIE hardkoduj `` ręcznie
+> w layoutcie — to zaszywa domenę klienta w kodzie. Użyj MediaPreconnect, który
+> bierze ją z env.
\ No newline at end of file
diff --git a/src/exports/rsc.ts b/src/exports/rsc.ts
index cdd4fb9..eab8730 100644
--- a/src/exports/rsc.ts
+++ b/src/exports/rsc.ts
@@ -12,3 +12,4 @@ export type {
EnhanceProps,
RenderBlocksProps,
} from '../modules/blocks/index.js'
+export { MediaPreconnect } from '../modules/storage/MediaPreconnect.js'
diff --git a/src/modules/storage/MediaPreconnect.tsx b/src/modules/storage/MediaPreconnect.tsx
new file mode 100644
index 0000000..2bc987b
--- /dev/null
+++ b/src/modules/storage/MediaPreconnect.tsx
@@ -0,0 +1,38 @@
+/**
+ * Emits + for the media CDN
+ * domain (R2_PUBLIC_URL), so the browser opens the TLS/DNS connection to the
+ * media host early — before it hits the first . Saves ~150–300ms on the
+ * first image load.
+ *
+ * Reads the domain from R2_PUBLIC_URL (the same env var buildR2Storage uses),
+ * so there's ONE source of truth — no per-project hardcoded domain. Renders
+ * nothing when R2_PUBLIC_URL isn't set (local disk / no CDN → nothing to
+ * preconnect).
+ *
+ * Server Component — drop it in the of your locale layout:
+ *
+ * import { MediaPreconnect } from '@intecion/ipal-kit/rsc'
+ * // in (or top of — Next hoists link tags):
+ *
+ */
+export function MediaPreconnect() {
+ const publicUrl = process.env.R2_PUBLIC_URL?.replace(/\/$/, '')
+ if (!publicUrl) {return null}
+
+ // Origin only (scheme + host) — preconnect targets an origin, not a path.
+ let origin: string
+ try {
+ origin = new URL(publicUrl).origin
+ } catch {
+ return null // malformed URL → skip rather than emit a broken tag
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default MediaPreconnect