Stylowanie Cusdis: Jak przejąć kontrolę nad iframe bez modyfikowania źródła
Spis treści
Wstęp#
Ostatnio przyglądałem się implementacji systemu komentarzy opartego na Cusdis. To lekkie i privacy-friendly rozwiązanie, ale ma jedną cechę, która z perspektywy UI jest wyzwaniem: widget renderuje się wewnątrz iframe.
Co to dla nas oznacza? Iframe tworzy odizolowane środowisko. Style CSS, które mamy w naszym projekcie, nie “przeciekają” do środka. W efekcie, jeśli nasz layout ma specyficzny branding, domyślny, minimalistyczny wygląd Cusdis będzie wyglądał jak ciało obce.
Przeanalizowałem ten temat i znalazłem sposób, by to obejść, zachowując czystość kodu.
Wstrzykiwanie stylów#
Zauważyłem, że Cusdis renderuje ramkę w specyficzny sposób. Zamiast używać atrybutu src wskazującego na zewnętrzny adres URL, korzysta z srcdoc. Dzięki temu kod HTML widgetu jest osadzony bezpośrednio w atrybucie jako ciąg znaków.
To kluczowa różnica. Dzięki temu iframe dziedziczy Origin (pochodzenie) naszej strony głównej, zamiast tworzyć własny. W praktyce oznacza to, że nie blokuje nas polityka CORS. Możemy dostać się do obiektu contentDocument wewnątrz ramki i wstrzyknąć tam własne reguły.
Poniżej mechanizm, który opiera się na nasłuchiwaniu, kiedy ramka będzie gotowa.
const iframe = document.querySelector('#cusdis_thread iframe');
iframe.addEventListener('load', () => {
// Ustawiam opóźnienie, aby dać czas na załadowanie zewnętrznych zasobów Cusdis
setTimeout(() => {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.head) return;
// Zapobiegam duplikowaniu stylów
if (doc.getElementById('my-custom-styles')) return;
const style = doc.createElement('style');
style.id = 'my-custom-styles';
style.textContent = `
/* Nadpisujemy style z użyciem !important, by wygrać priorytetem */
body {
font-family: inherit !important;
background: #1a1a1a !important;
}
`;
doc.head.appendChild(style);
} catch (e) {
// Złapanie ewentualnych błędów dostępu
console.error('Nie udało się wstrzyknąć stylów:', e);
}
}, 100);
});
Po co setTimeout i zdarzenie load? Cusdis ładuje swoje własne skrypty i style asynchronicznie. Jeśli wstrzykniemy nasz CSS zbyt wcześnie, zostanie on nadpisany przez oryginalne style widgetu, które załadują się ułamek sekundy później.
Wstrzykując style po zdarzeniu load i dodając je na samym końcu sekcji <head>, mamy pewność, że nasze reguły wygrają w kaskadzie stylów.
Obsługa dynamicznego ładowania (SPA)#
W nowoczesnych aplikacjach, zwłaszcza typu SPA (Single Page Application), iframe może nie istnieć w momencie ładowania strony. Może pojawić się później, np. po przejściu na podstronę artykułu.
MutationObserver pozwala nam nasłuchiwać zmian w drzewie DOM i reagować dokładnie w momencie, gdy widget zostanie dołączony do struktury strony.
const container = document.querySelector('#cusdis_thread');
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'IFRAME') {
// Mamy go! Podpinamy nasz listener
node.addEventListener('load', () => injectStyles(node));
}
}
}
});
observer.observe(container, { childList: true, subtree: true });
Synchronizacja z motywem strony (Dark Mode)#
Osobiście nie lubię sztywnych wartości (hardcoding). Jeśli zmienimy kolor tła w głównym motywie, nie chcemy pamiętać o ręcznej aktualizacji skryptu komentarzy.
Rozwiązaniem jest pobranie wartości bezpośrednio z CSS Custom Properties (zmiennych CSS) naszej strony i przekazanie ich do iframe’a.
function getThemeColors() {
const root = getComputedStyle(document.documentElement);
return {
background: root.getPropertyValue('--background').trim(),
foreground: root.getPropertyValue('--foreground').trim(),
accent: root.getPropertyValue('--accent').trim()
};
}
function generateCSS(colors) {
return `
body {
background: ${colors.background} !important;
color: ${colors.foreground} !important;
}
a, button {
color: ${colors.accent} !important;
}
`;
}
Dzięki temu widget automatycznie dostosuje się do zmian w Waszym Design Systemie.
Problem z wysokością (Scrollbary)#
Kolejna rzecz, którą zauważyłem podczas testów, to problemy z wysokością. Teoretycznie Cusdis używa postMessage do informowania strony-matki o swojej wysokości, ale w praktyce działa to różnie. Często pojawiał się nieestetyczny, wewnętrzny pasek przewijania.
Zdecydowałem się na przejęcie kontroli nad tym procesem. Musimy sami obliczyć wysokość zawartości iframe’a i wymusić ją na elemencie.
function updateHeight(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.body) return;
// Pobieram maksimum z trzech różnych metryk dla pewności cross-browser
const height = Math.max(
doc.body.scrollHeight,
doc.documentElement.scrollHeight,
doc.body.offsetHeight
);
iframe.style.height = height + 'px';
} catch (e) {
// Fallback w razie błędu
}
}
Obserwowanie zmian w czasie rzeczywistym#
Komentarze to żywy organizm – dochodzą nowe wpisy, użytkownicy klikają “odpowiedz”. Wysokość obliczona raz, na początku, szybko stanie się nieaktualna.
Tutaj z pomocą przychodzi nam ResizeObserver oraz ponownie MutationObserver, zapięte na wnętrze ramki.
function setupHeightObserver(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.body) return;
let timer;
const update = () => {
// Prosty debounce, żeby nie zarżnąć wydajności przy szybkich zmianach
clearTimeout(timer);
timer = setTimeout(() => updateHeight(iframe), 100);
};
// Nasłuchuj zmian rozmiaru okna i zmian w strukturze DOM (nowe komentarze)
new ResizeObserver(update).observe(doc.body);
new MutationObserver(update).observe(doc.body, {
childList: true,
subtree: true,
attributes: true
});
} catch (e) {}
}
Gotowe rozwiązanie: Kod produkcyjny#
Zebrałem te wszystkie elementy w jeden, samowystarczalny moduł. Możecie go wkleić do swojego projektu. Skrypt sam wykryje, kiedy i czy widget się załadował, a następnie zaaplikuje style i naprawi wysokość.
(function() {
const CONTAINER = '#cusdis_thread';
function injectStyles(iframe) {
iframe.addEventListener('load', () => {
setTimeout(() => {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.head || doc.getElementById('cusdis-custom')) return;
const style = doc.createElement('style');
style.id = 'cusdis-custom';
style.textContent = `
/* Tutaj wklej swoje style lub logikę pobierania zmiennych */
body { background: #1a1a1a !important; }
`;
doc.head.appendChild(style);
// Od razu uruchamiamy obserwatora wysokości
setupHeightObserver(iframe);
} catch (e) {}
}, 100);
});
}
function updateHeight(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.body) return;
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
iframe.style.height = h + 'px';
} catch (e) {}
}
function setupHeightObserver(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (!doc?.body) return;
let timer;
const update = () => {
clearTimeout(timer);
timer = setTimeout(() => updateHeight(iframe), 100);
};
new ResizeObserver(update).observe(doc.body);
new MutationObserver(update).observe(doc.body, {
childList: true, subtree: true, attributes: true
});
} catch (e) {}
}
function init() {
const container = document.querySelector(CONTAINER);
if (!container) return;
// Scenariusz 1: Iframe pojawi się dynamicznie
new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.tagName === 'IFRAME') injectStyles(node);
}
}
}).observe(container, { childList: true, subtree: true });
// Scenariusz 2: Iframe już tam jest
const iframe = container.querySelector('iframe');
if (iframe) injectStyles(iframe);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Wnioski#
Cusdis to świetne narzędzie, ale jego stylowanie wymaga nieco “gimnastyki” z JavaScriptem. Metoda srcdoc daje nam jednak furtkę, którą warto wykorzystać.
Moim zdaniem podejście z wstrzykiwaniem CSS przez JS (JavaScript Injection) daje najlepszy balans. Z jednej strony mamy pełną kontrolę wizualną i synchronizację z motywem strony. Z drugiej — nie musimy hostować własnej instancji Cusdis ani modyfikować kodu źródłowego widgetu, co znacznie ułatwia utrzymanie projektu w przyszłości.