Free early-access launch: LAUNCH_MODE policy, CRO, pipeline hardening, AI-SEO, ops
- launch.ts policy: LAUNCH_MODE=free_early_access default; effective access = published && (free || early); intended tier (access) never rewritten - Billing off server-side: /api/subscribe 503 honest copy; subscribe CTAs render disabled 'قريباً' from first paint; /subscription early-access panel + /api/notify (LaunchNotifications) with honest 503 when mail transport off - Signup trust: verification email at signup (Users afterChange), no confirm-password, 429 message fidelity, typo suggestion, live password hint - Agent pipeline: pickFields strips access/readingMinutes, sourceId dedup, secret-scan 422 (pattern names only), string-content rejected (was silently replaced), publish stays explicit opt-in - lexical.ts: root unwrap fix — lexicalToText/countLexicalWords returned 0 for real serialized trees (secret scan + readingMinutes were inert) - llms-full.txt: fetchIndexDocs(true) — bodies were never selected - Content honesty: contentType/authorName/sourceId fields + migration, readingMinutes computed at write (160 wpm) + backfill script, byline, 'حُدّث' freshness - SEO/AI-SEO: metadata rewrite (published-only, og:image 1200x630), Article + BreadcrumbList + FAQPage + Organization JSON-LD, isAccessibleForFree honest, table renderer, robots AI-crawler policy (10 named bots), llms.txt + llms-full.txt, /topics/* hubs + sitemap, noindex auth pages, www 301 - Ops: backup-db.sh (restore rehearsed on VPS), docs copies (launch readiness + marketing audit + owner-inputs for L-03), GLOSSARY.md entries Verified: tsc --noEmit 0, npm run build 0, local smoke (ghost VIP read, subscribe/notify/agent APIs, signup token, 429 on 6th, llms/topics/sitemap, og:image/JSON-LD)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
/* Early-access notification signup (L-01). 200 → success copy;
|
||||
503/4xx → the server's Arabic error verbatim; 5xx/network → generic fallback. */
|
||||
export default function NotifyForm() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [pending, setPending] = useState(false)
|
||||
const [done, setDone] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (pending) return
|
||||
setPending(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch('/api/notify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
const data = (await res.json().catch(() => null)) as { error?: string; alreadySubscribed?: boolean } | null
|
||||
if (res.ok) {
|
||||
setDone(data?.alreadySubscribed ? 'بريدك مسجَّل لدينا بالفعل — سنراسلك عند تفعيل العضوية.' : 'تم! سنراسلك عند تفعيل العضوية.')
|
||||
} else {
|
||||
setError(data?.error ?? 'تعذر الحفظ، حاول مرة أخرى.')
|
||||
}
|
||||
} catch {
|
||||
setError('تعذر الحفظ، حاول مرة أخرى.')
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return <p className="notify-done" role="status">{done}</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="notify-form" onSubmit={submit}>
|
||||
<label className="sr-only" htmlFor="notify-email">البريد الإلكتروني</label>
|
||||
<input
|
||||
id="notify-email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
dir="ltr"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" type="submit" disabled={pending || email.trim() === ''}>
|
||||
{pending ? 'جارٍ الحفظ…' : 'أعلمني عند تفعيل العضوية'}
|
||||
</button>
|
||||
{error && <p className="notify-error" role="alert">{error}</p>}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user