Repository navigation
Expand file tree
/
Copy pathworker.js
More file actions
108 lines (98 loc) · 5.49 KB
/
Copy pathworker.js
File metadata and controls
108 lines (98 loc) · 5.49 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
104
105
106
107
108
/**
* Cloudflare Worker — dynamic OpenGraph tag injection for StockTI
*
* When a social crawler fetches /result?r={prototypeId}, this worker:
* 1. Fetches the static index.html from ASSETS
* 2. Uses HTMLRewriter to replace generic meta tags with
* personality-specific title, description and canonical URL
* 3. Returns the transformed HTML so crawlers see the correct preview
*
* For all other requests the worker passes through to static ASSETS.
* SPA routing (serving index.html for unmatched client-side routes) is
* handled by `not_found_handling = "single-page-application"` in wrangler.toml.
*/
const SITE_URL = 'https://stti.gwo.app'
/** Minimal personality metadata needed for OG tags (mirrors src/data/prototypes.ts). */
const PROTO_META = {
buffett: { name: '沃伦·巴菲特', label: '价值守护者', icon: '🎩', tagline: '你是那种会研究10年再出手的猎人' },
lynch: { name: '彼得·林奇', label: '生活选股家', icon: '🔍', tagline: '你的选股雷达藏在日常生活里' },
burry: { name: '迈克尔·伯里', label: '大空头', icon: '📉', tagline: '你会在所有人狂欢时,独自翻地下室账本' },
hwang: { name: '比尔·黄', label: '杠杆信徒', icon: '💣', tagline: '信仰+集中+杠杆,你的投资三位一体' },
cathie: { name: '凯西·伍德', label: '创新传道士', icon: '🚀', tagline: '你对未来的信念,强到可以撑过任何回撤' },
livermore: { name: '杰西·利弗莫尔', label: '投机传奇', icon: '🎲', tagline: '你是市场情绪的读心术大师' },
marks: { name: '霍华德·马克斯', label: '周期猎手', icon: '⚖️', tagline: '你的武器是耐心和第二层思维' },
dalio: { name: '雷·达利奥', label: '全天候机器', icon: '🌍', tagline: '你把市场看成一台可以被理解的精密机器' },
softbank: { name: '孙正义', label: '愿景赌徒', icon: '🌌', tagline: '你看的不是PE,你看的是谁能统治未来' },
bogle: { name: '约翰·博格', label: '指数信徒', icon: '📦', tagline: '你做完18道生活题,发现自己最像「不选股」的人' },
retail: { name: '散户韭菜', label: '热点追风者', icon: '🌶️', tagline: '你永远相信「明天一定涨回来」' },
}
/**
* Build the three OG fields that vary per personality.
* @param {string} protoId
* @returns {{ title: string, description: string, canonicalUrl: string }}
*/
function buildOGMeta(protoId) {
const proto = PROTO_META[protoId]
return {
title: `${proto.icon} 我的投资人格是${proto.name}(${proto.label})— StockTI`,
description: `${proto.tagline}。来测测你是哪种投资人格?18道生活题,揭开你的投资基因。`,
canonicalUrl: `${SITE_URL}/result?r=${protoId}`,
}
}
export default {
/**
* @param {Request} request
* @param {{ ASSETS: { fetch(r: Request): Promise<Response> } }} env
*/
async fetch(request, env) {
const url = new URL(request.url)
const rParam = url.searchParams.get('r')
// ── Dynamic OG for shared result pages ──────────────────────────────────
if (url.pathname === '/result' && rParam && PROTO_META[rParam]) {
// Fetch the root index.html (the SPA shell)
const indexResponse = await env.ASSETS.fetch(new Request(`${url.origin}/`))
if (!indexResponse.ok) return indexResponse
const { title, description, canonicalUrl } = buildOGMeta(rParam)
return new HTMLRewriter()
.on('title', {
element(el) { el.setInnerContent(title) },
})
.on('meta[name="description"]', {
element(el) { el.setAttribute('content', description) },
})
.on('link[rel="canonical"]', {
element(el) { el.setAttribute('href', canonicalUrl) },
})
.on('meta[property="og:url"]', {
element(el) { el.setAttribute('content', canonicalUrl) },
})
.on('meta[property="og:title"]', {
element(el) { el.setAttribute('content', title) },
})
.on('meta[property="og:description"]', {
element(el) { el.setAttribute('content', description) },
})
.on('meta[name="twitter:title"]', {
element(el) { el.setAttribute('content', title) },
})
.on('meta[name="twitter:description"]', {
element(el) { el.setAttribute('content', description) },
})
.transform(indexResponse)
}
// ── Static asset pass-through ─────────────────────────────────────────────
// not_found_handling = "single-page-application" in wrangler.toml ensures
// the ASSETS binding returns index.html (200) for any path without a
// matching file, so Vue Router can handle client-side navigation.
const response = await env.ASSETS.fetch(request)
// Add CORS header to static assets so that tools like html2canvas can
// fetch CSS/JS bundles cross-origin when needed (e.g. share image capture).
const contentType = response.headers.get('content-type') ?? ''
if (contentType.includes('text/css') || contentType.includes('javascript') || contentType.includes('font')) {
const headers = new Headers(response.headers)
headers.set('Access-Control-Allow-Origin', '*')
return new Response(response.body, { status: response.status, headers })
}
return response
},
}