Extract OpenGraph, Twitter Cards, and Google structured data that affects search results appearance
Size
14.4 KB
Version
1.0.1
Created
Apr 5, 2026
Updated
11 days ago
1// ==UserScript==
2// @name SEO Meta & Structured Data Explorer
3// @description Extract OpenGraph, Twitter Cards, and Google structured data that affects search results appearance
4// @version 1.0.1
5// @match *://*/*
6// @icon https://framerusercontent.com/images/30hVevKHnJCJrY87n1ScK6rwCM4.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('OpenGraph Explorer: Extension loaded');
12
13 // Debounce function to prevent excessive calls
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Extract OpenGraph meta tags from the page
27 function extractOpenGraphData() {
28 console.log('OpenGraph Explorer: Extracting OpenGraph data');
29 const ogTags = {};
30 const metaTags = document.querySelectorAll('meta[property^="og:"], meta[name^="og:"]');
31
32 metaTags.forEach(tag => {
33 const property = tag.getAttribute('property') || tag.getAttribute('name');
34 const content = tag.getAttribute('content');
35 if (property && content) {
36 ogTags[property] = content;
37 }
38 });
39
40 // Also check for Twitter Card tags as they're related
41 const twitterTags = document.querySelectorAll('meta[name^="twitter:"]');
42 twitterTags.forEach(tag => {
43 const name = tag.getAttribute('name');
44 const content = tag.getAttribute('content');
45 if (name && content) {
46 ogTags[name] = content;
47 }
48 });
49
50 console.log('OpenGraph Explorer: Found tags:', ogTags);
51 return ogTags;
52 }
53
54 // Create the OpenGraph Explorer UI
55 function createOpenGraphExplorer() {
56 console.log('OpenGraph Explorer: Creating UI');
57
58 // Remove existing explorer if it exists
59 const existingExplorer = document.getElementById('og-explorer');
60 if (existingExplorer) {
61 existingExplorer.remove();
62 }
63
64 const existingToggle = document.getElementById('og-toggle-btn');
65 if (existingToggle) {
66 existingToggle.remove();
67 }
68
69 // Create toggle button
70 const toggleBtn = document.createElement('button');
71 toggleBtn.id = 'og-toggle-btn';
72 toggleBtn.innerHTML = '🔍 OG';
73 toggleBtn.title = 'Toggle OpenGraph Explorer';
74 toggleBtn.style.cssText = `
75 position: fixed;
76 top: 20px;
77 right: 20px;
78 z-index: 10000;
79 background: #2563eb;
80 color: white;
81 border: none;
82 border-radius: 8px;
83 padding: 10px 12px;
84 font-size: 14px;
85 font-weight: 600;
86 cursor: pointer;
87 box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
88 transition: all 0.2s ease;
89 `;
90
91 // Create main explorer panel
92 const explorer = document.createElement('div');
93 explorer.id = 'og-explorer';
94 explorer.style.cssText = `
95 position: fixed;
96 top: 70px;
97 right: 20px;
98 width: 400px;
99 max-height: 80vh;
100 background: white;
101 border: 1px solid #e5e7eb;
102 border-radius: 12px;
103 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
104 z-index: 9999;
105 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
106 display: none;
107 overflow: hidden;
108 `;
109
110 // Extract OpenGraph data
111 const ogData = extractOpenGraphData();
112 const hasData = Object.keys(ogData).length > 0;
113
114 // Create header
115 const header = document.createElement('div');
116 header.style.cssText = `
117 background: #f8fafc;
118 padding: 16px 20px;
119 border-bottom: 1px solid #e5e7eb;
120 display: flex;
121 justify-content: space-between;
122 align-items: center;
123 `;
124
125 const title = document.createElement('h3');
126 title.textContent = 'OpenGraph Explorer';
127 title.style.cssText = `
128 margin: 0;
129 font-size: 16px;
130 font-weight: 600;
131 color: #1f2937;
132 `;
133
134 const closeBtn = document.createElement('button');
135 closeBtn.innerHTML = '×';
136 closeBtn.style.cssText = `
137 background: none;
138 border: none;
139 font-size: 20px;
140 color: #6b7280;
141 cursor: pointer;
142 padding: 0;
143 width: 24px;
144 height: 24px;
145 display: flex;
146 align-items: center;
147 justify-content: center;
148 `;
149
150 header.appendChild(title);
151 header.appendChild(closeBtn);
152
153 // Create content area
154 const content = document.createElement('div');
155 content.style.cssText = `
156 max-height: 60vh;
157 overflow-y: auto;
158 padding: 0;
159 `;
160
161 if (!hasData) {
162 const noDataMsg = document.createElement('div');
163 noDataMsg.style.cssText = `
164 padding: 40px 20px;
165 text-align: center;
166 color: #6b7280;
167 `;
168 noDataMsg.innerHTML = `
169 <div style="font-size: 48px; margin-bottom: 16px;">📄</div>
170 <div style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">No OpenGraph Tags Found</div>
171 <div style="font-size: 14px;">This page doesn't have OpenGraph meta tags.</div>
172 `;
173 content.appendChild(noDataMsg);
174 } else {
175 // Create copy all button
176 const copyAllBtn = document.createElement('button');
177 copyAllBtn.textContent = 'Copy All Data';
178 copyAllBtn.style.cssText = `
179 width: calc(100% - 40px);
180 margin: 20px;
181 padding: 10px;
182 background: #10b981;
183 color: white;
184 border: none;
185 border-radius: 6px;
186 font-size: 14px;
187 font-weight: 500;
188 cursor: pointer;
189 transition: background 0.2s ease;
190 `;
191
192 copyAllBtn.addEventListener('click', async () => {
193 try {
194 const jsonData = JSON.stringify(ogData, null, 2);
195 await GM.setClipboard(jsonData);
196 copyAllBtn.textContent = 'Copied!';
197 copyAllBtn.style.background = '#059669';
198 setTimeout(() => {
199 copyAllBtn.textContent = 'Copy All Data';
200 copyAllBtn.style.background = '#10b981';
201 }, 2000);
202 } catch (error) {
203 console.error('OpenGraph Explorer: Copy failed:', error);
204 }
205 });
206
207 content.appendChild(copyAllBtn);
208
209 // Create tags list
210 const tagsList = document.createElement('div');
211 tagsList.style.cssText = `
212 padding: 0 20px 20px;
213 `;
214
215 Object.entries(ogData).forEach(([property, value]) => {
216 const tagItem = document.createElement('div');
217 tagItem.style.cssText = `
218 margin-bottom: 16px;
219 padding: 12px;
220 background: #f9fafb;
221 border: 1px solid #e5e7eb;
222 border-radius: 8px;
223 `;
224
225 const propertyLabel = document.createElement('div');
226 propertyLabel.textContent = property;
227 propertyLabel.style.cssText = `
228 font-size: 12px;
229 font-weight: 600;
230 color: #374151;
231 margin-bottom: 6px;
232 text-transform: uppercase;
233 letter-spacing: 0.5px;
234 `;
235
236 const valueContainer = document.createElement('div');
237 valueContainer.style.cssText = `
238 display: flex;
239 align-items: flex-start;
240 gap: 8px;
241 `;
242
243 const valueText = document.createElement('div');
244 valueText.style.cssText = `
245 flex: 1;
246 font-size: 14px;
247 color: #1f2937;
248 word-break: break-word;
249 line-height: 1.4;
250 `;
251
252 // Handle different types of content
253 if (property.includes('image') && value.startsWith('http')) {
254 const img = document.createElement('img');
255 img.src = value;
256 img.style.cssText = `
257 max-width: 100%;
258 max-height: 100px;
259 border-radius: 4px;
260 margin-bottom: 8px;
261 display: block;
262 `;
263 valueText.appendChild(img);
264
265 const urlText = document.createElement('div');
266 urlText.textContent = value;
267 urlText.style.cssText = `
268 font-size: 12px;
269 color: #6b7280;
270 word-break: break-all;
271 `;
272 valueText.appendChild(urlText);
273 } else if (value.startsWith('http')) {
274 const link = document.createElement('a');
275 link.href = value;
276 link.textContent = value;
277 link.target = '_blank';
278 link.style.cssText = `
279 color: #2563eb;
280 text-decoration: none;
281 `;
282 valueText.appendChild(link);
283 } else {
284 valueText.textContent = value;
285 }
286
287 const copyBtn = document.createElement('button');
288 copyBtn.innerHTML = '📋';
289 copyBtn.title = 'Copy value';
290 copyBtn.style.cssText = `
291 background: none;
292 border: 1px solid #d1d5db;
293 border-radius: 4px;
294 padding: 4px 6px;
295 cursor: pointer;
296 font-size: 12px;
297 color: #6b7280;
298 transition: all 0.2s ease;
299 `;
300
301 copyBtn.addEventListener('click', async () => {
302 try {
303 await GM.setClipboard(value);
304 copyBtn.innerHTML = '✓';
305 copyBtn.style.color = '#10b981';
306 setTimeout(() => {
307 copyBtn.innerHTML = '📋';
308 copyBtn.style.color = '#6b7280';
309 }, 2000);
310 } catch (error) {
311 console.error('OpenGraph Explorer: Copy failed:', error);
312 }
313 });
314
315 valueContainer.appendChild(valueText);
316 valueContainer.appendChild(copyBtn);
317
318 tagItem.appendChild(propertyLabel);
319 tagItem.appendChild(valueContainer);
320 tagsList.appendChild(tagItem);
321 });
322
323 content.appendChild(tagsList);
324 }
325
326 explorer.appendChild(header);
327 explorer.appendChild(content);
328
329 // Add event listeners
330 let isVisible = false;
331
332 toggleBtn.addEventListener('click', () => {
333 isVisible = !isVisible;
334 explorer.style.display = isVisible ? 'block' : 'none';
335 toggleBtn.style.background = isVisible ? '#1d4ed8' : '#2563eb';
336 });
337
338 closeBtn.addEventListener('click', () => {
339 isVisible = false;
340 explorer.style.display = 'none';
341 toggleBtn.style.background = '#2563eb';
342 });
343
344 // Add hover effects
345 toggleBtn.addEventListener('mouseenter', () => {
346 if (!isVisible) {
347 toggleBtn.style.background = '#1d4ed8';
348 }
349 });
350
351 toggleBtn.addEventListener('mouseleave', () => {
352 if (!isVisible) {
353 toggleBtn.style.background = '#2563eb';
354 }
355 });
356
357 // Append to page
358 document.body.appendChild(toggleBtn);
359 document.body.appendChild(explorer);
360
361 console.log('OpenGraph Explorer: UI created successfully');
362 }
363
364 // Initialize the extension
365 function init() {
366 console.log('OpenGraph Explorer: Initializing extension');
367
368 // Wait for page to be ready
369 if (document.readyState === 'loading') {
370 document.addEventListener('DOMContentLoaded', createOpenGraphExplorer);
371 } else {
372 createOpenGraphExplorer();
373 }
374
375 // Re-scan for OpenGraph tags when page content changes
376 const debouncedUpdate = debounce(createOpenGraphExplorer, 1000);
377
378 const observer = new MutationObserver((mutations) => {
379 let shouldUpdate = false;
380 mutations.forEach((mutation) => {
381 if (mutation.type === 'childList') {
382 mutation.addedNodes.forEach((node) => {
383 if (node.nodeType === Node.ELEMENT_NODE) {
384 if (node.tagName === 'META' || node.querySelector('meta')) {
385 shouldUpdate = true;
386 }
387 }
388 });
389 }
390 });
391
392 if (shouldUpdate) {
393 console.log('OpenGraph Explorer: Page content changed, updating');
394 debouncedUpdate();
395 }
396 });
397
398 observer.observe(document.head, {
399 childList: true,
400 subtree: true
401 });
402
403 console.log('OpenGraph Explorer: Extension initialized successfully');
404 }
405
406 // Start the extension
407 init();
408})();