lib/schema.ts
name: PERSON.name,
// 画面表記は Shingo Kumon に統一しているため、日本語での検索(漢字・かな)と
// 結び付けられるよう alternateName に両方を残す。
...(lang === 'ja' ? { alternateName: [PERSON.nameJa, PERSON.nameJaKana] } : {}),
jobTitle: PERSON.jobTitle[lang],
url: lang === 'en' ? `${SITE_URL}/en` : SITE_URL,
image: PERSON.image,
sameAs: [...PERSON.sameAs],
knowsAbout: [...PERSON.knowsAbout],
worksFor: { '@id': BUSINESS_ID },
}
}
export function websiteSchema(lang: Lang) {
return {
'@type': 'WebSite',
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
// 同一IPからの連投を抑制する。サーバーレスではインスタンスごとのメモリなので
// 完璧な制限ではないが、素朴なスクリプトによる連続送信は防げる。
const RATE_LIMIT = { max: 3, windowMs: 10 * 60 * 1000 }
const hits = new Map<string, number[]>()
function isRateLimited(ip: string) {
const now = Date.now()
const recent = (hits.get(ip) ?? []).filter((t) => now - t < RATE_LIMIT.windowMs)
if (recent.length >= RATE_LIMIT.max) {
hits.set(ip, recent)
return true
}
recent.push(now)
hits.set(ip, recent)
lib/schema.ts
name: PERSON.name,
// 画面表記は Shingo Kumon に統一しているため、日本語での検索(漢字・かな)と
// 結び付けられるよう alternateName に両方を残す。
...(lang === 'ja' ? { alternateName: [PERSON.nameJa, PERSON.nameJaKana] } : {}),
jobTitle: PERSON.jobTitle[lang],
url: lang === 'en' ? `${SITE_URL}/en` : SITE_URL,
image: PERSON.image,
sameAs: [...PERSON.sameAs],
knowsAbout: [...PERSON.knowsAbout],
worksFor: { '@id': BUSINESS_ID },
}
}
export function websiteSchema(lang: Lang) {
return {
'@type': 'WebSite',
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
// 同一IPからの連投を抑制する。サーバーレスではインスタンスごとのメモリなので
// 完璧な制限ではないが、素朴なスクリプトによる連続送信は防げる。
const RATE_LIMIT = { max: 3, windowMs: 10 * 60 * 1000 }
const hits = new Map<string, number[]>()
function isRateLimited(ip: string) {
const now = Date.now()
const recent = (hits.get(ip) ?? []).filter((t) => now - t < RATE_LIMIT.windowMs)
if (recent.length >= RATE_LIMIT.max) {
hits.set(ip, recent)
return true
}
recent.push(now)
hits.set(ip, recent)