@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.
Files changed (126) hide show
  1. package/package.json +16 -3
  2. package/packages/convex/dist/index.d.ts +56 -0
  3. package/packages/convex/dist/index.js +133 -0
  4. package/packages/convex/dist/index.js.map +1 -0
  5. package/packages/react/README.md +1 -1
  6. package/packages/react/dist/index.d.ts +156 -0
  7. package/packages/react/dist/index.js +567 -0
  8. package/packages/react/dist/index.js.map +1 -0
  9. package/packages/react-css/README.md +1 -1
  10. package/packages/react-css/dist/index.cjs +571 -0
  11. package/packages/react-css/dist/index.cjs.map +1 -0
  12. package/packages/react-css/{src/seo.css → dist/index.css} +7 -153
  13. package/packages/react-css/dist/index.css.map +1 -0
  14. package/packages/react-css/dist/index.d.cts +53 -0
  15. package/packages/react-css/dist/index.d.ts +53 -0
  16. package/packages/react-css/dist/index.js +539 -0
  17. package/packages/react-css/dist/index.js.map +1 -0
  18. package/packages/shared/README.md +1 -1
  19. package/packages/shared/dist/index.d.ts +262 -0
  20. package/packages/shared/dist/index.js +381 -0
  21. package/packages/shared/dist/index.js.map +1 -0
  22. package/packages/solidjs/README.md +1 -1
  23. package/packages/solidjs/dist/index.d.ts +133 -0
  24. package/packages/solidjs/dist/index.js +416 -0
  25. package/packages/solidjs/dist/index.js.map +1 -0
  26. package/packages/solidjs-css/README.md +1 -1
  27. package/packages/solidjs-css/dist/index.cjs +399 -0
  28. package/packages/solidjs-css/dist/index.cjs.map +1 -0
  29. package/packages/solidjs-css/{src/seo.css → dist/index.css} +7 -153
  30. package/packages/solidjs-css/dist/index.css.map +1 -0
  31. package/packages/solidjs-css/dist/index.d.cts +53 -0
  32. package/packages/solidjs-css/dist/index.d.ts +53 -0
  33. package/packages/solidjs-css/dist/index.js +367 -0
  34. package/packages/solidjs-css/dist/index.js.map +1 -0
  35. package/.changeset/config.json +0 -11
  36. package/.github/CODEOWNERS +0 -1
  37. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -16
  38. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -11
  39. package/.github/PULL_REQUEST_TEMPLATE.md +0 -10
  40. package/.github/dependabot.yml +0 -11
  41. package/.github/workflows/ci.yml +0 -23
  42. package/.github/workflows/release.yml +0 -29
  43. package/.nvmrc +0 -1
  44. package/.project/ACCOUNT.yaml +0 -4
  45. package/.project/IDEAS.yaml +0 -7
  46. package/.project/PROJECT.yaml +0 -11
  47. package/.project/ROADMAP.yaml +0 -15
  48. package/CODE_OF_CONDUCT.md +0 -16
  49. package/CONTRIBUTING.md +0 -26
  50. package/SECURITY.md +0 -15
  51. package/SUPPORT.md +0 -8
  52. package/packages/convex/package.json +0 -42
  53. package/packages/convex/src/functions.ts +0 -5
  54. package/packages/convex/src/mutations.ts +0 -83
  55. package/packages/convex/src/queries.ts +0 -57
  56. package/packages/convex/src/schema.ts +0 -23
  57. package/packages/convex/tsconfig.json +0 -19
  58. package/packages/convex/tsup.config.ts +0 -18
  59. package/packages/react/package.json +0 -49
  60. package/packages/react/src/components/ArticleJsonLd.tsx +0 -42
  61. package/packages/react/src/components/BreadcrumbsJsonLd.tsx +0 -24
  62. package/packages/react/src/components/MetaEditor.tsx +0 -147
  63. package/packages/react/src/components/SEOHead.tsx +0 -107
  64. package/packages/react/src/components/SEOPreview.tsx +0 -42
  65. package/packages/react/src/components/SEOScoreCard.tsx +0 -51
  66. package/packages/react/src/components/SitemapViewer.tsx +0 -36
  67. package/packages/react/src/components/index.ts +0 -7
  68. package/packages/react/src/hooks/index.ts +0 -4
  69. package/packages/react/src/hooks/useSEO.ts +0 -27
  70. package/packages/react/src/hooks/useSEOAdmin.ts +0 -42
  71. package/packages/react/src/hooks/useSEOScore.ts +0 -7
  72. package/packages/react/src/hooks/useSitemap.ts +0 -8
  73. package/packages/react/src/index.ts +0 -51
  74. package/packages/react/src/index.tsx +0 -11
  75. package/packages/react/src/pages/SEOAdminPage.tsx +0 -101
  76. package/packages/react/src/pages/SEOAnalyticsPage.tsx +0 -96
  77. package/packages/react/src/pages/index.ts +0 -2
  78. package/packages/react/tsconfig.json +0 -19
  79. package/packages/react/tsup.config.ts +0 -12
  80. package/packages/react-css/package.json +0 -36
  81. package/packages/react-css/src/components/ArticleJsonLd.tsx +0 -42
  82. package/packages/react-css/src/components/BreadcrumbsJsonLd.tsx +0 -24
  83. package/packages/react-css/src/components/MetaEditor.tsx +0 -147
  84. package/packages/react-css/src/components/SEOHead.tsx +0 -95
  85. package/packages/react-css/src/components/SEOPreview.tsx +0 -42
  86. package/packages/react-css/src/components/SEOScoreCard.tsx +0 -42
  87. package/packages/react-css/src/components/SitemapViewer.tsx +0 -36
  88. package/packages/react-css/src/components/index.ts +0 -7
  89. package/packages/react-css/src/index.ts +0 -9
  90. package/packages/react-css/src/pages/SEOAdminPage.tsx +0 -88
  91. package/packages/react-css/src/pages/SEOAnalyticsPage.tsx +0 -82
  92. package/packages/react-css/src/pages/index.ts +0 -2
  93. package/packages/react-css/tsup.config.ts +0 -2
  94. package/packages/shared/package.json +0 -42
  95. package/packages/shared/src/__tests__/seo.test.ts +0 -70
  96. package/packages/shared/src/config.ts +0 -297
  97. package/packages/shared/src/index.ts +0 -207
  98. package/packages/shared/tsconfig.json +0 -18
  99. package/packages/shared/tsup.config.ts +0 -11
  100. package/packages/shared/vitest.config.ts +0 -4
  101. package/packages/solidjs/package.json +0 -45
  102. package/packages/solidjs/src/components/ArticleJsonLd.tsx +0 -35
  103. package/packages/solidjs/src/components/BreadcrumbsJsonLd.tsx +0 -24
  104. package/packages/solidjs/src/components/MetaEditor.tsx +0 -155
  105. package/packages/solidjs/src/components/SEOHead.tsx +0 -109
  106. package/packages/solidjs/src/components/SEOPreview.tsx +0 -42
  107. package/packages/solidjs/src/components/SEOScoreCard.tsx +0 -57
  108. package/packages/solidjs/src/components/SitemapViewer.tsx +0 -44
  109. package/packages/solidjs/src/components/index.ts +0 -7
  110. package/packages/solidjs/src/index.ts +0 -11
  111. package/packages/solidjs/src/pages/SEOAdminPage.tsx +0 -104
  112. package/packages/solidjs/src/pages/SEOAnalyticsPage.tsx +0 -102
  113. package/packages/solidjs/src/pages/index.ts +0 -2
  114. package/packages/solidjs/src/primitives/index.ts +0 -4
  115. package/packages/solidjs/src/primitives/useSEO.ts +0 -27
  116. package/packages/solidjs/src/primitives/useSEOAdmin.ts +0 -42
  117. package/packages/solidjs/src/primitives/useSEOScore.ts +0 -7
  118. package/packages/solidjs/src/primitives/useSitemap.ts +0 -8
  119. package/packages/solidjs/tsconfig.json +0 -20
  120. package/packages/solidjs/tsup.config.ts +0 -12
  121. package/packages/solidjs-css/package.json +0 -35
  122. package/packages/solidjs-css/src/index.ts +0 -5
  123. package/packages/solidjs-css/src/primitives/index.ts +0 -1
  124. package/packages/solidjs-css/tsup.config.ts +0 -2
  125. package/pnpm-workspace.yaml +0 -2
  126. 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,11 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: { index: 'src/index.ts' },
5
- outDir: 'dist',
6
- format: ['esm'],
7
- dts: true,
8
- sourcemap: true,
9
- clean: true,
10
- treeshake: true,
11
- })
@@ -1,4 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
- export default defineConfig({
3
- test: { globals: true, environment: 'node' },
4
- })
@@ -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
- }