@geenius/seo 0.1.0 → 0.3.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 +16 -3
- package/packages/convex/dist/index.d.ts +56 -0
- package/packages/convex/dist/index.js +133 -0
- package/packages/convex/dist/index.js.map +1 -0
- package/packages/react/README.md +1 -1
- package/packages/react/dist/index.d.ts +156 -0
- package/packages/react/dist/index.js +567 -0
- package/packages/react/dist/index.js.map +1 -0
- package/packages/react-css/README.md +1 -1
- package/packages/react-css/dist/index.cjs +571 -0
- package/packages/react-css/dist/index.cjs.map +1 -0
- package/packages/react-css/{src/seo.css → dist/index.css} +7 -153
- package/packages/react-css/dist/index.css.map +1 -0
- package/packages/react-css/dist/index.d.cts +53 -0
- package/packages/react-css/dist/index.d.ts +53 -0
- package/packages/react-css/dist/index.js +539 -0
- package/packages/react-css/dist/index.js.map +1 -0
- package/packages/shared/README.md +1 -1
- package/packages/shared/dist/index.d.ts +262 -0
- package/packages/shared/dist/index.js +381 -0
- package/packages/shared/dist/index.js.map +1 -0
- package/packages/solidjs/README.md +1 -1
- package/packages/solidjs/dist/index.d.ts +133 -0
- package/packages/solidjs/dist/index.js +416 -0
- package/packages/solidjs/dist/index.js.map +1 -0
- package/packages/solidjs-css/README.md +1 -1
- package/packages/solidjs-css/dist/index.cjs +399 -0
- package/packages/solidjs-css/dist/index.cjs.map +1 -0
- package/packages/solidjs-css/{src/seo.css → dist/index.css} +7 -153
- package/packages/solidjs-css/dist/index.css.map +1 -0
- package/packages/solidjs-css/dist/index.d.cts +53 -0
- package/packages/solidjs-css/dist/index.d.ts +53 -0
- package/packages/solidjs-css/dist/index.js +367 -0
- package/packages/solidjs-css/dist/index.js.map +1 -0
- package/.changeset/config.json +0 -11
- package/.github/CODEOWNERS +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -16
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -10
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/ci.yml +0 -23
- package/.github/workflows/release.yml +0 -29
- package/.nvmrc +0 -1
- package/.project/ACCOUNT.yaml +0 -4
- package/.project/IDEAS.yaml +0 -7
- package/.project/PROJECT.yaml +0 -11
- package/.project/ROADMAP.yaml +0 -15
- package/CODE_OF_CONDUCT.md +0 -16
- package/CONTRIBUTING.md +0 -26
- package/SECURITY.md +0 -15
- package/SUPPORT.md +0 -8
- package/packages/convex/package.json +0 -42
- package/packages/convex/src/functions.ts +0 -5
- package/packages/convex/src/mutations.ts +0 -83
- package/packages/convex/src/queries.ts +0 -57
- package/packages/convex/src/schema.ts +0 -23
- package/packages/convex/tsconfig.json +0 -19
- package/packages/convex/tsup.config.ts +0 -18
- package/packages/react/package.json +0 -49
- package/packages/react/src/components/ArticleJsonLd.tsx +0 -42
- package/packages/react/src/components/BreadcrumbsJsonLd.tsx +0 -24
- package/packages/react/src/components/MetaEditor.tsx +0 -147
- package/packages/react/src/components/SEOHead.tsx +0 -107
- package/packages/react/src/components/SEOPreview.tsx +0 -42
- package/packages/react/src/components/SEOScoreCard.tsx +0 -51
- package/packages/react/src/components/SitemapViewer.tsx +0 -36
- package/packages/react/src/components/index.ts +0 -7
- package/packages/react/src/hooks/index.ts +0 -4
- package/packages/react/src/hooks/useSEO.ts +0 -27
- package/packages/react/src/hooks/useSEOAdmin.ts +0 -42
- package/packages/react/src/hooks/useSEOScore.ts +0 -7
- package/packages/react/src/hooks/useSitemap.ts +0 -8
- package/packages/react/src/index.ts +0 -51
- package/packages/react/src/index.tsx +0 -11
- package/packages/react/src/pages/SEOAdminPage.tsx +0 -101
- package/packages/react/src/pages/SEOAnalyticsPage.tsx +0 -96
- package/packages/react/src/pages/index.ts +0 -2
- package/packages/react/tsconfig.json +0 -19
- package/packages/react/tsup.config.ts +0 -12
- package/packages/react-css/package.json +0 -36
- package/packages/react-css/src/components/ArticleJsonLd.tsx +0 -42
- package/packages/react-css/src/components/BreadcrumbsJsonLd.tsx +0 -24
- package/packages/react-css/src/components/MetaEditor.tsx +0 -147
- package/packages/react-css/src/components/SEOHead.tsx +0 -95
- package/packages/react-css/src/components/SEOPreview.tsx +0 -42
- package/packages/react-css/src/components/SEOScoreCard.tsx +0 -42
- package/packages/react-css/src/components/SitemapViewer.tsx +0 -36
- package/packages/react-css/src/components/index.ts +0 -7
- package/packages/react-css/src/index.ts +0 -9
- package/packages/react-css/src/pages/SEOAdminPage.tsx +0 -88
- package/packages/react-css/src/pages/SEOAnalyticsPage.tsx +0 -82
- package/packages/react-css/src/pages/index.ts +0 -2
- package/packages/react-css/tsup.config.ts +0 -2
- package/packages/shared/package.json +0 -42
- package/packages/shared/src/__tests__/seo.test.ts +0 -70
- package/packages/shared/src/config.ts +0 -297
- package/packages/shared/src/index.ts +0 -207
- package/packages/shared/tsconfig.json +0 -18
- package/packages/shared/tsup.config.ts +0 -11
- package/packages/shared/vitest.config.ts +0 -4
- package/packages/solidjs/package.json +0 -45
- package/packages/solidjs/src/components/ArticleJsonLd.tsx +0 -35
- package/packages/solidjs/src/components/BreadcrumbsJsonLd.tsx +0 -24
- package/packages/solidjs/src/components/MetaEditor.tsx +0 -155
- package/packages/solidjs/src/components/SEOHead.tsx +0 -109
- package/packages/solidjs/src/components/SEOPreview.tsx +0 -42
- package/packages/solidjs/src/components/SEOScoreCard.tsx +0 -57
- package/packages/solidjs/src/components/SitemapViewer.tsx +0 -44
- package/packages/solidjs/src/components/index.ts +0 -7
- package/packages/solidjs/src/index.ts +0 -11
- package/packages/solidjs/src/pages/SEOAdminPage.tsx +0 -104
- package/packages/solidjs/src/pages/SEOAnalyticsPage.tsx +0 -102
- package/packages/solidjs/src/pages/index.ts +0 -2
- package/packages/solidjs/src/primitives/index.ts +0 -4
- package/packages/solidjs/src/primitives/useSEO.ts +0 -27
- package/packages/solidjs/src/primitives/useSEOAdmin.ts +0 -42
- package/packages/solidjs/src/primitives/useSEOScore.ts +0 -7
- package/packages/solidjs/src/primitives/useSitemap.ts +0 -8
- package/packages/solidjs/tsconfig.json +0 -20
- package/packages/solidjs/tsup.config.ts +0 -12
- package/packages/solidjs-css/package.json +0 -35
- package/packages/solidjs-css/src/index.ts +0 -5
- package/packages/solidjs-css/src/primitives/index.ts +0 -1
- package/packages/solidjs-css/tsup.config.ts +0 -2
- package/pnpm-workspace.yaml +0 -2
- package/tsconfig.json +0 -23
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
interface AnalyticsSummary {
|
|
4
|
-
path: string
|
|
5
|
-
views: number
|
|
6
|
-
avgTimeOnPage: number
|
|
7
|
-
bounceRate: number
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function SEOAnalyticsPage() {
|
|
11
|
-
const [topPages, setTopPages] = useState<AnalyticsSummary[]>([])
|
|
12
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
setIsLoading(true)
|
|
16
|
-
// Placeholder for Convex query integration
|
|
17
|
-
// const result = await client.query(api.queries.getTopPages, { limit: 10 })
|
|
18
|
-
// setTopPages(result)
|
|
19
|
-
setIsLoading(false)
|
|
20
|
-
}, [])
|
|
21
|
-
|
|
22
|
-
if (isLoading) return <div className="p-4">Loading...</div>
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<div className="max-w-6xl mx-auto p-6">
|
|
26
|
-
<h1 className="text-3xl font-bold mb-6">SEO Analytics</h1>
|
|
27
|
-
|
|
28
|
-
<div className="space-y-6">
|
|
29
|
-
{/* Top Pages */}
|
|
30
|
-
<div className="border border-gray-200 rounded p-4">
|
|
31
|
-
<h2 className="font-semibold mb-4">Top Pages by Views</h2>
|
|
32
|
-
|
|
33
|
-
<div className="overflow-x-auto">
|
|
34
|
-
<table className="w-full text-sm">
|
|
35
|
-
<thead className="bg-gray-100 border-b">
|
|
36
|
-
<tr>
|
|
37
|
-
<th className="text-left p-3">Page</th>
|
|
38
|
-
<th className="text-right p-3">Views</th>
|
|
39
|
-
<th className="text-right p-3">Avg Time (s)</th>
|
|
40
|
-
<th className="text-right p-3">Bounce Rate</th>
|
|
41
|
-
</tr>
|
|
42
|
-
</thead>
|
|
43
|
-
<tbody>
|
|
44
|
-
{topPages.map((page, i) => (
|
|
45
|
-
<tr key={i} className="border-b hover:bg-gray-50">
|
|
46
|
-
<td className="p-3 font-mono text-xs">{page.path}</td>
|
|
47
|
-
<td className="p-3 text-right">{page.views.toLocaleString()}</td>
|
|
48
|
-
<td className="p-3 text-right">{page.avgTimeOnPage.toFixed(1)}</td>
|
|
49
|
-
<td className="p-3 text-right">{(page.bounceRate * 100).toFixed(1)}%</td>
|
|
50
|
-
</tr>
|
|
51
|
-
))}
|
|
52
|
-
</tbody>
|
|
53
|
-
</table>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
{topPages.length === 0 && (
|
|
57
|
-
<p className="text-center text-gray-500 py-8">No analytics data available</p>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
{/* Analytics Summary */}
|
|
62
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
63
|
-
<div className="border border-gray-200 rounded p-4">
|
|
64
|
-
<h3 className="text-sm text-gray-600 mb-1">Total Views</h3>
|
|
65
|
-
<p className="text-2xl font-bold">
|
|
66
|
-
{topPages.reduce((sum, p) => sum + p.views, 0).toLocaleString()}
|
|
67
|
-
</p>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div className="border border-gray-200 rounded p-4">
|
|
71
|
-
<h3 className="text-sm text-gray-600 mb-1">Average Time on Page</h3>
|
|
72
|
-
<p className="text-2xl font-bold">
|
|
73
|
-
{topPages.length > 0
|
|
74
|
-
? (topPages.reduce((sum, p) => sum + p.avgTimeOnPage, 0) / topPages.length).toFixed(1)
|
|
75
|
-
: '0'}
|
|
76
|
-
s
|
|
77
|
-
</p>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div className="border border-gray-200 rounded p-4">
|
|
81
|
-
<h3 className="text-sm text-gray-600 mb-1">Average Bounce Rate</h3>
|
|
82
|
-
<p className="text-2xl font-bold">
|
|
83
|
-
{topPages.length > 0
|
|
84
|
-
? (
|
|
85
|
-
(topPages.reduce((sum, p) => sum + p.bounceRate, 0) / topPages.length) *
|
|
86
|
-
100
|
|
87
|
-
).toFixed(1)
|
|
88
|
-
: '0'}
|
|
89
|
-
%
|
|
90
|
-
</p>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "dist",
|
|
5
|
-
"rootDir": "src",
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"resolveJsonModule": true,
|
|
11
|
-
"isolatedModules": true,
|
|
12
|
-
"target": "ES2022",
|
|
13
|
-
"module": "ESNext",
|
|
14
|
-
"moduleResolution": "bundler"
|
|
15
|
-
},
|
|
16
|
-
"include": [
|
|
17
|
-
"src"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@geenius-seo/react-css",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "Geenius SEO — react components with vanilla CSS",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.mjs",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js",
|
|
13
|
-
"types": "./dist/index.d.ts"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsup",
|
|
18
|
-
"lint": "tsc --noEmit",
|
|
19
|
-
"clean": "rm -rf dist"
|
|
20
|
-
},
|
|
21
|
-
"files": ["dist"],
|
|
22
|
-
"publishConfig": { "access": "public" },
|
|
23
|
-
"peerDependencies": { "react": ">=18.0.0" },
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@geenius-seo/shared": "workspace:*"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/react": "^19.0.0",
|
|
29
|
-
"react": "^19.2.4",
|
|
30
|
-
"tsup": "^8.5.1",
|
|
31
|
-
"typescript": "~6.0.2"
|
|
32
|
-
},
|
|
33
|
-
"author": "Antigravity HQ",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"engines": { "node": ">=20.0.0" }
|
|
36
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import { articleSchema } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface ArticleJsonLdProps {
|
|
5
|
-
title: string
|
|
6
|
-
description: string
|
|
7
|
-
author: string
|
|
8
|
-
datePublished: string
|
|
9
|
-
url: string
|
|
10
|
-
image?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function ArticleJsonLd({
|
|
14
|
-
title,
|
|
15
|
-
description,
|
|
16
|
-
author,
|
|
17
|
-
datePublished,
|
|
18
|
-
url,
|
|
19
|
-
image,
|
|
20
|
-
}: ArticleJsonLdProps) {
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const script = document.createElement('script')
|
|
23
|
-
script.type = 'application/ld+json'
|
|
24
|
-
script.textContent = JSON.stringify(
|
|
25
|
-
articleSchema({
|
|
26
|
-
title,
|
|
27
|
-
description,
|
|
28
|
-
author,
|
|
29
|
-
datePublished,
|
|
30
|
-
url,
|
|
31
|
-
image,
|
|
32
|
-
}),
|
|
33
|
-
)
|
|
34
|
-
document.head.appendChild(script)
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
document.head.removeChild(script)
|
|
38
|
-
}
|
|
39
|
-
}, [title, description, author, datePublished, url, image])
|
|
40
|
-
|
|
41
|
-
return <div className="seo__article-jsonld" />
|
|
42
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import { breadcrumbSchema } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface BreadcrumbsJsonLdProps {
|
|
5
|
-
items: {
|
|
6
|
-
name: string
|
|
7
|
-
url: string
|
|
8
|
-
}[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function BreadcrumbsJsonLd({ items }: BreadcrumbsJsonLdProps) {
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const script = document.createElement('script')
|
|
14
|
-
script.type = 'application/ld+json'
|
|
15
|
-
script.textContent = JSON.stringify(breadcrumbSchema(items))
|
|
16
|
-
document.head.appendChild(script)
|
|
17
|
-
|
|
18
|
-
return () => {
|
|
19
|
-
document.head.removeChild(script)
|
|
20
|
-
}
|
|
21
|
-
}, [items])
|
|
22
|
-
|
|
23
|
-
return <div className="seo__breadcrumbs" />
|
|
24
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import type { SEOMeta } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface MetaEditorProps {
|
|
5
|
-
meta: SEOMeta
|
|
6
|
-
onChange: (meta: SEOMeta) => void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function MetaEditor({ meta, onChange }: MetaEditorProps) {
|
|
10
|
-
const [localMeta, setLocalMeta] = useState(meta)
|
|
11
|
-
|
|
12
|
-
const handleChange = (updates: Partial<SEOMeta>) => {
|
|
13
|
-
const updated = { ...localMeta, ...updates }
|
|
14
|
-
setLocalMeta(updated)
|
|
15
|
-
onChange(updated)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div className="seo__meta-editor">
|
|
20
|
-
<div className="seo__meta-field">
|
|
21
|
-
<label className="seo__meta-field-label">
|
|
22
|
-
Title ({localMeta.title.length}/60)
|
|
23
|
-
</label>
|
|
24
|
-
<input
|
|
25
|
-
type="text"
|
|
26
|
-
value={localMeta.title}
|
|
27
|
-
onChange={(e) =>
|
|
28
|
-
handleChange({
|
|
29
|
-
title: e.target.value.slice(0, 60),
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
className="seo__meta-field-input"
|
|
33
|
-
placeholder="Page title"
|
|
34
|
-
/>
|
|
35
|
-
<div className="seo__meta-char-count">
|
|
36
|
-
{localMeta.title.length < 30
|
|
37
|
-
? 'Too short'
|
|
38
|
-
: localMeta.title.length > 60
|
|
39
|
-
? 'Too long'
|
|
40
|
-
: 'Good length'}
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div className="seo__meta-field">
|
|
45
|
-
<label className="seo__meta-field-label">
|
|
46
|
-
Description ({localMeta.description.length}/160)
|
|
47
|
-
</label>
|
|
48
|
-
<textarea
|
|
49
|
-
value={localMeta.description}
|
|
50
|
-
onChange={(e) =>
|
|
51
|
-
handleChange({
|
|
52
|
-
description: e.target.value.slice(0, 160),
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
className="seo__meta-field-textarea"
|
|
56
|
-
placeholder="Page description"
|
|
57
|
-
/>
|
|
58
|
-
<div className="seo__meta-char-count">
|
|
59
|
-
{localMeta.description.length < 50
|
|
60
|
-
? 'Too short'
|
|
61
|
-
: localMeta.description.length > 160
|
|
62
|
-
? 'Too long'
|
|
63
|
-
: 'Good length'}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div className="seo__meta-field">
|
|
68
|
-
<label className="seo__meta-field-label">Keywords</label>
|
|
69
|
-
<input
|
|
70
|
-
type="text"
|
|
71
|
-
value={localMeta.keywords.join(', ')}
|
|
72
|
-
onChange={(e) =>
|
|
73
|
-
handleChange({
|
|
74
|
-
keywords: e.target.value.split(',').map((k) => k.trim()),
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
className="seo__meta-field-input"
|
|
78
|
-
placeholder="keyword1, keyword2, keyword3"
|
|
79
|
-
/>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<div className="seo__meta-field">
|
|
83
|
-
<label className="seo__meta-field-label">Canonical URL</label>
|
|
84
|
-
<input
|
|
85
|
-
type="url"
|
|
86
|
-
value={localMeta.canonical || ''}
|
|
87
|
-
onChange={(e) =>
|
|
88
|
-
handleChange({
|
|
89
|
-
canonical: e.target.value,
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
className="seo__meta-field-input"
|
|
93
|
-
placeholder="https://example.com/page"
|
|
94
|
-
/>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div className="seo__meta-field">
|
|
98
|
-
<label className="seo__meta-field-label">OG Image URL</label>
|
|
99
|
-
<input
|
|
100
|
-
type="url"
|
|
101
|
-
value={localMeta.og.image || ''}
|
|
102
|
-
onChange={(e) =>
|
|
103
|
-
handleChange({
|
|
104
|
-
og: { ...localMeta.og, image: e.target.value },
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
className="seo__meta-field-input"
|
|
108
|
-
placeholder="https://example.com/image.jpg"
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div className="seo__meta-field">
|
|
113
|
-
<label className="seo__meta-field-label">Twitter Card Type</label>
|
|
114
|
-
<select
|
|
115
|
-
value={localMeta.twitter.card}
|
|
116
|
-
onChange={(e) =>
|
|
117
|
-
handleChange({
|
|
118
|
-
twitter: {
|
|
119
|
-
...localMeta.twitter,
|
|
120
|
-
card: e.target.value as 'summary' | 'summary_large_image',
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
className="seo__meta-field-select"
|
|
125
|
-
>
|
|
126
|
-
<option value="summary">Summary</option>
|
|
127
|
-
<option value="summary_large_image">Summary Large Image</option>
|
|
128
|
-
</select>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div className="seo__meta-field">
|
|
132
|
-
<label className="seo__meta-field-label">Robots Directive</label>
|
|
133
|
-
<input
|
|
134
|
-
type="text"
|
|
135
|
-
value={localMeta.robots || ''}
|
|
136
|
-
onChange={(e) =>
|
|
137
|
-
handleChange({
|
|
138
|
-
robots: e.target.value,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
className="seo__meta-field-input"
|
|
142
|
-
placeholder="index, follow"
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import type { SEOMeta } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface SEOHeadProps {
|
|
5
|
-
meta: SEOMeta
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function SEOHead({ meta }: SEOHeadProps) {
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
document.title = meta.title
|
|
11
|
-
|
|
12
|
-
const setMetaTag = (name: string, content: string, isProperty = false) => {
|
|
13
|
-
let tag = document.querySelector(
|
|
14
|
-
`meta[${isProperty ? 'property' : 'name'}="${name}"]`,
|
|
15
|
-
) as HTMLMetaElement
|
|
16
|
-
if (!tag) {
|
|
17
|
-
tag = document.createElement('meta')
|
|
18
|
-
isProperty ? tag.setAttribute('property', name) : tag.setAttribute('name', name)
|
|
19
|
-
document.head.appendChild(tag)
|
|
20
|
-
}
|
|
21
|
-
tag.content = content
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
setMetaTag('description', meta.description)
|
|
25
|
-
if (meta.keywords.length > 0) {
|
|
26
|
-
setMetaTag('keywords', meta.keywords.join(', '))
|
|
27
|
-
}
|
|
28
|
-
if (meta.robots) {
|
|
29
|
-
setMetaTag('robots', meta.robots)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setMetaTag('og:title', meta.og.title, true)
|
|
33
|
-
setMetaTag('og:description', meta.og.description, true)
|
|
34
|
-
if (meta.og.image) {
|
|
35
|
-
setMetaTag('og:image', meta.og.image, true)
|
|
36
|
-
if (meta.og.imageAlt) {
|
|
37
|
-
setMetaTag('og:image:alt', meta.og.imageAlt, true)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
setMetaTag('og:type', meta.og.type, true)
|
|
41
|
-
setMetaTag('og:url', meta.og.url, true)
|
|
42
|
-
if (meta.og.siteName) {
|
|
43
|
-
setMetaTag('og:site_name', meta.og.siteName, true)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setMetaTag('twitter:card', meta.twitter.card)
|
|
47
|
-
setMetaTag('twitter:title', meta.twitter.title)
|
|
48
|
-
setMetaTag('twitter:description', meta.twitter.description)
|
|
49
|
-
if (meta.twitter.image) {
|
|
50
|
-
setMetaTag('twitter:image', meta.twitter.image)
|
|
51
|
-
}
|
|
52
|
-
if (meta.twitter.creator) {
|
|
53
|
-
setMetaTag('twitter:creator', meta.twitter.creator)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (meta.canonical) {
|
|
57
|
-
let canonicalLink = document.querySelector('link[rel="canonical"]') as HTMLLinkElement
|
|
58
|
-
if (!canonicalLink) {
|
|
59
|
-
canonicalLink = document.createElement('link')
|
|
60
|
-
canonicalLink.rel = 'canonical'
|
|
61
|
-
document.head.appendChild(canonicalLink)
|
|
62
|
-
}
|
|
63
|
-
canonicalLink.href = meta.canonical
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (meta.alternates) {
|
|
67
|
-
Object.entries(meta.alternates).forEach(([lang, url]) => {
|
|
68
|
-
let altLink = document.querySelector(
|
|
69
|
-
`link[rel="alternate"][hreflang="${lang}"]`,
|
|
70
|
-
) as HTMLLinkElement
|
|
71
|
-
if (!altLink) {
|
|
72
|
-
altLink = document.createElement('link')
|
|
73
|
-
altLink.rel = 'alternate'
|
|
74
|
-
altLink.hrefLang = lang
|
|
75
|
-
document.head.appendChild(altLink)
|
|
76
|
-
}
|
|
77
|
-
altLink.href = url
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (meta.jsonLd && meta.jsonLd.length > 0) {
|
|
82
|
-
meta.jsonLd.forEach((schema) => {
|
|
83
|
-
let script = document.querySelector('script[type="application/ld+json"]') as HTMLScriptElement
|
|
84
|
-
if (!script) {
|
|
85
|
-
script = document.createElement('script')
|
|
86
|
-
script.type = 'application/ld+json'
|
|
87
|
-
document.head.appendChild(script)
|
|
88
|
-
}
|
|
89
|
-
script.textContent = JSON.stringify(schema)
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
}, [meta])
|
|
93
|
-
|
|
94
|
-
return <div className="seo__head" />
|
|
95
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { SEOMeta } from '@geenius-seo/shared'
|
|
2
|
-
|
|
3
|
-
interface SEOPreviewProps {
|
|
4
|
-
meta: SEOMeta
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function SEOPreview({ meta }: SEOPreviewProps) {
|
|
8
|
-
return (
|
|
9
|
-
<div className="seo__preview">
|
|
10
|
-
<div>
|
|
11
|
-
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem' }}>
|
|
12
|
-
Google Search Preview
|
|
13
|
-
</h3>
|
|
14
|
-
<div className="seo__preview-google">
|
|
15
|
-
<div className="seo__preview-google-url">{new URL(meta.og.url).hostname}</div>
|
|
16
|
-
<div className="seo__preview-google-title">{meta.title}</div>
|
|
17
|
-
<div className="seo__preview-google-desc">{meta.description}</div>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<div>
|
|
22
|
-
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem' }}>
|
|
23
|
-
Social Media Preview
|
|
24
|
-
</h3>
|
|
25
|
-
<div className="seo__preview-social">
|
|
26
|
-
{meta.og.image && (
|
|
27
|
-
<img
|
|
28
|
-
src={meta.og.image}
|
|
29
|
-
alt={meta.og.imageAlt || meta.og.title}
|
|
30
|
-
className="seo__preview-social-image"
|
|
31
|
-
/>
|
|
32
|
-
)}
|
|
33
|
-
<div className="seo__preview-social-info">
|
|
34
|
-
<div className="seo__preview-social-title">{meta.og.title}</div>
|
|
35
|
-
<div className="seo__preview-social-desc">{meta.og.description}</div>
|
|
36
|
-
<div className="seo__preview-social-url">{meta.og.url}</div>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useSEOScore } from '@geenius-seo/react'
|
|
2
|
-
import type { SEOMeta } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface SEOScoreCardProps {
|
|
5
|
-
meta: SEOMeta
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function SEOScoreCard({ meta }: SEOScoreCardProps) {
|
|
9
|
-
const { score, issues } = useSEOScore(meta)
|
|
10
|
-
|
|
11
|
-
const scoreClass =
|
|
12
|
-
score >= 80 ? 'seo__score-number--good' : score >= 50 ? 'seo__score-number--fair' : 'seo__score-number--poor'
|
|
13
|
-
const barClass =
|
|
14
|
-
score >= 80 ? 'seo__score-bar-fill--good' : score >= 50 ? 'seo__score-bar-fill--fair' : 'seo__score-bar-fill--poor'
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="seo__score-card">
|
|
18
|
-
<div className="seo__score-card-header">
|
|
19
|
-
<div className="seo__score-card-title">SEO Score</div>
|
|
20
|
-
<span className={`seo__score-number ${scoreClass}`}>{score}/100</span>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div className="seo__score-bar">
|
|
24
|
-
<div className={`seo__score-bar-fill ${barClass}`} style={{ width: `${score}%` }} />
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
{issues.length > 0 && (
|
|
28
|
-
<div>
|
|
29
|
-
<div className="seo__issue-list-title">Issues Found</div>
|
|
30
|
-
<ul className="seo__issue-list">
|
|
31
|
-
{issues.map((issue, i) => (
|
|
32
|
-
<li key={i} className="seo__issue-item">
|
|
33
|
-
<span className="seo__issue-item-bullet">•</span>
|
|
34
|
-
{issue}
|
|
35
|
-
</li>
|
|
36
|
-
))}
|
|
37
|
-
</ul>
|
|
38
|
-
</div>
|
|
39
|
-
)}
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { SitemapEntry } from '@geenius-seo/shared'
|
|
2
|
-
|
|
3
|
-
interface SitemapViewerProps {
|
|
4
|
-
entries: SitemapEntry[]
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function SitemapViewer({ entries }: SitemapViewerProps) {
|
|
8
|
-
return (
|
|
9
|
-
<div>
|
|
10
|
-
<table className="seo__sitemap-table">
|
|
11
|
-
<thead className="seo__sitemap-table-head">
|
|
12
|
-
<tr>
|
|
13
|
-
<th className="seo__sitemap-table-header">URL</th>
|
|
14
|
-
<th className="seo__sitemap-table-header">Last Modified</th>
|
|
15
|
-
<th className="seo__sitemap-table-header">Change Frequency</th>
|
|
16
|
-
<th className="seo__sitemap-table-header">Priority</th>
|
|
17
|
-
</tr>
|
|
18
|
-
</thead>
|
|
19
|
-
<tbody>
|
|
20
|
-
{entries.map((entry, i) => (
|
|
21
|
-
<tr key={i} className="seo__sitemap-row">
|
|
22
|
-
<td className="seo__sitemap-cell">
|
|
23
|
-
<a href={entry.url} target="_blank" rel="noopener noreferrer" className="seo__sitemap-url">
|
|
24
|
-
{entry.url}
|
|
25
|
-
</a>
|
|
26
|
-
</td>
|
|
27
|
-
<td className="seo__sitemap-cell seo__sitemap-cell-muted">{entry.lastmod || '-'}</td>
|
|
28
|
-
<td className="seo__sitemap-cell seo__sitemap-cell-muted">{entry.changefreq || '-'}</td>
|
|
29
|
-
<td className="seo__sitemap-cell seo__sitemap-cell-muted">{entry.priority || '-'}</td>
|
|
30
|
-
</tr>
|
|
31
|
-
))}
|
|
32
|
-
</tbody>
|
|
33
|
-
</table>
|
|
34
|
-
</div>
|
|
35
|
-
)
|
|
36
|
-
}
|