Skip to main content

art10m's BookStack Hacks

<head>

<!-- ============================================================================
     BOOKSTACK CUSTOM HEAD CONFIGURATION

     This file contains custom styles, scripts, and integrations for BookStack:
     - Typography and layout adjustments (zoom, colors, spacing)
     - CodeMirror 6 line wrapping for code blocks
     - Markdown-it configuration (soft line breaks)
     - MathJax integration for LaTeX math rendering
     - Mermaid diagram rendering with theme synchronization
     - Light/Dark mode toggle button (works for both guests and logged-in users)
     ============================================================================ -->

<!-- ==========================================================================
     CONSOLIDATED STYLES
     ========================================================================== -->
<style>
  /* --------------------------------------------------------------------------
     DETAILS ELEMENT SPACING
     Ensures proper bottom margin for the last child inside <details> elements
     -------------------------------------------------------------------------- */
  .page-content details > *:last-child {
    margin-bottom: .2em;
  }

  /* --------------------------------------------------------------------------
     BLOCKQUOTE ADJUSTMENTS
     - Adds consistent vertical spacing for first/last children
     - Disables overflow scrolling to prevent unwanted scrollbars
     -------------------------------------------------------------------------- */
  .content-wrap blockquote > :last-child {
    margin-bottom: .3em;
  }

  .content-wrap blockquote > :first-child {
    margin-top: .3em;
  }

  .content-wrap blockquote {
    overflow: visible;
    overflow-x: visible;
    overflow-y: visible;
  }

  /* --------------------------------------------------------------------------
     PAGE CONTENT ZOOM AND TYPOGRAPHY
     - Applies 1.15x zoom to page content for better readability
     - Compensates heading size with 0.9x zoom to maintain visual hierarchy
     -------------------------------------------------------------------------- */
  .page-content {
    zoom: 1.15;
  }

  .page-content h1,
  .page-content h2,
  .page-content h3,
  .page-content h4,
  .page-content h5,
  .page-content h6 {
    zoom: .9;
  }

  /* --------------------------------------------------------------------------
     COLOR SCHEME: LIGHT MODE
     - Dark text on light background for optimal contrast
     - Slightly muted headings for visual hierarchy
     -------------------------------------------------------------------------- */
  html .page-content {
    color: hsl(0 0% 10%);
  }

  html .page-content h1,
  html .page-content h2,
  html .page-content h3,
  html .page-content h4,
  html .page-content h5,
  html .page-content h6 {
    color: hsl(0 0% 30%);
  }

  /* --------------------------------------------------------------------------
     COLOR SCHEME: DARK MODE
     - Light text on dark background
     - Adjusted heading colors for dark theme
     - Muted gutter colors for CodeMirror editor
     -------------------------------------------------------------------------- */
  html.dark-mode .page-content {
    color: hsl(0 0% 90%);
  }

  html.dark-mode .page-content h1,
  html.dark-mode .page-content h2,
  html.dark-mode .page-content h3,
  html.dark-mode .page-content h4,
  html.dark-mode .page-content h5,
  html.dark-mode .page-content h6 {
    color: hsl(0 0% 70%);
  }

  html.dark-mode .ͼo .cm-gutters {
    color: hsl(0 0% 33%);
  }

  /* --------------------------------------------------------------------------
     CODEMIRROR EDITOR STYLING
     Removes border-radius for a cleaner, squared appearance
     -------------------------------------------------------------------------- */
  .page-content .cm-editor {
    border-radius: 0;
  }

  /* --------------------------------------------------------------------------
     MERMAID DIAGRAM CONTAINER
     - Dashed border for visual identification during development/editing
     - Centered layout with horizontal scroll for large diagrams
     -------------------------------------------------------------------------- */
  .mermaid-container {
    border: 1px dashed #4238ff !important;
  }

  .mermaid {
    margin: 1em auto;
    overflow-x: auto;
    text-align: center;
  }

  .mermaid svg {
    max-width: 100%;
    height: auto;
    display: inline-block;
  }

  /* --------------------------------------------------------------------------
     THEME TOGGLE BUTTON (FIXED POSITION)
     - Positioned at bottom-left corner for easy access
     - Circular button with hover/active states
     - Semi-transparent by default, full opacity on hover
     -------------------------------------------------------------------------- */
  .theme-toggle-fixed {
    position: fixed;
    bottom: 20px;
    left: 20px;
    z-index: 9999;
    background: var(--color-primary, #206ea7);
    border: none;
    border-radius: 50%;
    width: 44px;
    height: 44px;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
    opacity: 0.7;
  }

  .theme-toggle-fixed:hover {
    transform: scale(1.1);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
    opacity: 1;
  }

  .theme-toggle-fixed:active {
    transform: scale(0.95);
  }

  .theme-toggle-fixed svg {
    width: 22px;
    height: 22px;
    fill: #ffffff;
  }
</style>

<!-- ==========================================================================
     MATHJAX CONFIGURATION
     Enables LaTeX math rendering with $ for inline and $$ for display math
     ========================================================================== -->
<script>
  window.MathJax = {
    tex: {
      inlineMath: [['$', '$']],
      displayMath: [['$$', '$$']]
    }
  };
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-mml-chtml.js"></script>

<!-- ==========================================================================
     BOOKSTACK EVENT LISTENERS AND THEME TOGGLE
     - CodeMirror 6 line wrapping configuration
     - Markdown-it soft line break configuration
     - Light/Dark mode toggle for guests and logged-in users
     ========================================================================== -->
<script>
  (function () {
    'use strict';

    /* ========================================================================
       SHARED THEME STORAGE KEY
       Used by both the toggle button and Mermaid for consistent theme state
       ======================================================================== */
    var THEME_STORAGE_KEY = 'bookstack-guest-dark-mode';

    // Expose storage key globally for the Mermaid module to access
    window.BOOKSTACK_THEME_STORAGE_KEY = THEME_STORAGE_KEY;

    /* ========================================================================
       CODEMIRROR 6: LINE WRAPPING FOR CODE BLOCKS
       Listens for the CM6 pre-init event and enables line wrapping
       for content code blocks to prevent horizontal scrolling
       ======================================================================== */
    window.addEventListener('library-cm6::pre-init', function (event) {
      var detail = event.detail;
      var config = detail.editorViewConfig;
      var EditorView = detail.libEditorView;

      // Only apply line wrapping to content code blocks (not the main editor)
      if (detail.usage === 'content-code-block') {
        config.extensions.push(EditorView.lineWrapping);
      }
    });

    /* ========================================================================
       MARKDOWN-IT: SOFT LINE BREAKS
       Configures the Markdown editor to convert single newlines to <br> tags
       (GFM-style line breaks)
       ======================================================================== */
    window.addEventListener('editor-markdown::setup', function (event) {
      event.detail.markdownIt.set({breaks: true});
    });

    /* ========================================================================
       LIGHT/DARK MODE TOGGLE BUTTON

       Features:
       - Works for both guests (localStorage) and logged-in users (server-side)
       - Applies saved preference immediately to prevent flash of wrong theme
       - Detects system color scheme preference as fallback for guests
       ======================================================================== */

    /**
     * Checks if the guest has dark mode enabled based on localStorage
     * Falls back to system preference if no stored value exists
     * @returns {boolean} True if dark mode should be enabled
     */
    function isGuestDarkModeEnabled() {
      var stored = localStorage.getItem(THEME_STORAGE_KEY);
      if (stored !== null) {
        return stored === 'true';
      }
      // Fallback: Check system color scheme preference
      return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }

    /**
     * Determines if the current user is logged in
     * Checks for CSRF token AND user menu elements (both required)
     * @returns {boolean} True if user is logged in
     */
    function isUserLoggedIn() {
      var csrfMeta = document.querySelector('meta[name="csrf-token"]');
      var hasToken = csrfMeta && csrfMeta.getAttribute('content');
      var hasUserMenu = document.querySelector('.dropdown-container [data-shortcut="favourites_view"]')
        || document.querySelector('[href*="/logout"]');
      return !!(hasToken && hasUserMenu);
    }

    /**
     * Applies the theme by toggling the 'dark-mode' class on <html>
     * @param {boolean} isDark - Whether to enable dark mode
     */
    function applyTheme(isDark) {
      if (isDark) {
        document.documentElement.classList.add('dark-mode');
      } else {
        document.documentElement.classList.remove('dark-mode');
      }
    }

    // ---------------------------------------------------------------------------
    // IMMEDIATE THEME APPLICATION (before DOMContentLoaded)
    // Prevents flash of wrong theme by applying saved preference early
    // ---------------------------------------------------------------------------
    var currentlyDark = document.documentElement.classList.contains('dark-mode');
    var guestPref = localStorage.getItem(THEME_STORAGE_KEY);

    if (guestPref !== null) {
      if (!currentlyDark && guestPref === 'true') {
        document.documentElement.classList.add('dark-mode');
      } else if (guestPref === 'false' && currentlyDark) {
        document.documentElement.classList.remove('dark-mode');
      }
    }

    // ---------------------------------------------------------------------------
    // CREATE TOGGLE BUTTON (after DOM is ready)
    // ---------------------------------------------------------------------------
    document.addEventListener('DOMContentLoaded', function () {
      var isDarkMode = document.documentElement.classList.contains('dark-mode');
      var loggedIn = isUserLoggedIn();

      // SVG icons for sun (light mode) and moon (dark mode)
      var sunIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 15.31 23.31 12 20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6"/></svg>';
      var moonIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/></svg>';

      // Create the toggle button element
      var button = document.createElement('button');
      button.className = 'theme-toggle-fixed';
      button.type = 'button';
      button.title = isDarkMode ? 'Activate Light Mode' : 'Activate Dark Mode';
      button.innerHTML = isDarkMode ? sunIcon : moonIcon;

      // Handle click: server-side for logged-in users, client-side for guests
      button.addEventListener('click', function () {
        if (loggedIn) {
          // Logged-in users: Submit form to BookStack's preference endpoint
          var csrfToken = '';
          var csrfMeta = document.querySelector('meta[name="csrf-token"]');
          if (csrfMeta) {
            csrfToken = csrfMeta.getAttribute('content') || '';
          }

          var form = document.createElement('form');
          form.method = 'POST';
          form.action = '/preferences/toggle-dark-mode';
          form.innerHTML =
            '<input type="hidden" name="_token" value="' + csrfToken + '">' +
            '<input type="hidden" name="_method" value="PATCH">' +
            '<input type="hidden" name="_return" value="' + window.location.href + '">';
          document.body.appendChild(form);
          form.submit();
        } else {
          // Guests: Toggle theme client-side and save to localStorage
          var nowDark = document.documentElement.classList.contains('dark-mode');
          var newMode = !nowDark;

          applyTheme(newMode);
          localStorage.setItem(THEME_STORAGE_KEY, newMode.toString());

          // Update button appearance
          button.innerHTML = newMode ? sunIcon : moonIcon;
          button.title = newMode ? 'Activate Light Mode' : 'Activate Dark Mode';
        }
      });

      document.body.appendChild(button);
    });
  })();
</script>

<!-- ==========================================================================
     MERMAID DIAGRAM RENDERING (ES MODULE)

     Features:
     - Automatic detection of mermaid code blocks (multiple selector formats)
     - Theme synchronization with BookStack's dark/light mode
     - Uses shared localStorage key with theme toggle button
     - Theme changes apply on next page load (no live re-rendering)
     ========================================================================== -->
<script type="module">
  import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";

  // Use the shared storage key from the main script
  const STORAGE_KEY = window.BOOKSTACK_THEME_STORAGE_KEY || "bookstack-guest-dark-mode";

  /* ==========================================================================
     THEME DETECTION UTILITIES
     ========================================================================== */

  /**
   * Checks if BookStack is currently in dark mode
   * @returns {boolean} True if 'dark-mode' class is present on <html>
   */
  function isBookStackDarkMode() {
    return document.documentElement.classList.contains("dark-mode");
  }

  /**
   * Retrieves the stored theme preference from localStorage
   * @returns {boolean|null} true = dark, false = light, null = not set
   */
  function getStoredThemeIsDark() {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored === null) return null;
    return stored === "true";
  }

  /**
   * Saves the theme preference to localStorage
   * @param {boolean} isDark - Whether dark mode is enabled
   */
  function storeThemeIsDark(isDark) {
    localStorage.setItem(STORAGE_KEY, isDark ? "true" : "false");
  }

  /**
   * Determines the Mermaid theme based on current BookStack mode
   * Uses actual DOM state (dark-mode class) for accurate theme selection
   * @returns {string} Mermaid theme name ("dark" or "default")
   */
  function getMermaidTheme() {
    return isBookStackDarkMode() ? "dark" : "default";
  }

  /* ==========================================================================
     MERMAID INITIALIZATION
     Disabled auto-start to allow manual control over rendering
     ========================================================================== */
  mermaid.initialize({
    startOnLoad: false,
    securityLevel: "strict",
    theme: getMermaidTheme()
  });

  /* ==========================================================================
     DOM UTILITIES FOR MERMAID CODE BLOCKS
     ========================================================================== */

  /**
   * Finds all elements containing Mermaid diagram source code
   * Supports multiple code block formats used by different editors
   * @returns {HTMLElement[]} Array of source elements
   */
  function findMermaidSources() {
    const selectors = [
      "pre.mermaid",
      "pre > code.language-mermaid",
      "pre > code.lang-mermaid",
      "code.language-mermaid"
    ];

    const nodes = Array.from(document.querySelectorAll(selectors.join(",")));

    // Normalize to parent <pre> element when applicable
    return nodes.map(n =>
      (n.tagName.toLowerCase() === "code" && n.parentElement?.tagName.toLowerCase() === "pre")
        ? n.parentElement
        : n
    );
  }

  /**
   * Extracts the text content (Mermaid code) from a source element
   * @param {HTMLElement} node - The source element
   * @returns {string} The trimmed Mermaid code
   */
  function extractCodeFromNode(node) {
    const codeEl = node.tagName.toLowerCase() === "pre"
      ? (node.querySelector("code") || node)
      : node;

    return (codeEl.textContent || "").trim();
  }

  /**
   * Replaces the original code block with a Mermaid container div
   * Initially hidden to prevent flash of unstyled content
   * @param {HTMLElement} originalNode - The original code block element
   * @param {string} code - The Mermaid diagram code
   * @returns {HTMLElement} The new container element
   */
  function replaceWithContainer(originalNode, code) {
    const container = document.createElement("div");
    container.className = "mermaid";
    container.textContent = code;
    container.style.visibility = "hidden";

    originalNode.replaceWith(container);
    return container;
  }

  /* ==========================================================================
     PAGE READY DETECTION
     ========================================================================== */

  /**
   * Waits for the page to be fully loaded and rendered
   * Uses multiple techniques to ensure DOM is stable:
   * 1. Wait for 'load' event (or skip if already complete)
   * 2. Wait for two animation frames (ensures layout is calculated)
   * 3. Additional timeout for any async CSS/font loading
   */
  async function waitForPageReady() {
    // Wait for window load event
    await new Promise(resolve => {
      if (document.readyState === "complete") return resolve();
      window.addEventListener("load", () => resolve(), {once: true});
    });

    // Wait for layout stabilization (two animation frames)
    await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));

    // Additional buffer for async resources
    await new Promise(r => setTimeout(r, 150));
  }

  /* ==========================================================================
     MAIN RENDERING FUNCTION
     ========================================================================== */

  /**
   * Main entry point: finds, transforms, and renders all Mermaid diagrams
   * Theme is determined once at render time and applied consistently
   * Theme changes will take effect on next page load
   */
  async function renderAllMermaid() {
    const sources = findMermaidSources();

    // Even without diagrams, ensure theme state is stored for consistency
    if (!sources.length) {
      await waitForPageReady();
      storeThemeIsDark(isBookStackDarkMode());
      return;
    }

    // Transform code blocks into Mermaid containers
    const containers = [];
    for (const src of sources) {
      const code = extractCodeFromNode(src);
      if (!code) continue;

      const container = replaceWithContainer(src, code);
      // Store original code for potential future use
      container.setAttribute("data-original-code", code);
      containers.push(container);
    }

    await waitForPageReady();

    // Render all diagrams with the current theme
    for (const el of containers) {
      try {
        await mermaid.run({nodes: [el]});
        el.style.visibility = "visible";
      } catch (e) {
        el.style.visibility = "visible";
        console.error("Mermaid render failed for element:", el, e);
      }
    }

    // Store current theme state for next page load
    storeThemeIsDark(isBookStackDarkMode());
  }

  // Start rendering process
  renderAllMermaid();
</script>

functions.php

/opt/bookstack/bookstack/www/themes/custom/functions.php

<?php

// Import the ThemeEvents class which contains event constants for the theming system
use BookStack\Theming\ThemeEvents;
// Import the Theme facade to register event listeners
use BookStack\Facades\Theme;

/**
 * Register a listener for the CommonMark environment configuration event.
 * This hook is triggered when BookStack sets up the Markdown parser.
 *
 * CommonMark is the Markdown parsing library used by BookStack.
 */
Theme::listen(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, function ($environment) {

  // Merge custom configuration into the CommonMark environment
  $environment->mergeConfig([
    'renderer' => [
      // Convert soft line breaks (single newlines) into <br> HTML tags
      // By default, CommonMark ignores single newlines in Markdown.
      // This setting makes single line breaks visible in the rendered output,
      // which is useful for preserving line formatting in user content.
      'soft_break' => "<br>",
    ]
  ]);

  // Return the modified environment so other listeners can further customize it
  return $environment;
});

oEmbeds in <head>

<style>
  .auto-embed {
    position: relative;
    width: 100%;
    max-width: 100%;
    margin: 1rem 0;
    overflow: hidden;
    background: #000;
  }

  .auto-embed.video {
    aspect-ratio: 16/9;
  }

  .auto-embed.audio {
    aspect-ratio: 16/5;
  }

  .auto-embed.code {
    aspect-ratio: 4/3;
    min-height: 400px;
  }

  .auto-embed.square {
    aspect-ratio: 1/1;
  }

  .auto-embed iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
  }
</style>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const providers = [
      // 🎬 Video
      {
        regex: /youtu(?:be\.com\/watch\?v=|\.be\/)([\w-]+)/i,
        embed: id => `https://www.youtube-nocookie.com/embed/${id}`, type: 'video'
      },
      {
        regex: /vimeo\.com\/(\d+)/i,
        embed: id => `https://player.vimeo.com/video/${id}`, type: 'video'
      },
      {
        regex: /dailymotion\.com\/video\/([\w-]+)/i,
        embed: id => `https://www.dailymotion.com/embed/video/${id}`, type: 'video'
      },
      {
        regex: /twitch\.tv\/videos\/(\d+)/i,
        embed: id => `https://player.twitch.tv/?video=${id}&parent=${location.hostname}`, type: 'video'
      },
      {
        regex: /twitch\.tv\/([\w-]+)$/i,
        embed: id => `https://player.twitch.tv/?channel=${id}&parent=${location.hostname}`, type: 'video'
      },
      {
        regex: /loom\.com\/share\/([\w-]+)/i,
        embed: id => `https://www.loom.com/embed/${id}`, type: 'video'
      },
      {
        regex: /streamable\.com\/([\w-]+)/i,
        embed: id => `https://streamable.com/e/${id}`, type: 'video'
      },

      // 🎵 Audio
      {
        regex: /open\.spotify\.com\/(track|album|playlist|episode)\/([\w-]+)/i,
        embed: (t, id) => `https://open.spotify.com/embed/${t}/${id}`, type: 'audio'
      },
      {
        regex: /soundcloud\.com\/([\w-]+\/[\w-]+)/i,
        embed: path => `https://w.soundcloud.com/player/?url=https://soundcloud.com/${path}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false`,
        type: 'audio'
      },

      // 💻 Code - CodePen

      // ✅ CodePen v2.0 Editor-Format (DIREKTES IFRAME!)
      {
        regex: /codepen\.io\/editor\/([\w-]+)\/pen\/([\w-]+)/i,
        embed: (user, penId) => `https://codepen.io/editor/${user}/embed/${penId}?default-tab=html%2Cresult&theme-id=dark&editable=true`,
        type: 'code'
      },

      // ✅ Klassisches CodePen-Format
      {
        regex: /codepen\.io\/([\w-]+)\/(?:pen|full|details)\/([\w-]+)/i,
        embed: (user, penId) => `https://codepen.io/${user}/embed/${penId}?default-tab=html%2Cresult&theme-id=dark&editable=true`,
        type: 'code'
      },

      // Andere Code-Plattformen
      {
        regex: /codesandbox\.io\/s\/([\w-]+)/i,
        embed: id => `https://codesandbox.io/embed/${id}`, type: 'code'
      },
      {
        regex: /stackblitz\.com\/edit\/([\w-]+)/i,
        embed: id => `https://stackblitz.com/edit/${id}?embed=1`, type: 'code'
      },
      {
        regex: /jsfiddle\.net\/([\w-]+\/[\w-]+)/i,
        embed: path => `https://jsfiddle.net/${path}/embedded/result,js,html,css/`, type: 'code'
      },
      {
        regex: /gist\.github\.com\/([\w-]+)\/([\w-]+)/i,
        embed: (u, id) => `data:text/html,<script src="https://gist.github.com/${u}/${id}.js"><\/script>`,
        type: 'code'
      },

      // 🗺️ Maps & Design
      {
        regex: /figma\.com\/(file|design)\/([\w-]+)/i,
        embed: (t, id) => `https://www.figma.com/embed?embed_host=bookstack&url=https://www.figma.com/${t}/${id}`,
        type: 'code'
      },
      {
        regex: /google\.com\/maps\/embed\?pb=([^&\s]+)/i,
        embed: pb => `https://www.google.com/maps/embed?pb=${pb}`, type: 'video'
      },
    ];

    document.querySelectorAll('p').forEach(p => {
      const text = p.textContent.trim();
      if (!/^https?:\/\/\S+$/i.test(text)) return;

      for (const {regex, embed, type} of providers) {
        const match = text.match(regex);
        if (!match) continue;

        const src = embed(...match.slice(1));
        p.outerHTML = `<div class="auto-embed ${type}"><iframe src="${src}" allowfullscreen loading="lazy" allowtransparency="true"></iframe></div>`;
        return;
      }
    });
  });
</script>

And this must be added to docker-compose.yml

Under environment:

- ALLOWED_IFRAME_SOURCES=https://*.codepen.io https://codepen.io https://*.jsfiddle.net https://jsfiddle.net https://*.codesandbox.io https://codesandbox.io https://open.spotify.com https://gist.github.com https://*.youtube.com https://youtube.com https://www.youtube-nocookie.com https://*.vimeo.com https://player.vimeo.com https://*.dailymotion.com https://*.twitch.tv https://player.twitch.tv https://clips.twitch.tv https://*.tiktok.com https://*.loom.com https://*.wistia.com https://fast.wistia.net https://streamable.com https://*.vidyard.com https://rumble.com https://*.soundcloud.com https://w.soundcloud.com https://embed.music.apple.com https://*.deezer.com https://widget.deezer.com https://*.bandcamp.com https://*.mixcloud.com https://*.audiomack.com https://anchor.fm https://*.transistor.fm https://*.stackblitz.com https://*.replit.com https://*.glitch.com https://jsbin.com https://*.observablehq.com https://carbon.now.sh https://asciinema.org https://platform.twitter.com https://*.instagram.com https://*.reddit.com https://*.linkedin.com https://assets.pinterest.com https://*.figma.com https://*.canva.com https://docs.google.com https://*.pitch.com https://prezi.com https://*.slideshare.net https://speakerdeck.com https://maps.google.com https://*.openstreetmap.org https://*.notion.so https://*.notion.site https://*.airtable.com https://*.typeform.com https://form.typeform.com https://calendly.com https://*.miro.com https://excalidraw.com https://*.diagrams.net https://whimsical.com https://*.lottiefiles.com https://lottie.host https://*.sketchfab.com

https://www.youtube.com/watch?v=UclrVWafRAI