もふもふ技術部

IT技術系mofmofメディア

はてなブログのSEO効果を高めるために各記事に著者プロフィールとJSON-LDへの情報追加をやってみた

こんにちは。出口です。

今回は、はてなブログのSEO効果を高めるための施策を行ったので、その辺りの話です。

大きく分けて2つの対応を行いました。

  1. 記事下への著者情報の追加
  2. JSON-LDへの著者情報等の追加

どちらもJavaScriptでの対応になります。

記事下への著者情報の追加

まず1つ目、記事下への著者情報の追加です。

はてなブログには、元々著者情報が表示されています。

これは、はてなプロフィールをベースにしているので、はてなID表示名, はてなプロフィールへのリンクぐらいしか表示されません。

これを次のように改善しました。

画像のようなものが記事の一番下に表示されているはずです。

こちらはJavaScriptを使って表示しています。

次のようなコードです。

const profiles = [
  {
    hatenaId: 'deg84',
    name: '出口 達也',
    title: 'エンジニアリングコーチ',
    photo: 'https://www.mof-mof.co.jp/images/about/about_deguchi_update2_detail.jpg',
    aboutPageUrl: 'https://www.mof-mof.co.jp/member/tatsuya-deguchi',
    profile: '大阪のIT系専門学校を卒業後、東京のWEB制作会社に入社し、フロントエンドやバックエンドの開発経験を経て、退職までディレクターに従事。\nその後独立し、月額制とアジャイル開発をベースにしたサービス開発を行う。\n当時、担当していたサービス開発の増員依頼をきっかけに以前より親交のあった原田さんに相談し、以降、共に開発を行う。\n会社の成長に合わせて、エンジニアの技術力低下に懸念を抱いた小畑さんからの依頼により、エンジニアリングコーチとしてmofmofにジョイン。\n現在は、1on1の実施や開発プロセスの改善など、チームの開発生産性向上に取り組んでいる。',
    githubUserName: 'deg84',
    twitterUserName: 'deg84'
  }
];

function createProfileCard(profile) {
  const container = document.querySelector(".entry-footer-section.track-inview-by-gtm");
  if (!container) return;
  if (!profile.profile) return;

  const snsLinks = [
    profile.githubUserName && `<a class="github" href="https://github.com/${profile.githubUserName}" target="_blank">
      <i class="blogicon-gist lg"></i>
      <span class="inner-text">GitHub</span>
    </a>`,
    profile.twitterUserName && `<a class="twitter" href="https://www.twitter.com/${profile.twitterUserName}" target="_blank">
      <i class="blogicon-twitter lg"></i>
      <span class="inner-text">Twitter</span>
    </a>`,
    profile.facebookUserName && `<a class="facebook" href="https://www.facebook.com/${profile.facebookUserName}" target="_blank">
      <i class="blogicon-facebook lg"></i>
      <span class="inner-text">Facebook</span>
    </a>`
  ].filter(Boolean);

  const aboutImageHtml = (profile.aboutPageUrl && profile.aboutImageUrl) ? `<a href="${profile.aboutPageUrl}" class="mtb-icon" target="_blank"><img src="${profile.aboutImageUrl}" alt="${profile.name}"></a>` : '';
  const titleHtml = profile.title ? `<span class="mtb-title">${profile.title}</span>` : '';
  const aboutPageLink = profile.aboutPageUrl ? `<div class=""><a href="${profile.aboutPageUrl}" class="mtb-profile-link" target="_blank">詳しいプロフィール</a></div>`: "";
  const snsLinksHtml = snsLinks.length ? `<div class="mtb-sns-links">${snsLinks.join('')}</div>` : '';

  const profileCardHtml = `
    <div class="mtb-profile-card">
      <div class="mtb-profile">
        ${aboutImageHtml}
        <div class="mtb-name">${profile.name}${titleHtml}</div>
        ${snsLinksHtml}
      </div>
      <div class="mtb-profile-body">
        <p>${profile.profile}</p>
        ${aboutPageLink}
      </div>
    </div>
    <iframe src="https://blog.hatena.ne.jp/mofmof-inc/6801883189062159186.hatenablog-oem.com/subscribe/iframe" allowtransparency="true" frameborder="0" scrolling="no" width="150" height="28"></iframe>
  `;

  container.insertAdjacentHTML('afterend', profileCardHtml);
}

複数人で運用しているので、data-user-nameの値を使って出し分けるようになっています。1

JSON-LDへの著者情報等の追加

もう1つがJSON-LDへの追加です。Google検索で用いられる構造化データを作ります。2

はてなブログでは、元々JSON-LDが生成されて埋め込まれているのですが、なぜかauthorの項目がありません。

デフォルト状態だと以下のようになってます。

<script type="application/ld+json">
    {
        "@context":"http://schema.org",
        "@type":"Article",
        "dateModified": "2024-02-15T11:00:35+09:00",
        "datePublished": "2024-02-07T09:00:00+09:00",
        "headline":"技術ブログをNuxt + Netlify + Contentfulから、はてなブログ for DevBlogに移行しました",
        "image":[
            "ttps://ogimage.blog.st-hatena.com/6801883189062159186/6801883189062596867/1707962436"
        ]
    }
</script>

これを以下のように書き換えました。

<script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "Article",
    "fileFormat": "text/html",
    "isAccessibleForFree": true,
    "dateModified": "2024-02-15T02:00:35Z",
    "datePublished": "2024-02-07T09:00:00+09:00",
    "headline": "技術ブログをNuxt + Netlify + Contentfulから、はてなブログ for DevBlogに移行しました",
    "image": "https://ogimage.blog.st-hatena.com/6801883189062159186/6801883189062596867/1707962436",
    "name": "技術ブログをNuxt + Netlify + Contentfulから、はてなブログ for DevBlogに移行しました",
    "url": "https://www.mof-mof.co.jp/tech-blog/tech-blog-moved-hatena-devblog",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://www.mof-mof.co.jp/tech-blog/tech-blog-moved-hatena-devblog"
    },
    "thumbnailUrl": "https://ogimage.blog.st-hatena.com/6801883189062159186/6801883189062596867/1707962436",
    "description": "こんにちは。出口です。 タイトルにある通り、技術ブログをはてなブログに移行しました。 この記事では、なぜ移行することになったのか、どうやって移行したのか、移行で苦労したところなどをまとめておきたいと思います。 もし脱セルフホストブログ、脱Contentfulや、はてなブログへの移行をお考えであれば参考になるのではないかと思います。 なぜ移行したのか Nuxt 3への移行が大変すぎる Contentfulへの不満が募ってきた 当初の計画 改めて移行を考える 移行について 記事移行 インポート機能を使ってWXRを取り込む場合 AtomPubを使ったパターンの場合 サブディレクトリオプション Net…",
    "keywords": ["Nuxt.js", "Netlify", "Contentful"],
    "encoding": { "@type": "MediaObject", "encodingFormat": "utf-8" },
    "author": {
      "@type": "Person",
      "address": "Japan",
      "name": "出口 達也",
      "url": "https://www.mof-mof.co.jp/member/tatsuya-deguchi",
      "image": "https://www.mof-mof.co.jp/images/about/about_deguchi_update2_detail.jpg"
    },
    "publisher": {
      "@type": "Organization",
      "name": "もふもふ技術部",
      "url": "https://www.mof-mof.co.jp/tech-blog/",
      "logo": {
        "@type": "ImageObject",
        "url": "https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png"
      }
    },
    "copyrightHolder": {
      "@type": "Person",
      "address": "Japan",
      "name": "出口 達也",
      "url": "https://www.mof-mof.co.jp/member/tatsuya-deguchi",
      "image": "https://www.mof-mof.co.jp/images/about/about_deguchi_update2_detail.jpg"
    },
    "inLanguage": ["ja", "en"],
    "genre": ["Nuxt.js", "Netlify", "Contentful"]
  }
</script>

めちゃくちゃ項目増えました。

コードは次のような感じです。

function createJsonLd(profile) {
  const tryOrUndefined = (func) => {
    try {
      return func();
    } catch (e) {
      return undefined;
    }
  };

  const scriptElement = document.querySelector('script[type="application/ld+json"]');
  if (!scriptElement) return;

  const articleObj = {
    "@context": "http://schema.org",
    "@type": "Article",
    "fileFormat": "text/html",
    "isAccessibleForFree": true,
    ...JSON.parse(scriptElement.innerText)
  };

  const name = tryOrUndefined(() => document.querySelector("h1.entry-title").innerText);
  const headline = name ? name.substr(0, 110) : undefined;
  const uri = tryOrUndefined(() => document.querySelector(".entry-title-link").getAttribute("href")) ||
              tryOrUndefined(() => document.querySelector('[property="og:url"]').getAttribute("content"));
  const image = tryOrUndefined(() => document.querySelector('[itemprop="image"]').getAttribute("content"));
  const description = tryOrUndefined(() => document.querySelector('[name="description"]').getAttribute("content"));
  const datePublished = tryOrUndefined(() => document.querySelector('[pubdate]').getAttribute("datetime"));
  const dateModified = tryOrUndefined(() => document.querySelector("time[itemprop]").getAttribute("datetime"));
  const personImage = tryOrUndefined(() => document.querySelector('.profile-icon').getAttribute("src"));

  const authorHatenaId = document.querySelector('[data-user-name]').getAttribute('data-user-name');
  const authorName = document.querySelector('[data-user-name]').getAttribute('data-user-name');
  const authorUrl = document.querySelector('link[rel="author"]').getAttribute("href")

  const person = {
    "@type": "Person",
    "address": "Japan",
    "name": profile.name || authorName,
    "url": profile.aboutPageUrl || authorUrl,
    "image": profile.aboutImageUrl || personImage
  };

  const publisherName = tryOrUndefined(() => document.querySelector("[data-blog-name]").getAttribute("data-blog-name"));
  const publisherUrl = tryOrUndefined(() => document.querySelector("[data-blog-uri]").getAttribute("data-blog-uri")); 
  const publisherLogoImageUrl = "https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png";
  const publisher = {
    "@type": "Organization",
    ...(publisherName && { "name": publisherName }),
    ...(publisherUrl && { "url": publisherUrl }),
    ...(publisherLogoImageUrl && { 
      "logo": {
        "@type": "ImageObject",
        "url": publisherLogoImageUrl
      }
    })
  };

  const keywords = tryOrUndefined(() => 
    [...document.querySelectorAll(".entry-category-link")].map(item => item.innerText)
  );
  const genre = keywords;
  const charset = tryOrUndefined(() => document.querySelector('[charset]').getAttribute("charset"));
  const copyrightYear = datePublished ? datePublished.match(/^(\d{4})-/)[1] : undefined;
  const inLanguage = tryOrUndefined(() => document.querySelector('[data-avail-langs]').getAttribute("data-avail-langs").split(" "));

  articleObj.name = name || articleObj.name;
  articleObj.headline = headline || articleObj.headline;
  articleObj.url = uri || articleObj.url;
  articleObj.mainEntityOfPage = uri ? { "@type": "WebPage", "@id": uri } : articleObj.mainEntityOfPage;
  articleObj.image = image || articleObj.image;
  articleObj.thumbnailUrl = image || articleObj.thumbnailUrl;
  articleObj.description = description || articleObj.description;
  articleObj.keywords = keywords || articleObj.keywords;
  articleObj.encoding = charset ? { "@type": "MediaObject", "encodingFormat": charset } : articleObj.encoding;
  articleObj.author = person;
  articleObj.publisher = publisher;
  articleObj.copyrightHolder = person;
  articleObj.copyrightYear = copyrightYear || articleObj.copyrightYear;
  articleObj.datePublished = datePublished || articleObj.datePublished;
  articleObj.dateModified = dateModified || articleObj.dateModified;
  articleObj.inLanguage = inLanguage || articleObj.inLanguage;
  articleObj.genre = genre || articleObj.genre;

  scriptElement.innerText = JSON.stringify(articleObj);

  // link rel="author"を書き換える
  if(profile.aboutPageUrl) {
    document.querySelector('link[rel="author"]').setAttribute('href', profile.aboutPageUrl)
  }
}

以下の記事を参考にしました。

yoh1496.hatenablog.com

まとめ

ということで各記事に著者プロフィールの表示とJSON-LDへの情報追加を行いました。

SEOの効果が出るのはしばらく先だと思いますので、また続報があったら記事にしたいと思います。

著者プロフィールの追加、JSON-LDの対応は、はてなブログに関わらずやっておいた方が良さそうですので、まだ実施されていない場合は、ぜひ実施をご検討ください。


  1. data-user-nameは通常はてなIDが入ってるので、それをキーにしています。
  2. Googleが推奨している形式がJSON-LDで、他にもmicrodata, RDFaなどがあります。