ブログで使っていたAll in One SEOをやめ、SEO周りの対応をスクラッチで実装しました

WordPressのSEO対策用プラグインで長い間使っていた、All in One SEOをやめて、独自でSchema.orgマークアップを実装したのでその備忘録の記事です。

All in One SEOはとてもよいプラグインなのですが、有料でないと肝心なところが使えなかったり、無料版の仕様が突然変わったりしてしまうことが多々あったので、独自でテーマファイルのfunction.phpにいくつかスクラッチで実装することにしました。

実装するにあたり、Claudeを活用しています。

OGPタグの追加

自分でX(twitter)などに共有するときなどに画像が表示されるようにするためOGPタグの追加をしています。WordPressの通常はアイキャッチの画像が選択され、表示されるのですが、設定していない場合なども対応できるように、アイキャッチがない場合は記事の一番最初に表示される画像を設定できるように工夫しています。

以下はサンプル(動作保証はしていないのでご自身の責任で)

/**
* OGPタグを追加（既存の設定がない場合）
*/
function maker_add_ogp_tags() {
// 既存のOGPタグをチェック
$has_existing_ogp = false;
foreach (wp_get_document_title() as $item) {
if (strpos($item, 'og:') !== false) {
$has_existing_ogp = true;
break;
}
}

if ($has_existing_ogp) {
return;
}

global $post;

// デフォルトのOGP設定
$ogp = array(
'og:site_name' => get_bloginfo('name'),
'og:locale' => get_locale()
);

if (is_singular('post')) {
$ogp['og:type'] = 'article';
$ogp['og:title'] = wp_strip_all_tags(get_the_title());
$ogp['og:description'] = wp_strip_all_tags(get_the_excerpt());
$ogp['og:url'] = get_permalink();

// アイキャッチ画像
if (has_post_thumbnail()) {
$thumbnail_id = get_post_thumbnail_id();
$thumbnail = wp_get_attachment_image_src($thumbnail_id, 'full');
if ($thumbnail) {
$ogp['og:image'] = $thumbnail[0];
$ogp['og:image:width'] = $thumbnail[1];
$ogp['og:image:height'] = $thumbnail[2];
}
}

// 投稿日時
$ogp['article:published_time'] = get_the_date('c');
$ogp['article:modified_time'] = get_the_modified_date('c');

// カテゴリー
$categories = get_the_category();
if (!empty($categories)) {
$ogp['article:section'] = $categories[0]->name;
}

// タグ
$tags = get_the_tags();
if (!empty($tags)) {
$ogp['article:tag'] = wp_list_pluck($tags, 'name');
}
} else {
$ogp['og:type'] = 'website';
}

// OGPタグの出力
echo "\n \n";
foreach ($ogp as $property => $content) {
if (is_array($content)) {
foreach ($content as $item) {
printf(' ' . "\n",
esc_attr($property),
esc_attr($item)
);
}
} else {
printf(' ' . "\n",
esc_attr($property),
esc_attr($content)
);
}
}

// Twitter Card
echo ' ' . "\n";
if (isset($ogp['og:title'])) {
echo ' ' . "\n";
}
if (isset($ogp['og:description'])) {
echo ' ' . "\n";
}
if (isset($ogp['og:image'])) {
echo ' ' . "\n";
}
}
add_action('wp_head', 'maker_add_ogp_tags', $priority['wp_head']);

Schema.orgマークアップを追加

記事の情報やパンクズ周りをSEO対策でもしっかり対応するためにSchema.orgマークアップを追加するようにしていて、現状は記事ページとトップページとページネーション先、カテゴリー一覧などに対応させていて、Google Search Consoleなどの挙動を見てから他の月別の一覧などに対応させるか検討しようと考えています。

以下はサンプル(動作保証はしていないのでご自身の責任で)

/**
* Schema.orgマークアップを追加
*/
function maker_add_schema_markup() {
if (is_home()) {
// ブログトップページ用のスキーマ
$posts = get_posts(array(
'posts_per_page' => 10,
'post_status' => 'publish'
));

$blog_posts = array();
foreach ($posts as $post) {
$thumbnail = get_the_post_thumbnail_url($post->ID, 'full');
$author_data = get_userdata($post->post_author);

$blog_post = array(
'@type' => 'BlogPosting',
'headline' => wp_strip_all_tags(get_the_title($post)),
'description' => wp_strip_all_tags(get_the_excerpt($post)),
'datePublished' => get_the_date('c', $post),
'dateModified' => get_the_modified_date('c', $post),
'url' => get_permalink($post),
'author' => array(
'@type' => 'Person',
'name' => $author_data->display_name,
'url' => get_author_posts_url($author_data->ID)
)
);

if ($thumbnail) {
$thumbnail_id = get_post_thumbnail_id($post->ID);
$thumbnail_meta = wp_get_attachment_metadata($thumbnail_id);
$blog_post['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnail,
'width' => $thumbnail_meta['width'] ?? 1200,
'height' => $thumbnail_meta['height'] ?? 630
);
}

$blog_posts[] = $blog_post;
}

$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Blog',
'url' => get_home_url(),
'name' => get_bloginfo('name'),
'description' => get_bloginfo('description'),
'publisher' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'logo' => array(
'@type' => 'ImageObject',
'url' => get_site_icon_url() ?: '',
'width' => 512,
'height' => 512
)
),
'inLanguage' => get_bloginfo('language'),
'copyrightYear' => date('Y'),
'copyrightHolder' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name')
),
'blogPost' => $blog_posts
);

} elseif (is_singular('post')) {
// 既存の単一記事用のスキーマ処理
global $post;

$thumbnail = get_the_post_thumbnail_url($post->ID, 'full');
$excerpt = wp_strip_all_tags(get_the_excerpt());
$author_data = get_userdata($post->post_author);
$content = wp_strip_all_tags($post->post_content);
$word_count = str_word_count($content);
$reading_time = ceil($word_count / 200);

// 画像の処理
$images = array();
if ($thumbnail) {
$thumbnail_id = get_post_thumbnail_id($post->ID);
$thumbnail_meta = wp_get_attachment_metadata($thumbnail_id);
$images[] = array(
'@type' => 'ImageObject',
'url' => $thumbnail,
'width' => $thumbnail_meta['width'] ?? 1200,
'height' => $thumbnail_meta['height'] ?? 630,
'caption' => wp_strip_all_tags(get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true)),
'primaryImageOfPage' => true
);
}

// 記事内の画像を抽出
preg_match_all('/<img[^>]+>/i', $post->post_content, $img_tags);
foreach ($img_tags[0] as $img_tag) {
preg_match('/src="([^"]+)"/i', $img_tag, $src);
preg_match('/alt="([^"]+)"/i', $img_tag, $alt);
if (!empty($src[1])) {
$images[] = array(
'@type' => 'ImageObject',
'url' => $src[1],
'caption' => !empty($alt[1]) ? $alt[1] : '',
);
}
}

// 著者のスキーマ
$author_schema = array(
'@type' => 'Person',
'name' => $author_data->display_name,
'url' => get_author_posts_url($author_data->ID)
);

if (!empty($author_data->user_description)) {
$author_schema['description'] = wp_strip_all_tags($author_data->user_description);
}

// 著者のアバター画像
$author_avatar_url = get_avatar_url($author_data->ID, array('size' => 96));
if ($author_avatar_url) {
$author_schema['image'] = array(
'@type' => 'ImageObject',
'url' => $author_avatar_url,
'width' => 96,
'height' => 96
);
}

// メインのスキーマ構築
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'BlogPosting',
'mainEntityOfPage' => array(
'@type' => 'WebPage',
'@id' => get_permalink()
),
'headline' => wp_strip_all_tags(get_the_title()),
'description' => $excerpt,
'datePublished' => get_the_date('c'),
'dateModified' => get_the_modified_date('c'),
'author' => $author_schema,
'publisher' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'logo' => array(
'@type' => 'ImageObject',
'url' => get_site_icon_url() ?: '',
'width' => 512,
'height' => 512
)
),
'image' => $images,
'articleBody' => $content,
'wordCount' => $word_count,
'timeRequired' => "PT{$reading_time}M",
'inLanguage' => get_bloginfo('language'),
'copyrightYear' => get_the_date('Y'),
'copyrightHolder' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name')
)
);

// カテゴリーとタグの処理
$categories = get_the_category();
$category_names = wp_list_pluck($categories, 'name');
$tags = get_the_tags();
$tag_names = $tags ? wp_list_pluck($tags, 'name') : array();

if (!empty($category_names)) {
$schema['articleSection'] = implode(', ', $category_names);
}

if (!empty($tag_names)) {
$schema['keywords'] = implode(', ', array_merge($category_names, $tag_names));
}

// コメントの処理
if (comments_open()) {
$schema['commentCount'] = get_comments_number();

$comments = get_comments(array(
'post_id' => $post->ID,
'number' => 5,
'status' => 'approve',
'order' => 'DESC'
));

if (!empty($comments)) {
$schema['comment'] = array_map(function($comment) {
return array(
'@type' => 'Comment',
'dateCreated' => mysql2date('c', $comment->comment_date),
'text' => wp_strip_all_tags($comment->comment_content),
'author' => array(
'@type' => 'Person',
'name' => $comment->comment_author
)
);
}, $comments);
}
}
} elseif (is_category()) {
// カテゴリーアーカイブページ用のスキーマ
$category = get_queried_object();
$posts = get_posts(array(
'posts_per_page' => 10,
'category' => $category->term_id,
'post_status' => 'publish'
));

$list_items = array();
foreach ($posts as $post) {
$thumbnail = get_the_post_thumbnail_url($post->ID, 'full');

$item = array(
'@type' => 'ListItem',
'position' => count($list_items) + 1,
'url' => get_permalink($post),
'name' => wp_strip_all_tags(get_the_title($post)),
'description' => wp_strip_all_tags(get_the_excerpt($post)),
'datePublished' => get_the_date('c', $post)
);

if ($thumbnail) {
$thumbnail_id = get_post_thumbnail_id($post->ID);
$thumbnail_meta = wp_get_attachment_metadata($thumbnail_id);
$item['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnail,
'width' => $thumbnail_meta['width'] ?? 1200,
'height' => $thumbnail_meta['height'] ?? 630
);
}

$list_items[] = $item;
}

$schema = array(
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'mainEntity' => array(
'@type' => 'ItemList',
'itemListElement' => $list_items
),
'name' => single_cat_title('', false),
'description' => wp_strip_all_tags(category_description()),
'url' => get_category_link($category->term_id),
'publisher' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'logo' => array(
'@type' => 'ImageObject',
'url' => get_site_icon_url() ?: '',
'width' => 512,
'height' => 512
)
),
'inLanguage' => get_bloginfo('language'),
'isPartOf' => array(
'@type' => 'WebSite',
'name' => get_bloginfo('name'),
'url' => get_home_url()
)
);

// カテゴリーに親カテゴリーが存在する場合
if ($category->parent) {
$parent_category = get_category($category->parent);
$schema['breadcrumb'] = array(
'@type' => 'BreadcrumbList',
'itemListElement' => array(
array(
'@type' => 'ListItem',
'position' => 1,
'name' => 'Home',
'item' => get_home_url()
),
array(
'@type' => 'ListItem',
'position' => 2,
'name' => $parent_category->name,
'item' => get_category_link($parent_category->term_id)
),
array(
'@type' => 'ListItem',
'position' => 3,
'name' => $category->name,
'item' => get_category_link($category->term_id)
)
)
);
} else {
$schema['breadcrumb'] = array(
'@type' => 'BreadcrumbList',
'itemListElement' => array(
array(
'@type' => 'ListItem',
'position' => 1,
'name' => 'Home',
'item' => get_home_url()
),
array(
'@type' => 'ListItem',
'position' => 2,
'name' => $category->name,
'item' => get_category_link($category->term_id)
)
)
);
}
} elseif (is_archive() && !is_category()) {
// アーカイブページ用のスキーマ（カテゴリーアーカイブ以外）
$archive_title = get_the_archive_title();
$archive_description = get_the_archive_description();

$posts = get_posts(array(
'posts_per_page' => 10,
'post_status' => 'publish'
));

$list_items = array();
foreach ($posts as $post) {
$thumbnail = get_the_post_thumbnail_url($post->ID, 'full');

$item = array(
'@type' => 'ListItem',
'position' => count($list_items) + 1,
'url' => get_permalink($post),
'name' => wp_strip_all_tags(get_the_title($post)),
'description' => wp_strip_all_tags(get_the_excerpt($post)),
'datePublished' => get_the_date('c', $post)
);

if ($thumbnail) {
$thumbnail_id = get_post_thumbnail_id($post->ID);
$thumbnail_meta = wp_get_attachment_metadata($thumbnail_id);
$item['image'] = array(
'@type' => 'ImageObject',
'url' => $thumbnail,
'width' => $thumbnail_meta['width'] ?? 1200,
'height' => $thumbnail_meta['height'] ?? 630
);
}

$list_items[] = $item;
}

$schema = array(
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'mainEntity' => array(
'@type' => 'ItemList',
'itemListElement' => $list_items
),
'name' => wp_strip_all_tags($archive_title),
'description' => wp_strip_all_tags($archive_description),
'url' => get_permalink(),
'publisher' => array(
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'logo' => array(
'@type' => 'ImageObject',
'url' => get_site_icon_url() ?: '',
'width' => 512,
'height' => 512
)
),
'inLanguage' => get_bloginfo('language'),
'isPartOf' => array(
'@type' => 'WebSite',
'name' => get_bloginfo('name'),
'url' => get_home_url()
)
);

// パンくずリストの追加
$schema['breadcrumb'] = array(
'@type' => 'BreadcrumbList',
'itemListElement' => array(
array(
'@type' => 'ListItem',
'position' => 1,
'name' => 'Home',
'item' => get_home_url()
),
array(
'@type' => 'ListItem',
'position' => 2,
'name' => wp_strip_all_tags($archive_title),
'item' => get_permalink()
)
)
);
}

// JSON-LDとして出力
if (isset($schema)) {
$json = wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json) {
echo "\n \n";
echo ' ' . $json . " \n";
}
}
}
add_action('wp_head', 'add_meta_description', 1);

sitemap.xmlの追加

sitemap.xmlは動的に生成されるようにしており、
設定画面から更新することでsitemap.xmlを更新するようにしています。

以下はサンプル(動作保証はしていないのでご自身の責任で)

# 以下を.htaccessに追加（既存のWordPressルールの前）
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^sitemap\.xml$ /feed/sitemap/ [L,R=301]
RewriteRule ^sitemap-([^/]+)\.xml$ /feed/sitemap-$1/ [L,R=301]
</IfModule>

// function.phpに追加
add_action('init', 'add_sitemap_rules');
add_action('do_feed_sitemap', 'generate_sitemap');
add_action('do_feed_sitemap-posts', 'generate_posts_sitemap');
add_action('do_feed_sitemap-categories', 'generate_categories_sitemap');
add_action('do_feed_sitemap-archives', 'generate_archives_sitemap');

function add_sitemap_rules() {
global $wp_rewrite;
add_feed('sitemap', 'generate_sitemap');
add_feed('sitemap-posts', 'generate_posts_sitemap');
add_feed('sitemap-categories', 'generate_categories_sitemap');
add_feed('sitemap-archives', 'generate_archives_sitemap');
$wp_rewrite->flush_rules();
}

function generate_sitemap() {
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
echo '<sitemap><loc>' . home_url('/feed/sitemap-posts/') . '</loc></sitemap>' . "\n";
echo '<sitemap><loc>' . home_url('/feed/sitemap-categories/') . '</loc></sitemap>' . "\n";
echo '<sitemap><loc>' . home_url('/feed/sitemap-archives/') . '</loc></sitemap>' . "\n";
echo '</sitemapindex>';
exit;
}

function generate_posts_sitemap() {
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

$posts = get_posts(array(
'post_type' => 'post',
'posts_per_page' => -1,
'post_status' => 'publish'
));

foreach($posts as $post) {
echo '<url>' . "\n";
echo '<loc>' . get_permalink($post->ID) . '</loc>' . "\n";
echo '<lastmod>' . get_the_modified_date('Y-m-d\TH:i:s+00:00', $post->ID) . '</lastmod>' . "\n";
echo '<changefreq>weekly</changefreq>' . "\n";
echo '<priority>0.8</priority>' . "\n";
echo '</url>' . "\n";
}

echo '</urlset>';
exit;
}

function generate_categories_sitemap() {
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

$categories = get_categories(array(
'hide_empty' => false
));

foreach($categories as $category) {
echo '<url>' . "\n";
echo '<loc>' . get_category_link($category->term_id) . '</loc>' . "\n";
echo '<changefreq>weekly</changefreq>' . "\n";
echo '<priority>0.6</priority>' . "\n";
echo '</url>' . "\n";
}

echo '</urlset>';
exit;
}

function generate_archives_sitemap() {
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

$archives = wp_get_archives(array(
'type' => 'monthly',
'echo' => 0
));

preg_match_all('/<a href=[\'"](.*?)[\'"]/', $archives, $matches);

foreach($matches[1] as $archive_url) {
echo '<url>' . "\n";
echo '<loc>' . esc_url($archive_url) . '</loc>' . "\n";
echo '<changefreq>monthly</changefreq>' . "\n";
echo '<priority>0.4</priority>' . "\n";
echo '</url>' . "\n";
}

echo '</urlset>';
exit;
}

これらを実装する際にAIを活用

AIを利用する前は、自分で調べ、コードを書いてテストをして動くのか、意図通りに動いているかをひとつひとつ確認していたのですが、Claudeを使い始めてからは、○○のようなことはできるか？という視点でサンプルのコードを出力、テスト→エラーコードを分析の流れを繰り返して、数日かかっていた今回のような処理を半日程度で実装できるようになりました。

昨年からAIを取り入れて、記事作成時に使える情報取得のツールなどを作成しつつ、より表示の高速化やSEO対策でも活用できないかと考えた末に、プラグインを取り除きつつ、しっかりとSEO対策をする方法を考え、今回のOGPタグやSchema.orgマークアップ、sitemap.xmlの追加を行うことにしました。

現状、アクセスが下がったりといったこともないため、今回の対策は今のところうまくいっているようなので引き続き、小さなところからコツコツと改善を進めていきます。