Taken from: [Excalidraw Publish js and css files](https://excalidraw-obsidian.online/WIKI/03%20Advanced%20Usage/Publish%20js%20and%20css%20files) These files customize an Obsidian Publish site with enhanced SVG handling and a complete dark mode theme. See the [official Obsidian Publish docs](https://help.obsidian.md/publish/customize) for general customization guidance. > [!Note] Features > - **Dark mode support**: Automatic dark mode based on system preference, plus manual toggle support via `.theme-dark` class > - **Excalidraw SVG handling**: Interactive zoom, pan, and enlarge functionality for embedded SVG files > - **Clean embeds**: Hides markdown embed titles and removes borders for a seamless look > - **Custom fonts**: Loads Excalidraw's Virgil, Cascadia, and Assistant fonts for diagram rendering ## `publish.css` ```css @font-face { font-family: "Virgil"; src: url("https://excalidraw.com/Virgil.woff2"); } @font-face { font-family: "Cascadia"; src: url("https://excalidraw.com/Cascadia.woff2"); } @font-face { font-family: "Assistant"; src: url("https://excalidraw.com/Assistant-Regular.woff2"); } :root { --font-stack: "Assistant", "Cascadia", "Virgil", "Segoe UI", system-ui, sans-serif; --page-width: min(1024px, 90vw); --excalidraw-radius: 8px; --background-primary: #fff6f0; --background-secondary: #f5ece6; --background-tertiary: #ede0d8; --text-normal: #1a1a1a; --text-muted: #555555; --text-faint: #707070; --code-background: #f5f2f0; --code-border: #ddd; --inline-code-background: #f0ebe7; --inline-code-border: #ccc; --link-color: #2563eb; --link-color-hover: #1d4ed8; --hr-color: #d1c8c2; --blockquote-border: #c4b5ac; --table-border: #ddd; --table-header-bg: #f0ebe7; --table-row-alt-bg: #faf7f5; } body { font-family: var(--font-stack); margin: 0; background-color: var(--background-primary); color: var(--text-normal); } div.markdown-embed-title { display: none; } div.markdown-embed { border: none; padding: 0; background-color: inherit; } div.excalidraw-svg { height: 100%; } svg.excalidraw-svg { max-width: 100%; max-height: 90vh; width: var(--page-width); } svg.excalidraw-svg.ex-pageheight { width: initial; height: 100%; } svg.excalidraw-svg.ex-pagewidth { width: 90vw; height: initial; } .excalidraw-svg .text { width: 100%; text-align: center; } div.excalidraw-svg.enlarged { position: fixed; inset: 0; z-index: 10; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; flex-direction: column; } @media (prefers-color-scheme: dark) { body:not(.theme-light) { --background-primary: #1a1a1a; --background-secondary: #252525; --background-tertiary: #2e2e2e; --text-normal: #e0e0e0; --text-muted: #a0a0a0; --text-faint: #707070; --code-background: #2a2a2a; --code-border: #3a3a3a; --inline-code-background: #2d2d2d; --inline-code-border: #444444; --link-color: #7eb8f7; --link-color-hover: #a8d1ff; --hr-color: #3a3a3a; --blockquote-border: #555555; --table-border: #3a3a3a; --table-header-bg: #252525; --table-row-alt-bg: #222222; background-color: var(--background-primary); color: var(--text-normal); } } body.theme-dark { --background-primary: #1a1a1a; --background-secondary: #252525; --background-tertiary: #2e2e2e; --text-normal: #e0e0e0; --text-muted: #a0a0a0; --text-faint: #707070; --code-background: #2a2a2a; --code-border: #3a3a3a; --inline-code-background: #2d2d2d; --inline-code-border: #444444; --link-color: #7eb8f7; --link-color-hover: #a8d1ff; --hr-color: #3a3a3a; --blockquote-border: #555555; --table-border: #3a3a3a; --table-header-bg: #252525; --table-row-alt-bg: #222222; background-color: var(--background-primary); color: var(--text-normal); } :is(body.theme-dark, body:not(.theme-light)) pre, :is(body.theme-dark, body:not(.theme-light)) pre[class*="language-"] { background-color: var(--code-background) !important; border: 1px solid var(--code-border) !important; color: #d4d4d4 !important; } :is(body.theme-dark, body:not(.theme-light)) code:not(pre code) { background-color: var(--inline-code-background) !important; border: 1px solid var(--inline-code-border) !important; color: #c9d1d9 !important; } :is(body.theme-dark, body:not(.theme-light)) a { color: var(--link-color) !important; } :is(body.theme-dark, body:not(.theme-light)) a:hover { color: var(--link-color-hover) !important; } :is(body.theme-dark, body:not(.theme-light)) hr { border-color: var(--hr-color) !important; } :is(body.theme-dark, body:not(.theme-light)) blockquote { border-left-color: var(--blockquote-border) !important; color: var(--text-muted) !important; } :is(body.theme-dark, body:not(.theme-light)) table { border-color: var(--table-border) !important; } :is(body.theme-dark, body:not(.theme-light)) th { background-color: var(--table-header-bg) !important; border-color: var(--table-border) !important; } :is(body.theme-dark, body:not(.theme-light)) td { border-color: var(--table-border) !important; } :is(body.theme-dark, body:not(.theme-light)) tr:nth-child(even) { background-color: var(--table-row-alt-bg) !important; } :is(body.theme-dark, body:not(.theme-light)) .callout, :is(body.theme-dark, body:not(.theme-light)) .site-body-left-column { background-color: var(--background-secondary) !important; } :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg { background-color: var(--background-secondary) !important; border-radius: var(--excalidraw-radius); } :is(body.theme-dark, body:not(.theme-light)) div.excalidraw-svg.enlarged { background-color: var(--background-primary) !important; } :is(body.theme-dark, body:not(.theme-light)) input[type="search"], :is(body.theme-dark, body:not(.theme-light)) .search-input { background-color: var(--background-tertiary) !important; border-color: var(--code-border) !important; color: var(--text-normal) !important; } :is(body.theme-dark, body:not(.theme-light)) .token.string, :is(body.theme-dark, body:not(.theme-light)) .hljs-string { color: #ce9178 !important; } :is(body.theme-dark, body:not(.theme-light)) .token.keyword, :is(body.theme-dark, body:not(.theme-light)) .hljs-keyword { color: #569cd6 !important; } :is(body.theme-dark, body:not(.theme-light)) .token.comment, :is(body.theme-dark, body:not(.theme-light)) .hljs-comment { color: #6a9955 !important; } :is(body.theme-dark, body:not(.theme-light)) .token.number, :is(body.theme-dark, body:not(.theme-light)) .hljs-number { color: #b5cea8 !important; } :is(body.theme-dark, body:not(.theme-light)) .token.property, :is(body.theme-dark, body:not(.theme-light)) .hljs-attr { color: #9cdcfe !important; } :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg > rect[fill="#ffffff"], :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg > rect[fill="#fff"], :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg > rect[fill="white"] { fill: var(--background-secondary) !important; } :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg [stroke="#1e1e1e"], :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg [stroke="#000000"], :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg [stroke="#000"], :is(body.theme-dark, body:not(.theme-light)) svg.excalidraw-svg [stroke="black"] { stroke: #e0e0e0 !important; } a.site-body-left-column-site-name { display: none; } div.site-footer { display: none; } ``` ## `publish.js` This script enables interactive SVG viewing on your publish site. It converts embedded Excalidraw SVG images into interactive elements with zoom, pan, and fullscreen capabilities. > [!Tip] Controls > - **Enlarge**: Click and hold for 1 second to toggle fullscreen view > - **Zoom**: Hold `SHIFT` + scroll wheel > - **Pan**: Click and drag (when enlarged) > - **Reset**: Press `ESC` to collapse and reset view > [!Note] Additional features > - **Link conversion**: Transforms `obsidian://open?vault=` links into relative site URLs for proper navigation > - **iframe handling**: Hides site chrome when the page is embedded in an iframe > - **Device detection**: Adjusts behavior for desktop vs. mobile/tablet devices ```javascript const clickToEnlarge = "Click and hold to enlarge. SHIFT + wheel to zoom. ESC to reset."; const clickToCollapse = "ESC to reset. Click and hold to collapse. SHIFT + wheel to zoom"; //check if in iFrame - if yes the page is assumed to be an embedded frame if (window.self !== window.top) { const elements = [ "div.site-body-right-column", "div.site-body-left-column", "div.site-header", "div.site-footer", ]; elements.forEach((x) => { document.querySelectorAll(x).forEach((div) => { div.style.display = "none"; }); }); } const baseUrl = `${window.location.origin}/`; const [isDesktop, isMobile, isTablet] = (() => { const userAgent = navigator.userAgent; const mobileKeywords = [ "Mobile", "Android", "iPhone", "iPad", "Windows Phone", ]; const isMobile = mobileKeywords.some((keyword) => userAgent.includes(keyword), ); const isTablet = /iPad/i.test(userAgent) || (isMobile && !/Mobile/i.test(userAgent)); const isDesktop = !isMobile && !isTablet; return [isDesktop, isMobile, isTablet]; })(); const addNavigationToDiv = (container) => { const svgElement = container?.querySelector(".excalidraw-svg"); if (!svgElement) return; container.addClass("excalidraw-svg"); svgElement.removeAttribute("width"); svgElement.removeAttribute("height"); if (!isDesktop) return; const textDiv = document.createElement("div"); textDiv.className = "text"; textDiv.textContent = clickToEnlarge; container.appendChild(textDiv); let isEnlarged = false; let timeout = null; let isReadyToPan = false; let isPanning = false; let zoomLevel = 1; let panX = 0; let panY = 0; let pinchStartDistance = 0; let panStartX = 0; let panStartY = 0; const clearEnlargeTimeout = () => { if (timeout) clearTimeout(timeout); timeout = null; }; const enablePointerEvents = (val) => { svgElement.querySelectorAll("a").forEach((el) => { el.style.pointerEvents = val ? "all" : "none"; }); }; const applyTransform = () => { svgElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`; clearEnlargeTimeout(); }; //Wheel zoom svgElement.addEventListener("wheel", (event) => { if (!event.shiftKey) return; if (event.deltaY > 0) { zoomLevel -= zoomLevel > 4 ? zoomLevel > 6 ? zoomLevel > 10 ? 0.4 : 0.3 : 0.2 : 0.1; } else { zoomLevel += zoomLevel > 4 ? zoomLevel > 6 ? zoomLevel > 10 ? 0.4 : 0.3 : 0.2 : 0.1; } applyTransform(); }); // Panning svgElement.addEventListener("mousedown", (event) => { isReadyToPan = true; panStartX = event.clientX; panStartY = event.clientY; }); svgElement.addEventListener("mousemove", (event) => { const deltaX = event.clientX - panStartX; const deltaY = event.clientY - panStartY; const distance = Math.sqrt(deltaX ** 2 + deltaY ** 2); if (isReadyToPan && distance > 20) { if (!isPanning) { enablePointerEvents(false); isPanning = true; } panX += deltaX / zoomLevel; panY += deltaY / zoomLevel; panStartX = event.clientX; panStartY = event.clientY; applyTransform(); } }); svgElement.addEventListener("mouseup", () => { enablePointerEvents(true); isPanning = false; isReadyToPan = false; }); svgElement.addEventListener("mouseleave", () => { enablePointerEvents(true); isPanning = false; isReadyToPan = false; }); //abort on Escape document.addEventListener("keydown", (event) => { if (event.key === "Escape") { enablePointerEvents(true); isEnlarged = false; isPanning = false; isReadyToPan = false; container.classList.remove("enlarged"); textDiv.textContent = clickToEnlarge; zoomLevel = 1; panX = 0; panY = 0; applyTransform(); } }); //Enlarge on long click svgElement.addEventListener("mouseup", () => clearEnlargeTimeout()); svgElement.addEventListener("mousedown", () => { timeout = setTimeout(() => { timeout = null; if (isPanning) return; isReadyToPan = false; if (isEnlarged) { // Collapse the image container.classList.remove("enlarged"); textDiv.textContent = clickToEnlarge; } else { // Enlarge the image container.addClass("enlarged"); textDiv.textContent = clickToCollapse; } isEnlarged = !isEnlarged; }, 1000); }); applyTransform(); }; const processIMG = (img) => { const svgURL = img.src; const container = img.parentElement; fetch(svgURL) .then((response) => { if (response.ok) { return response.text(); } throw new Error("Failed to fetch SVG"); }) .then((svgContent) => { svgContainer = document.createElement("div"); svgContainer.innerHTML = svgContent; svgContainer .querySelectorAll(`a[href^="obsidian://open?vault="`) .forEach((el) => { el.setAttribute( "href", unescape( el .getAttribute("href") .replace(/.*&file=/, baseUrl) .replaceAll(" ", "+"), ), ); }); svgContainer .querySelectorAll(`iframe[src^="obsidian://open?vault="`) .forEach((el) => { el.setAttribute( "src", unescape( el .getAttribute("src") .replace(/.*&file=/, baseUrl) .replaceAll(" ", "+"), ), ); }); container.removeChild(img); container.appendChild(svgContainer); addNavigationToDiv(svgContainer); }) .catch((error) => { console.error("Error: " + error); }); }; const addImgMutationObserver = () => { const targetElement = document.body; const handleImgAddition = (mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if ( node instanceof Element && node.querySelector(`img[alt$=".svg"]`) ) { processIMG(node.querySelector(`img[alt$=".svg"]`)); } }); } } }; const observer = new MutationObserver(handleImgAddition); const config = { childList: true, subtree: true }; observer.observe(targetElement, config); }; //process images after loading document.body.querySelectorAll(`img[alt$=".svg"`).forEach((img) => { processIMG(img); }); addImgMutationObserver(); ```