-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathseoCore.ts
More file actions
103 lines (91 loc) · 3.75 KB
/
Copy pathseoCore.ts
File metadata and controls
103 lines (91 loc) · 3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/** Shared SEO types and HTML helpers — no router/exploration dependencies. */
export const DEFAULT_SITE_NAME = 'Feel Your Protocol' as const
export interface PageSeo {
path: string
title: string
description: string
canonicalUrl: string
imageUrl: string
imageWidth: number
imageHeight: number
imageAlt: string
noindex?: boolean
jsonLd?: object | object[]
}
/** Strip HTML tags and collapse whitespace for meta descriptions. */
export function stripHtml(html: string): string {
return html
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
export function truncateDescription(text: string, maxLength = 160): string {
if (text.length <= maxLength) return text
const cut = text.slice(0, maxLength - 1)
const lastSpace = cut.lastIndexOf(' ')
return `${(lastSpace > 80 ? cut.slice(0, lastSpace) : cut).trim()}…`
}
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}
export function inferOgImageType(imageUrl: string): string | undefined {
const path = imageUrl.split('?')[0]?.toLowerCase() ?? ''
if (path.endsWith('.webp')) return 'image/webp'
if (path.endsWith('.png')) return 'image/png'
if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'image/jpeg'
return undefined
}
/** Inject title, meta, canonical, Open Graph, and JSON-LD into a Vite-built index.html shell. */
export function injectSeoIntoHtml(
html: string,
seo: PageSeo,
siteName: string = DEFAULT_SITE_NAME,
): string {
const headTags: string[] = [
`<meta name="description" content="${escapeHtml(seo.description)}">`,
`<link rel="canonical" href="${escapeHtml(seo.canonicalUrl)}">`,
`<meta property="og:type" content="website">`,
`<meta property="og:site_name" content="${escapeHtml(siteName)}">`,
`<meta property="og:title" content="${escapeHtml(seo.title)}">`,
`<meta property="og:description" content="${escapeHtml(seo.description)}">`,
`<meta property="og:url" content="${escapeHtml(seo.canonicalUrl)}">`,
`<meta property="og:image" content="${escapeHtml(seo.imageUrl)}">`,
`<meta property="og:image:width" content="${seo.imageWidth}">`,
`<meta property="og:image:height" content="${seo.imageHeight}">`,
`<meta property="og:image:alt" content="${escapeHtml(seo.imageAlt)}">`,
]
const imageType = inferOgImageType(seo.imageUrl)
if (imageType) {
headTags.push(`<meta property="og:image:type" content="${imageType}">`)
}
headTags.push(
`<meta name="twitter:card" content="summary_large_image">`,
`<meta name="twitter:title" content="${escapeHtml(seo.title)}">`,
`<meta name="twitter:description" content="${escapeHtml(seo.description)}">`,
`<meta name="twitter:image" content="${escapeHtml(seo.imageUrl)}">`,
`<meta name="twitter:image:alt" content="${escapeHtml(seo.imageAlt)}">`,
)
if (seo.noindex) {
headTags.push('<meta name="robots" content="noindex, follow">')
}
if (seo.jsonLd) {
const json = JSON.stringify(seo.jsonLd).replace(/</g, '\\u003c')
headTags.push(`<script type="application/ld+json" id="page-seo-jsonld">${json}</script>`)
}
let out = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(seo.title)}</title>`)
out = out.replace(/<meta\s+name="description"[^>]*>\s*/g, '')
out = out.replace(/<link rel="canonical"[^>]*>\s*/g, '')
out = out.replace(/<meta property="og:[^"]+"[^>]*>\s*/g, '')
out = out.replace(/<meta name="twitter:[^"]+"[^>]*>\s*/g, '')
out = out.replace(/<meta name="robots"[^>]*>\s*/g, '')
out = out.replace(
/<script type="application\/ld\+json" id="page-seo-jsonld"[^>]*>[\s\S]*?<\/script>\s*/g,
'',
)
out = out.replace('</head>', ` ${headTags.join('\n ')}\n </head>`)
return out
}