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.
- package/dist/Breadcrumbs.d.ts +25 -0
- package/dist/SEOAudit.d.ts +11 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.esm.js +285 -4
- package/dist/index.js +287 -2
- package/dist/robots.d.ts +20 -0
- package/dist/sitemap.d.ts +17 -0
- package/dist/useSEO.d.ts +9 -1
- package/package.json +12 -7
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/robots.d.ts
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
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": "^
|
|
38
|
-
"@types/react": "^
|
|
42
|
+
"@types/node": "^20.0.0",
|
|
43
|
+
"@types/react": "^18.0.0",
|
|
39
44
|
"rollup": "^3.0.0",
|
|
40
|
-
"tslib": "^2.
|
|
45
|
+
"tslib": "^2.6.0",
|
|
41
46
|
"typescript": "^5.0.0"
|
|
42
47
|
},
|
|
43
48
|
"repository": {
|