smart-seo-lite 1.0.0 → 2.0.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.
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ export interface BreadcrumbItem {
3
+ name: string;
4
+ url: string;
5
+ }
6
+ interface BreadcrumbsProps {
7
+ items: BreadcrumbItem[];
8
+ separator?: string;
9
+ className?: string;
10
+ linkClassName?: string;
11
+ activeClassName?: string;
12
+ }
13
+ /**
14
+ * Breadcrumbs - Visual breadcrumb navigation that ALSO auto-injects
15
+ * valid BreadcrumbList JSON-LD schema. One component, two SEO wins.
16
+ *
17
+ * @example
18
+ * <Breadcrumbs items={[
19
+ * { name: "Home", url: "/" },
20
+ * { name: "Blog", url: "/blog" },
21
+ * { name: "My Post", url: "/blog/my-post" },
22
+ * ]} />
23
+ */
24
+ export declare function Breadcrumbs({ items, separator, className, linkClassName, activeClassName, }: BreadcrumbsProps): React.JSX.Element | null;
25
+ export {};
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ /**
3
+ * SEOAudit - Dev-only floating widget that scans the current page
4
+ * and shows a live SEO score with actionable issues.
5
+ * Automatically disabled in production (NODE_ENV !== "development").
6
+ *
7
+ * @example
8
+ * // Drop it once near the root of your app
9
+ * <SEOAudit />
10
+ */
11
+ export declare function SEOAudit(): React.JSX.Element | null;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export { useSEO } from "./useSEO";
2
- export type { SEOProps } from "./useSEO";
2
+ export type { SEOProps, AlternateLocale } from "./useSEO";
3
3
  export { Schema } from "./Schema";
4
4
  export { SEOImg } from "./SEOImg";
5
+ export { Breadcrumbs } from "./Breadcrumbs";
6
+ export type { BreadcrumbItem } from "./Breadcrumbs";
7
+ export { SEOAudit } from "./SEOAudit";
8
+ export { generateSitemap } from "./sitemap";
9
+ export type { SitemapRoute } from "./sitemap";
10
+ export { generateRobotsTxt } from "./robots";
11
+ export type { RobotsOptions, RobotsRule } from "./robots";
package/dist/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
 
3
3
  function setMeta(name, content, attr = "name") {
4
4
  if (typeof document === "undefined")
@@ -22,6 +22,18 @@ function setLink(rel, href) {
22
22
  }
23
23
  el.setAttribute("href", href);
24
24
  }
25
+ function setAlternateLink(hreflang, href) {
26
+ if (typeof document === "undefined")
27
+ return;
28
+ let el = document.querySelector(`link[rel="alternate"][hreflang="${hreflang}"]`);
29
+ if (!el) {
30
+ el = document.createElement("link");
31
+ el.setAttribute("rel", "alternate");
32
+ el.setAttribute("hreflang", hreflang);
33
+ document.head.appendChild(el);
34
+ }
35
+ el.setAttribute("href", href);
36
+ }
25
37
  /**
26
38
  * useSEO - Automatically handles:
27
39
  * ✅ title, description
@@ -37,7 +49,8 @@ function setLink(rel, href) {
37
49
  * image: "/banner.png"
38
50
  * });
39
51
  */
40
- function useSEO({ title, description, image, url, siteName = "My App", type = "website", twitterHandle, noIndex = false, canonical, keywords = [], }) {
52
+ function useSEO({ title, description, image, url, siteName = "My App", type = "website", twitterHandle, noIndex = false, canonical, keywords = [], alternateLocales = [], }) {
53
+ const localesKey = alternateLocales.map((l) => `${l.locale}:${l.url}`).join(",");
41
54
  useEffect(() => {
42
55
  if (typeof document === "undefined")
43
56
  return;
@@ -75,7 +88,21 @@ function useSEO({ title, description, image, url, siteName = "My App", type = "w
75
88
  // --- Canonical ---
76
89
  if (canonicalURL)
77
90
  setLink("canonical", canonicalURL);
78
- }, [title, description, image, url, siteName, type, twitterHandle, noIndex, canonical, keywords]);
91
+ // --- Hreflang (multilingual SEO) ---
92
+ alternateLocales.forEach((alt) => setAlternateLink(alt.locale, alt.url));
93
+ }, [
94
+ title,
95
+ description,
96
+ image,
97
+ url,
98
+ siteName,
99
+ type,
100
+ twitterHandle,
101
+ noIndex,
102
+ canonical,
103
+ keywords,
104
+ localesKey,
105
+ ]);
79
106
  }
80
107
 
81
108
  function buildSchema(type, data) {
@@ -273,4 +300,258 @@ function SEOImg(_a) {
273
300
  return (React.createElement("img", Object.assign({ src: src, alt: resolvedAlt, loading: loading === "auto" ? undefined : loading, width: width, height: height, className: className, style: style }, rest)));
274
301
  }
275
302
 
276
- export { SEOImg, Schema, useSEO };
303
+ /**
304
+ * Breadcrumbs - Visual breadcrumb navigation that ALSO auto-injects
305
+ * valid BreadcrumbList JSON-LD schema. One component, two SEO wins.
306
+ *
307
+ * @example
308
+ * <Breadcrumbs items={[
309
+ * { name: "Home", url: "/" },
310
+ * { name: "Blog", url: "/blog" },
311
+ * { name: "My Post", url: "/blog/my-post" },
312
+ * ]} />
313
+ */
314
+ function Breadcrumbs({ items, separator = "/", className, linkClassName, activeClassName, }) {
315
+ if (!items || items.length === 0)
316
+ return null;
317
+ const schema = {
318
+ "@context": "https://schema.org",
319
+ "@type": "BreadcrumbList",
320
+ itemListElement: items.map((item, i) => ({
321
+ "@type": "ListItem",
322
+ position: i + 1,
323
+ name: item.name,
324
+ item: item.url,
325
+ })),
326
+ };
327
+ return (React.createElement(React.Fragment, null,
328
+ React.createElement("nav", { "aria-label": "Breadcrumb", className: className },
329
+ React.createElement("ol", { style: {
330
+ display: "flex",
331
+ flexWrap: "wrap",
332
+ alignItems: "center",
333
+ listStyle: "none",
334
+ padding: 0,
335
+ margin: 0,
336
+ gap: "8px",
337
+ } }, items.map((item, i) => {
338
+ const isLast = i === items.length - 1;
339
+ return (React.createElement("li", { key: item.url, style: { display: "flex", alignItems: "center", gap: "8px" } },
340
+ isLast ? (React.createElement("span", { "aria-current": "page", className: activeClassName }, item.name)) : (React.createElement("a", { href: item.url, className: linkClassName }, item.name)),
341
+ !isLast && React.createElement("span", { "aria-hidden": "true" }, separator)));
342
+ }))),
343
+ React.createElement("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } })));
344
+ }
345
+
346
+ function runAudit() {
347
+ const issues = [];
348
+ if (typeof document === "undefined")
349
+ return issues;
350
+ // Title
351
+ const title = document.title;
352
+ if (!title) {
353
+ issues.push({ type: "error", message: "Missing <title> tag" });
354
+ }
355
+ else if (title.length < 10 || title.length > 60) {
356
+ issues.push({ type: "warning", message: `Title is ${title.length} chars (ideal: 10-60)` });
357
+ }
358
+ else {
359
+ issues.push({ type: "success", message: "Title length looks good" });
360
+ }
361
+ // Meta description
362
+ const desc = document.querySelector('meta[name="description"]');
363
+ if (!desc || !desc.content) {
364
+ issues.push({ type: "error", message: "Missing meta description" });
365
+ }
366
+ else if (desc.content.length < 50 || desc.content.length > 160) {
367
+ issues.push({ type: "warning", message: `Description is ${desc.content.length} chars (ideal: 50-160)` });
368
+ }
369
+ else {
370
+ issues.push({ type: "success", message: "Meta description length looks good" });
371
+ }
372
+ // H1 check
373
+ const h1s = document.querySelectorAll("h1");
374
+ if (h1s.length === 0) {
375
+ issues.push({ type: "error", message: "No <h1> found on page" });
376
+ }
377
+ else if (h1s.length > 1) {
378
+ issues.push({ type: "warning", message: `Found ${h1s.length} <h1> tags (should be exactly 1)` });
379
+ }
380
+ else {
381
+ issues.push({ type: "success", message: "Exactly one <h1> found" });
382
+ }
383
+ // Image alt text
384
+ const imgs = document.querySelectorAll("img");
385
+ const missingAlt = Array.from(imgs).filter((img) => !img.getAttribute("alt"));
386
+ if (missingAlt.length > 0) {
387
+ issues.push({ type: "error", message: `${missingAlt.length} image(s) missing alt text` });
388
+ }
389
+ else if (imgs.length > 0) {
390
+ issues.push({ type: "success", message: `All ${imgs.length} images have alt text` });
391
+ }
392
+ // Canonical
393
+ const canonical = document.querySelector('link[rel="canonical"]');
394
+ if (!canonical) {
395
+ issues.push({ type: "warning", message: "Missing canonical link" });
396
+ }
397
+ else {
398
+ issues.push({ type: "success", message: "Canonical URL is set" });
399
+ }
400
+ // Open Graph
401
+ const ogTitle = document.querySelector('meta[property="og:title"]');
402
+ if (!ogTitle) {
403
+ issues.push({ type: "warning", message: "Missing og:title (Open Graph)" });
404
+ }
405
+ else {
406
+ issues.push({ type: "success", message: "Open Graph tags present" });
407
+ }
408
+ // Viewport (mobile SEO)
409
+ const viewport = document.querySelector('meta[name="viewport"]');
410
+ if (!viewport) {
411
+ issues.push({ type: "error", message: "Missing viewport meta tag" });
412
+ }
413
+ return issues;
414
+ }
415
+ const colors = {
416
+ error: "#ff5f57",
417
+ warning: "#febc2e",
418
+ success: "#28c840",
419
+ };
420
+ const icons = {
421
+ error: "✕",
422
+ warning: "⚠",
423
+ success: "✓",
424
+ };
425
+ /**
426
+ * SEOAudit - Dev-only floating widget that scans the current page
427
+ * and shows a live SEO score with actionable issues.
428
+ * Automatically disabled in production (NODE_ENV !== "development").
429
+ *
430
+ * @example
431
+ * // Drop it once near the root of your app
432
+ * <SEOAudit />
433
+ */
434
+ function SEOAudit() {
435
+ const [issues, setIssues] = useState([]);
436
+ const [open, setOpen] = useState(false);
437
+ useEffect(() => {
438
+ if (process.env.NODE_ENV !== "development")
439
+ return;
440
+ const timer = setTimeout(() => setIssues(runAudit()), 600);
441
+ return () => clearTimeout(timer);
442
+ }, []);
443
+ if (process.env.NODE_ENV !== "development")
444
+ return null;
445
+ if (issues.length === 0)
446
+ return null;
447
+ const errors = issues.filter((i) => i.type === "error").length;
448
+ const warnings = issues.filter((i) => i.type === "warning").length;
449
+ const score = Math.max(0, 100 - errors * 20 - warnings * 8);
450
+ const scoreColor = score >= 80 ? "#28c840" : score >= 50 ? "#febc2e" : "#ff5f57";
451
+ return (React.createElement("div", { style: {
452
+ position: "fixed",
453
+ bottom: 20,
454
+ right: 20,
455
+ zIndex: 999999,
456
+ fontFamily: "'JetBrains Mono', monospace",
457
+ fontSize: 13,
458
+ } },
459
+ open && (React.createElement("div", { style: {
460
+ background: "#0d1221",
461
+ border: "1px solid #1e2a45",
462
+ borderRadius: 12,
463
+ padding: 16,
464
+ width: 320,
465
+ marginBottom: 12,
466
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
467
+ maxHeight: 360,
468
+ overflowY: "auto",
469
+ } },
470
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 } },
471
+ React.createElement("span", { style: { color: "#e8edf8", fontWeight: 700 } }, "SEO Audit"),
472
+ React.createElement("span", { style: { color: scoreColor, fontWeight: 700 } },
473
+ score,
474
+ "/100")),
475
+ issues.map((issue, i) => (React.createElement("div", { key: i, style: {
476
+ display: "flex",
477
+ gap: 8,
478
+ alignItems: "flex-start",
479
+ padding: "6px 0",
480
+ borderTop: i === 0 ? "none" : "1px solid #1e2a45",
481
+ color: "#a8b3c7",
482
+ fontSize: 12,
483
+ lineHeight: 1.5,
484
+ } },
485
+ React.createElement("span", { style: { color: colors[issue.type], flexShrink: 0 } }, icons[issue.type]),
486
+ React.createElement("span", null, issue.message)))))),
487
+ React.createElement("button", { onClick: () => setOpen(!open), style: {
488
+ background: "#0d1221",
489
+ border: `2px solid ${scoreColor}`,
490
+ borderRadius: "50%",
491
+ width: 56,
492
+ height: 56,
493
+ color: scoreColor,
494
+ fontWeight: 700,
495
+ fontSize: 16,
496
+ cursor: "pointer",
497
+ boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
498
+ }, title: "smart-seo-lite SEO Audit" }, score)));
499
+ }
500
+
501
+ /**
502
+ * generateSitemap - Builds a valid sitemap.xml string from a list of routes.
503
+ * Use this in a Next.js API route, app/sitemap.xml/route.ts, or a build script.
504
+ *
505
+ * @example
506
+ * const xml = generateSitemap("https://myapp.com", [
507
+ * { url: "/", priority: 1.0, changeFrequency: "daily" },
508
+ * { url: "/about", priority: 0.8 },
509
+ * ]);
510
+ */
511
+ function generateSitemap(baseUrl, routes) {
512
+ const base = baseUrl.replace(/\/$/, "");
513
+ const urlEntries = routes
514
+ .map((route) => {
515
+ const loc = `${base}${route.url.startsWith("/") ? "" : "/"}${route.url}`;
516
+ const parts = [` <loc>${loc}</loc>`];
517
+ if (route.lastModified)
518
+ parts.push(` <lastmod>${route.lastModified}</lastmod>`);
519
+ if (route.changeFrequency)
520
+ parts.push(` <changefreq>${route.changeFrequency}</changefreq>`);
521
+ if (route.priority !== undefined)
522
+ parts.push(` <priority>${route.priority}</priority>`);
523
+ return ` <url>\n${parts.join("\n")}\n </url>`;
524
+ })
525
+ .join("\n");
526
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urlEntries}\n</urlset>`;
527
+ }
528
+
529
+ /**
530
+ * generateRobotsTxt - Builds a valid robots.txt string.
531
+ * Use this in a Next.js API route, app/robots.txt/route.ts, or a build script.
532
+ *
533
+ * @example
534
+ * const txt = generateRobotsTxt({
535
+ * disallow: ["/admin", "/api"],
536
+ * sitemap: "https://myapp.com/sitemap.xml",
537
+ * });
538
+ */
539
+ function generateRobotsTxt(options = {}) {
540
+ var _a;
541
+ const rules = ((_a = options.rules) === null || _a === void 0 ? void 0 : _a.length)
542
+ ? options.rules
543
+ : [{ userAgent: options.userAgent, allow: options.allow, disallow: options.disallow }];
544
+ const lines = [];
545
+ rules.forEach((rule) => {
546
+ lines.push(`User-agent: ${rule.userAgent || "*"}`);
547
+ (rule.allow || []).forEach((path) => lines.push(`Allow: ${path}`));
548
+ (rule.disallow || []).forEach((path) => lines.push(`Disallow: ${path}`));
549
+ lines.push("");
550
+ });
551
+ if (options.sitemap) {
552
+ lines.push(`Sitemap: ${options.sitemap}`);
553
+ }
554
+ return lines.join("\n").trim();
555
+ }
556
+
557
+ export { Breadcrumbs, SEOAudit, SEOImg, Schema, generateRobotsTxt, generateSitemap, useSEO };
package/dist/index.js CHANGED
@@ -24,6 +24,18 @@ function setLink(rel, href) {
24
24
  }
25
25
  el.setAttribute("href", href);
26
26
  }
27
+ function setAlternateLink(hreflang, href) {
28
+ if (typeof document === "undefined")
29
+ return;
30
+ let el = document.querySelector(`link[rel="alternate"][hreflang="${hreflang}"]`);
31
+ if (!el) {
32
+ el = document.createElement("link");
33
+ el.setAttribute("rel", "alternate");
34
+ el.setAttribute("hreflang", hreflang);
35
+ document.head.appendChild(el);
36
+ }
37
+ el.setAttribute("href", href);
38
+ }
27
39
  /**
28
40
  * useSEO - Automatically handles:
29
41
  * ✅ title, description
@@ -39,7 +51,8 @@ function setLink(rel, href) {
39
51
  * image: "/banner.png"
40
52
  * });
41
53
  */
42
- function useSEO({ title, description, image, url, siteName = "My App", type = "website", twitterHandle, noIndex = false, canonical, keywords = [], }) {
54
+ function useSEO({ title, description, image, url, siteName = "My App", type = "website", twitterHandle, noIndex = false, canonical, keywords = [], alternateLocales = [], }) {
55
+ const localesKey = alternateLocales.map((l) => `${l.locale}:${l.url}`).join(",");
43
56
  React.useEffect(() => {
44
57
  if (typeof document === "undefined")
45
58
  return;
@@ -77,7 +90,21 @@ function useSEO({ title, description, image, url, siteName = "My App", type = "w
77
90
  // --- Canonical ---
78
91
  if (canonicalURL)
79
92
  setLink("canonical", canonicalURL);
80
- }, [title, description, image, url, siteName, type, twitterHandle, noIndex, canonical, keywords]);
93
+ // --- Hreflang (multilingual SEO) ---
94
+ alternateLocales.forEach((alt) => setAlternateLink(alt.locale, alt.url));
95
+ }, [
96
+ title,
97
+ description,
98
+ image,
99
+ url,
100
+ siteName,
101
+ type,
102
+ twitterHandle,
103
+ noIndex,
104
+ canonical,
105
+ keywords,
106
+ localesKey,
107
+ ]);
81
108
  }
82
109
 
83
110
  function buildSchema(type, data) {
@@ -275,6 +302,264 @@ function SEOImg(_a) {
275
302
  return (React.createElement("img", Object.assign({ src: src, alt: resolvedAlt, loading: loading === "auto" ? undefined : loading, width: width, height: height, className: className, style: style }, rest)));
276
303
  }
277
304
 
305
+ /**
306
+ * Breadcrumbs - Visual breadcrumb navigation that ALSO auto-injects
307
+ * valid BreadcrumbList JSON-LD schema. One component, two SEO wins.
308
+ *
309
+ * @example
310
+ * <Breadcrumbs items={[
311
+ * { name: "Home", url: "/" },
312
+ * { name: "Blog", url: "/blog" },
313
+ * { name: "My Post", url: "/blog/my-post" },
314
+ * ]} />
315
+ */
316
+ function Breadcrumbs({ items, separator = "/", className, linkClassName, activeClassName, }) {
317
+ if (!items || items.length === 0)
318
+ return null;
319
+ const schema = {
320
+ "@context": "https://schema.org",
321
+ "@type": "BreadcrumbList",
322
+ itemListElement: items.map((item, i) => ({
323
+ "@type": "ListItem",
324
+ position: i + 1,
325
+ name: item.name,
326
+ item: item.url,
327
+ })),
328
+ };
329
+ return (React.createElement(React.Fragment, null,
330
+ React.createElement("nav", { "aria-label": "Breadcrumb", className: className },
331
+ React.createElement("ol", { style: {
332
+ display: "flex",
333
+ flexWrap: "wrap",
334
+ alignItems: "center",
335
+ listStyle: "none",
336
+ padding: 0,
337
+ margin: 0,
338
+ gap: "8px",
339
+ } }, items.map((item, i) => {
340
+ const isLast = i === items.length - 1;
341
+ return (React.createElement("li", { key: item.url, style: { display: "flex", alignItems: "center", gap: "8px" } },
342
+ isLast ? (React.createElement("span", { "aria-current": "page", className: activeClassName }, item.name)) : (React.createElement("a", { href: item.url, className: linkClassName }, item.name)),
343
+ !isLast && React.createElement("span", { "aria-hidden": "true" }, separator)));
344
+ }))),
345
+ React.createElement("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schema) } })));
346
+ }
347
+
348
+ function runAudit() {
349
+ const issues = [];
350
+ if (typeof document === "undefined")
351
+ return issues;
352
+ // Title
353
+ const title = document.title;
354
+ if (!title) {
355
+ issues.push({ type: "error", message: "Missing <title> tag" });
356
+ }
357
+ else if (title.length < 10 || title.length > 60) {
358
+ issues.push({ type: "warning", message: `Title is ${title.length} chars (ideal: 10-60)` });
359
+ }
360
+ else {
361
+ issues.push({ type: "success", message: "Title length looks good" });
362
+ }
363
+ // Meta description
364
+ const desc = document.querySelector('meta[name="description"]');
365
+ if (!desc || !desc.content) {
366
+ issues.push({ type: "error", message: "Missing meta description" });
367
+ }
368
+ else if (desc.content.length < 50 || desc.content.length > 160) {
369
+ issues.push({ type: "warning", message: `Description is ${desc.content.length} chars (ideal: 50-160)` });
370
+ }
371
+ else {
372
+ issues.push({ type: "success", message: "Meta description length looks good" });
373
+ }
374
+ // H1 check
375
+ const h1s = document.querySelectorAll("h1");
376
+ if (h1s.length === 0) {
377
+ issues.push({ type: "error", message: "No <h1> found on page" });
378
+ }
379
+ else if (h1s.length > 1) {
380
+ issues.push({ type: "warning", message: `Found ${h1s.length} <h1> tags (should be exactly 1)` });
381
+ }
382
+ else {
383
+ issues.push({ type: "success", message: "Exactly one <h1> found" });
384
+ }
385
+ // Image alt text
386
+ const imgs = document.querySelectorAll("img");
387
+ const missingAlt = Array.from(imgs).filter((img) => !img.getAttribute("alt"));
388
+ if (missingAlt.length > 0) {
389
+ issues.push({ type: "error", message: `${missingAlt.length} image(s) missing alt text` });
390
+ }
391
+ else if (imgs.length > 0) {
392
+ issues.push({ type: "success", message: `All ${imgs.length} images have alt text` });
393
+ }
394
+ // Canonical
395
+ const canonical = document.querySelector('link[rel="canonical"]');
396
+ if (!canonical) {
397
+ issues.push({ type: "warning", message: "Missing canonical link" });
398
+ }
399
+ else {
400
+ issues.push({ type: "success", message: "Canonical URL is set" });
401
+ }
402
+ // Open Graph
403
+ const ogTitle = document.querySelector('meta[property="og:title"]');
404
+ if (!ogTitle) {
405
+ issues.push({ type: "warning", message: "Missing og:title (Open Graph)" });
406
+ }
407
+ else {
408
+ issues.push({ type: "success", message: "Open Graph tags present" });
409
+ }
410
+ // Viewport (mobile SEO)
411
+ const viewport = document.querySelector('meta[name="viewport"]');
412
+ if (!viewport) {
413
+ issues.push({ type: "error", message: "Missing viewport meta tag" });
414
+ }
415
+ return issues;
416
+ }
417
+ const colors = {
418
+ error: "#ff5f57",
419
+ warning: "#febc2e",
420
+ success: "#28c840",
421
+ };
422
+ const icons = {
423
+ error: "✕",
424
+ warning: "⚠",
425
+ success: "✓",
426
+ };
427
+ /**
428
+ * SEOAudit - Dev-only floating widget that scans the current page
429
+ * and shows a live SEO score with actionable issues.
430
+ * Automatically disabled in production (NODE_ENV !== "development").
431
+ *
432
+ * @example
433
+ * // Drop it once near the root of your app
434
+ * <SEOAudit />
435
+ */
436
+ function SEOAudit() {
437
+ const [issues, setIssues] = React.useState([]);
438
+ const [open, setOpen] = React.useState(false);
439
+ React.useEffect(() => {
440
+ if (process.env.NODE_ENV !== "development")
441
+ return;
442
+ const timer = setTimeout(() => setIssues(runAudit()), 600);
443
+ return () => clearTimeout(timer);
444
+ }, []);
445
+ if (process.env.NODE_ENV !== "development")
446
+ return null;
447
+ if (issues.length === 0)
448
+ return null;
449
+ const errors = issues.filter((i) => i.type === "error").length;
450
+ const warnings = issues.filter((i) => i.type === "warning").length;
451
+ const score = Math.max(0, 100 - errors * 20 - warnings * 8);
452
+ const scoreColor = score >= 80 ? "#28c840" : score >= 50 ? "#febc2e" : "#ff5f57";
453
+ return (React.createElement("div", { style: {
454
+ position: "fixed",
455
+ bottom: 20,
456
+ right: 20,
457
+ zIndex: 999999,
458
+ fontFamily: "'JetBrains Mono', monospace",
459
+ fontSize: 13,
460
+ } },
461
+ open && (React.createElement("div", { style: {
462
+ background: "#0d1221",
463
+ border: "1px solid #1e2a45",
464
+ borderRadius: 12,
465
+ padding: 16,
466
+ width: 320,
467
+ marginBottom: 12,
468
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
469
+ maxHeight: 360,
470
+ overflowY: "auto",
471
+ } },
472
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 } },
473
+ React.createElement("span", { style: { color: "#e8edf8", fontWeight: 700 } }, "SEO Audit"),
474
+ React.createElement("span", { style: { color: scoreColor, fontWeight: 700 } },
475
+ score,
476
+ "/100")),
477
+ issues.map((issue, i) => (React.createElement("div", { key: i, style: {
478
+ display: "flex",
479
+ gap: 8,
480
+ alignItems: "flex-start",
481
+ padding: "6px 0",
482
+ borderTop: i === 0 ? "none" : "1px solid #1e2a45",
483
+ color: "#a8b3c7",
484
+ fontSize: 12,
485
+ lineHeight: 1.5,
486
+ } },
487
+ React.createElement("span", { style: { color: colors[issue.type], flexShrink: 0 } }, icons[issue.type]),
488
+ React.createElement("span", null, issue.message)))))),
489
+ React.createElement("button", { onClick: () => setOpen(!open), style: {
490
+ background: "#0d1221",
491
+ border: `2px solid ${scoreColor}`,
492
+ borderRadius: "50%",
493
+ width: 56,
494
+ height: 56,
495
+ color: scoreColor,
496
+ fontWeight: 700,
497
+ fontSize: 16,
498
+ cursor: "pointer",
499
+ boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
500
+ }, title: "smart-seo-lite SEO Audit" }, score)));
501
+ }
502
+
503
+ /**
504
+ * generateSitemap - Builds a valid sitemap.xml string from a list of routes.
505
+ * Use this in a Next.js API route, app/sitemap.xml/route.ts, or a build script.
506
+ *
507
+ * @example
508
+ * const xml = generateSitemap("https://myapp.com", [
509
+ * { url: "/", priority: 1.0, changeFrequency: "daily" },
510
+ * { url: "/about", priority: 0.8 },
511
+ * ]);
512
+ */
513
+ function generateSitemap(baseUrl, routes) {
514
+ const base = baseUrl.replace(/\/$/, "");
515
+ const urlEntries = routes
516
+ .map((route) => {
517
+ const loc = `${base}${route.url.startsWith("/") ? "" : "/"}${route.url}`;
518
+ const parts = [` <loc>${loc}</loc>`];
519
+ if (route.lastModified)
520
+ parts.push(` <lastmod>${route.lastModified}</lastmod>`);
521
+ if (route.changeFrequency)
522
+ parts.push(` <changefreq>${route.changeFrequency}</changefreq>`);
523
+ if (route.priority !== undefined)
524
+ parts.push(` <priority>${route.priority}</priority>`);
525
+ return ` <url>\n${parts.join("\n")}\n </url>`;
526
+ })
527
+ .join("\n");
528
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urlEntries}\n</urlset>`;
529
+ }
530
+
531
+ /**
532
+ * generateRobotsTxt - Builds a valid robots.txt string.
533
+ * Use this in a Next.js API route, app/robots.txt/route.ts, or a build script.
534
+ *
535
+ * @example
536
+ * const txt = generateRobotsTxt({
537
+ * disallow: ["/admin", "/api"],
538
+ * sitemap: "https://myapp.com/sitemap.xml",
539
+ * });
540
+ */
541
+ function generateRobotsTxt(options = {}) {
542
+ var _a;
543
+ const rules = ((_a = options.rules) === null || _a === void 0 ? void 0 : _a.length)
544
+ ? options.rules
545
+ : [{ userAgent: options.userAgent, allow: options.allow, disallow: options.disallow }];
546
+ const lines = [];
547
+ rules.forEach((rule) => {
548
+ lines.push(`User-agent: ${rule.userAgent || "*"}`);
549
+ (rule.allow || []).forEach((path) => lines.push(`Allow: ${path}`));
550
+ (rule.disallow || []).forEach((path) => lines.push(`Disallow: ${path}`));
551
+ lines.push("");
552
+ });
553
+ if (options.sitemap) {
554
+ lines.push(`Sitemap: ${options.sitemap}`);
555
+ }
556
+ return lines.join("\n").trim();
557
+ }
558
+
559
+ exports.Breadcrumbs = Breadcrumbs;
560
+ exports.SEOAudit = SEOAudit;
278
561
  exports.SEOImg = SEOImg;
279
562
  exports.Schema = Schema;
563
+ exports.generateRobotsTxt = generateRobotsTxt;
564
+ exports.generateSitemap = generateSitemap;
280
565
  exports.useSEO = useSEO;
@@ -0,0 +1,20 @@
1
+ export interface RobotsRule {
2
+ userAgent?: string;
3
+ allow?: string[];
4
+ disallow?: string[];
5
+ }
6
+ export interface RobotsOptions extends RobotsRule {
7
+ sitemap?: string;
8
+ rules?: RobotsRule[];
9
+ }
10
+ /**
11
+ * generateRobotsTxt - Builds a valid robots.txt string.
12
+ * Use this in a Next.js API route, app/robots.txt/route.ts, or a build script.
13
+ *
14
+ * @example
15
+ * const txt = generateRobotsTxt({
16
+ * disallow: ["/admin", "/api"],
17
+ * sitemap: "https://myapp.com/sitemap.xml",
18
+ * });
19
+ */
20
+ export declare function generateRobotsTxt(options?: RobotsOptions): string;
@@ -0,0 +1,17 @@
1
+ export interface SitemapRoute {
2
+ url: string;
3
+ lastModified?: string;
4
+ changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
5
+ priority?: number;
6
+ }
7
+ /**
8
+ * generateSitemap - Builds a valid sitemap.xml string from a list of routes.
9
+ * Use this in a Next.js API route, app/sitemap.xml/route.ts, or a build script.
10
+ *
11
+ * @example
12
+ * const xml = generateSitemap("https://myapp.com", [
13
+ * { url: "/", priority: 1.0, changeFrequency: "daily" },
14
+ * { url: "/about", priority: 0.8 },
15
+ * ]);
16
+ */
17
+ export declare function generateSitemap(baseUrl: string, routes: SitemapRoute[]): string;
package/dist/useSEO.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export interface AlternateLocale {
2
+ /** Language/region code, e.g. "en-US", "fr", "x-default" */
3
+ locale: string;
4
+ /** Fully-qualified URL for that locale's version of the page */
5
+ url: string;
6
+ }
1
7
  export interface SEOProps {
2
8
  title?: string;
3
9
  description?: string;
@@ -9,6 +15,8 @@ export interface SEOProps {
9
15
  noIndex?: boolean;
10
16
  canonical?: string;
11
17
  keywords?: string[];
18
+ /** Multilingual SEO: renders <link rel="alternate" hreflang="..."> for each locale */
19
+ alternateLocales?: AlternateLocale[];
12
20
  }
13
21
  /**
14
22
  * useSEO - Automatically handles:
@@ -25,4 +33,4 @@ export interface SEOProps {
25
33
  * image: "/banner.png"
26
34
  * });
27
35
  */
28
- export declare function useSEO({ title, description, image, url, siteName, type, twitterHandle, noIndex, canonical, keywords, }: SEOProps): void;
36
+ export declare function useSEO({ title, description, image, url, siteName, type, twitterHandle, noIndex, canonical, keywords, alternateLocales, }: SEOProps): void;
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "smart-seo-lite",
3
- "version": "1.0.0",
4
- "type": "module",
5
- "description": "Add SEO to your React/Next.js app in 3 lines of code. Auto meta tags, JSON-LD schema, and smart image SEO.",
3
+ "version": "2.0.0",
4
+ "description": "Add SEO to your React/Next.js app in 3 lines of code. Auto meta tags, JSON-LD schema, smart image SEO, breadcrumbs, sitemap/robots.txt generators, hreflang support, and a live dev SEO audit widget.",
6
5
  "main": "dist/index.js",
7
6
  "module": "dist/index.esm.js",
8
7
  "types": "dist/index.d.ts",
@@ -24,7 +23,13 @@
24
23
  "json-ld",
25
24
  "twitter-card",
26
25
  "canonical",
27
- "image-seo"
26
+ "image-seo",
27
+ "breadcrumbs",
28
+ "sitemap",
29
+ "robots-txt",
30
+ "hreflang",
31
+ "seo-audit",
32
+ "core-web-vitals"
28
33
  ],
29
34
  "author": "Virendra",
30
35
  "license": "MIT",
@@ -34,10 +39,10 @@
34
39
  },
35
40
  "devDependencies": {
36
41
  "@rollup/plugin-typescript": "^11.0.0",
37
- "@types/node": "^25.6.0",
38
- "@types/react": "^19.2.14",
42
+ "@types/node": "^20.0.0",
43
+ "@types/react": "^18.0.0",
39
44
  "rollup": "^3.0.0",
40
- "tslib": "^2.8.1",
45
+ "tslib": "^2.6.0",
41
46
  "typescript": "^5.0.0"
42
47
  },
43
48
  "repository": {