@jjlmoya/utils-tools 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 +63 -0
- package/src/category/i18n/en.ts +172 -0
- package/src/category/i18n/es.ts +172 -0
- package/src/category/i18n/fr.ts +172 -0
- package/src/category/index.ts +23 -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 +90 -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/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +23 -0
- package/src/tests/title_quality.test.ts +56 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/date-diff-calculator/bibliography.astro +14 -0
- package/src/tool/date-diff-calculator/component.astro +370 -0
- package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
- package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
- package/src/tool/date-diff-calculator/index.ts +22 -0
- package/src/tool/date-diff-calculator/seo.astro +14 -0
- package/src/tool/date-diff-calculator/ui.ts +17 -0
- package/src/tool/drive-direct-link/bibliography.astro +14 -0
- package/src/tool/drive-direct-link/component.astro +280 -0
- package/src/tool/drive-direct-link/i18n/en.ts +118 -0
- package/src/tool/drive-direct-link/i18n/es.ts +118 -0
- package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
- package/src/tool/drive-direct-link/index.ts +22 -0
- package/src/tool/drive-direct-link/seo.astro +14 -0
- package/src/tool/drive-direct-link/ui.ts +10 -0
- package/src/tool/email-list-cleaner/bibliography.astro +14 -0
- package/src/tool/email-list-cleaner/component.astro +375 -0
- package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
- package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
- package/src/tool/email-list-cleaner/index.ts +22 -0
- package/src/tool/email-list-cleaner/seo.astro +14 -0
- package/src/tool/email-list-cleaner/ui.ts +15 -0
- package/src/tool/env-badge-spain/bibliography.astro +14 -0
- package/src/tool/env-badge-spain/component.astro +303 -0
- package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
- package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
- package/src/tool/env-badge-spain/i18n/en.ts +153 -0
- package/src/tool/env-badge-spain/i18n/es.ts +153 -0
- package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
- package/src/tool/env-badge-spain/index.ts +22 -0
- package/src/tool/env-badge-spain/seo.astro +14 -0
- package/src/tool/env-badge-spain/ui.ts +53 -0
- package/src/tool/morse-beacon/bibliography.astro +14 -0
- package/src/tool/morse-beacon/component.astro +534 -0
- package/src/tool/morse-beacon/i18n/en.ts +157 -0
- package/src/tool/morse-beacon/i18n/es.ts +157 -0
- package/src/tool/morse-beacon/i18n/fr.ts +157 -0
- package/src/tool/morse-beacon/index.ts +22 -0
- package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
- package/src/tool/morse-beacon/seo.astro +14 -0
- package/src/tool/morse-beacon/ui.ts +18 -0
- package/src/tool/password-generator/bibliography.astro +14 -0
- package/src/tool/password-generator/component.astro +259 -0
- package/src/tool/password-generator/components/Config.astro +227 -0
- package/src/tool/password-generator/components/Display.astro +147 -0
- package/src/tool/password-generator/components/Strength.astro +70 -0
- package/src/tool/password-generator/i18n/en.ts +166 -0
- package/src/tool/password-generator/i18n/es.ts +166 -0
- package/src/tool/password-generator/i18n/fr.ts +166 -0
- package/src/tool/password-generator/index.ts +22 -0
- package/src/tool/password-generator/seo.astro +14 -0
- package/src/tool/password-generator/ui.ts +16 -0
- package/src/tool/routes/bibliography.astro +14 -0
- package/src/tool/routes/component.astro +543 -0
- package/src/tool/routes/i18n/en.ts +157 -0
- package/src/tool/routes/i18n/es.ts +157 -0
- package/src/tool/routes/i18n/fr.ts +157 -0
- package/src/tool/routes/index.ts +22 -0
- package/src/tool/routes/logic/GeocodingService.ts +60 -0
- package/src/tool/routes/logic/RouteManager.ts +192 -0
- package/src/tool/routes/logic/RouteService.ts +66 -0
- package/src/tool/routes/seo.astro +14 -0
- package/src/tool/routes/ui.ts +16 -0
- package/src/tool/rule-of-three/bibliography.astro +14 -0
- package/src/tool/rule-of-three/component.astro +369 -0
- package/src/tool/rule-of-three/i18n/en.ts +171 -0
- package/src/tool/rule-of-three/i18n/es.ts +171 -0
- package/src/tool/rule-of-three/i18n/fr.ts +171 -0
- package/src/tool/rule-of-three/index.ts +22 -0
- package/src/tool/rule-of-three/seo.astro +14 -0
- package/src/tool/rule-of-three/ui.ts +13 -0
- package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
- package/src/tool/seo-content-optimizer/component.astro +552 -0
- package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
- package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
- package/src/tool/seo-content-optimizer/index.ts +22 -0
- package/src/tool/seo-content-optimizer/seo.astro +14 -0
- package/src/tool/seo-content-optimizer/ui.ts +29 -0
- package/src/tool/speed-reader/bibliography.astro +14 -0
- package/src/tool/speed-reader/component.astro +586 -0
- package/src/tool/speed-reader/i18n/en.ts +152 -0
- package/src/tool/speed-reader/i18n/es.ts +152 -0
- package/src/tool/speed-reader/i18n/fr.ts +152 -0
- package/src/tool/speed-reader/index.ts +22 -0
- package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
- package/src/tool/speed-reader/seo.astro +14 -0
- package/src/tool/speed-reader/ui.ts +14 -0
- package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
- package/src/tool/text-pixel-calculator/component.astro +315 -0
- package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
- package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
- package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
- package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
- package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
- package/src/tool/text-pixel-calculator/index.ts +22 -0
- package/src/tool/text-pixel-calculator/seo.astro +14 -0
- package/src/tool/text-pixel-calculator/ui.ts +15 -0
- package/src/tool/whatsapp-link/bibliography.astro +14 -0
- package/src/tool/whatsapp-link/component.astro +455 -0
- package/src/tool/whatsapp-link/i18n/en.ts +128 -0
- package/src/tool/whatsapp-link/i18n/es.ts +128 -0
- package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
- package/src/tool/whatsapp-link/index.ts +22 -0
- package/src/tool/whatsapp-link/seo.astro +14 -0
- package/src/tool/whatsapp-link/ui.ts +15 -0
- package/src/tools.ts +15 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SeoContentOptimizerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
const t = (ui ?? {}) as SeoContentOptimizerUI;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="sco-root" data-ui={JSON.stringify(t)}>
|
|
13
|
+
<div class="sco-tabs">
|
|
14
|
+
<button class="sco-tab active" data-tab="text" type="button">{t.tabText}</button>
|
|
15
|
+
<button class="sco-tab" data-tab="html" type="button">{t.tabHtml}</button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="sco-grid">
|
|
19
|
+
<div class="sco-input-card">
|
|
20
|
+
<textarea
|
|
21
|
+
id="sco-input"
|
|
22
|
+
class="sco-textarea"
|
|
23
|
+
placeholder={t.textareaPlaceholder}
|
|
24
|
+
spellcheck="false"
|
|
25
|
+
></textarea>
|
|
26
|
+
|
|
27
|
+
<div class="sco-stats">
|
|
28
|
+
<div class="sco-stat">
|
|
29
|
+
<span class="sco-stat-value" id="sco-chars">0</span>
|
|
30
|
+
<span class="sco-stat-label">{t.statsChars}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="sco-stat">
|
|
33
|
+
<span class="sco-stat-value" id="sco-words">0</span>
|
|
34
|
+
<span class="sco-stat-label">{t.statsWords}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="sco-stat">
|
|
37
|
+
<span class="sco-stat-value" id="sco-time">0s</span>
|
|
38
|
+
<span class="sco-stat-label">{t.statsReading}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="sco-stat">
|
|
41
|
+
<span class="sco-stat-value" id="sco-sentences">0</span>
|
|
42
|
+
<span class="sco-stat-label">{t.statsSentences}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="sco-panels">
|
|
48
|
+
<div class="sco-panel">
|
|
49
|
+
<h3 class="sco-panel-title">{t.checklistTitle}</h3>
|
|
50
|
+
<div id="sco-checklist" class="sco-checklist">
|
|
51
|
+
<div class="sco-check-item">
|
|
52
|
+
<div class="sco-check-status sco-error"><svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg></div>
|
|
53
|
+
<span>{t.checkInsufficient}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="sco-panel" id="sco-density-panel">
|
|
59
|
+
<h3 class="sco-panel-title">{t.keywordsTitle}</h3>
|
|
60
|
+
<div id="sco-keywords" class="sco-keywords">
|
|
61
|
+
<div class="sco-empty">{t.emptyState}</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="sco-panel" id="sco-technical-panel" style="display: none">
|
|
66
|
+
<h3 class="sco-panel-title">{t.technicalTitle}</h3>
|
|
67
|
+
<div class="sco-tech-stats">
|
|
68
|
+
<div class="sco-tech-stat">
|
|
69
|
+
<span class="sco-tech-label">{t.h1Label}</span>
|
|
70
|
+
<span class="sco-tech-value" id="sco-h1">0</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="sco-tech-stat">
|
|
73
|
+
<span class="sco-tech-label">{t.linksLabel}</span>
|
|
74
|
+
<span class="sco-tech-value" id="sco-links">0</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="sco-tech-stat">
|
|
77
|
+
<span class="sco-tech-label">{t.imgsLabel}</span>
|
|
78
|
+
<span class="sco-tech-value" id="sco-imgs">0</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="sco-tech-stat">
|
|
81
|
+
<span class="sco-tech-label">{t.altsLabel}</span>
|
|
82
|
+
<span class="sco-tech-value" id="sco-alts">0</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
.sco-root {
|
|
92
|
+
--sco-blue: #3b82f6;
|
|
93
|
+
--sco-green: #10b981;
|
|
94
|
+
--sco-red: #ef4444;
|
|
95
|
+
--sco-muted: #64748b;
|
|
96
|
+
--sco-tab-bg: rgba(255, 255, 255, 0.5);
|
|
97
|
+
--sco-tab-active-bg: #fff;
|
|
98
|
+
--sco-tab-active-color: #3b82f6;
|
|
99
|
+
--sco-card-bg: rgba(255, 255, 255, 0.7);
|
|
100
|
+
--sco-card-border: rgba(255, 255, 255, 0.3);
|
|
101
|
+
--sco-textarea-color: #1e293b;
|
|
102
|
+
--sco-stat-bg: #f8fafc;
|
|
103
|
+
--sco-stat-border: rgba(0, 0, 0, 0.05);
|
|
104
|
+
--sco-panel-bg: #fff;
|
|
105
|
+
--sco-panel-border: #e2e8f0;
|
|
106
|
+
--sco-check-bg: #f8fafc;
|
|
107
|
+
--sco-keyword-bg: #f1f5f9;
|
|
108
|
+
--sco-keyword-name: #1e293b;
|
|
109
|
+
--sco-tech-border: #f1f5f9;
|
|
110
|
+
|
|
111
|
+
width: 100%;
|
|
112
|
+
max-width: 1200px;
|
|
113
|
+
margin: 0 auto;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 2rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:global(.theme-dark) .sco-root {
|
|
120
|
+
--sco-tab-bg: rgba(15, 23, 42, 0.5);
|
|
121
|
+
--sco-tab-active-bg: #1e293b;
|
|
122
|
+
--sco-tab-active-color: #60a5fa;
|
|
123
|
+
--sco-card-bg: rgba(15, 23, 42, 0.6);
|
|
124
|
+
--sco-card-border: rgba(255, 255, 255, 0.05);
|
|
125
|
+
--sco-textarea-color: #f1f5f9;
|
|
126
|
+
--sco-stat-bg: #1e293b;
|
|
127
|
+
--sco-stat-border: rgba(255, 255, 255, 0.05);
|
|
128
|
+
--sco-panel-bg: #0f172a;
|
|
129
|
+
--sco-panel-border: #1e293b;
|
|
130
|
+
--sco-check-bg: #1e293b;
|
|
131
|
+
--sco-keyword-bg: #0f172a;
|
|
132
|
+
--sco-keyword-name: #f1f5f9;
|
|
133
|
+
--sco-tech-border: #1e293b;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sco-tabs {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 1rem;
|
|
139
|
+
background: var(--sco-tab-bg);
|
|
140
|
+
backdrop-filter: blur(10px);
|
|
141
|
+
padding: 0.5rem;
|
|
142
|
+
border-radius: 1rem;
|
|
143
|
+
width: fit-content;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.sco-tab {
|
|
147
|
+
padding: 0.75rem 1.5rem;
|
|
148
|
+
border-radius: 0.75rem;
|
|
149
|
+
font-weight: 700;
|
|
150
|
+
font-size: 0.9rem;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
border: none;
|
|
153
|
+
transition: background 0.2s, color 0.2s;
|
|
154
|
+
background: transparent;
|
|
155
|
+
color: var(--sco-muted);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.sco-tab.active {
|
|
159
|
+
background: var(--sco-tab-active-bg);
|
|
160
|
+
color: var(--sco-tab-active-color);
|
|
161
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.sco-grid {
|
|
165
|
+
display: grid;
|
|
166
|
+
grid-template-columns: 1fr;
|
|
167
|
+
gap: 1.5rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (min-width: 1024px) {
|
|
171
|
+
.sco-grid {
|
|
172
|
+
grid-template-columns: 1.5fr 1fr;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.sco-input-card {
|
|
177
|
+
background: var(--sco-card-bg);
|
|
178
|
+
backdrop-filter: blur(20px);
|
|
179
|
+
border: 1px solid var(--sco-card-border);
|
|
180
|
+
border-radius: 1.5rem;
|
|
181
|
+
padding: 1.5rem;
|
|
182
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sco-textarea {
|
|
186
|
+
width: 100%;
|
|
187
|
+
min-height: 450px;
|
|
188
|
+
background: transparent;
|
|
189
|
+
border: none;
|
|
190
|
+
resize: none;
|
|
191
|
+
font-size: 1.1rem;
|
|
192
|
+
line-height: 1.6;
|
|
193
|
+
color: var(--sco-textarea-color);
|
|
194
|
+
box-sizing: border-box;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.sco-textarea:focus {
|
|
198
|
+
outline: none;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.sco-stats {
|
|
202
|
+
display: grid;
|
|
203
|
+
grid-template-columns: repeat(4, 1fr);
|
|
204
|
+
gap: 1rem;
|
|
205
|
+
margin-top: 1.5rem;
|
|
206
|
+
padding-top: 1.5rem;
|
|
207
|
+
border-top: 1px solid var(--sco-stat-border);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@media (max-width: 480px) {
|
|
211
|
+
.sco-stats {
|
|
212
|
+
grid-template-columns: repeat(2, 1fr);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.sco-stat {
|
|
217
|
+
text-align: center;
|
|
218
|
+
padding: 1rem;
|
|
219
|
+
background: var(--sco-stat-bg);
|
|
220
|
+
border-radius: 1rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sco-stat-value {
|
|
224
|
+
display: block;
|
|
225
|
+
font-size: 1.5rem;
|
|
226
|
+
font-weight: 800;
|
|
227
|
+
color: var(--sco-blue);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sco-stat-label {
|
|
231
|
+
font-size: 0.75rem;
|
|
232
|
+
font-weight: 700;
|
|
233
|
+
color: var(--sco-muted);
|
|
234
|
+
text-transform: uppercase;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.sco-panels {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 1.5rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.sco-panel {
|
|
244
|
+
background: var(--sco-panel-bg);
|
|
245
|
+
border: 1px solid var(--sco-panel-border);
|
|
246
|
+
border-radius: 1.25rem;
|
|
247
|
+
padding: 1.5rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.sco-panel-title {
|
|
251
|
+
font-size: 0.875rem;
|
|
252
|
+
font-weight: 800;
|
|
253
|
+
margin: 0 0 1.25rem;
|
|
254
|
+
text-transform: uppercase;
|
|
255
|
+
letter-spacing: 0.05em;
|
|
256
|
+
color: var(--sco-muted);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.sco-checklist {
|
|
260
|
+
display: flex;
|
|
261
|
+
flex-direction: column;
|
|
262
|
+
gap: 0.75rem;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
:global(.sco-check-item) {
|
|
266
|
+
display: flex;
|
|
267
|
+
gap: 0.75rem;
|
|
268
|
+
align-items: flex-start;
|
|
269
|
+
padding: 0.75rem;
|
|
270
|
+
border-radius: 0.75rem;
|
|
271
|
+
background: var(--sco-check-bg, #f8fafc);
|
|
272
|
+
font-size: 0.85rem;
|
|
273
|
+
font-weight: 500;
|
|
274
|
+
color: inherit;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
:global(.sco-check-status) {
|
|
278
|
+
width: 20px;
|
|
279
|
+
height: 20px;
|
|
280
|
+
border-radius: 50%;
|
|
281
|
+
flex-shrink: 0;
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
font-size: 0.7rem;
|
|
286
|
+
font-weight: 900;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
:global(.sco-success) {
|
|
290
|
+
background: #dcfce7;
|
|
291
|
+
color: #15803d;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
:global(.sco-warning) {
|
|
295
|
+
background: #fef9c3;
|
|
296
|
+
color: #a16207;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
:global(.sco-error) {
|
|
300
|
+
background: #fee2e2;
|
|
301
|
+
color: #b91c1c;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.sco-keywords {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
gap: 0.5rem;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
:global(.sco-keyword-item) {
|
|
311
|
+
display: flex;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
padding: 0.5rem 0.75rem;
|
|
314
|
+
background: var(--sco-keyword-bg, #f1f5f9);
|
|
315
|
+
border-radius: 0.5rem;
|
|
316
|
+
font-size: 0.9rem;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
:global(.sco-keyword-name) {
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
color: var(--sco-keyword-name, #1e293b);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
:global(.sco-keyword-count) {
|
|
325
|
+
font-weight: 700;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.sco-empty {
|
|
329
|
+
font-size: 0.875rem;
|
|
330
|
+
color: var(--sco-muted);
|
|
331
|
+
text-align: center;
|
|
332
|
+
padding: 0.5rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
:global(.sco-empty-injected) {
|
|
336
|
+
font-size: 0.875rem;
|
|
337
|
+
color: var(--sco-muted, #64748b);
|
|
338
|
+
text-align: center;
|
|
339
|
+
padding: 0.5rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sco-tech-stats {
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-direction: column;
|
|
345
|
+
gap: 1.25rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.sco-tech-stat {
|
|
349
|
+
display: flex;
|
|
350
|
+
justify-content: space-between;
|
|
351
|
+
align-items: center;
|
|
352
|
+
padding-bottom: 0.75rem;
|
|
353
|
+
border-bottom: 1px solid var(--sco-tech-border);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.sco-tech-label {
|
|
357
|
+
font-size: 0.8rem;
|
|
358
|
+
font-weight: 700;
|
|
359
|
+
color: var(--sco-muted);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.sco-tech-value {
|
|
363
|
+
font-size: 0.8rem;
|
|
364
|
+
font-weight: 600;
|
|
365
|
+
color: var(--sco-blue);
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
368
|
+
|
|
369
|
+
<script>
|
|
370
|
+
import type { SeoContentOptimizerUI } from './ui';
|
|
371
|
+
|
|
372
|
+
const root = document.querySelector('.sco-root') as HTMLElement;
|
|
373
|
+
const t = JSON.parse(root?.dataset.ui ?? '{}') as SeoContentOptimizerUI;
|
|
374
|
+
const stopWords = new Set<string>(JSON.parse(t.stopWords ?? '[]'));
|
|
375
|
+
|
|
376
|
+
const textarea = document.getElementById('sco-input') as HTMLTextAreaElement;
|
|
377
|
+
const charsEl = document.getElementById('sco-chars');
|
|
378
|
+
const wordsEl = document.getElementById('sco-words');
|
|
379
|
+
const timeEl = document.getElementById('sco-time');
|
|
380
|
+
const sentencesEl = document.getElementById('sco-sentences');
|
|
381
|
+
const keywordsEl = document.getElementById('sco-keywords');
|
|
382
|
+
const checklistEl = document.getElementById('sco-checklist');
|
|
383
|
+
const densityPanel = document.getElementById('sco-density-panel');
|
|
384
|
+
const technicalPanel = document.getElementById('sco-technical-panel');
|
|
385
|
+
const h1El = document.getElementById('sco-h1');
|
|
386
|
+
const linksEl = document.getElementById('sco-links');
|
|
387
|
+
const imgsEl = document.getElementById('sco-imgs');
|
|
388
|
+
const altsEl = document.getElementById('sco-alts');
|
|
389
|
+
|
|
390
|
+
let activeTab = 'text';
|
|
391
|
+
|
|
392
|
+
function analyzeHTML(html: string): void {
|
|
393
|
+
const parser = new DOMParser();
|
|
394
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
395
|
+
const h1s = doc.querySelectorAll('h1');
|
|
396
|
+
const links = doc.querySelectorAll('a');
|
|
397
|
+
const imgs = doc.querySelectorAll('img');
|
|
398
|
+
const missingAlts = Array.from(imgs).filter((img) => !img.getAttribute('alt') || img.getAttribute('alt')?.trim() === '').length;
|
|
399
|
+
|
|
400
|
+
if (h1El) {
|
|
401
|
+
h1El.textContent = h1s.length.toString();
|
|
402
|
+
h1El.style.color = h1s.length === 1 ? '#10b981' : '#ef4444';
|
|
403
|
+
}
|
|
404
|
+
if (linksEl) linksEl.textContent = links.length.toString();
|
|
405
|
+
if (imgsEl) imgsEl.textContent = imgs.length.toString();
|
|
406
|
+
if (altsEl) {
|
|
407
|
+
altsEl.textContent = missingAlts.toString();
|
|
408
|
+
altsEl.style.color = missingAlts > 0 ? '#ef4444' : '#10b981';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function buildChecks(wordCount: number, sentenceCount: number, sentences: string[], rawText: string): { status: string; text: string }[] {
|
|
413
|
+
const checks: { status: string; text: string }[] = [];
|
|
414
|
+
|
|
415
|
+
if (wordCount < 300) {
|
|
416
|
+
checks.push({ status: 'sco-error', text: t.checkInsufficient });
|
|
417
|
+
} else if (wordCount >= 900) {
|
|
418
|
+
checks.push({ status: 'sco-success', text: t.checkPillar });
|
|
419
|
+
} else {
|
|
420
|
+
checks.push({ status: 'sco-success', text: t.checkGoodLength });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const longSentences = sentences.filter((s) => s.trim().split(/\s+/).length > 20).length;
|
|
424
|
+
const longRatio = sentenceCount > 0 ? (longSentences / sentenceCount) * 100 : 0;
|
|
425
|
+
if (longRatio > 25) {
|
|
426
|
+
checks.push({ status: 'sco-warning', text: t.checkLongSentences });
|
|
427
|
+
} else if (wordCount > 50) {
|
|
428
|
+
checks.push({ status: 'sco-success', text: t.checkGoodReadability });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const paragraphs = rawText.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
432
|
+
const megaParas = paragraphs.filter((p) => p.trim().split(/\s+/).length > 150).length;
|
|
433
|
+
if (megaParas > 0) {
|
|
434
|
+
checks.push({ status: 'sco-error', text: t.checkLongParagraphs });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (activeTab === 'html') {
|
|
438
|
+
const parser = new DOMParser();
|
|
439
|
+
const doc = parser.parseFromString(rawText, 'text/html');
|
|
440
|
+
const h1s = doc.querySelectorAll('h1');
|
|
441
|
+
const h2s = doc.querySelectorAll('h2');
|
|
442
|
+
const title = doc.querySelector('title');
|
|
443
|
+
|
|
444
|
+
if (h1s.length === 0) {
|
|
445
|
+
checks.push({ status: 'sco-error', text: t.checkMissingH1 });
|
|
446
|
+
} else if (h1s.length > 1) {
|
|
447
|
+
checks.push({ status: 'sco-warning', text: t.checkMultipleH1 });
|
|
448
|
+
}
|
|
449
|
+
if (h2s.length === 0 && wordCount > 300) {
|
|
450
|
+
checks.push({ status: 'sco-warning', text: t.checkMissingH2 });
|
|
451
|
+
}
|
|
452
|
+
if (!title) {
|
|
453
|
+
checks.push({ status: 'sco-error', text: t.checkMissingTitle });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return checks;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const SVG_CHECK = '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M21 7L9 19l-5.5-5.5 1.41-1.41L9 16.17 19.59 5.59 21 7z"/></svg>';
|
|
461
|
+
const SVG_ALERT = '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>';
|
|
462
|
+
const SVG_CLOSE = '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>';
|
|
463
|
+
const STATUS_ICON: Record<string, string> = { 'sco-success': SVG_CHECK, 'sco-warning': SVG_ALERT, 'sco-error': SVG_CLOSE };
|
|
464
|
+
|
|
465
|
+
function updateChecklist(wordCount: number, sentenceCount: number, sentences: string[], rawText: string): void {
|
|
466
|
+
if (!checklistEl) return;
|
|
467
|
+
const checks = buildChecks(wordCount, sentenceCount, sentences, rawText);
|
|
468
|
+
checklistEl.innerHTML = checks
|
|
469
|
+
.map((c) => `<div class="sco-check-item"><div class="sco-check-status ${c.status}">${STATUS_ICON[c.status] ?? 'x'}</div><span>${c.text}</span></div>`)
|
|
470
|
+
.join('');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function updateKeywords(words: string[]): void {
|
|
474
|
+
if (!keywordsEl) return;
|
|
475
|
+
const counts: Record<string, number> = {};
|
|
476
|
+
words.forEach((word) => {
|
|
477
|
+
const norm = word.toLowerCase().replace(/[.,!?;:()[\]]/g, '');
|
|
478
|
+
if (norm.length > 3 && !stopWords.has(norm)) {
|
|
479
|
+
counts[norm] = (counts[norm] ?? 0) + 1;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
|
|
484
|
+
if (sorted.length === 0) {
|
|
485
|
+
keywordsEl.innerHTML = `<div class="sco-empty-injected">${t.analyzing}</div>`;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
keywordsEl.innerHTML = sorted
|
|
490
|
+
.map(([name, count]) => {
|
|
491
|
+
const density = (count / words.length) * 100;
|
|
492
|
+
let color = '#64748b';
|
|
493
|
+
if (density > 3.5) color = '#ef4444';
|
|
494
|
+
else if (density >= 0.8) color = '#10b981';
|
|
495
|
+
return `<div class="sco-keyword-item"><span class="sco-keyword-name">${name}</span><span class="sco-keyword-count" style="color:${color}">${density.toFixed(1)}% (${count})</span></div>`;
|
|
496
|
+
})
|
|
497
|
+
.join('');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function analyzeText(rawText: string): void {
|
|
501
|
+
let text = rawText;
|
|
502
|
+
|
|
503
|
+
if (activeTab === 'html') {
|
|
504
|
+
const parser = new DOMParser();
|
|
505
|
+
const doc = parser.parseFromString(rawText.replace(/></g, '> <'), 'text/html');
|
|
506
|
+
text = doc.body.textContent ?? '';
|
|
507
|
+
analyzeHTML(rawText);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const words = text.trim().split(/\s+/).filter((w) => w.length > 0);
|
|
511
|
+
const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
512
|
+
const totalMin = words.length / 200;
|
|
513
|
+
const mins = Math.floor(totalMin);
|
|
514
|
+
const secs = Math.floor((totalMin - mins) * 60);
|
|
515
|
+
const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
516
|
+
|
|
517
|
+
if (charsEl) charsEl.textContent = text.length.toLocaleString();
|
|
518
|
+
if (wordsEl) wordsEl.textContent = words.length.toLocaleString();
|
|
519
|
+
if (timeEl) timeEl.textContent = timeStr;
|
|
520
|
+
if (sentencesEl) sentencesEl.textContent = sentences.length.toString();
|
|
521
|
+
|
|
522
|
+
updateKeywords(words);
|
|
523
|
+
updateChecklist(words.length, sentences.length, sentences, rawText);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
textarea.addEventListener('input', () => analyzeText(textarea.value));
|
|
527
|
+
|
|
528
|
+
textarea.addEventListener('paste', (e) => {
|
|
529
|
+
const paste = e.clipboardData?.getData('text') ?? '';
|
|
530
|
+
if (paste.trim().startsWith('<') && activeTab === 'text') {
|
|
531
|
+
const htmlTab = document.querySelector<HTMLElement>('[data-tab="html"]');
|
|
532
|
+
if (htmlTab) htmlTab.click();
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
document.querySelectorAll<HTMLButtonElement>('.sco-tab').forEach((tab) => {
|
|
537
|
+
tab.addEventListener('click', () => {
|
|
538
|
+
document.querySelectorAll('.sco-tab').forEach((t) => t.classList.remove('active'));
|
|
539
|
+
tab.classList.add('active');
|
|
540
|
+
activeTab = tab.dataset.tab ?? 'text';
|
|
541
|
+
|
|
542
|
+
if (activeTab === 'html') {
|
|
543
|
+
if (densityPanel) densityPanel.style.display = 'none';
|
|
544
|
+
if (technicalPanel) technicalPanel.style.display = 'block';
|
|
545
|
+
} else {
|
|
546
|
+
if (densityPanel) densityPanel.style.display = 'block';
|
|
547
|
+
if (technicalPanel) technicalPanel.style.display = 'none';
|
|
548
|
+
}
|
|
549
|
+
analyzeText(textarea.value);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
</script>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { SeoContentOptimizerUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const faqData = [
|
|
6
|
+
{
|
|
7
|
+
question: 'How does this tool help my SEO ranking?',
|
|
8
|
+
answer: 'By analyzing keyword density and sentence readability, you ensure your content is easy to understand for users and relevant to search engines, avoiding over-optimization penalties.',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
question: 'What HTML elements does the technical analysis check?',
|
|
12
|
+
answer: 'It verifies the existence and uniqueness of the H1 tag, the presence of H2/H3 subheadings, and alt attributes on images.',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: 'Is my content stored on any server?',
|
|
16
|
+
answer: 'No. The analysis runs 100% locally in your browser. Your content never leaves your computer.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
question: 'Is it compatible with Yoast SEO criteria?',
|
|
20
|
+
answer: 'Yes, we implement similar criteria to Yoast: sentence length, paragraph distribution, and heading hierarchy.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const howToData = [
|
|
25
|
+
{ name: 'Write or paste your text', text: 'Enter the content you want to analyze in the main text area.' },
|
|
26
|
+
{ name: 'Review the optimization checklist', text: 'Check the length, readability, and keyword density alerts in the side panel.' },
|
|
27
|
+
{ name: 'Analyze technical HTML', text: 'Switch to the technical analysis tab to verify H1 tags, image alts, and metadata structure.' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
31
|
+
'@context': 'https://schema.org',
|
|
32
|
+
'@type': 'FAQPage',
|
|
33
|
+
mainEntity: faqData.map((item) => ({
|
|
34
|
+
'@type': 'Question',
|
|
35
|
+
name: item.question,
|
|
36
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const howToSchema: WithContext<HowTo> = {
|
|
41
|
+
'@context': 'https://schema.org',
|
|
42
|
+
'@type': 'HowTo',
|
|
43
|
+
name: 'How to optimize content for SEO',
|
|
44
|
+
step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
48
|
+
'@context': 'https://schema.org',
|
|
49
|
+
'@type': 'SoftwareApplication',
|
|
50
|
+
name: 'SEO Content Optimizer',
|
|
51
|
+
applicationCategory: 'UtilitiesApplication',
|
|
52
|
+
operatingSystem: 'Web',
|
|
53
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
54
|
+
description: 'Analyze keyword density, readability, and technical HTML structure of your texts in real time, without sending data to any server.',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ui: SeoContentOptimizerUI = {
|
|
58
|
+
tabText: 'Text Analysis',
|
|
59
|
+
tabHtml: 'Technical HTML Analysis',
|
|
60
|
+
textareaPlaceholder: 'Write your text or paste your HTML code here...',
|
|
61
|
+
statsChars: 'Characters',
|
|
62
|
+
statsWords: 'Words',
|
|
63
|
+
statsReading: 'Reading',
|
|
64
|
+
statsSentences: 'Sentences',
|
|
65
|
+
checklistTitle: 'Optimization Checklist',
|
|
66
|
+
keywordsTitle: 'Keyword Density',
|
|
67
|
+
technicalTitle: 'Technical HTML Analysis',
|
|
68
|
+
h1Label: 'H1 Detected',
|
|
69
|
+
linksLabel: 'Links (a)',
|
|
70
|
+
imgsLabel: 'Images (img)',
|
|
71
|
+
altsLabel: 'Missing alts',
|
|
72
|
+
emptyState: 'No data',
|
|
73
|
+
analyzing: 'Analyzing...',
|
|
74
|
+
checkInsufficient: 'Insufficient length (< 300 words)',
|
|
75
|
+
checkPillar: 'Excellent pillar content (> 900 words)',
|
|
76
|
+
checkGoodLength: 'Good length for SEO',
|
|
77
|
+
checkLongSentences: 'Too many long sentences (> 25% of text)',
|
|
78
|
+
checkGoodReadability: 'Optimal sentence readability',
|
|
79
|
+
checkLongParagraphs: 'Split very long paragraphs (> 150 words)',
|
|
80
|
+
checkMissingH1: 'Missing H1 tag',
|
|
81
|
+
checkMultipleH1: 'Multiple H1 tags detected',
|
|
82
|
+
checkMissingH2: 'Missing subheadings (H2)',
|
|
83
|
+
checkMissingTitle: 'Missing meta title tag',
|
|
84
|
+
stopWords: JSON.stringify(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'it', 'its', 'this', 'that', 'these', 'those', 'not', 'no', 'so', 'up', 'if', 'my', 'your', 'his', 'her', 'our', 'their']),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const content: ToolLocaleContent<SeoContentOptimizerUI> = {
|
|
88
|
+
slug: 'seo-content-optimizer',
|
|
89
|
+
title: 'SEO Content Optimizer',
|
|
90
|
+
description: 'Analyze keyword density, readability, and technical HTML structure of your texts in real time. Free and private SEO tool.',
|
|
91
|
+
ui,
|
|
92
|
+
faqTitle: 'Frequently Asked Questions',
|
|
93
|
+
faq: faqData,
|
|
94
|
+
howTo: howToData,
|
|
95
|
+
bibliographyTitle: 'References',
|
|
96
|
+
bibliography: [
|
|
97
|
+
{ name: "Google's SEO Starter Guide", url: 'https://developers.google.com/search/docs/fundamentals/seo-starter-guide' },
|
|
98
|
+
{ name: 'Yoast SEO readability criteria', url: 'https://yoast.com/what-is-readability/' },
|
|
99
|
+
],
|
|
100
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
101
|
+
seo: [
|
|
102
|
+
{ type: 'title', level: 2, text: 'SEO Content Optimizer: Keywords, Readability and Structure' },
|
|
103
|
+
{
|
|
104
|
+
type: 'paragraph',
|
|
105
|
+
html: 'Content quality is no longer measured solely by the keywords you include, but by how you structure your information to be digestible for both humans and Google crawlers. Our <strong>real-time SEO analysis tool</strong> gives you full control over keyword density, paragraph readability, and fundamental HTML technical elements.',
|
|
106
|
+
},
|
|
107
|
+
{ type: 'title', level: 3, text: 'Keyword Density and Semantic Relevance' },
|
|
108
|
+
{
|
|
109
|
+
type: 'paragraph',
|
|
110
|
+
html: '<strong>Keyword density</strong> indicates how often a term appears compared to the rest of the text. Excess repetition triggers <em>keyword stuffing</em> filters, while very low density may make it difficult for search engines to identify your article\'s central topic.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'list',
|
|
114
|
+
items: [
|
|
115
|
+
'<strong>Relevance analysis:</strong> Identify if the most repeated words match your search intent.',
|
|
116
|
+
'<strong>Penalty prevention:</strong> Avoid over-repeating terms that may appear spammy.',
|
|
117
|
+
'<strong>Semantic optimization:</strong> Find balance between technical terms and natural language.',
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{ type: 'title', level: 3, text: 'Readability in the Yoast Style' },
|
|
121
|
+
{
|
|
122
|
+
type: 'paragraph',
|
|
123
|
+
html: 'Readability is an indirect but crucial ranking factor. If users leave because paragraphs are endless blocks of text, your <em>Dwell Time</em> drops. The analyzer detects long sentences (over 20 words), paragraphs over 150 words, and checks for subheadings.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'tip',
|
|
127
|
+
title: 'Reading time',
|
|
128
|
+
html: 'Most users decide whether to read an article in under 10 seconds. Knowing the estimated reading time (calculated at an average of 200 words per minute) helps reduce bounce rate by setting reader expectations.',
|
|
129
|
+
},
|
|
130
|
+
{ type: 'title', level: 3, text: 'Technical HTML Analysis' },
|
|
131
|
+
{
|
|
132
|
+
type: 'paragraph',
|
|
133
|
+
html: 'Paste your source code to verify key elements: uniqueness of the <strong>H1</strong>, presence of H2/H3 subheadings, images without <code>alt</code> attributes, and existence of the <code>title</code> tag. All processing happens in your browser without sending data to any server.',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|