@kaikybrofc/omnizap-system 2.2.8 → 2.2.10

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.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +194 -0
  3. package/docs/seo/satellite-page-template.md +89 -0
  4. package/docs/seo/satellite-pages-phase1.json +486 -0
  5. package/package.json +3 -1
  6. package/public/api-docs/index.html +78 -22
  7. package/public/bot-whatsapp-para-grupo/index.html +276 -0
  8. package/public/bot-whatsapp-sem-programar/index.html +276 -0
  9. package/public/comandos/index.html +413 -0
  10. package/public/como-automatizar-avisos-no-whatsapp/index.html +276 -0
  11. package/public/como-criar-comandos-whatsapp/index.html +276 -0
  12. package/public/como-evitar-spam-no-whatsapp/index.html +276 -0
  13. package/public/como-moderar-grupo-whatsapp/index.html +276 -0
  14. package/public/como-organizar-comunidade-whatsapp/index.html +276 -0
  15. package/public/css/github-project-panel.css +8 -8
  16. package/public/css/stickers-admin.css +31 -31
  17. package/public/css/styles.css +17 -16
  18. package/public/index.html +701 -1181
  19. package/public/js/apps/apiDocsApp.js +39 -6
  20. package/public/js/apps/homeApp.js +157 -410
  21. package/public/js/apps/stickersApp.js +42 -0
  22. package/public/licenca/index.html +9 -9
  23. package/public/login/index.html +26 -22
  24. package/public/melhor-bot-whatsapp-para-grupos/index.html +276 -0
  25. package/public/sitemap.xml +45 -0
  26. package/public/stickers/create/index.html +7 -6
  27. package/public/stickers/index.html +72 -5
  28. package/public/termos-de-uso/index.html +10 -10
  29. package/public/user/index.html +25 -21
  30. package/scripts/generate-seo-satellite-pages.mjs +434 -0
  31. package/server/controllers/stickerCatalogController.js +341 -700
  32. package/kaikybrofc-omnizap-system-2.2.7.tgz +0 -0
  33. package/kaikybrofc-omnizap-system-2.2.8.tgz +0 -0
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_CONFIG_PATH = 'docs/seo/satellite-pages-phase1.json';
6
+ const DEFAULT_OUTPUT_DIR = 'public';
7
+ const SITE_ORIGIN = 'https://omnizap.shop';
8
+
9
+ const getArgValue = (name) => {
10
+ const index = process.argv.indexOf(name);
11
+ if (index === -1) return null;
12
+ return process.argv[index + 1] || null;
13
+ };
14
+
15
+ const configPath = getArgValue('--config') || DEFAULT_CONFIG_PATH;
16
+ const outputDir = getArgValue('--out') || DEFAULT_OUTPUT_DIR;
17
+
18
+ const escapeHtml = (value) =>
19
+ String(value || '')
20
+ .replace(/&/g, '&')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;')
24
+ .replace(/'/g, '&#39;');
25
+
26
+ const toSafeJson = (value) => JSON.stringify(value, null, 0).replace(/</g, '\\u003c');
27
+
28
+ const normalizeSlug = (slug) =>
29
+ String(slug || '')
30
+ .trim()
31
+ .replace(/^\/+/, '')
32
+ .replace(/\/+$/, '')
33
+ .toLowerCase();
34
+
35
+ const ensurePageConfig = (page) => {
36
+ const slug = normalizeSlug(page?.slug);
37
+ if (!slug || !/^[a-z0-9-]+$/.test(slug)) {
38
+ throw new Error(`slug invalido: ${page?.slug || '<vazio>'}`);
39
+ }
40
+
41
+ const requiredFields = ['title', 'description', 'h1', 'intro'];
42
+ for (const field of requiredFields) {
43
+ const value = String(page?.[field] || '').trim();
44
+ if (!value) {
45
+ throw new Error(`campo obrigatorio ausente em ${slug}: ${field}`);
46
+ }
47
+ }
48
+
49
+ return {
50
+ ...page,
51
+ slug,
52
+ title: String(page.title).trim(),
53
+ description: String(page.description).trim(),
54
+ h1: String(page.h1).trim(),
55
+ intro: String(page.intro).trim(),
56
+ intent_label: String(page.intent_label || 'Guia pratico').trim(),
57
+ keywords: Array.isArray(page.keywords) ? page.keywords.filter(Boolean).map((item) => String(item).trim()).filter(Boolean) : [],
58
+ sections: Array.isArray(page.sections) ? page.sections : [],
59
+ faq: Array.isArray(page.faq) ? page.faq : [],
60
+ related_links: Array.isArray(page.related_links) ? page.related_links : [],
61
+ };
62
+ };
63
+
64
+ const renderSection = (section) => {
65
+ const title = String(section?.title || '').trim();
66
+ const paragraphs = Array.isArray(section?.paragraphs)
67
+ ? section.paragraphs.map((item) => String(item || '').trim()).filter(Boolean)
68
+ : [];
69
+ const bullets = Array.isArray(section?.bullets)
70
+ ? section.bullets.map((item) => String(item || '').trim()).filter(Boolean)
71
+ : [];
72
+
73
+ if (!title && paragraphs.length === 0 && bullets.length === 0) return '';
74
+
75
+ const paragraphsHtml = paragraphs.map((paragraph) => ` <p>${escapeHtml(paragraph)}</p>`).join('\n');
76
+ const bulletsHtml = bullets.length
77
+ ? `\n <ul>\n${bullets.map((bullet) => ` <li>${escapeHtml(bullet)}</li>`).join('\n')}\n </ul>`
78
+ : '';
79
+
80
+ return `<section class="card">\n ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}\n${paragraphsHtml}${bulletsHtml}\n </section>`;
81
+ };
82
+
83
+ const renderFaq = (faqEntries) => {
84
+ if (!faqEntries.length) return '';
85
+
86
+ const items = faqEntries
87
+ .map((entry) => {
88
+ const question = String(entry?.q || '').trim();
89
+ const answer = String(entry?.a || '').trim();
90
+ if (!question || !answer) return '';
91
+ return ` <details class="faq-item">\n <summary>${escapeHtml(question)}</summary>\n <p>${escapeHtml(answer)}</p>\n </details>`;
92
+ })
93
+ .filter(Boolean)
94
+ .join('\n');
95
+
96
+ if (!items) return '';
97
+
98
+ return `<section class="card">\n <h2>Perguntas frequentes</h2>\n <div class="faq-list">\n${items}\n </div>\n </section>`;
99
+ };
100
+
101
+ const withRequiredLinks = (relatedLinks) => {
102
+ const requiredLinks = [
103
+ { href: '/', label: 'OmniZap Home' },
104
+ { href: '/stickers/', label: 'Catálogo de Stickers' },
105
+ { href: '/comandos/', label: 'Biblioteca de Comandos' },
106
+ { href: '/api-docs/', label: 'Área de Desenvolvedor' },
107
+ { href: '/login/', label: 'Adicionar bot agora' },
108
+ ];
109
+
110
+ const allLinks = [...requiredLinks, ...relatedLinks];
111
+ const dedup = new Map();
112
+ for (const link of allLinks) {
113
+ const href = String(link?.href || '').trim();
114
+ const label = String(link?.label || '').trim();
115
+ if (!href || !label) continue;
116
+ if (!dedup.has(href)) dedup.set(href, { href, label });
117
+ }
118
+
119
+ return Array.from(dedup.values());
120
+ };
121
+
122
+ const renderLinks = (links) => {
123
+ if (!links.length) return '';
124
+
125
+ return `<section class="card">\n <h2>Links úteis</h2>\n <div class="links-grid">\n${links
126
+ .map((link) => ` <a href="${escapeHtml(link.href)}">${escapeHtml(link.label)}</a>`)
127
+ .join('\n')}\n </div>\n </section>`;
128
+ };
129
+
130
+ const renderPageHtml = (page, generatedAt) => {
131
+ const canonicalPath = `/${page.slug}/`;
132
+ const canonicalUrl = `${SITE_ORIGIN}${canonicalPath}`;
133
+ const keywordsContent = page.keywords.join(', ');
134
+
135
+ const faqEntities = page.faq
136
+ .map((entry) => ({
137
+ '@type': 'Question',
138
+ name: String(entry?.q || '').trim(),
139
+ acceptedAnswer: {
140
+ '@type': 'Answer',
141
+ text: String(entry?.a || '').trim(),
142
+ },
143
+ }))
144
+ .filter((item) => item.name && item.acceptedAnswer.text);
145
+
146
+ const webPageSchema = {
147
+ '@context': 'https://schema.org',
148
+ '@type': 'WebPage',
149
+ name: page.title,
150
+ description: page.description,
151
+ inLanguage: 'pt-BR',
152
+ url: canonicalUrl,
153
+ isPartOf: {
154
+ '@type': 'WebSite',
155
+ name: 'OmniZap System',
156
+ url: SITE_ORIGIN,
157
+ },
158
+ };
159
+
160
+ const faqSchema = {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'FAQPage',
163
+ mainEntity: faqEntities,
164
+ };
165
+
166
+ const sectionsHtml = page.sections.map(renderSection).filter(Boolean).join('\n\n');
167
+ const faqHtml = renderFaq(page.faq);
168
+ const linksHtml = renderLinks(withRequiredLinks(page.related_links));
169
+
170
+ return `<!doctype html>
171
+ <html lang="pt-BR">
172
+ <head>
173
+ <meta charset="utf-8" />
174
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
175
+ <title>${escapeHtml(page.title)}</title>
176
+ <meta name="description" content="${escapeHtml(page.description)}" />
177
+ ${keywordsContent ? `<meta name="keywords" content="${escapeHtml(keywordsContent)}" />` : ''}
178
+ <meta name="robots" content="index, follow" />
179
+ <link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
180
+ <link rel="icon" type="image/png" href="/assets/images/brand-icon-192.png" />
181
+
182
+ <meta property="og:type" content="article" />
183
+ <meta property="og:locale" content="pt_BR" />
184
+ <meta property="og:site_name" content="OmniZap System" />
185
+ <meta property="og:title" content="${escapeHtml(page.title)}" />
186
+ <meta property="og:description" content="${escapeHtml(page.description)}" />
187
+ <meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
188
+ <meta property="og:image" content="https://omnizap.shop/assets/images/hero-banner-1280.jpg" />
189
+
190
+ <meta name="twitter:card" content="summary_large_image" />
191
+ <meta name="twitter:title" content="${escapeHtml(page.title)}" />
192
+ <meta name="twitter:description" content="${escapeHtml(page.description)}" />
193
+ <meta name="twitter:image" content="https://omnizap.shop/assets/images/hero-banner-1280.jpg" />
194
+
195
+ <script type="application/ld+json">${toSafeJson(webPageSchema)}</script>
196
+ ${faqEntities.length ? `<script type="application/ld+json">${toSafeJson(faqSchema)}</script>` : ''}
197
+
198
+ <style>
199
+ :root {
200
+ --bg: #0f172a;
201
+ --bg-2: #111827;
202
+ --line: rgba(255, 255, 255, 0.05);
203
+ --text: #f8fafc;
204
+ --muted: #94a3b8;
205
+ --card: #1e293bd9;
206
+ --accent: #2563eb;
207
+ --accent-2: #7c3aed;
208
+ --cta: #22c55e;
209
+ --cta-hover: #16a34a;
210
+ }
211
+
212
+ * { box-sizing: border-box; }
213
+ body {
214
+ margin: 0;
215
+ font-family: "Manrope", system-ui, -apple-system, sans-serif;
216
+ color: var(--text);
217
+ background:
218
+ radial-gradient(58rem 22rem at -10% -8%, #2563eb24, transparent 60%),
219
+ radial-gradient(62rem 26rem at 112% -12%, #7c3aed22, transparent 58%),
220
+ linear-gradient(165deg, var(--bg), var(--bg-2));
221
+ }
222
+
223
+ .wrap { width: min(980px, 92vw); margin: 0 auto; padding: 22px 0 42px; }
224
+
225
+ .top {
226
+ display: flex;
227
+ flex-wrap: wrap;
228
+ gap: 8px;
229
+ margin-bottom: 14px;
230
+ }
231
+
232
+ .top a {
233
+ color: var(--text);
234
+ text-decoration: none;
235
+ border: 1px solid var(--line);
236
+ border-radius: 10px;
237
+ padding: 8px 11px;
238
+ background: #111827;
239
+ font-size: 14px;
240
+ font-weight: 700;
241
+ }
242
+
243
+ .hero,
244
+ .card {
245
+ border: 1px solid rgba(255, 255, 255, 0.05);
246
+ border-radius: 16px;
247
+ background: var(--card);
248
+ padding: 16px;
249
+ margin-bottom: 12px;
250
+ }
251
+
252
+ .pill {
253
+ display: inline-flex;
254
+ border: 1px solid #45689f;
255
+ border-radius: 999px;
256
+ padding: 5px 10px;
257
+ font-size: 12px;
258
+ font-weight: 800;
259
+ text-transform: uppercase;
260
+ letter-spacing: .3px;
261
+ color: #cde4ff;
262
+ background: #16274a96;
263
+ margin-bottom: 10px;
264
+ }
265
+
266
+ h1, h2 {
267
+ margin: 0 0 8px;
268
+ font-family: "Sora", "Manrope", sans-serif;
269
+ letter-spacing: -0.02em;
270
+ }
271
+
272
+ h1 {
273
+ font-size: clamp(29px, 4vw, 42px);
274
+ line-height: 1.08;
275
+ background: linear-gradient(92deg, #f3f8ff 0%, #60a5fa 45%, #a78bfa 100%);
276
+ -webkit-background-clip: text;
277
+ background-clip: text;
278
+ color: transparent;
279
+ }
280
+
281
+ h2 { font-size: clamp(22px, 2.8vw, 30px); }
282
+
283
+ p, li {
284
+ margin: 0 0 10px;
285
+ color: var(--muted);
286
+ line-height: 1.65;
287
+ font-size: 16px;
288
+ }
289
+
290
+ ul { margin: 0; padding-left: 18px; }
291
+
292
+ .cta {
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: 8px;
296
+ margin-top: 12px;
297
+ }
298
+
299
+ .btn {
300
+ text-decoration: none;
301
+ border: 1px solid rgba(255, 255, 255, 0.05);
302
+ border-radius: 11px;
303
+ padding: 10px 13px;
304
+ color: var(--text);
305
+ background: #111827;
306
+ font-weight: 800;
307
+ font-size: 14px;
308
+ }
309
+
310
+ .btn.primary {
311
+ border-color: transparent;
312
+ color: #0f172a;
313
+ background: var(--cta);
314
+ }
315
+
316
+ .btn.primary:hover { background: var(--cta-hover); }
317
+
318
+ .faq-list {
319
+ display: grid;
320
+ gap: 9px;
321
+ }
322
+
323
+ .faq-item {
324
+ border: 1px solid rgba(255, 255, 255, 0.05);
325
+ border-radius: 12px;
326
+ background: #1e293bb8;
327
+ padding: 0 12px;
328
+ }
329
+
330
+ .faq-item summary {
331
+ cursor: pointer;
332
+ list-style: none;
333
+ font-weight: 800;
334
+ color: #ebf4ff;
335
+ padding: 12px 0;
336
+ }
337
+
338
+ .faq-item summary::-webkit-details-marker { display: none; }
339
+
340
+ .faq-item p {
341
+ margin: 0;
342
+ padding: 0 0 12px;
343
+ font-size: 15px;
344
+ }
345
+
346
+ .links-grid {
347
+ display: grid;
348
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
349
+ gap: 8px;
350
+ }
351
+
352
+ .links-grid a {
353
+ text-decoration: none;
354
+ color: #dbecff;
355
+ border: 1px solid #365686;
356
+ border-radius: 10px;
357
+ padding: 9px 10px;
358
+ background: #10203d;
359
+ font-weight: 700;
360
+ }
361
+
362
+ .meta {
363
+ margin-top: 8px;
364
+ font-size: 13px;
365
+ color: #95b2d8;
366
+ }
367
+ </style>
368
+ </head>
369
+ <body>
370
+ <main class="wrap">
371
+ <nav class="top" aria-label="Navegação interna">
372
+ <a href="/">Início</a>
373
+ <a href="/stickers/">Stickers</a>
374
+ <a href="/api-docs/">API Docs</a>
375
+ <a href="/login/">Adicionar Bot</a>
376
+ </nav>
377
+
378
+ <header class="hero">
379
+ <span class="pill">${escapeHtml(page.intent_label)}</span>
380
+ <h1>${escapeHtml(page.h1)}</h1>
381
+ <p>${escapeHtml(page.intro)}</p>
382
+ <div class="cta">
383
+ <a class="btn primary" href="/login/">Adicionar ao meu grupo</a>
384
+ <a class="btn" href="/">Conhecer OmniZap</a>
385
+ </div>
386
+ <p class="meta">Página atualizada em ${escapeHtml(generatedAt)}</p>
387
+ </header>
388
+
389
+ ${sectionsHtml}
390
+
391
+ ${faqHtml}
392
+
393
+ ${linksHtml}
394
+ </main>
395
+ </body>
396
+ </html>`;
397
+ };
398
+
399
+ const run = async () => {
400
+ const absoluteConfigPath = path.resolve(configPath);
401
+ const absoluteOutputDir = path.resolve(outputDir);
402
+
403
+ const rawConfig = await fs.readFile(absoluteConfigPath, 'utf8');
404
+ const parsedConfig = JSON.parse(rawConfig);
405
+ const pages = Array.isArray(parsedConfig?.pages) ? parsedConfig.pages : [];
406
+ const generatedAt = String(parsedConfig?.generated_at || new Date().toISOString().slice(0, 10)).trim();
407
+
408
+ if (!pages.length) {
409
+ throw new Error('config sem paginas');
410
+ }
411
+
412
+ const generatedFiles = [];
413
+
414
+ for (const pageConfig of pages) {
415
+ const page = ensurePageConfig(pageConfig);
416
+ const html = renderPageHtml(page, generatedAt);
417
+ const targetDir = path.join(absoluteOutputDir, page.slug);
418
+ const targetFile = path.join(targetDir, 'index.html');
419
+
420
+ await fs.mkdir(targetDir, { recursive: true });
421
+ await fs.writeFile(targetFile, html, 'utf8');
422
+ generatedFiles.push(targetFile);
423
+ }
424
+
425
+ process.stdout.write(`Paginas geradas: ${generatedFiles.length}\n`);
426
+ for (const filePath of generatedFiles) {
427
+ process.stdout.write(`- ${path.relative(process.cwd(), filePath)}\n`);
428
+ }
429
+ };
430
+
431
+ run().catch((error) => {
432
+ process.stderr.write(`Erro ao gerar paginas satelite: ${error.message}\n`);
433
+ process.exitCode = 1;
434
+ });