@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,207 +0,0 @@
|
|
|
1
|
-
// ─── SEO Metadata Interfaces ─────────────────────────────────────
|
|
2
|
-
|
|
3
|
-
export interface OGMeta {
|
|
4
|
-
title: string
|
|
5
|
-
description: string
|
|
6
|
-
image?: string
|
|
7
|
-
imageAlt?: string
|
|
8
|
-
type: 'website' | 'article' | 'product'
|
|
9
|
-
url: string
|
|
10
|
-
siteName?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface TwitterMeta {
|
|
14
|
-
card: 'summary' | 'summary_large_image'
|
|
15
|
-
title: string
|
|
16
|
-
description: string
|
|
17
|
-
image?: string
|
|
18
|
-
creator?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface SEOMeta {
|
|
22
|
-
title: string
|
|
23
|
-
description: string
|
|
24
|
-
keywords: string[]
|
|
25
|
-
canonical?: string
|
|
26
|
-
og: OGMeta
|
|
27
|
-
twitter: TwitterMeta
|
|
28
|
-
robots?: string
|
|
29
|
-
alternates?: Record<string, string>
|
|
30
|
-
jsonLd?: Record<string, unknown>[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface SitemapEntry {
|
|
34
|
-
url: string
|
|
35
|
-
lastmod?: string
|
|
36
|
-
changefreq?: string
|
|
37
|
-
priority?: number
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface RobotsTxt {
|
|
41
|
-
allow: string[]
|
|
42
|
-
disallow: string[]
|
|
43
|
-
sitemap?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface SEOConfig {
|
|
47
|
-
siteName: string
|
|
48
|
-
siteUrl: string
|
|
49
|
-
defaultImage?: string
|
|
50
|
-
twitterHandle?: string
|
|
51
|
-
locale?: string
|
|
52
|
-
googleAnalyticsId?: string
|
|
53
|
-
googleTagManagerId?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ─── Utility Functions ──────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
export function buildTitle(pageTitle: string, siteName: string, sep = ' | '): string {
|
|
59
|
-
return `${pageTitle}${sep}${siteName}`
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function truncateDescription(desc: string, max = 160): string {
|
|
63
|
-
if (desc.length <= max) return desc
|
|
64
|
-
return desc.slice(0, max - 3) + '...'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── JSON-LD Schema Builders ────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
export function articleSchema(data: {
|
|
70
|
-
title: string
|
|
71
|
-
description: string
|
|
72
|
-
author: string
|
|
73
|
-
datePublished: string
|
|
74
|
-
url: string
|
|
75
|
-
image?: string
|
|
76
|
-
}): Record<string, unknown> {
|
|
77
|
-
return {
|
|
78
|
-
'@context': 'https://schema.org',
|
|
79
|
-
'@type': 'Article',
|
|
80
|
-
...data,
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function productSchema(data: {
|
|
85
|
-
name: string
|
|
86
|
-
description: string
|
|
87
|
-
price: number
|
|
88
|
-
currency: string
|
|
89
|
-
url: string
|
|
90
|
-
image?: string
|
|
91
|
-
}): Record<string, unknown> {
|
|
92
|
-
return {
|
|
93
|
-
'@context': 'https://schema.org',
|
|
94
|
-
'@type': 'Product',
|
|
95
|
-
name: data.name,
|
|
96
|
-
description: data.description,
|
|
97
|
-
url: data.url,
|
|
98
|
-
image: data.image,
|
|
99
|
-
offers: {
|
|
100
|
-
'@type': 'Offer',
|
|
101
|
-
price: data.price,
|
|
102
|
-
priceCurrency: data.currency,
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function orgSchema(data: {
|
|
108
|
-
name: string
|
|
109
|
-
url: string
|
|
110
|
-
logo?: string
|
|
111
|
-
sameAs?: string[]
|
|
112
|
-
}): Record<string, unknown> {
|
|
113
|
-
return {
|
|
114
|
-
'@context': 'https://schema.org',
|
|
115
|
-
'@type': 'Organization',
|
|
116
|
-
...data,
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function breadcrumbSchema(items: {
|
|
121
|
-
name: string
|
|
122
|
-
url: string
|
|
123
|
-
}[]): Record<string, unknown> {
|
|
124
|
-
return {
|
|
125
|
-
'@context': 'https://schema.org',
|
|
126
|
-
'@type': 'BreadcrumbList',
|
|
127
|
-
itemListElement: items.map((item, i) => ({
|
|
128
|
-
'@type': 'ListItem',
|
|
129
|
-
position: i + 1,
|
|
130
|
-
name: item.name,
|
|
131
|
-
item: item.url,
|
|
132
|
-
})),
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function faqSchema(items: {
|
|
137
|
-
question: string
|
|
138
|
-
answer: string
|
|
139
|
-
}[]): Record<string, unknown> {
|
|
140
|
-
return {
|
|
141
|
-
'@context': 'https://schema.org',
|
|
142
|
-
'@type': 'FAQPage',
|
|
143
|
-
mainEntity: items.map((q) => ({
|
|
144
|
-
'@type': 'Question',
|
|
145
|
-
name: q.question,
|
|
146
|
-
acceptedAnswer: {
|
|
147
|
-
'@type': 'Answer',
|
|
148
|
-
text: q.answer,
|
|
149
|
-
},
|
|
150
|
-
})),
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function buildCanonical(path: string, baseUrl: string): string {
|
|
155
|
-
return `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function buildAlternates(
|
|
159
|
-
locales: string[],
|
|
160
|
-
baseUrl: string,
|
|
161
|
-
path: string,
|
|
162
|
-
): Record<string, string> {
|
|
163
|
-
return Object.fromEntries(locales.map((l) => [l, `${baseUrl}/${l}${path}`]))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function generateRobotsTxt(config: RobotsTxt): string {
|
|
167
|
-
const lines = ['User-agent: *']
|
|
168
|
-
config.allow.forEach((p) => lines.push(`Allow: ${p}`))
|
|
169
|
-
config.disallow.forEach((p) => lines.push(`Disallow: ${p}`))
|
|
170
|
-
if (config.sitemap) lines.push(`Sitemap: ${config.sitemap}`)
|
|
171
|
-
return lines.join('\n')
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function generateSitemapXml(entries: SitemapEntry[]): string {
|
|
175
|
-
const urls = entries
|
|
176
|
-
.map(
|
|
177
|
-
(e) =>
|
|
178
|
-
` <url>\n <loc>${e.url}</loc>${e.lastmod ? `\n <lastmod>${e.lastmod}</lastmod>` : ''}${e.changefreq ? `\n <changefreq>${e.changefreq}</changefreq>` : ''}${e.priority !== undefined ? `\n <priority>${e.priority}</priority>` : ''}\n </url>`,
|
|
179
|
-
)
|
|
180
|
-
.join('\n')
|
|
181
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>`
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function estimateReadingTime(content: string): number {
|
|
185
|
-
return Math.ceil(content.split(/\s+/).length / 200)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function calcSEOScore(meta: SEOMeta): {
|
|
189
|
-
score: number
|
|
190
|
-
issues: string[]
|
|
191
|
-
} {
|
|
192
|
-
const issues: string[] = []
|
|
193
|
-
|
|
194
|
-
if (!meta.title || meta.title.length < 10) issues.push('Title too short (< 10 chars)')
|
|
195
|
-
if (meta.title && meta.title.length > 60) issues.push('Title too long (> 60 chars)')
|
|
196
|
-
if (!meta.description || meta.description.length < 50)
|
|
197
|
-
issues.push('Description too short (< 50 chars)')
|
|
198
|
-
if (meta.description && meta.description.length > 160)
|
|
199
|
-
issues.push('Description too long (> 160 chars)')
|
|
200
|
-
if (!meta.keywords || meta.keywords.length === 0) issues.push('No keywords specified')
|
|
201
|
-
if (!meta.canonical) issues.push('No canonical URL')
|
|
202
|
-
if (!meta.og.image) issues.push('No OG image')
|
|
203
|
-
if (!meta.twitter.image) issues.push('No Twitter image')
|
|
204
|
-
|
|
205
|
-
const score = Math.max(0, 100 - issues.length * 12)
|
|
206
|
-
return { score, issues }
|
|
207
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "dist",
|
|
5
|
-
"rootDir": "src",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"forceConsistentCasingInFileNames": true,
|
|
9
|
-
"resolveJsonModule": true,
|
|
10
|
-
"isolatedModules": true,
|
|
11
|
-
"target": "ES2022",
|
|
12
|
-
"module": "ESNext",
|
|
13
|
-
"moduleResolution": "bundler"
|
|
14
|
-
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src"
|
|
17
|
-
]
|
|
18
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@geenius-seo/solidjs",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Geenius Seo — SolidJS components & primitives",
|
|
7
|
-
"author": "Antigravity HQ",
|
|
8
|
-
"license": "MIT",
|
|
9
|
-
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
11
|
-
},
|
|
12
|
-
"main": "./dist/index.js",
|
|
13
|
-
"module": "./dist/index.js",
|
|
14
|
-
"types": "./dist/index.d.ts",
|
|
15
|
-
"exports": {
|
|
16
|
-
".": {
|
|
17
|
-
"types": "./dist/index.d.ts",
|
|
18
|
-
"import": "./dist/index.js"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist",
|
|
23
|
-
"src"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsup",
|
|
27
|
-
"clean": "rm -rf dist",
|
|
28
|
-
"type-check": "tsc --noEmit",
|
|
29
|
-
"prepublishOnly": "pnpm clean && pnpm build"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@geenius-seo/shared": "workspace:*"
|
|
33
|
-
},
|
|
34
|
-
"devDependencies": {
|
|
35
|
-
"solid-js": "^1.9.0",
|
|
36
|
-
"tsup": "^8.5.1",
|
|
37
|
-
"typescript": "~6.0.2"
|
|
38
|
-
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"solid-js": "^1.8.0 || ^1.9.0"
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">=20.0.0"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createEffect } from 'solid-js'
|
|
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(props: ArticleJsonLdProps) {
|
|
14
|
-
createEffect(() => {
|
|
15
|
-
const script = document.createElement('script')
|
|
16
|
-
script.type = 'application/ld+json'
|
|
17
|
-
script.textContent = JSON.stringify(
|
|
18
|
-
articleSchema({
|
|
19
|
-
title: props.title,
|
|
20
|
-
description: props.description,
|
|
21
|
-
author: props.author,
|
|
22
|
-
datePublished: props.datePublished,
|
|
23
|
-
url: props.url,
|
|
24
|
-
image: props.image,
|
|
25
|
-
}),
|
|
26
|
-
)
|
|
27
|
-
document.head.appendChild(script)
|
|
28
|
-
|
|
29
|
-
return () => {
|
|
30
|
-
document.head.removeChild(script)
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
return null
|
|
35
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createEffect } from 'solid-js'
|
|
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(props: BreadcrumbsJsonLdProps) {
|
|
12
|
-
createEffect(() => {
|
|
13
|
-
const script = document.createElement('script')
|
|
14
|
-
script.type = 'application/ld+json'
|
|
15
|
-
script.textContent = JSON.stringify(breadcrumbSchema(props.items))
|
|
16
|
-
document.head.appendChild(script)
|
|
17
|
-
|
|
18
|
-
return () => {
|
|
19
|
-
document.head.removeChild(script)
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { createSignal } from 'solid-js'
|
|
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(props: MetaEditorProps) {
|
|
10
|
-
const [localMeta, setLocalMeta] = createSignal(props.meta)
|
|
11
|
-
|
|
12
|
-
const handleChange = (updates: Partial<SEOMeta>) => {
|
|
13
|
-
const updated = { ...localMeta(), ...updates }
|
|
14
|
-
setLocalMeta(updated)
|
|
15
|
-
props.onChange(updated)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div class="space-y-6 p-4 border border-gray-200 rounded">
|
|
20
|
-
{/* Title */}
|
|
21
|
-
<div>
|
|
22
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">
|
|
23
|
-
Title {localMeta().title.length}/60
|
|
24
|
-
</label>
|
|
25
|
-
<input
|
|
26
|
-
type="text"
|
|
27
|
-
value={localMeta().title}
|
|
28
|
-
onInput={(e) =>
|
|
29
|
-
handleChange({
|
|
30
|
-
title: e.currentTarget.value.slice(0, 60),
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
34
|
-
placeholder="Page title"
|
|
35
|
-
/>
|
|
36
|
-
<p class="text-xs text-gray-500 mt-1">
|
|
37
|
-
{localMeta().title.length < 30
|
|
38
|
-
? 'Too short'
|
|
39
|
-
: localMeta().title.length > 60
|
|
40
|
-
? 'Too long'
|
|
41
|
-
: 'Good length'}
|
|
42
|
-
</p>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
{/* Description */}
|
|
46
|
-
<div>
|
|
47
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">
|
|
48
|
-
Description {localMeta().description.length}/160
|
|
49
|
-
</label>
|
|
50
|
-
<textarea
|
|
51
|
-
value={localMeta().description}
|
|
52
|
-
onInput={(e) =>
|
|
53
|
-
handleChange({
|
|
54
|
-
description: e.currentTarget.value.slice(0, 160),
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
58
|
-
rows={3}
|
|
59
|
-
placeholder="Page description"
|
|
60
|
-
/>
|
|
61
|
-
<p class="text-xs text-gray-500 mt-1">
|
|
62
|
-
{localMeta().description.length < 50
|
|
63
|
-
? 'Too short'
|
|
64
|
-
: localMeta().description.length > 160
|
|
65
|
-
? 'Too long'
|
|
66
|
-
: 'Good length'}
|
|
67
|
-
</p>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
{/* Keywords */}
|
|
71
|
-
<div>
|
|
72
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">Keywords</label>
|
|
73
|
-
<input
|
|
74
|
-
type="text"
|
|
75
|
-
value={localMeta().keywords.join(', ')}
|
|
76
|
-
onInput={(e) =>
|
|
77
|
-
handleChange({
|
|
78
|
-
keywords: e.currentTarget.value.split(',').map((k) => k.trim()),
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
82
|
-
placeholder="keyword1, keyword2, keyword3"
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
{/* Canonical */}
|
|
87
|
-
<div>
|
|
88
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">Canonical URL</label>
|
|
89
|
-
<input
|
|
90
|
-
type="url"
|
|
91
|
-
value={localMeta().canonical || ''}
|
|
92
|
-
onInput={(e) =>
|
|
93
|
-
handleChange({
|
|
94
|
-
canonical: e.currentTarget.value,
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
98
|
-
placeholder="https://example.com/page"
|
|
99
|
-
/>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
{/* OG Image */}
|
|
103
|
-
<div>
|
|
104
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">OG Image URL</label>
|
|
105
|
-
<input
|
|
106
|
-
type="url"
|
|
107
|
-
value={localMeta().og.image || ''}
|
|
108
|
-
onInput={(e) =>
|
|
109
|
-
handleChange({
|
|
110
|
-
og: { ...localMeta().og, image: e.currentTarget.value },
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
114
|
-
placeholder="https://example.com/image.jpg"
|
|
115
|
-
/>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
{/* Twitter Card */}
|
|
119
|
-
<div>
|
|
120
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">Twitter Card Type</label>
|
|
121
|
-
<select
|
|
122
|
-
value={localMeta().twitter.card}
|
|
123
|
-
onChange={(e) =>
|
|
124
|
-
handleChange({
|
|
125
|
-
twitter: {
|
|
126
|
-
...localMeta().twitter,
|
|
127
|
-
card: e.currentTarget.value as 'summary' | 'summary_large_image',
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
132
|
-
>
|
|
133
|
-
<option value="summary">Summary</option>
|
|
134
|
-
<option value="summary_large_image">Summary Large Image</option>
|
|
135
|
-
</select>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{/* Robots */}
|
|
139
|
-
<div>
|
|
140
|
-
<label class="block text-sm font-semibold text-gray-900 mb-1">Robots Directive</label>
|
|
141
|
-
<input
|
|
142
|
-
type="text"
|
|
143
|
-
value={localMeta().robots || ''}
|
|
144
|
-
onInput={(e) =>
|
|
145
|
-
handleChange({
|
|
146
|
-
robots: e.currentTarget.value,
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
class="w-full px-3 py-2 border border-gray-300 rounded text-sm"
|
|
150
|
-
placeholder="index, follow"
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
)
|
|
155
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { createEffect } from 'solid-js'
|
|
2
|
-
import type { SEOMeta } from '@geenius-seo/shared'
|
|
3
|
-
|
|
4
|
-
interface SEOHeadProps {
|
|
5
|
-
meta: SEOMeta
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function SEOHead(props: SEOHeadProps) {
|
|
9
|
-
createEffect(() => {
|
|
10
|
-
const meta = props.meta
|
|
11
|
-
|
|
12
|
-
// Set title
|
|
13
|
-
document.title = meta.title
|
|
14
|
-
|
|
15
|
-
// Set meta tags
|
|
16
|
-
const setMetaTag = (name: string, content: string, isProperty = false) => {
|
|
17
|
-
let tag = document.querySelector(
|
|
18
|
-
`meta[${isProperty ? 'property' : 'name'}="${name}"]`,
|
|
19
|
-
) as HTMLMetaElement
|
|
20
|
-
if (!tag) {
|
|
21
|
-
tag = document.createElement('meta')
|
|
22
|
-
isProperty ? tag.setAttribute('property', name) : tag.setAttribute('name', name)
|
|
23
|
-
document.head.appendChild(tag)
|
|
24
|
-
}
|
|
25
|
-
tag.content = content
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Description
|
|
29
|
-
setMetaTag('description', meta.description)
|
|
30
|
-
|
|
31
|
-
// Keywords
|
|
32
|
-
if (meta.keywords.length > 0) {
|
|
33
|
-
setMetaTag('keywords', meta.keywords.join(', '))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Robots
|
|
37
|
-
if (meta.robots) {
|
|
38
|
-
setMetaTag('robots', meta.robots)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// OG tags
|
|
42
|
-
setMetaTag('og:title', meta.og.title, true)
|
|
43
|
-
setMetaTag('og:description', meta.og.description, true)
|
|
44
|
-
if (meta.og.image) {
|
|
45
|
-
setMetaTag('og:image', meta.og.image, true)
|
|
46
|
-
if (meta.og.imageAlt) {
|
|
47
|
-
setMetaTag('og:image:alt', meta.og.imageAlt, true)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
setMetaTag('og:type', meta.og.type, true)
|
|
51
|
-
setMetaTag('og:url', meta.og.url, true)
|
|
52
|
-
if (meta.og.siteName) {
|
|
53
|
-
setMetaTag('og:site_name', meta.og.siteName, true)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Twitter tags
|
|
57
|
-
setMetaTag('twitter:card', meta.twitter.card)
|
|
58
|
-
setMetaTag('twitter:title', meta.twitter.title)
|
|
59
|
-
setMetaTag('twitter:description', meta.twitter.description)
|
|
60
|
-
if (meta.twitter.image) {
|
|
61
|
-
setMetaTag('twitter:image', meta.twitter.image)
|
|
62
|
-
}
|
|
63
|
-
if (meta.twitter.creator) {
|
|
64
|
-
setMetaTag('twitter:creator', meta.twitter.creator)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Canonical
|
|
68
|
-
if (meta.canonical) {
|
|
69
|
-
let canonicalLink = document.querySelector('link[rel="canonical"]') as HTMLLinkElement
|
|
70
|
-
if (!canonicalLink) {
|
|
71
|
-
canonicalLink = document.createElement('link')
|
|
72
|
-
canonicalLink.rel = 'canonical'
|
|
73
|
-
document.head.appendChild(canonicalLink)
|
|
74
|
-
}
|
|
75
|
-
canonicalLink.href = meta.canonical
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Alternates
|
|
79
|
-
if (meta.alternates) {
|
|
80
|
-
Object.entries(meta.alternates).forEach(([lang, url]) => {
|
|
81
|
-
let altLink = document.querySelector(
|
|
82
|
-
`link[rel="alternate"][hreflang="${lang}"]`,
|
|
83
|
-
) as HTMLLinkElement
|
|
84
|
-
if (!altLink) {
|
|
85
|
-
altLink = document.createElement('link')
|
|
86
|
-
altLink.rel = 'alternate'
|
|
87
|
-
altLink.hrefLang = lang
|
|
88
|
-
document.head.appendChild(altLink)
|
|
89
|
-
}
|
|
90
|
-
altLink.href = url
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// JSON-LD
|
|
95
|
-
if (meta.jsonLd && meta.jsonLd.length > 0) {
|
|
96
|
-
meta.jsonLd.forEach((schema) => {
|
|
97
|
-
let script = document.querySelector('script[type="application/ld+json"]') as HTMLScriptElement
|
|
98
|
-
if (!script) {
|
|
99
|
-
script = document.createElement('script')
|
|
100
|
-
script.type = 'application/ld+json'
|
|
101
|
-
document.head.appendChild(script)
|
|
102
|
-
}
|
|
103
|
-
script.textContent = JSON.stringify(schema)
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
@@ -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(props: SEOPreviewProps) {
|
|
8
|
-
return (
|
|
9
|
-
<div class="space-y-6">
|
|
10
|
-
{/* Google SERP Preview */}
|
|
11
|
-
<div class="space-y-2">
|
|
12
|
-
<h3 class="text-sm font-semibold text-gray-700">Google Search Preview</h3>
|
|
13
|
-
<div class="bg-white p-4 rounded border border-gray-200">
|
|
14
|
-
<div class="text-xs text-green-700">{new URL(props.meta.og.url).hostname}</div>
|
|
15
|
-
<div class="text-lg text-blue-600 hover:underline cursor-pointer truncate">
|
|
16
|
-
{props.meta.title}
|
|
17
|
-
</div>
|
|
18
|
-
<div class="text-sm text-gray-600 line-clamp-2">{props.meta.description}</div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
{/* Social Media Preview */}
|
|
23
|
-
<div class="space-y-2">
|
|
24
|
-
<h3 class="text-sm font-semibold text-gray-700">Social Media Preview</h3>
|
|
25
|
-
<div class="bg-gray-900 rounded overflow-hidden max-w-sm">
|
|
26
|
-
{props.meta.og.image && (
|
|
27
|
-
<img
|
|
28
|
-
src={props.meta.og.image}
|
|
29
|
-
alt={props.meta.og.imageAlt || props.meta.og.title}
|
|
30
|
-
class="w-full h-48 object-cover"
|
|
31
|
-
/>
|
|
32
|
-
)}
|
|
33
|
-
<div class="p-3 text-white">
|
|
34
|
-
<div class="font-semibold text-sm truncate">{props.meta.og.title}</div>
|
|
35
|
-
<div class="text-xs text-gray-400 line-clamp-2">{props.meta.og.description}</div>
|
|
36
|
-
<div class="text-xs text-gray-500 mt-1 truncate">{props.meta.og.url}</div>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
42
|
-
}
|