<style type="text/css">
.related-posts {
margin-top: 20px;
margin-bottom: 20px;
border-top: 1px solid ; /*上線*/
border-bottom: 1px solid ; /*下線*/
}
.related-posts-title {
text-align: center; /*関連記事中央寄せ*/
font-size: 20px; /*関連記事文字サイズ*/
font-weight: bold; /*関連記事太文字*/
padding-top: 20px;
padding-bottom: 5px;
}
.related-posts-body {
overflow-wrap: break-word;
word-wrap: break-word;
}
/* ul */
.related-posts-list-items {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
list-style: none;
padding: 0;
padding-bottom: 10px;
}
/* li */
.related-posts-item {
flex-shrink: 0;
min-width: 0;
width: 240px; /*画像幅*/
overflow-wrap: break-word;
word-wrap: break-word;
text-align: center;
transition: .3s box-shadow cubic-bezier(.4,0,.2,1);
margin-top: 20px;
}
.related-posts-item:hover {
opacity: .8;
box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)
}
@media (max-width: 768px) {
.related-posts-item {
max-width: 240px; /*画像幅*/
width: 100%;
margin-left: 0;
}
}
.related-posts-item-link {
text-decoration: none;
display: block;
width: 100%;
height: 100%;
}
.related-posts-item-text {
text-align: center; /*記事タイトル中央寄せ*/
padding: 0 10px;
color: ;/*記事タイトル色*/
font-size: 0.9em; /*記事タイトル文字サイズ*/
}
.related-posts-item-date {
text-align: end; /*日付末右寄せ*/
color: ; /*日付文字色*/
font-size: 0px; /*日付文字サイズ*/
}
</style>
<script>
/**
* 設定 ここから
*/
// 関連記事の最大表示数
const maxRelatedPosts = 3;
// ラベル要素を取得するためのCSSセレクター(テーマによってはセレクターが違う場合があると思われ)
const labelSelector = '.post-labels > a';
// 関連記事のリストのタイトル
const relatedPostsTitle = '関連記事';
// 追加する要素のセレクター
const targetSelector = '.post';
// 実行するまでの遅延(1000は一秒)
// (遅延する意味は無いけど非同期で実行されていることが確認できる)
const delayTime = 1000;
/**
* 設定 ここまで
*/
// 現在のページのホストからパスまで example.com/path/to/index.html
const uri = `${location.hostname}${location.pathname}`;
// 現在のページと同じURLかどうかを確認する正規表現
const reCurrentPageURL = new RegExp(
`https?:\\/\\/${uri.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')}`
);
function parseJson(json) {
const items = [];
for (const entry of json.feed.entry) {
for (const link of entry.link) {
if (!('rel' in link) || !('type' in link) || !('href' in link)) {
continue;
}
if (link.rel != 'alternate' || link.type != 'text/html') continue;
if (reCurrentPageURL.test(link.href)) continue;
const item = {
href: link.href,
title: entry.title?.$t,
summary: entry.summary?.$t,
published: entry.published?.$t,
updated: entry.updated?.$t,
thumbnail: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAACWBAMAAAAF9IgvAAAAElBMVEXLy8v+/v7Jycnv7+/W1tbh4eG0JFuyAAAFfElEQVR42u1ZTWOrOAx0HHqv1sldK8i9Ds0dSHrP5///K29kCCENDfS9ffSwnlMhkLGk0chxjYmIiIiIiIiIiIiIiIiIiIiIiIiIiIiI+FFYy/xwj6ejZ/l0A9d2uvAfYw8r+CmITBW8PDArN09WdmuH1/S3kCS7R/BUwZf0iKX9SXbHj30gI9cj31m3fWB3/exmdBd+p1tLyj/jC/bRUvwW+5GlAyNZf+b/e+qGHa/Ylt3wV7H/FdUdRfhWK/4i9s/15BF1r6N5zr63NrlUHdGOin1MhsewH23iadOZrn3sFp20G9FZd8+NY9+Sq27zPgF7bb4rOuLaHhZYTXJAJ1ZF/dB2yaZ0GIVY5RzvgjL1FbN65IcnTSduKPIlP+tAZT/Qm9zYTcs+p1eVhF+IDa7kFk3AJZ6YYWkgl4Mv9PtLWuPS2jR4RsGSNr4pQ7EfqOK+2FP9i62yJ0RqBEX4QA6OZUavys6eKrAKIgiJ9+RORO9WVjX7xrJ9zu7p9kQndo0D6UTmBWl4l2xLb/UHYOcZLbEn40yfAb1OB8O4dEawfuE57fXZ3VN9hthdl70bO60zY8AOQ67YZiBs2UulMDInKlhf8g7dyim94eqAe6m2srqXDMQ+x8NfxA66ELtHoNaA9cqOzGNBgvJTERZ6ctA7MlIlbFZng9iPtX8M1Z39W9YfOxSBkkJsqj/Bw3VvHILqoDMNk5TjX3rBF6kejUGWjKouxM5PN2nKnmX7vrqDfU5rqA5x00L3mrMQp6pOuHT0ChH6nAp0ZelWEEWCrIitzSKtYzfMA+z2uqtGfNjet26TkvELrkLs/+g0SFt2ZN6VTooVnahIwgppof3nMouZgR5c0V4Gh33IvC0aj5cNUntlR90rMKDjruzzG7uduRnBU2iLe9b618w71ZvTOaVen9JZN2nD7FdtMIT6LqYTe5VqQ/axS0mwQtaMgz2DX6HkYjTz2eVyOV7dZphdGltneAXdJixiNwktRDWv7Nxl55nL/CtWtoIwM3ygmQrsBnqEBkazo9pti6F6ndgL613DrtmZ3bHjA0PrFPlH+xl+0Z2Csm/xLWt43SbPT5cRmedG5Ifg0V3V4Xsb1Ukw/lvdoQhN+zFVH9ImQPbrume7D9ju1W2G2TM611NFse+yQ7rvvbFDdSgtaq19gAydTjmKFJahMlhDQns7+IMs1L2kpSbe37Pb0GB+2bJzh93MHGcOXgjVgU2hotd1VVCLZv4oo3ZWGGAwMdtMpbvYLR8celkwXDHuytppm9jR5bBA1XzzptNBDPeQF1rbxm3skNNKWc9h38POOinBfHD6g8+7O9XBZvfIcIVBob29Bd9MJ5vMw4qOw0cRYF8F2uqFHtlBluVYmejWF6pe8K3fg9er3xT6N0q8gvGvUHydA7ZVHfPzvQ1CB7/zPeyoB/YNC4HA3HnnIabgC7XmEdaOQ1uG2WfDKMJzxVZHf0obuM7lLM/Zzw1tH3uBN1O3wCbCh59Y9RdxU/dgEyk2PPTK2rV+Kc3vwr3h687KPq/76dMPuZYdAevGIcN8MybU5a0ZWJpZ7CqDTSFBGYLGeEJBwjKxnRKd73/GDsOG1JLqvNdue8nzc+NK8nGWIvvgcMCS7UxyCWLkFbZRNjnlG2wqJbuc68z/Jns4wMHkqM8SxLTGhVs6Q7GDxkxm3VDpYMZSkAqdr7pY3Ar7ebH2d9nvvWrkkYJtV2llaML3sS95qoObki73hwf1lLFTsR/lDroll+lid961hyb4y3t13cli7zs14p88s/reydMf4ON0yTcd5Cd1lalOibkxhTvhTXgezQ/skx6Hy6d/FrCZkn5Sth87Do+IiIiIiIiIiIiIiIiIiIiIiIiIiIiI+H/iF/5oF83c0tPeAAAAAElFTkSuQmCC',
};
if ('media$thumbnail' in entry) {
item.thumbnail = entry.media$thumbnail.url.replace(
/([=/])s72-[^/&]+/,
'$1w240-h120-n'
);
}
items.push(item);
break;
}
}
return items;
}
function getDateFormattedString(dateString) {
const date = new Date(dateString);
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
function createRelatedPostsListItem(item) {
/* item {
href: string;
title: string;
summary: string;
published: string;
updated: string;
thumbnail: string;
} */
const li = document.createElement('li');
li.className = 'related-posts-item';
const link = document.createElement('a');
link.className = 'related-posts-item-link';
link.href = item.href;
const img = document.createElement('img');
img.className = 'related-posts-item-image';
img.src = item.thumbnail;
img.alt = item.title;
img.title = item.title;
img.width = '240';
img.height = '120';
const div = document.createElement('div');
div.className = 'related-posts-item-text';
const title = document.createElement('p');
title.className = 'related-posts-item-title';
title.innerText = item.title;
div.appendChild(title);
const date = document.createElement('p');
date.className = 'related-posts-item-date';
date.innerText = getDateFormattedString(
item.updated ? item.updated : item.published
);
div.appendChild(date);
link.appendChild(img);
link.appendChild(div);
li.appendChild(link);
return li;
}
function createRelatedPosts(items) {
const divRelatedPosts = document.createElement('div');
divRelatedPosts.className = 'related-posts';
const divTitle = document.createElement('div');
divTitle.className = 'related-posts-title';
divTitle.innerText = relatedPostsTitle;
const divBody = document.createElement('div');
divBody.className = 'related-posts-body';
const ul = document.createElement('ul');
ul.className = 'related-posts-list-items';
items.forEach((item) => {
const li = createRelatedPostsListItem(item);
ul.appendChild(li);
});
divRelatedPosts.appendChild(divTitle);
divRelatedPosts.appendChild(divBody);
divBody.appendChild(ul);
return divRelatedPosts;
}
function responsesToJson(responses) {
return Promise.all(responses.map((response) => response.json()));
}
function failure(error) {
console.log(`${error.message}`);
}
// 引数の data は JSON の配列
function success(data) {
let items = [];
for (const json of data) {
const item = parseJson(json);
// マージして重複を除く
items = [...items, ...item].filter((item, index, array) => {
return array.findIndex((i) => i.href === item.href) === index;
});
}
if (items.length == 0) return;
const relatedPosts = createRelatedPosts(
// slice の範囲指定は配列の要素の個数よりも最大数(maxRelatedPosts)が大きくてもエラーにはならない
items.sort(() => Math.random() - 0.5).slice(0, maxRelatedPosts)
);
const targetElement = document.querySelector(targetSelector);
if (!targetElement) return;
targetElement.appendChild(relatedPosts);
}
// ページのラベルを取得
function getLabels() {
const labels = [];
const re = /\/search\/label\/([^\/]+)/;
const list = document.querySelectorAll(labelSelector);
for (const a of list) {
const m = re.exec(a.href);
if (!m) continue;
labels.push(m[1]);
}
return labels;
}
function handleEvent() {
new Promise((resolve, reject) => {
setTimeout(() => {
const labels = getLabels();
if (!labels.length) return;
const hostname = location.hostname;
const maxResults = 30;
const promises = [];
// 以下のドキュメントには "|" で OR、"," で AND と書かれている。
// https://developers.google.com/gdata/docs/2.0/reference?hl=ja#Queries
// "label1|label2" のようにすると、それぞれのラベルを持つ記事が取得できるはずだけど、
// 機能しなかったのでループを回してラベルごとにリクエストを送る必要がある。
for (const label of labels) {
const rawUrl = `https://${hostname}/feeds/posts/default?alt=json&category=${encodeURIComponent(label)}&max-results=${maxResults}`;
try {
const promise = fetch(rawUrl);
promises.push(promise);
} catch (error) {
console.error(error.message);
}
}
Promise.all(promises)
.then(responsesToJson)
.then(success)
.catch(failure);
resolve(null);
}, delayTime);
});
}
addEventListener('DOMContentLoaded', handleEvent);
</script>