@jjlmoya/utils-social 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +60 -0
- package/src/category/i18n/en.ts +23 -0
- package/src/category/i18n/es.ts +23 -0
- package/src/category/i18n/fr.ts +23 -0
- package/src/category/index.ts +13 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +27 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/redditFormatter/bibliography.astro +14 -0
- package/src/tool/redditFormatter/component.astro +246 -0
- package/src/tool/redditFormatter/i18n/en.ts +193 -0
- package/src/tool/redditFormatter/i18n/es.ts +193 -0
- package/src/tool/redditFormatter/i18n/fr.ts +193 -0
- package/src/tool/redditFormatter/index.ts +34 -0
- package/src/tool/redditFormatter/logic.ts +24 -0
- package/src/tool/redditFormatter/seo.astro +14 -0
- package/src/tool/redditFormatter/ui.ts +13 -0
- package/src/tool/safeZones/bibliography.astro +14 -0
- package/src/tool/safeZones/component.astro +857 -0
- package/src/tool/safeZones/i18n/en.ts +244 -0
- package/src/tool/safeZones/i18n/es.ts +244 -0
- package/src/tool/safeZones/i18n/fr.ts +244 -0
- package/src/tool/safeZones/index.ts +34 -0
- package/src/tool/safeZones/logic.ts +137 -0
- package/src/tool/safeZones/seo.astro +14 -0
- package/src/tool/safeZones/ui.ts +34 -0
- package/src/tool/socialImageResizer/bibliography.astro +14 -0
- package/src/tool/socialImageResizer/component.astro +498 -0
- package/src/tool/socialImageResizer/i18n/en.ts +211 -0
- package/src/tool/socialImageResizer/i18n/es.ts +211 -0
- package/src/tool/socialImageResizer/i18n/fr.ts +211 -0
- package/src/tool/socialImageResizer/index.ts +34 -0
- package/src/tool/socialImageResizer/logic.ts +133 -0
- package/src/tool/socialImageResizer/seo.astro +14 -0
- package/src/tool/socialImageResizer/ui.ts +7 -0
- package/src/tool/socialValueCalculator/bibliography.astro +14 -0
- package/src/tool/socialValueCalculator/component.astro +829 -0
- package/src/tool/socialValueCalculator/i18n/en.ts +310 -0
- package/src/tool/socialValueCalculator/i18n/es.ts +310 -0
- package/src/tool/socialValueCalculator/i18n/fr.ts +310 -0
- package/src/tool/socialValueCalculator/index.ts +34 -0
- package/src/tool/socialValueCalculator/logic.ts +60 -0
- package/src/tool/socialValueCalculator/seo.astro +14 -0
- package/src/tool/socialValueCalculator/ui.ts +31 -0
- package/src/tool/tinderPhotoOptimizer/bibliography.astro +14 -0
- package/src/tool/tinderPhotoOptimizer/component.astro +1514 -0
- package/src/tool/tinderPhotoOptimizer/i18n/en.ts +248 -0
- package/src/tool/tinderPhotoOptimizer/i18n/es.ts +248 -0
- package/src/tool/tinderPhotoOptimizer/i18n/fr.ts +248 -0
- package/src/tool/tinderPhotoOptimizer/index.ts +34 -0
- package/src/tool/tinderPhotoOptimizer/logic.ts +199 -0
- package/src/tool/tinderPhotoOptimizer/seo.astro +14 -0
- package/src/tool/tinderPhotoOptimizer/ui.ts +37 -0
- package/src/tool/youtubeThumbnail/bibliography.astro +14 -0
- package/src/tool/youtubeThumbnail/component.astro +419 -0
- package/src/tool/youtubeThumbnail/i18n/en.ts +207 -0
- package/src/tool/youtubeThumbnail/i18n/es.ts +207 -0
- package/src/tool/youtubeThumbnail/i18n/fr.ts +207 -0
- package/src/tool/youtubeThumbnail/index.ts +34 -0
- package/src/tool/youtubeThumbnail/logic.ts +7 -0
- package/src/tool/youtubeThumbnail/seo.astro +14 -0
- package/src/tool/youtubeThumbnail/ui.ts +13 -0
- package/src/tool/youtubeThumbnailPreviewer/bibliography.astro +14 -0
- package/src/tool/youtubeThumbnailPreviewer/component.astro +791 -0
- package/src/tool/youtubeThumbnailPreviewer/i18n/en.ts +321 -0
- package/src/tool/youtubeThumbnailPreviewer/i18n/es.ts +321 -0
- package/src/tool/youtubeThumbnailPreviewer/i18n/fr.ts +321 -0
- package/src/tool/youtubeThumbnailPreviewer/index.ts +34 -0
- package/src/tool/youtubeThumbnailPreviewer/seo.astro +14 -0
- package/src/tool/youtubeThumbnailPreviewer/ui.ts +19 -0
- package/src/tools.ts +10 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { YoutubeThumbnailUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: KnownLocale;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
const t = (ui ?? {}) as YoutubeThumbnailUI;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="ytx-root" data-error-invalid={t.errorInvalid}>
|
|
15
|
+
<div class="ytx-input-panel">
|
|
16
|
+
<div class="ytx-url-container">
|
|
17
|
+
<svg class="ytx-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
18
|
+
<path d="M10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 12.3533 19.1869 14.5168 17.8209 16.2307L23.7335 22.1432C24.124 22.5338 24.1241 23.1669 23.7335 23.5574C23.3429 23.948 22.7098 23.948 22.3193 23.5574L16.4068 17.6449C14.693 19.0108 12.5295 19.824 10.1762 19.824L10 20ZM10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18Z"/>
|
|
19
|
+
</svg>
|
|
20
|
+
<input
|
|
21
|
+
type="url"
|
|
22
|
+
id="ytx-url-input"
|
|
23
|
+
placeholder={t.urlPlaceholder}
|
|
24
|
+
autocomplete="off"
|
|
25
|
+
spellcheck="false"
|
|
26
|
+
/>
|
|
27
|
+
<button id="ytx-paste-btn" type="button" class="ytx-paste-btn" aria-label={t.pasteAriaLabel}>
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
|
|
29
|
+
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2ZM13 18H7V16H13V18ZM17 14H7V12H17V14ZM11 10H7V8H11V10ZM13 9V3.5L18.5 9H13Z"/>
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
<p class="ytx-helper-text">{t.helperText}</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div id="ytx-results" class="ytx-results-panel ytx-hidden">
|
|
37
|
+
<div class="ytx-main-card">
|
|
38
|
+
<div class="ytx-ribbon">{t.ribbonLabel}</div>
|
|
39
|
+
<img id="ytx-main-img" src="" alt={t.imgAlt} />
|
|
40
|
+
<div class="ytx-card-actions">
|
|
41
|
+
<button id="ytx-download-btn" class="ytx-download-btn">
|
|
42
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor">
|
|
43
|
+
<path d="M12 16L7 11L8.4 9.55L11 12.15V4H13V12.15L15.6 9.55L17 11L12 16ZM6 20C5.45 20 4.97917 19.8042 4.5875 19.4125C4.19583 19.0208 4 18.55 4 18V15H6V18H18V15H20V18C20 18.55 19.8042 19.0208 19.4125 19.4125C19.0208 19.8042 18.55 20 18 20H6Z"/>
|
|
44
|
+
</svg>
|
|
45
|
+
{t.downloadBtn}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="ytx-resolutions-grid">
|
|
51
|
+
<div class="ytx-res-card">
|
|
52
|
+
<p class="ytx-res-label">{t.qualityMedium}</p>
|
|
53
|
+
<img id="ytx-mq-img" src="" alt="Medium Quality" />
|
|
54
|
+
<a id="ytx-mq-link" href="#" target="_blank" rel="noopener noreferrer">{t.viewImage}</a>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="ytx-res-card">
|
|
57
|
+
<p class="ytx-res-label">{t.qualityStandard}</p>
|
|
58
|
+
<img id="ytx-sd-img" src="" alt="Standard Quality" />
|
|
59
|
+
<a id="ytx-sd-link" href="#" target="_blank" rel="noopener noreferrer">{t.viewImage}</a>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="ytx-res-card">
|
|
62
|
+
<p class="ytx-res-label">{t.qualityLow}</p>
|
|
63
|
+
<img id="ytx-hq-img" src="" alt="Low Quality" />
|
|
64
|
+
<a id="ytx-hq-link" href="#" target="_blank" rel="noopener noreferrer">{t.viewImage}</a>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div id="ytx-error" class="ytx-error-panel ytx-hidden">
|
|
70
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32">
|
|
71
|
+
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z"/>
|
|
72
|
+
</svg>
|
|
73
|
+
<span id="ytx-error-msg"></span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
import { extractVideoId } from './logic';
|
|
79
|
+
|
|
80
|
+
let currentVideoId = '';
|
|
81
|
+
let errorInvalid = 'Invalid YouTube URL.';
|
|
82
|
+
let urlInput: HTMLInputElement | null = null;
|
|
83
|
+
let resultsPanel: HTMLDivElement | null = null;
|
|
84
|
+
let errorPanel: HTMLDivElement | null = null;
|
|
85
|
+
let errorMsg: HTMLSpanElement | null = null;
|
|
86
|
+
let mainImg: HTMLImageElement | null = null;
|
|
87
|
+
|
|
88
|
+
function updateThumbnails(videoId: string) {
|
|
89
|
+
currentVideoId = videoId;
|
|
90
|
+
resultsPanel?.classList.remove('ytx-hidden');
|
|
91
|
+
errorPanel?.classList.add('ytx-hidden');
|
|
92
|
+
const base = `https://img.youtube.com/vi/${videoId}`;
|
|
93
|
+
const hqUrl = `${base}/hqdefault.jpg`;
|
|
94
|
+
if (mainImg) {
|
|
95
|
+
mainImg.src = `${base}/maxresdefault.jpg`;
|
|
96
|
+
mainImg.onload = () => { if (mainImg!.naturalWidth < 200) mainImg!.src = hqUrl; };
|
|
97
|
+
}
|
|
98
|
+
const set = (id: string, attr: 'src' | 'href', val: string) => { const el = document.getElementById(id); if (el) (el as HTMLElement & Record<string, string>)[attr] = val; };
|
|
99
|
+
set('ytx-mq-img', 'src', `${base}/mqdefault.jpg`);
|
|
100
|
+
set('ytx-sd-img', 'src', `${base}/sddefault.jpg`);
|
|
101
|
+
set('ytx-hq-img', 'src', hqUrl);
|
|
102
|
+
set('ytx-mq-link', 'href', `${base}/mqdefault.jpg`);
|
|
103
|
+
set('ytx-sd-link', 'href', `${base}/sddefault.jpg`);
|
|
104
|
+
set('ytx-hq-link', 'href', hqUrl);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleInput(url: string) {
|
|
108
|
+
if (!url) { resultsPanel?.classList.add('ytx-hidden'); errorPanel?.classList.add('ytx-hidden'); return; }
|
|
109
|
+
const videoId = extractVideoId(url);
|
|
110
|
+
if (videoId) { updateThumbnails(videoId); return; }
|
|
111
|
+
resultsPanel?.classList.add('ytx-hidden');
|
|
112
|
+
errorPanel?.classList.remove('ytx-hidden');
|
|
113
|
+
if (errorMsg) errorMsg.textContent = errorInvalid;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handlePaste() {
|
|
117
|
+
try {
|
|
118
|
+
const text = await navigator.clipboard.readText();
|
|
119
|
+
if (urlInput) { urlInput.value = text; urlInput.dispatchEvent(new Event('input')); }
|
|
120
|
+
} catch { void 0; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleDownload() {
|
|
124
|
+
if (!mainImg?.src || !currentVideoId) return;
|
|
125
|
+
try {
|
|
126
|
+
const blob = await (await fetch(mainImg.src)).blob();
|
|
127
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
128
|
+
const a = document.createElement('a');
|
|
129
|
+
a.href = blobUrl; a.download = `youtube-thumbnail-${currentVideoId}.jpg`;
|
|
130
|
+
document.body.appendChild(a); a.click();
|
|
131
|
+
document.body.removeChild(a); URL.revokeObjectURL(blobUrl);
|
|
132
|
+
} catch { window.open(mainImg.src, '_blank'); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function init() {
|
|
136
|
+
const root = document.querySelector('.ytx-root') as HTMLElement | null;
|
|
137
|
+
if (!root) return;
|
|
138
|
+
errorInvalid = root.dataset.errorInvalid || 'Invalid YouTube URL.';
|
|
139
|
+
urlInput = document.getElementById('ytx-url-input') as HTMLInputElement | null;
|
|
140
|
+
resultsPanel = document.getElementById('ytx-results') as HTMLDivElement | null;
|
|
141
|
+
errorPanel = document.getElementById('ytx-error') as HTMLDivElement | null;
|
|
142
|
+
errorMsg = document.getElementById('ytx-error-msg') as HTMLSpanElement | null;
|
|
143
|
+
mainImg = document.getElementById('ytx-main-img') as HTMLImageElement | null;
|
|
144
|
+
urlInput?.addEventListener('input', (e) => handleInput((e.target as HTMLInputElement).value.trim()));
|
|
145
|
+
document.getElementById('ytx-paste-btn')?.addEventListener('click', handlePaste);
|
|
146
|
+
document.getElementById('ytx-download-btn')?.addEventListener('click', handleDownload);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
document.addEventListener('astro:page-load', init);
|
|
150
|
+
init();
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style>
|
|
154
|
+
.ytx-root {
|
|
155
|
+
--bg: #fff;
|
|
156
|
+
--bg-muted: #f8fafc;
|
|
157
|
+
--bg-img: #f1f5f9;
|
|
158
|
+
--text: #1e293b;
|
|
159
|
+
--text-label: #334155;
|
|
160
|
+
--text-muted: #64748b;
|
|
161
|
+
--text-placeholder: #cbd5e1;
|
|
162
|
+
--border: #e2e8f0;
|
|
163
|
+
--paste-bg: #f1f5f9;
|
|
164
|
+
--paste-hover: #e2e8f0;
|
|
165
|
+
--paste-color: #64748b;
|
|
166
|
+
--paste-hover-color: #0f172a;
|
|
167
|
+
--error-bg: #fef2f2;
|
|
168
|
+
--error-text: #b91c1c;
|
|
169
|
+
--shadow: rgba(0, 0, 0, 0.08);
|
|
170
|
+
|
|
171
|
+
max-width: 900px;
|
|
172
|
+
margin: 0 auto;
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
gap: 2rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
:global(.theme-dark) .ytx-root {
|
|
179
|
+
--bg: #0f172a;
|
|
180
|
+
--bg-muted: #020617;
|
|
181
|
+
--bg-img: #1e293b;
|
|
182
|
+
--text: #f1f5f9;
|
|
183
|
+
--text-label: #cbd5e1;
|
|
184
|
+
--text-muted: #94a3b8;
|
|
185
|
+
--text-placeholder: #475569;
|
|
186
|
+
--border: #1e293b;
|
|
187
|
+
--paste-bg: #1e293b;
|
|
188
|
+
--paste-hover: #334155;
|
|
189
|
+
--paste-color: #94a3b8;
|
|
190
|
+
--paste-hover-color: #f1f5f9;
|
|
191
|
+
--error-bg: rgba(239, 68, 68, 0.1);
|
|
192
|
+
--error-text: #fca5a5;
|
|
193
|
+
--shadow: rgba(0, 0, 0, 0.3);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.ytx-input-panel {
|
|
197
|
+
display: flex;
|
|
198
|
+
flex-direction: column;
|
|
199
|
+
gap: 0.75rem;
|
|
200
|
+
align-items: center;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.ytx-url-container {
|
|
204
|
+
position: relative;
|
|
205
|
+
width: 100%;
|
|
206
|
+
max-width: 700px;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
background: var(--bg);
|
|
210
|
+
border: 2px solid transparent;
|
|
211
|
+
border-radius: 50px;
|
|
212
|
+
box-shadow: 0 10px 40px var(--shadow);
|
|
213
|
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.ytx-url-container:focus-within {
|
|
217
|
+
border-color: #ef4444;
|
|
218
|
+
box-shadow: 0 10px 40px rgba(239, 68, 68, 0.2);
|
|
219
|
+
transform: translateY(-2px);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ytx-search-icon {
|
|
223
|
+
position: absolute;
|
|
224
|
+
left: 1.5rem;
|
|
225
|
+
width: 24px;
|
|
226
|
+
height: 24px;
|
|
227
|
+
color: var(--text-muted);
|
|
228
|
+
flex-shrink: 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#ytx-url-input {
|
|
232
|
+
width: 100%;
|
|
233
|
+
padding: 1.25rem 3.5rem;
|
|
234
|
+
padding-right: 4.5rem;
|
|
235
|
+
font-size: 1.1rem;
|
|
236
|
+
border: none;
|
|
237
|
+
border-radius: 50px;
|
|
238
|
+
background: transparent;
|
|
239
|
+
color: var(--text);
|
|
240
|
+
outline: none;
|
|
241
|
+
font-weight: 500;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#ytx-url-input::placeholder {
|
|
245
|
+
color: var(--text-placeholder);
|
|
246
|
+
font-weight: 400;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.ytx-paste-btn {
|
|
250
|
+
position: absolute;
|
|
251
|
+
right: 0.5rem;
|
|
252
|
+
width: 44px;
|
|
253
|
+
height: 44px;
|
|
254
|
+
border: none;
|
|
255
|
+
border-radius: 50%;
|
|
256
|
+
background: var(--paste-bg);
|
|
257
|
+
color: var(--paste-color);
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
transition: all 0.2s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.ytx-paste-btn:hover {
|
|
266
|
+
background: var(--paste-hover);
|
|
267
|
+
color: var(--paste-hover-color);
|
|
268
|
+
transform: scale(1.05);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.ytx-paste-btn:active { transform: scale(0.95); }
|
|
272
|
+
|
|
273
|
+
.ytx-helper-text {
|
|
274
|
+
color: var(--text-muted);
|
|
275
|
+
font-size: 0.9rem;
|
|
276
|
+
font-weight: 500;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.ytx-results-panel {
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 2rem;
|
|
283
|
+
transition: opacity 0.4s;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.ytx-main-card {
|
|
287
|
+
position: relative;
|
|
288
|
+
background: var(--bg);
|
|
289
|
+
border-radius: 20px;
|
|
290
|
+
padding: 1rem;
|
|
291
|
+
box-shadow: 0 10px 30px var(--shadow);
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
align-items: center;
|
|
295
|
+
gap: 1.5rem;
|
|
296
|
+
overflow: hidden;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.ytx-ribbon {
|
|
300
|
+
position: absolute;
|
|
301
|
+
top: 2rem;
|
|
302
|
+
left: -2.5rem;
|
|
303
|
+
background: #ef4444;
|
|
304
|
+
color: white;
|
|
305
|
+
padding: 0.5rem 3rem;
|
|
306
|
+
font-weight: 800;
|
|
307
|
+
font-size: 0.8rem;
|
|
308
|
+
text-transform: uppercase;
|
|
309
|
+
transform: rotate(-45deg);
|
|
310
|
+
box-shadow: 0 4px 10px rgba(239, 68, 68, 0.3);
|
|
311
|
+
z-index: 10;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#ytx-main-img {
|
|
315
|
+
width: 100%;
|
|
316
|
+
border-radius: 12px;
|
|
317
|
+
box-shadow: 0 5px 15px var(--shadow);
|
|
318
|
+
display: block;
|
|
319
|
+
background: var(--bg-muted);
|
|
320
|
+
min-height: 300px;
|
|
321
|
+
object-fit: cover;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.ytx-card-actions {
|
|
325
|
+
width: 100%;
|
|
326
|
+
display: flex;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.ytx-download-btn {
|
|
331
|
+
background: #ef4444;
|
|
332
|
+
color: white;
|
|
333
|
+
padding: 1rem 2rem;
|
|
334
|
+
border-radius: 50px;
|
|
335
|
+
border: none;
|
|
336
|
+
font-size: 1.1rem;
|
|
337
|
+
font-weight: 700;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 0.75rem;
|
|
342
|
+
transition: all 0.2s;
|
|
343
|
+
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.3);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.ytx-download-btn:hover {
|
|
347
|
+
background: #dc2626;
|
|
348
|
+
transform: translateY(-2px);
|
|
349
|
+
box-shadow: 0 12px 30px rgba(239, 68, 68, 0.4);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.ytx-download-btn:active { transform: translateY(1px); }
|
|
353
|
+
|
|
354
|
+
.ytx-resolutions-grid {
|
|
355
|
+
display: grid;
|
|
356
|
+
grid-template-columns: repeat(3, 1fr);
|
|
357
|
+
gap: 1.5rem;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@media (max-width: 650px) {
|
|
361
|
+
.ytx-resolutions-grid { grid-template-columns: 1fr; }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.ytx-res-card {
|
|
365
|
+
background: var(--bg);
|
|
366
|
+
border-radius: 16px;
|
|
367
|
+
padding: 1rem;
|
|
368
|
+
box-shadow: 0 4px 15px var(--shadow);
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-direction: column;
|
|
371
|
+
gap: 0.75rem;
|
|
372
|
+
align-items: center;
|
|
373
|
+
text-align: center;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.ytx-res-card img {
|
|
377
|
+
width: 100%;
|
|
378
|
+
border-radius: 8px;
|
|
379
|
+
aspect-ratio: 16/9;
|
|
380
|
+
object-fit: cover;
|
|
381
|
+
background: var(--bg-img);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.ytx-res-label {
|
|
385
|
+
margin: 0;
|
|
386
|
+
font-weight: 700;
|
|
387
|
+
color: var(--text-label);
|
|
388
|
+
font-size: 0.95rem;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.ytx-res-card a {
|
|
392
|
+
color: #3b82f6;
|
|
393
|
+
text-decoration: none;
|
|
394
|
+
font-weight: 600;
|
|
395
|
+
font-size: 0.9rem;
|
|
396
|
+
transition: color 0.2s;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.ytx-res-card a:hover {
|
|
400
|
+
color: #2563eb;
|
|
401
|
+
text-decoration: underline;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.ytx-error-panel {
|
|
405
|
+
display: flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 1rem;
|
|
408
|
+
background: var(--error-bg);
|
|
409
|
+
border-left: 4px solid #ef4444;
|
|
410
|
+
color: var(--error-text);
|
|
411
|
+
padding: 1.5rem;
|
|
412
|
+
border-radius: 12px;
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.ytx-hidden {
|
|
417
|
+
display: none;
|
|
418
|
+
}
|
|
419
|
+
</style>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowToThing, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { YoutubeThumbnailUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const slug = 'youtube-thumbnail-extractor';
|
|
6
|
+
const title = 'YouTube Thumbnail Extractor (HD)';
|
|
7
|
+
const description =
|
|
8
|
+
'Extract and download the original thumbnail image from any YouTube video at maximum resolution and without watermarks using just the URL.';
|
|
9
|
+
|
|
10
|
+
const faqData = [
|
|
11
|
+
{
|
|
12
|
+
question: 'At what resolution are thumbnails extracted?',
|
|
13
|
+
answer:
|
|
14
|
+
"The tool always tries to fetch the image at 'MaxRes' resolution (1280x720 or 1920x1080), which is the original quality uploaded by the video creator. If the author did not upload an HD thumbnail, it will automatically show the next highest available resolution.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: 'What types of links are supported?',
|
|
18
|
+
answer:
|
|
19
|
+
'It supports traditional youtube.com/watch URLs, short youtu.be links, mobile links, and even YouTube Shorts URLs transparently.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
question: 'Is it legal to download YouTube thumbnails?',
|
|
23
|
+
answer:
|
|
24
|
+
'Yes, downloading is completely legal for personal use, design, parodies and inspiration (Fair Use). Be careful if you plan to use copyright-protected thumbnails for purely commercial purposes — in those cases it is important to check the video licence.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
question: 'Is there a daily usage limit?',
|
|
28
|
+
answer:
|
|
29
|
+
'There are no monthly or daily limits. Because this software operates client-side by resolving the URL pattern internally in your browser, we do not interact with traditional restrictive APIs.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howToData = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Copy the video URL',
|
|
36
|
+
text: 'Go to YouTube and copy the video link from the browser address bar or the share button.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Paste the link',
|
|
40
|
+
text: 'Enter the URL into the extractor field or use the paste-from-clipboard button.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Select the quality',
|
|
44
|
+
text: 'The tool will display all available resolutions, from maximum quality (1080p) down to the lowest.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Download the image',
|
|
48
|
+
text: 'Click the download button to save the thumbnail to your device.',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
53
|
+
'@context': 'https://schema.org',
|
|
54
|
+
'@type': 'FAQPage',
|
|
55
|
+
mainEntity: faqData.map((item) => ({
|
|
56
|
+
'@type': 'Question',
|
|
57
|
+
name: item.question,
|
|
58
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const howToSchema: WithContext<HowToThing> = {
|
|
63
|
+
'@context': 'https://schema.org',
|
|
64
|
+
'@type': 'HowTo',
|
|
65
|
+
name: title,
|
|
66
|
+
description,
|
|
67
|
+
step: howToData.map((step, i) => ({
|
|
68
|
+
'@type': 'HowToStep',
|
|
69
|
+
position: i + 1,
|
|
70
|
+
name: step.name,
|
|
71
|
+
text: step.text,
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
76
|
+
'@context': 'https://schema.org',
|
|
77
|
+
'@type': 'SoftwareApplication',
|
|
78
|
+
name: title,
|
|
79
|
+
description,
|
|
80
|
+
applicationCategory: 'UtilityApplication',
|
|
81
|
+
operatingSystem: 'All',
|
|
82
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
83
|
+
inLanguage: 'en',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const content: ToolLocaleContent<YoutubeThumbnailUI> = {
|
|
87
|
+
slug,
|
|
88
|
+
title,
|
|
89
|
+
description,
|
|
90
|
+
faqTitle: 'Frequently Asked Questions',
|
|
91
|
+
faq: faqData,
|
|
92
|
+
bibliographyTitle: 'Documentation & References',
|
|
93
|
+
bibliography: [
|
|
94
|
+
{
|
|
95
|
+
name: 'YouTube Help: Add a custom video thumbnail',
|
|
96
|
+
url: 'https://support.google.com/youtube/answer/72431',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'YouTube Creator Academy: Make great thumbnails',
|
|
100
|
+
url: 'https://creatoracademy.youtube.com/',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Google Developers: YouTube Data API — Thumbnails',
|
|
104
|
+
url: 'https://developers.google.com/youtube/v3/docs/thumbnails',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
howTo: howToData,
|
|
108
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
109
|
+
seo: [
|
|
110
|
+
{
|
|
111
|
+
type: 'title',
|
|
112
|
+
text: 'Download YouTube Thumbnails in HD for Free',
|
|
113
|
+
level: 2,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'paragraph',
|
|
117
|
+
html: 'If you need to <strong>download the thumbnail of a YouTube video</strong> at its original size or maximum quality (Full HD or 4K), this free tool is the fastest solution. You can get the cover image from any channel to find inspiration, create your own designs or use it in presentations.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'tip',
|
|
121
|
+
title: 'Extract Cover Images Without Software',
|
|
122
|
+
html: '<p>No need to install software or register. Just copy and paste the video link and you can <strong>save the YouTube thumbnail</strong> directly to your computer or phone.</p>',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'title',
|
|
126
|
+
text: 'How to Get the Thumbnail of a YouTube Video?',
|
|
127
|
+
level: 3,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'paragraph',
|
|
131
|
+
html: 'The process to <strong>grab the image from a YouTube video</strong> is very straightforward. Follow these steps to get the image in seconds:',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'list',
|
|
135
|
+
items: [
|
|
136
|
+
'<strong>1. Copy the URL:</strong> Go to YouTube and copy the video link (either from the browser address bar or the share button).',
|
|
137
|
+
'<strong>2. Paste the link:</strong> Enter the URL into the search field of our thumbnail extractor.',
|
|
138
|
+
'<strong>3. Select the quality:</strong> We will show all available resolutions, from maximum quality (1080p) down to the lowest (360p).',
|
|
139
|
+
'<strong>4. Download the image:</strong> Click the download button to save the <i>thumbnail</i> to your device.',
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'title',
|
|
144
|
+
text: 'Supported Formats and Resolutions',
|
|
145
|
+
level: 3,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'paragraph',
|
|
149
|
+
html: 'Downloading YouTube video covers has never been easier. The tool automatically fetches the best possible image (known as <strong>maxresdefault</strong>), which are usually designs crafted in Photoshop by YouTubers. If the creator did not upload an HD image, you will get the next best option (<strong>hqdefault</strong> or <strong>mqdefault</strong>).',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'paragraph',
|
|
153
|
+
html: 'This utility is perfect for <i>Community Managers</i>, graphic designers, content creators, meme makers, or anyone who needs to <strong>download a video cover image</strong> for their projects. It is also compatible with <strong>YouTube Shorts</strong>, so you can grab their thumbnails in the same way.',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'title',
|
|
157
|
+
text: 'Professional Uses for YouTube Thumbnails',
|
|
158
|
+
level: 2,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: 'paragraph',
|
|
162
|
+
html: 'YouTube thumbnails are high-value visual assets. They represent hours of design work, A/B testing and CTR optimisation by creators. Knowing how to access and download them opens up a wide range of professional possibilities.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'grid',
|
|
166
|
+
columns: [
|
|
167
|
+
{
|
|
168
|
+
type: 'card',
|
|
169
|
+
title: 'Content Creators',
|
|
170
|
+
html: '<p>Analyse the thumbnails of viral videos in your niche to understand which visual elements drive the most clicks: typography, dominant colours, use of faces or text.</p><ul><li><strong>Visual benchmarking:</strong> Compare your thumbnails against the leaders in your sector.</li><li><strong>Design iteration:</strong> Download thumbnails as reference material in Photoshop or Canva.</li><li><strong>Inspiration archive:</strong> Build a library of high-converting thumbnails.</li></ul>',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'card',
|
|
174
|
+
title: 'Designers and Marketers',
|
|
175
|
+
html: '<p>For a graphic designer or <i>social media manager</i>, quick access to YouTube thumbnails is essential for presentations, client proposals and visual audits.</p><ul><li><strong>Client presentations:</strong> Include real competitor screenshots in your proposals.</li><li><strong>Brand audits:</strong> Evaluate the visual consistency of a YouTube channel.</li><li><strong>Moodboards:</strong> Collect high-quality visual references without blurry screenshots.</li></ul>',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: 'title',
|
|
181
|
+
text: 'Compatibility with YouTube Shorts',
|
|
182
|
+
level: 3,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'paragraph',
|
|
186
|
+
html: 'The extractor works perfectly with <strong>YouTube Shorts</strong>. Shorts have their own vertical (9:16) thumbnail format that is generated automatically from the video. This tool recognises Shorts URLs (<code>youtube.com/shorts/ID</code>) and extracts the best available thumbnail in exactly the same way as with standard videos.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: 'tip',
|
|
190
|
+
title: 'Privacy Guaranteed',
|
|
191
|
+
html: '<p>All extraction happens in your browser. No URLs or data are sent to any external server.</p>',
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
ui: {
|
|
195
|
+
urlPlaceholder: 'Paste the YouTube link here...',
|
|
196
|
+
pasteAriaLabel: 'Paste from clipboard',
|
|
197
|
+
helperText: 'Supported formats: youtube.com, youtu.be, shorts.',
|
|
198
|
+
ribbonLabel: 'Maximum Quality',
|
|
199
|
+
downloadBtn: 'Download at Maximum Resolution',
|
|
200
|
+
qualityMedium: 'Medium Quality',
|
|
201
|
+
qualityStandard: 'Standard Quality',
|
|
202
|
+
qualityLow: 'Low Quality',
|
|
203
|
+
viewImage: 'View Image',
|
|
204
|
+
errorInvalid: 'Invalid YouTube URL. Please check and try again.',
|
|
205
|
+
imgAlt: 'YouTube HD Thumbnail',
|
|
206
|
+
},
|
|
207
|
+
};
|