@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,88 +0,0 @@
1
- import { useState } from 'react'
2
- import { useSEOAdmin } from '@geenius-seo/react'
3
- import { MetaEditor } from '../components/MetaEditor'
4
- import { SEOPreview } from '../components/SEOPreview'
5
- import { SEOScoreCard } from '../components/SEOScoreCard'
6
- import type { SEOMeta } from '@geenius-seo/shared'
7
-
8
- export function SEOAdminPage() {
9
- const { pages, upsertMeta, deleteMeta, isLoading } = useSEOAdmin()
10
- const [selectedPath, setSelectedPath] = useState<string | null>(null)
11
- const [editingMeta, setEditingMeta] = useState<SEOMeta | null>(null)
12
-
13
- const currentPage = pages.find((p) => p.path === selectedPath)
14
-
15
- const handleSave = async () => {
16
- if (selectedPath && editingMeta) {
17
- await upsertMeta(selectedPath, editingMeta)
18
- setEditingMeta(null)
19
- }
20
- }
21
-
22
- const handleDelete = async () => {
23
- if (selectedPath) {
24
- await deleteMeta(selectedPath)
25
- setSelectedPath(null)
26
- setEditingMeta(null)
27
- }
28
- }
29
-
30
- if (isLoading) return <div style={{ padding: '1rem' }}>Loading...</div>
31
-
32
- return (
33
- <div className="seo__admin-container">
34
- <h1 className="seo__admin-title">SEO Admin</h1>
35
-
36
- <div className="seo__admin-grid">
37
- <div className="seo__admin-sidebar">
38
- <div className="seo__admin-sidebar-title">Pages</div>
39
- <div className="seo__admin-page-list">
40
- {pages.map((page) => (
41
- <button
42
- key={page.path}
43
- onClick={() => {
44
- setSelectedPath(page.path)
45
- setEditingMeta(page.meta)
46
- }}
47
- className={`seo__admin-page-button ${
48
- selectedPath === page.path ? 'seo__admin-page-button--active' : ''
49
- }`}
50
- >
51
- {page.path || '/'}
52
- </button>
53
- ))}
54
- </div>
55
- </div>
56
-
57
- {currentPage && editingMeta && (
58
- <div className="seo__admin-editor">
59
- <div>
60
- <h2 style={{ fontWeight: 600, marginBottom: '1rem' }}>Edit Metadata</h2>
61
- <MetaEditor meta={editingMeta} onChange={setEditingMeta} />
62
- </div>
63
-
64
- <div className="seo__space-y">
65
- <div>
66
- <h2 style={{ fontWeight: 600, marginBottom: '1rem' }}>Preview</h2>
67
- <SEOPreview meta={editingMeta} />
68
- </div>
69
-
70
- <SEOScoreCard meta={editingMeta} />
71
- </div>
72
- </div>
73
- )}
74
- </div>
75
-
76
- {currentPage && editingMeta && (
77
- <div className="seo__admin-actions">
78
- <button onClick={handleSave} className="seo__button seo__button--success">
79
- Save
80
- </button>
81
- <button onClick={handleDelete} className="seo__button seo__button--danger">
82
- Delete
83
- </button>
84
- </div>
85
- )}
86
- </div>
87
- )
88
- }
@@ -1,82 +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
- setIsLoading(false)
17
- }, [])
18
-
19
- const totalViews = topPages.reduce((sum, p) => sum + p.views, 0)
20
- const avgTimeOnPage =
21
- topPages.length > 0
22
- ? topPages.reduce((sum, p) => sum + p.avgTimeOnPage, 0) / topPages.length
23
- : 0
24
- const avgBounceRate =
25
- topPages.length > 0
26
- ? (topPages.reduce((sum, p) => sum + p.bounceRate, 0) / topPages.length) * 100
27
- : 0
28
-
29
- if (isLoading) return <div style={{ padding: '1rem' }}>Loading...</div>
30
-
31
- return (
32
- <div className="seo__analytics-container">
33
- <h1 className="seo__analytics-title">SEO Analytics</h1>
34
-
35
- <div className="seo__analytics-table-wrapper">
36
- <table className="seo__sitemap-table">
37
- <thead className="seo__sitemap-table-head">
38
- <tr>
39
- <th className="seo__sitemap-table-header">Page</th>
40
- <th className="seo__sitemap-table-header">Views</th>
41
- <th className="seo__sitemap-table-header">Avg Time (s)</th>
42
- <th className="seo__sitemap-table-header">Bounce Rate</th>
43
- </tr>
44
- </thead>
45
- <tbody>
46
- {topPages.map((page, i) => (
47
- <tr key={i} className="seo__sitemap-row">
48
- <td className="seo__sitemap-cell">{page.path}</td>
49
- <td className="seo__sitemap-cell">{page.views.toLocaleString()}</td>
50
- <td className="seo__sitemap-cell">{page.avgTimeOnPage.toFixed(1)}</td>
51
- <td className="seo__sitemap-cell">{(page.bounceRate * 100).toFixed(1)}%</td>
52
- </tr>
53
- ))}
54
- </tbody>
55
- </table>
56
- </div>
57
-
58
- {topPages.length === 0 && (
59
- <p style={{ textAlign: 'center', color: 'var(--seo-text-muted)', padding: '2rem' }}>
60
- No analytics data available
61
- </p>
62
- )}
63
-
64
- <div className="seo__analytics-summary">
65
- <div className="seo__analytics-card">
66
- <div className="seo__analytics-card-label">Total Views</div>
67
- <div className="seo__analytics-card-value">{totalViews.toLocaleString()}</div>
68
- </div>
69
-
70
- <div className="seo__analytics-card">
71
- <div className="seo__analytics-card-label">Average Time on Page</div>
72
- <div className="seo__analytics-card-value">{avgTimeOnPage.toFixed(1)}s</div>
73
- </div>
74
-
75
- <div className="seo__analytics-card">
76
- <div className="seo__analytics-card-label">Average Bounce Rate</div>
77
- <div className="seo__analytics-card-value">{avgBounceRate.toFixed(1)}%</div>
78
- </div>
79
- </div>
80
- </div>
81
- )
82
- }
@@ -1,2 +0,0 @@
1
- export * from './SEOAdminPage'
2
- export * from './SEOAnalyticsPage'
@@ -1,2 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
- export default defineConfig({ entry: ['src/index.ts'], format: ['cjs', 'esm'], dts: true, clean: true, sourcemap: true, external: ['react'] })
@@ -1,42 +0,0 @@
1
- {
2
- "name": "@geenius-seo/shared",
3
- "version": "0.1.0",
4
- "private": false,
5
- "type": "module",
6
- "description": "Geenius Seo \u2014 Shared types & Convex schema",
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
- "test": "vitest run",
31
- "test:watch": "vitest",
32
- "test:coverage": "vitest run --coverage"
33
- },
34
- "devDependencies": {
35
- "tsup": "^8.5.1",
36
- "typescript": "~6.0.2",
37
- "vitest": "^4.0.0"
38
- },
39
- "engines": {
40
- "node": ">=20.0.0"
41
- }
42
- }
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildTitle, truncateDescription, articleSchema } from '../index'
3
-
4
- describe('buildTitle', () => {
5
- it('joins page title and site name with separator', () => {
6
- expect(buildTitle('Home', 'Geenius')).toBe('Home | Geenius')
7
- })
8
-
9
- it('allows custom separator', () => {
10
- expect(buildTitle('Blog', 'Geenius', ' — ')).toBe('Blog — Geenius')
11
- })
12
-
13
- it('handles empty page title', () => {
14
- expect(buildTitle('', 'Geenius')).toBe(' | Geenius')
15
- })
16
- })
17
-
18
- describe('truncateDescription', () => {
19
- it('returns short descriptions unchanged', () => {
20
- const short = 'A brief description.'
21
- expect(truncateDescription(short)).toBe(short)
22
- })
23
-
24
- it('truncates to 160 chars by default', () => {
25
- const long = 'x'.repeat(200)
26
- const result = truncateDescription(long)
27
- expect(result.length).toBe(160)
28
- expect(result.endsWith('...')).toBe(true)
29
- })
30
-
31
- it('respects custom max length', () => {
32
- const text = 'x'.repeat(100)
33
- const result = truncateDescription(text, 50)
34
- expect(result.length).toBe(50)
35
- expect(result.endsWith('...')).toBe(true)
36
- })
37
-
38
- it('returns exact-length descriptions unchanged', () => {
39
- const exact = 'x'.repeat(160)
40
- expect(truncateDescription(exact)).toBe(exact)
41
- })
42
- })
43
-
44
- describe('articleSchema', () => {
45
- it('generates valid JSON-LD structure', () => {
46
- const schema = articleSchema({
47
- title: 'Test Article',
48
- description: 'A test article',
49
- author: 'Author',
50
- datePublished: '2024-01-01',
51
- url: 'https://example.com/test',
52
- })
53
- expect(schema['@context']).toBe('https://schema.org')
54
- expect(schema['@type']).toBe('Article')
55
- expect(schema.title).toBe('Test Article')
56
- expect(schema.author).toBe('Author')
57
- })
58
-
59
- it('includes optional image when provided', () => {
60
- const schema = articleSchema({
61
- title: 'Test',
62
- description: 'Desc',
63
- author: 'Author',
64
- datePublished: '2024-01-01',
65
- url: 'https://example.com',
66
- image: 'https://example.com/img.jpg',
67
- })
68
- expect(schema.image).toBe('https://example.com/img.jpg')
69
- })
70
- })
@@ -1,297 +0,0 @@
1
- /**
2
- * SEO configuration and utilities
3
- */
4
-
5
- import type { SEOConfig, SEOMeta, OGMeta, TwitterMeta } from './index'
6
-
7
- /**
8
- * SEO configuration builder for type-safe setup
9
- *
10
- * @example
11
- * ```ts
12
- * const seoConfig = createSEOConfig()
13
- * .withSiteName('My Site')
14
- * .withSiteUrl('https://example.com')
15
- * .withDefaultImage('https://example.com/og-image.jpg')
16
- * .withTwitterHandle('@mysite')
17
- * ```
18
- */
19
- export class SEOConfigBuilder {
20
- private config: SEOConfig = {
21
- siteName: '',
22
- siteUrl: '',
23
- }
24
-
25
- /**
26
- * Sets the site name
27
- */
28
- withSiteName(name: string): this {
29
- this.config.siteName = name
30
- return this
31
- }
32
-
33
- /**
34
- * Sets the site URL
35
- */
36
- withSiteUrl(url: string): this {
37
- this.config.siteUrl = url
38
- return this
39
- }
40
-
41
- /**
42
- * Sets default OG image
43
- */
44
- withDefaultImage(url: string): this {
45
- this.config.defaultImage = url
46
- return this
47
- }
48
-
49
- /**
50
- * Sets Twitter handle
51
- */
52
- withTwitterHandle(handle: string): this {
53
- this.config.twitterHandle = handle
54
- return this
55
- }
56
-
57
- /**
58
- * Sets default locale
59
- */
60
- withLocale(locale: string): this {
61
- this.config.locale = locale
62
- return this
63
- }
64
-
65
- /**
66
- * Sets Google Analytics ID
67
- */
68
- withGoogleAnalyticsId(id: string): this {
69
- this.config.googleAnalyticsId = id
70
- return this
71
- }
72
-
73
- /**
74
- * Sets Google Tag Manager ID
75
- */
76
- withGoogleTagManagerId(id: string): this {
77
- this.config.googleTagManagerId = id
78
- return this
79
- }
80
-
81
- /**
82
- * Builds the configuration
83
- */
84
- build(): SEOConfig {
85
- if (!this.config.siteName || !this.config.siteUrl) {
86
- throw new Error('SEO config requires siteName and siteUrl')
87
- }
88
- return this.config
89
- }
90
- }
91
-
92
- /**
93
- * Creates a new SEO configuration builder
94
- */
95
- export function createSEOConfig(): SEOConfigBuilder {
96
- return new SEOConfigBuilder()
97
- }
98
-
99
- /**
100
- * Meta tags builder for individual pages
101
- *
102
- * @example
103
- * ```ts
104
- * const meta = createPageMeta({
105
- * title: 'Home',
106
- * description: 'Welcome to my site'
107
- * })
108
- * .withKeywords(['web', 'development'])
109
- * .withImage('https://example.com/image.jpg')
110
- * ```
111
- */
112
- export class PageMetaBuilder {
113
- private meta: Partial<SEOMeta> = {
114
- og: {} as OGMeta,
115
- twitter: {} as TwitterMeta,
116
- }
117
-
118
- constructor(private config: SEOConfig, title: string, description: string) {
119
- this.meta.title = title
120
- this.meta.description = description
121
- this.meta.og = {
122
- title,
123
- description,
124
- type: 'website',
125
- url: config.siteUrl,
126
- siteName: config.siteName,
127
- image: config.defaultImage,
128
- }
129
- this.meta.twitter = {
130
- card: 'summary_large_image',
131
- title,
132
- description,
133
- image: config.defaultImage,
134
- creator: config.twitterHandle,
135
- }
136
- }
137
-
138
- /**
139
- * Adds keywords
140
- */
141
- withKeywords(keywords: string[]): this {
142
- this.meta.keywords = keywords
143
- return this
144
- }
145
-
146
- /**
147
- * Sets canonical URL
148
- */
149
- withCanonical(url: string): this {
150
- this.meta.canonical = url
151
- return this
152
- }
153
-
154
- /**
155
- * Sets OG image
156
- */
157
- withImage(url: string, alt?: string): this {
158
- if (this.meta.og) {
159
- this.meta.og.image = url
160
- if (alt) this.meta.og.imageAlt = alt
161
- }
162
- if (this.meta.twitter) {
163
- this.meta.twitter.image = url
164
- }
165
- return this
166
- }
167
-
168
- /**
169
- * Sets OG type
170
- */
171
- withType(type: 'website' | 'article' | 'product'): this {
172
- if (this.meta.og) {
173
- this.meta.og.type = type
174
- }
175
- return this
176
- }
177
-
178
- /**
179
- * Adds JSON-LD schema
180
- */
181
- withJsonLd(schema: Record<string, unknown>): this {
182
- if (!this.meta.jsonLd) {
183
- this.meta.jsonLd = []
184
- }
185
- this.meta.jsonLd.push(schema)
186
- return this
187
- }
188
-
189
- /**
190
- * Sets robots meta
191
- */
192
- withRobots(robots: string): this {
193
- this.meta.robots = robots
194
- return this
195
- }
196
-
197
- /**
198
- * Adds alternate language links
199
- */
200
- withAlternates(alternates: Record<string, string>): this {
201
- this.meta.alternates = alternates
202
- return this
203
- }
204
-
205
- /**
206
- * Builds the meta tags
207
- */
208
- build(): SEOMeta {
209
- return {
210
- title: this.meta.title || '',
211
- description: this.meta.description || '',
212
- keywords: this.meta.keywords || [],
213
- canonical: this.meta.canonical,
214
- og: this.meta.og || ({ type: 'website', title: '', description: '', url: '' } as OGMeta),
215
- twitter: this.meta.twitter || ({ card: 'summary', title: '', description: '' } as TwitterMeta),
216
- robots: this.meta.robots,
217
- alternates: this.meta.alternates,
218
- jsonLd: this.meta.jsonLd,
219
- }
220
- }
221
- }
222
-
223
- /**
224
- * Creates page-specific meta tags
225
- */
226
- export function createPageMeta(config: SEOConfig, title: string, description: string): PageMetaBuilder {
227
- return new PageMetaBuilder(config, title, description)
228
- }
229
-
230
- /**
231
- * Validates SEO meta configuration
232
- */
233
- export function validateSEOMeta(meta: SEOMeta): { valid: boolean; errors: string[] } {
234
- const errors: string[] = []
235
-
236
- if (!meta.title || meta.title.length === 0) {
237
- errors.push('Title is required')
238
- }
239
- if (meta.title && meta.title.length > 60) {
240
- errors.push('Title should be under 60 characters')
241
- }
242
-
243
- if (!meta.description || meta.description.length === 0) {
244
- errors.push('Description is required')
245
- }
246
- if (meta.description && meta.description.length > 160) {
247
- errors.push('Description should be under 160 characters')
248
- }
249
-
250
- if (!meta.og?.image && !meta.twitter?.image) {
251
- errors.push('At least one image (OG or Twitter) is recommended')
252
- }
253
-
254
- if (!meta.keywords || meta.keywords.length === 0) {
255
- errors.push('At least one keyword is recommended')
256
- }
257
-
258
- return {
259
- valid: errors.length === 0,
260
- errors,
261
- }
262
- }
263
-
264
- /**
265
- * Presets for common SEO configurations
266
- */
267
- export const seoPresets = {
268
- /**
269
- * Blog article preset
270
- */
271
- article: (config: SEOConfig): Partial<SEOMeta> => ({
272
- og: { type: 'article', ...config },
273
- robots: 'index, follow',
274
- }),
275
-
276
- /**
277
- * E-commerce product preset
278
- */
279
- product: (config: SEOConfig): Partial<SEOMeta> => ({
280
- og: { type: 'product', ...config },
281
- robots: 'index, follow',
282
- }),
283
-
284
- /**
285
- * Service/company page preset
286
- */
287
- business: (config: SEOConfig): Partial<SEOMeta> => ({
288
- robots: 'index, follow',
289
- }),
290
-
291
- /**
292
- * Private/unlisted page preset
293
- */
294
- private: (): Partial<SEOMeta> => ({
295
- robots: 'noindex, nofollow',
296
- }),
297
- }