Skip to content

Commit b13efda

Browse files
committed
Improve blogcard performance with faster proxies and timeout
Performance improvements: - Add multiple proxy fallback (corsproxy.io as primary, allOrigins as backup) - Implement 5-second timeout per proxy (max 10 seconds total) - Add loading animation while fetching OGP data - Log elapsed time for debugging This should significantly reduce the wait time from 10+ seconds to under 5 seconds in most cases.
1 parent b4ceac2 commit b13efda

File tree

3 files changed

+122
-60
lines changed

3 files changed

+122
-60
lines changed

BLOGCARD_USAGE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ URLのみを指定すると、JavaScriptが自動的にOGP(Open Graph Protocol
102102

103103
- **手動指定を推奨**: 最も確実で安定した表示方法です
104104
- OGP情報の自動取得にはインターネット接続が必要です
105-
- CORS制限を回避するため、プロキシサービス(allOrigins)を使用していますが、不安定になることがあります
105+
- 自動取得は複数のプロキシサービス(corsproxy.io → allOrigins)を順番に試行します
106+
- 各プロキシは5秒でタイムアウトします(最大10秒で諦めます)
107+
- 自動取得中はローディングアニメーションが表示されます
106108
- 一部のサイトではOGP情報が正しく取得できない場合があります。その場合は手動で情報を指定してください
107-
- 自動取得はページ読み込み時に非同期で行われるため、表示されるまでに若干の時間がかかります
108109

109110
## デバッグ方法
110111

static/blogcard.js

Lines changed: 106 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,75 +12,113 @@
1212
}
1313
}
1414

15+
// タイムアウト付きfetch
16+
async function fetchWithTimeout(url, timeout = 5000) {
17+
const controller = new AbortController();
18+
const timeoutId = setTimeout(() => controller.abort(), timeout);
19+
20+
try {
21+
const response = await fetch(url, { signal: controller.signal });
22+
clearTimeout(timeoutId);
23+
return response;
24+
} catch (error) {
25+
clearTimeout(timeoutId);
26+
throw error;
27+
}
28+
}
29+
30+
// 複数のプロキシを試す
31+
const PROXY_SERVICES = [
32+
// corsproxy.io - 高速で信頼性が高い
33+
(url) => `https://corsproxy.io/?${encodeURIComponent(url)}`,
34+
// allOrigins - バックアップ
35+
(url) => `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`,
36+
];
37+
1538
// OGP情報を取得する関数
1639
async function fetchOGPData(url) {
1740
log('Fetching OGP data for:', url);
1841

19-
try {
20-
// CORS制限を回避するため、alloriginsを使用
21-
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
22-
log('Proxy URL:', proxyUrl);
42+
for (let i = 0; i < PROXY_SERVICES.length; i++) {
43+
const proxyUrl = PROXY_SERVICES[i](url);
44+
log(`Trying proxy ${i + 1}/${PROXY_SERVICES.length}:`, proxyUrl);
2345

24-
const response = await fetch(proxyUrl);
46+
try {
47+
const response = await fetchWithTimeout(proxyUrl, 5000);
2548

26-
if (!response.ok) {
27-
throw new Error(`HTTP error! status: ${response.status}`);
28-
}
49+
if (!response.ok) {
50+
log(`Proxy ${i + 1} failed with status:`, response.status);
51+
continue;
52+
}
2953

30-
const data = await response.json();
31-
const html = data.contents;
32-
log('HTML fetched, length:', html.length);
33-
34-
// HTMLパーサーを使用してOGPメタタグを抽出
35-
const parser = new DOMParser();
36-
const doc = parser.parseFromString(html, 'text/html');
37-
38-
// OGPメタタグから情報を取得
39-
const getMetaContent = (property) => {
40-
const element = doc.querySelector(`meta[property="${property}"]`) ||
41-
doc.querySelector(`meta[name="${property}"]`);
42-
return element ? element.getAttribute('content') : null;
43-
};
44-
45-
// タイトルを取得(OGP > title要素の順)
46-
const title = getMetaContent('og:title') ||
47-
doc.querySelector('title')?.textContent ||
48-
url;
49-
50-
// 説明を取得
51-
const description = getMetaContent('og:description') ||
52-
getMetaContent('description') ||
53-
'';
54-
55-
// 画像を取得
56-
let image = getMetaContent('og:image') || '';
57-
log('Raw image URL:', image);
58-
59-
// 相対URLを絶対URLに変換
60-
if (image && !image.startsWith('http')) {
61-
const urlObj = new URL(url);
62-
if (image.startsWith('//')) {
63-
image = urlObj.protocol + image;
64-
} else if (image.startsWith('/')) {
65-
image = urlObj.origin + image;
66-
} else {
67-
image = urlObj.origin + '/' + image;
54+
let html;
55+
if (i === 0) {
56+
// corsproxy.io returns HTML directly
57+
html = await response.text();
58+
} else if (i === 1) {
59+
// allOrigins returns JSON
60+
const data = await response.json();
61+
html = data.contents;
6862
}
69-
log('Converted image URL:', image);
70-
}
7163

72-
const result = {
73-
title: title.trim(),
74-
description: description.trim(),
75-
image: image
76-
};
64+
log('HTML fetched, length:', html.length);
65+
66+
// HTMLパーサーを使用してOGPメタタグを抽出
67+
const parser = new DOMParser();
68+
const doc = parser.parseFromString(html, 'text/html');
69+
70+
// OGPメタタグから情報を取得
71+
const getMetaContent = (property) => {
72+
const element = doc.querySelector(`meta[property="${property}"]`) ||
73+
doc.querySelector(`meta[name="${property}"]`);
74+
return element ? element.getAttribute('content') : null;
75+
};
76+
77+
// タイトルを取得(OGP > title要素の順)
78+
const title = getMetaContent('og:title') ||
79+
doc.querySelector('title')?.textContent ||
80+
url;
81+
82+
// 説明を取得
83+
const description = getMetaContent('og:description') ||
84+
getMetaContent('description') ||
85+
'';
86+
87+
// 画像を取得
88+
let image = getMetaContent('og:image') || '';
89+
log('Raw image URL:', image);
90+
91+
// 相対URLを絶対URLに変換
92+
if (image && !image.startsWith('http')) {
93+
const urlObj = new URL(url);
94+
if (image.startsWith('//')) {
95+
image = urlObj.protocol + image;
96+
} else if (image.startsWith('/')) {
97+
image = urlObj.origin + image;
98+
} else {
99+
image = urlObj.origin + '/' + image;
100+
}
101+
log('Converted image URL:', image);
102+
}
77103

78-
log('OGP data fetched:', result);
79-
return result;
80-
} catch (error) {
81-
console.error('[BlogCard] Error fetching OGP data:', error);
82-
return null;
104+
const result = {
105+
title: title.trim(),
106+
description: description.trim(),
107+
image: image
108+
};
109+
110+
log('OGP data fetched successfully:', result);
111+
return result;
112+
} catch (error) {
113+
log(`Proxy ${i + 1} error:`, error.message);
114+
// Try next proxy
115+
continue;
116+
}
83117
}
118+
119+
// All proxies failed
120+
console.error('[BlogCard] All proxies failed to fetch OGP data for:', url);
121+
return null;
84122
}
85123

86124
// ブログカードを更新する関数
@@ -141,8 +179,18 @@
141179
return;
142180
}
143181

182+
// ローディング状態を追加
183+
card.classList.add('loading');
184+
144185
// OGP情報を取得して更新
186+
const startTime = Date.now();
145187
const ogpData = await fetchOGPData(url);
188+
const elapsedTime = Date.now() - startTime;
189+
log(`Fetch completed in ${elapsedTime}ms`);
190+
191+
// ローディング状態を解除
192+
card.classList.remove('loading');
193+
146194
if (ogpData) {
147195
updateBlogCard(card, ogpData);
148196
}

static/style.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,19 @@ blockquote::before {
179179
transition: transform 0.2s ease, box-shadow 0.2s ease;
180180
}
181181

182+
.blogcard.loading .blogcard-thumbnail-placeholder {
183+
animation: pulse 1.5s ease-in-out infinite;
184+
}
185+
186+
@keyframes pulse {
187+
0%, 100% {
188+
opacity: 1;
189+
}
190+
50% {
191+
opacity: 0.5;
192+
}
193+
}
194+
182195
.blogcard:hover {
183196
transform: translateY(-2px);
184197
box-shadow: 0 4px 12px rgba(255, 168, 106, 0.2);

0 commit comments

Comments
 (0)