|
12 | 12 | } |
13 | 13 | } |
14 | 14 |
|
| 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 | + |
15 | 38 | // OGP情報を取得する関数 |
16 | 39 | async function fetchOGPData(url) { |
17 | 40 | log('Fetching OGP data for:', url); |
18 | 41 |
|
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); |
23 | 45 |
|
24 | | - const response = await fetch(proxyUrl); |
| 46 | + try { |
| 47 | + const response = await fetchWithTimeout(proxyUrl, 5000); |
25 | 48 |
|
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 | + } |
29 | 53 |
|
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; |
68 | 62 | } |
69 | | - log('Converted image URL:', image); |
70 | | - } |
71 | 63 |
|
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 | + } |
77 | 103 |
|
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 | + } |
83 | 117 | } |
| 118 | + |
| 119 | + // All proxies failed |
| 120 | + console.error('[BlogCard] All proxies failed to fetch OGP data for:', url); |
| 121 | + return null; |
84 | 122 | } |
85 | 123 |
|
86 | 124 | // ブログカードを更新する関数 |
|
141 | 179 | return; |
142 | 180 | } |
143 | 181 |
|
| 182 | + // ローディング状態を追加 |
| 183 | + card.classList.add('loading'); |
| 184 | + |
144 | 185 | // OGP情報を取得して更新 |
| 186 | + const startTime = Date.now(); |
145 | 187 | 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 | + |
146 | 194 | if (ogpData) { |
147 | 195 | updateBlogCard(card, ogpData); |
148 | 196 | } |
|
0 commit comments