xshoji's blog

Webページをスマホ向けのデザインに最適化する Bookmarklet

#bookmarklet #mobile #css #javascript

普段 iPhone を使っていて、Safari でブログをよく読むのですが、スマホ向けに最適化されていないページがまだ多く、見にくいと感じることがよくあります。 Safari には「リーダー表示」という機能があり、記事本文だけを抽出して見やすく表示できますが

iPhoneのSafariで記事を読んでいるときに気をそらすものを非表示にする - Apple サポート (日本)
https://support.apple.com/ja-jp/guide/iphone/iphdc30e3b86/ios

リーダー表示に対応していないページもあります。また、リーダー表示にしても、コードブロックや文中の変数のスタイルがわかりにくく、痒いところに手が届かないと感じることがあります。

そうした悩みがあり、スマホ向けに最適化されていないページを見やすくする Bookmarklet(ブックマークレット)を作成しました。 今回は作成した Bookmarklet の紹介と使い方を説明します。

スマホで Bookmarklet を設定する方法

Bookmarkletの設定方法は以下の通りです。

  1. お使いのブラウザで適当なページをお気に入り登録する
  2. Bookmarkletのコードをコピーする
  3. 先ほど登録したお気に入りを編集する
  4. URLの部分にコピーしたBookmarkletのコードを貼り付ける
  5. 名前をわかりやすい名前に変更する ( 例: Optimize for Mobile )
  6. 保存する

これで設定は完了です。

Bookmarklet のコード

「2.」で設定するBookmarkletのコードを以下に示します。 だいぶ長いので、中身は気にせずにコピーして設定してください。 (気になる人はコードの中身を読んでみてください。)

やってることを文章で簡単に説明すると、Webページに適用されているCSSを全て無効にした後に自作のCSSを無理やり適用してる、という感じです。

Bookmarkletに設定するコードはこちら
javascript:(() => {
  'use strict';

  /* ==================== Configuration ==================== */

  /**
   * Configuration object for the optimizer
   * @const {Object}
   */
  const CONFIG = {
    /** Auto-apply optimization on page load */
    autoApplyOptimization: true,

    /** Style element ID for injected CSS */
    styleElementId: 'mobile-page-optimizer-style',

    /** Data attribute for storing original styles */
    originalStyleAttr: 'data-original-style',

    /** Data attribute for marking disabled elements */
    disabledAttr: 'data-mobile-optimizer-disabled',

    /** Button configuration */
    button: {
      textEnabled: 'Mobile View OFF',
      textDisabled: 'Mobile View ON',
      position: { top: '10px', left: '10px' },
      zIndex: '2147483647',
    },

    /** Colors for theming */
    colors: {
      background: '#e8e0d5',
      text: '#333333',
      textDark: '#111111',
      link: '#0060bf',
      linkVisited: '#5a3696',
      linkHover: '#0077cc',
      codeInline: '#a83232',
      codeBackground: '#f5f2ed',
      codeBorder: '#d5c7b5',
      emphasis: '#8B4513',
      buttonActive: '#28a745',
      buttonInactive: '#f8f9fa',
    },
  };

  /* ==================== CSS Management ==================== */

  /**
   * Disables or enables all existing CSS on the page
   * Stores original styles for restoration
   *
   * @param {boolean} isDisabled - Whether to disable CSS
   */
  const disableCss = (isDisabled) => {
    try {
      /* Disable external and internal stylesheets */
      Array.from(document.styleSheets).forEach((sheet) => {
        try {
          sheet.disabled = isDisabled;
        } catch (e) {
          /* Some stylesheets may throw security errors (CORS) */
          console.warn('Cannot disable stylesheet:', e);
        }
      });

      if (isDisabled) {
        /* Save and remove inline styles */
        document.querySelectorAll('[style]').forEach((el) => {
          el.setAttribute(CONFIG.originalStyleAttr, el.getAttribute('style'));
          el.removeAttribute('style');
        });

        /* Disable style elements (except our own) */
        document.querySelectorAll('style').forEach((el) => {
          if (el.id !== CONFIG.styleElementId) {
            el.setAttribute(CONFIG.disabledAttr, 'true');
            el.textContent = '';
          }
        });
      } else {
        /* Restore saved inline styles */
        document.querySelectorAll(`[${CONFIG.originalStyleAttr}]`).forEach((el) => {
          el.setAttribute('style', el.getAttribute(CONFIG.originalStyleAttr));
          el.removeAttribute(CONFIG.originalStyleAttr);
        });

        /* Reload page if we disabled any style elements */
        if (document.querySelector(`[${CONFIG.disabledAttr}]`)) {
          location.reload();
        }
      }
    } catch (error) {
      console.error('Error in disableCss:', error);
    }
  };

  /**
   * Generates optimized CSS for mobile viewing
   *
   * @returns {string} CSS string
   */
  const generateOptimizedCSS = () => {
    const { colors } = CONFIG;

    return `
        /* Base Styles - Common */
        * {
          box-sizing: border-box !important;
          overflow-wrap: break-word !important;
          word-wrap: break-word !important;
          word-break: break-word !important;
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif !important;
        }

        /* Prevent Horizontal Scrolling & Basic Layout */
        html, body, #root, #app, .container, .wrapper, .content, main, article, section, header, footer, nav, div {
          width: 100% !important;
          max-width: 100% !important;
          min-width: 0 !important;
          margin-left: 0 !important;
          margin-right: 0 !important;
          box-sizing: border-box !important;
          overflow-x: hidden !important;
          position: static !important;
          left: auto !important;
          right: auto !important;
        }

        /* Body Element Base Styles */
        body {
          padding: 8px !important;
          overflow-y: auto !important;
          background-color: ${colors.background} !important;
          font-size: 16px !important;
          line-height: 1.6 !important;
          text-align: left !important;
          color: ${colors.text} !important;
          min-height: 100vh !important;
          height: auto !important;
        }

        /* Div Settings */
        div {
          clear: both !important;
          float: none !important;
          display: block !important;
          overflow: visible !important;
          height: auto !important;
        }

        /* Heading Elements */
        h1, h2, h3, h4, h5, h6 {
          text-align: left !important;
          margin: 0.8em 0 0.4em 0 !important;
          padding: 0 8px !important;
          width: auto !important;
          color: ${colors.textDark} !important;
        }

        h1 { font-size: 1.4em !important; }
        h2 { font-size: 1.3em !important; }
        h3 { font-size: 1.1em !important; }

        /* Paragraphs */
        p {
          width: 100% !important;
          margin: 0.6em 0 !important;
          padding: 0 10px !important;
          font-size: 16px !important;
          overflow: visible !important;
          color: ${colors.text} !important;
        }

        /* List Items */
        li {
          width: 100% !important;
          margin: 0.2em 0 !important;
          padding: 0 0px !important;
          font-size: 16px !important;
          overflow: visible !important;
          color: ${colors.text} !important;
        }

        /* Media Elements - Images, Videos, etc. */
        img, video, iframe, canvas, object, embed, svg {
          max-width: 100% !important;
          height: auto !important;
          display: block !important;
        }

        /* Table Elements */
        table, tbody, thead, tr, td, th {
          display: block !important;
          width: 100% !important;
          max-width: 100% !important;
          min-width: 0 !important;
          border-collapse: collapse !important;
        }

        /* List Elements */
        ul, ol {
          display: block !important;
          margin-left: 10px !important;
          padding-left: 20px !important;
          list-style-position: outside !important;
        }

        /* List Markers */
        ul { list-style-type: disc !important; }
        ol { list-style-type: decimal !important; }
        ul ul { list-style-type: circle !important; }
        ul ol { list-style-type: decimal !important; }
        ol ul { list-style-type: disc !important; }
        ol ol { list-style-type: lower-alpha !important; }

        /* Inline Elements */
        span, a, em, strong, i, b {
          display: inline !important;
          max-width: 100% !important;
          width: auto !important;
        }

        /* Emphasis Elements */
        strong, b {
          font-weight: 700 !important;
          color: #000000 !important;
          letter-spacing: 0.02em !important;
        }

        /* Italic Elements */
        em, i {
          font-style: italic !important;
          color: ${colors.emphasis} !important;
          letter-spacing: 0.01em !important;
        }

        /* Link Elements */
        a {
          color: ${colors.link} !important;
          text-decoration: underline !important;
          font-weight: 500 !important;
          border-bottom: 1px dotted rgba(0, 96, 191, 0.3) !important;
          padding-bottom: 1px !important;
          min-height: 44px !important;
          min-width: 44px !important;
        }

        a:visited { color: ${colors.linkVisited} !important; }
        a:hover {
          color: ${colors.linkHover} !important;
          text-decoration: none !important;
          border-bottom: 1px solid rgba(0, 96, 191, 0.7) !important;
        }

        /* Touch Target Size */
        button, input, select, textarea {
          min-height: 44px !important;
          min-width: 44px !important;
        }

        /* Code Elements */
        code, kbd, samp {
          font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
          font-size: 12px !important;
          line-height: 1.4 !important;
        }

        /* Inline Code */
        code:not(pre code) {
          background-color: rgba(0, 0, 0, 0.05) !important;
          border-radius: 3px !important;
          padding: 2px 4px !important;
          color: ${colors.codeInline} !important;
        }

        /* Preformatted Text */
        pre {
          overflow-x: auto !important;
          white-space: pre !important;
          width: 100% !important;
          background-color: ${colors.codeBackground} !important;
          color: ${colors.text} !important;
          padding: 15px !important;
          border: 1px solid ${colors.codeBorder} !important;
          border-radius: 6px !important;
          margin: 10px 0 !important;
          font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
          font-size: 16px !important;
          line-height: 1.4 !important;
          display: block !important;
          word-wrap: normal !important;
          word-break: keep-all !important;
          overflow-wrap: normal !important;
          -webkit-overflow-scrolling: touch !important;
        }

        /* Code Inside Pre Tags */
        pre code {
          background-color: transparent !important;
          border: none !important;
          padding: 0 !important;
          margin: 0 !important;
          display: inline !important;
          white-space: pre !important;
          overflow: visible !important;
          word-wrap: normal !important;
          word-break: keep-all !important;
          overflow-wrap: normal !important;
          color: inherit !important;
        }

        /* Inline Code in Paragraphs */
        p code, li code, td code {
          padding: 2px 5px !important;
          border-radius: 3px !important;
          display: inline !important;
          white-space: normal !important;
          font-size: 90% !important;
          background-color: rgba(0, 0, 0, 0.05) !important;
          color: ${colors.codeInline} !important;
          border: 1px solid ${colors.codeBorder} !important;
        }

        /* Blockquote Elements */
        blockquote {
          margin: 1.2em 0 !important;
          padding: 15px !important;
          padding-left: 20px !important;
          background-color: rgba(0, 0, 0, 0.03) !important;
          border-left: 4px solid #a89968 !important;
          border-radius: 3px !important;
          font-style: italic !important;
          color: ${colors.textDark} !important;
          line-height: 1.7 !important;
          position: relative !important;
        }

        blockquote p {
          margin: 0.5em 0 !important;
          padding: 0 !important;
          font-style: italic !important;
        }

        blockquote p:first-child {
          margin-top: 0 !important;
        }

        blockquote p:last-child {
          margin-bottom: 0 !important;
        }

        /* Hidden Elements */
        aside, nav.sidebar, .sidebar, .ad-container, 
        svg.svg-icon, [class*="icon"], [class*="Icon"] {
          display: none !important;
        }
      `;
  };

  /**
   * Updates viewport meta tag for mobile optimization
   *
   * @param {boolean} isEnabled - Whether to enable mobile viewport
   */
  const updateViewportMeta = (isEnabled) => {
    const VIEWPORT_CONTENT = 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes';
    const ORIGINAL_CONTENT_ATTR = 'data-original-content';

    let viewportMeta = document.querySelector('meta[name="viewport"]');

    if (isEnabled) {
      if (viewportMeta) {
        if (!viewportMeta.hasAttribute(ORIGINAL_CONTENT_ATTR)) {
          viewportMeta.setAttribute(ORIGINAL_CONTENT_ATTR, viewportMeta.getAttribute('content'));
        }
      } else {
        viewportMeta = document.createElement('meta');
        viewportMeta.setAttribute('name', 'viewport');
        document.head.appendChild(viewportMeta);
      }
      viewportMeta.setAttribute('content', VIEWPORT_CONTENT);
    } else if (viewportMeta) {
      if (viewportMeta.hasAttribute(ORIGINAL_CONTENT_ATTR)) {
        viewportMeta.setAttribute('content', viewportMeta.getAttribute(ORIGINAL_CONTENT_ATTR));
        viewportMeta.removeAttribute(ORIGINAL_CONTENT_ATTR);
      } else {
        viewportMeta.remove();
      }
    }
  };

  /**
   * Applies or removes mobile-optimized CSS
   *
   * @param {boolean} isEnabled - Whether to enable optimization
   */
  const optimizeForSmartphone = (isEnabled) => {
    try {
      updateViewportMeta(isEnabled);

      let optimizeStyle = document.getElementById(CONFIG.styleElementId);

      if (isEnabled) {
        if (!optimizeStyle) {
          optimizeStyle = document.createElement('style');
          optimizeStyle.id = CONFIG.styleElementId;
          document.head.appendChild(optimizeStyle);
        }
        optimizeStyle.textContent = generateOptimizedCSS();
      } else if (optimizeStyle) {
        optimizeStyle.textContent = '';
      }
    } catch (error) {
      console.error('Error in optimizeForSmartphone:', error);
    }
  };

  /* ==================== UI Components ==================== */

  /**
   * Creates the toggle button for mobile optimization
   *
   * @returns {HTMLButtonElement} The created button element
   */
  const createToggleButton = () => {
    const button = document.createElement('button');
    const { button: btnConfig, colors } = CONFIG;

    /* Set initial state */
    let isOptimized = false;

    /* Apply base styles */
    Object.assign(button.style, {
      position: 'fixed',
      top: btnConfig.position.top,
      left: btnConfig.position.left,
      zIndex: btnConfig.zIndex,
      padding: '10px 15px',
      backgroundColor: colors.buttonInactive,
      color: 'black',
      border: '1px solid #ccc',
      borderRadius: '5px',
      fontSize: '16px',
      fontWeight: 'bold',
      cursor: 'pointer',
      boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
      transition: 'all 0.3s ease',
    });

    /**
     * Updates button appearance based on optimization state
     */
    const updateButtonState = () => {
      button.textContent = isOptimized ? btnConfig.textEnabled : btnConfig.textDisabled;
      button.style.backgroundColor = isOptimized ? colors.buttonActive : colors.buttonInactive;
      button.style.color = isOptimized ? 'white' : 'black';
    };

    updateButtonState();

    /* Add click handler */
    button.addEventListener('click', () => {
      try {
        isOptimized = !isOptimized;
        disableCss(isOptimized);
        optimizeForSmartphone(isOptimized);
        updateButtonState();
      } catch (error) {
        console.error('Error toggling optimization:', error);
      }
    });

    /* Add hover effect */
    button.addEventListener('mouseenter', () => {
      button.style.transform = 'translateY(-2px)';
      button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
    });

    button.addEventListener('mouseleave', () => {
      button.style.transform = 'translateY(0)';
      button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
    });

    document.body.appendChild(button);
    return button;
  };

  /* ==================== Initialization ==================== */

  /**
   * Applies optimization based on configuration
   *
   * @param {boolean} isOptimized - Whether to apply optimization
   */
  const applyOptimization = (isOptimized) => {
    try {
      disableCss(isOptimized);
      optimizeForSmartphone(isOptimized);
    } catch (error) {
      console.error('Error applying optimization:', error);
    }
  };

  /**
   * Initializes the mobile page optimizer
   */
  const initialize = () => {
    try {
      /* Auto-apply optimization if configured */
      if (CONFIG.autoApplyOptimization) {
        applyOptimization(true);
        return;
      }

      /* Create toggle button */
      const initButton = () => {
        /* Wait for body to be available */
        if (!document.body) {
          requestAnimationFrame(initButton);
          return;
        }
        createToggleButton();
      };

      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initButton);
      } else {
        initButton();
      }
    } catch (error) {
      console.error('Error initializing Mobile Page Optimizer:', error);
    }
  };

  /* ==================== Entry Point ==================== */

  initialize();
})();

設定した Bookmarklet の使い方

使い方は簡単で、スマホで見にくいページを開いた状態で、先ほど設定した Bookmarklet を実行するだけです。

実際使ってみたらどんな感じになるか

左がSafariのリーダー表示、右が今回作成したBookmarkletを実行した後の表示例です。

比較して気づいたのですが、若干暗めの見た目になってますね。このあたりは調整できますので、お好みに合わせて調整していただければと思います。

Safari のリーダー表示との主な違いは以下の通りです。

  • 箇条書きや引用ブロックで不要な隙間が入らない
  • 文章中に埋め込まれたコードブロックが見やすくなる
  • pre タグのコードブロックが見やすくなる
    • コードが少し小さめに表示され、スマホでも読みやすくなる
  • リーダー表示に対応していないページでも使えるので、どのページでも読みやすくできます

以上です。

まとめ

スマホで見にくい Web ページを簡単に見やすくする Bookmarklet を紹介しました。 Bookmarklet なので特別なアプリをインストールする必要はなく、簡単に設定できます。スマホでブログをよく読む方は、ぜひ試してみてください。

おまけ

実は、以前同様のことをする Bookmarklet があり、それを好んで使っていましたが、あるとき急に動かなくなってしまいました。

iPhoneのSafariが10倍便利になるブックマークレット10個 | カミアプ | AppleのニュースやIT系の情報をお届け
https://www.appps.jp/161142/
….
「開いているWebページを見やすく表示」

しばらく諦めていたのですが、AI が登場した際に自作できるのでは?と挑戦しようと思い立ち、今回紹介した Bookmarklet を作成した、という経緯です。