From 8f7e7a53c5253bddd9b26da1885106494346d602 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 22:25:03 +0000 Subject: [PATCH 1/4] docs(site): point Sentra Bot canonical URL at bot.sentrahai.com Map product marketing links to the bot subdomain and document how to assign the domain on Vercel project sentrabot-site without touching apex. Co-authored-by: Gaffer --- README.md | 8 ++++---- apps/docs/docs.json | 14 +++++++------- apps/site/DOMAIN.md | 34 +++++++++++++++++++++++++++++++++ apps/site/README.md | 3 +++ apps/site/src/html/Faq.html | 2 +- apps/site/src/html/Footer.html | 2 +- apps/site/src/html/Pricing.html | 2 +- 7 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 apps/site/DOMAIN.md diff --git a/README.md b/README.md index 208ba0b..dcb053a 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ -### [`SENTRA / BOT`](https://sentrahai.com/) +### [`SENTRA / BOT`](https://bot.sentrahai.com/) - + Sentra Bot — Intelligence for Autonomy. Operational signal: persistent agents · memory · routines · computers · human authority -Sentra Bot
+Sentra Bot
The autonomous intelligence layer of Sentra Artificial Intelligence.

@@ -38,7 +38,7 @@ The autonomous intelligence layer of Sentra Artificial Intelligence. ### `01 / ORIGIN SIGNAL` -**[Sentra Bot](https://sentrahai.com/)** is the operating layer for persistent, composable AI teammates you actually own — agents that can hold memory, execute routines, use computers, and work against your own models, your own data, and your own machines. +**[Sentra Bot](https://bot.sentrahai.com/)** is the operating layer for persistent, composable AI teammates you actually own — agents that can hold memory, execute routines, use computers, and work against your own models, your own data, and your own machines. The repository is not a single chatbot and not a thin model wrapper. It contains the full runtime surface: API, web and desktop clients, background worker, job orchestration, memory, computer providers, connectors, artifacts, realtime infrastructure, and a deliberately read-only clinical reconnaissance bridge. diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 62b91a8..d2a12e8 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -12,7 +12,7 @@ "logo": { "light": "/logo/light.png", "dark": "/logo/dark.png", - "href": "https://sentrahai.com" + "href": "https://bot.sentrahai.com" }, "appearance": { "default": "dark" }, "fonts": { "family": "Geist" }, @@ -26,18 +26,18 @@ ] }, "navbar": { - "links": [{ "label": "Mengenai Sentra", "href": "https://sentrahai.com/tentang.html" }], - "primary": { "type": "button", "label": "Coba SentraBot", "href": "https://sentrahai.com" } + "links": [{ "label": "Mengenai Sentra", "href": "https://bot.sentrahai.com/tentang.html" }], + "primary": { "type": "button", "label": "Coba SentraBot", "href": "https://bot.sentrahai.com" } }, "footer": { - "socials": { "website": "https://sentrahai.com" }, + "socials": { "website": "https://bot.sentrahai.com" }, "links": [ { "header": "Sentra", "items": [ - { "label": "Mengenai Sentra", "href": "https://sentrahai.com/tentang.html" }, - { "label": "Privasi", "href": "https://sentrahai.com/privasi.html" }, - { "label": "Ketentuan", "href": "https://sentrahai.com/ketentuan.html" } + { "label": "Mengenai Sentra", "href": "https://bot.sentrahai.com/tentang.html" }, + { "label": "Privasi", "href": "https://bot.sentrahai.com/privasi.html" }, + { "label": "Ketentuan", "href": "https://bot.sentrahai.com/ketentuan.html" } ] } ] diff --git a/apps/site/DOMAIN.md b/apps/site/DOMAIN.md new file mode 100644 index 0000000..3871133 --- /dev/null +++ b/apps/site/DOMAIN.md @@ -0,0 +1,34 @@ +# Domain map — Sentra Bot marketing site + +Canonical production host for this package (`apps/site`): + +**`https://bot.sentrahai.com`** + +| Host | Role | +|------|------| +| `sentrahai.com` / `www` | Brand hub perusahaan (bukan landing Bot) | +| `bot.sentrahai.com` | Landing + halaman publik Sentra Bot (`apps/site`) | +| `*.vercel.app` (`sentrabot-site`) | Preview / fallback deploy Vercel | + +DNS untuk `sentrahai.com` sudah di Vercel (`ns1`/`ns2.vercel-dns.com`). Subdomain `bot` harus di-assign ke project **`sentrabot-site`** (team yang deploy site ini). Apex **jangan** dipindah ke project ini — apex masih dipakai produk lain. + +## Assign domain (Vercel Dashboard) + +1. Buka project **`sentrabot-site`** → **Settings** → **Domains**. +2. Add **`bot.sentrahai.com`**. +3. Karena NS sudah Vercel DNS, record biasanya dibuat otomatis. +4. Tunggu status **Valid**. Verifikasi: + +```bash +curl -sI https://bot.sentrahai.com/ +# expect: HTTP/2 200 dan HTML Sentra Bot (bukan x-vercel-error: DEPLOYMENT_NOT_FOUND) +``` + +## Assign via CLI (butuh token) + +```bash +vercel link --project sentrabot-site --yes +vercel domains add bot.sentrahai.com +``` + +Env: `VERCEL_TOKEN` dengan akses team pemilik project. diff --git a/apps/site/README.md b/apps/site/README.md index 0b42a64..45f9570 100644 --- a/apps/site/README.md +++ b/apps/site/README.md @@ -18,6 +18,9 @@ Project Root Directory must be `apps/site`. `vercel.json` forces `npm ci` + pulls Electron/Playwright and can fail with `ERR_INVALID_THIS` / `ERR_PNPM_META_FETCH_FAIL` on Vercel). +Production domain: **`https://bot.sentrahai.com`** (see `DOMAIN.md`). Do not +point apex `sentrahai.com` at this project. + Deploy-critical images under `public/` and `src/` are stored as normal git blobs (not Git LFS). Vercel otherwise ships LFS pointer text (~130 bytes) as broken PNGs unless Project Settings → Git → Git LFS is enabled. diff --git a/apps/site/src/html/Faq.html b/apps/site/src/html/Faq.html index 655bda8..c76a34d 100644 --- a/apps/site/src/html/Faq.html +++ b/apps/site/src/html/Faq.html @@ -1 +1 @@ -

Pertanyaan yang sering diajukan

\ No newline at end of file +

Pertanyaan yang sering diajukan

\ No newline at end of file diff --git a/apps/site/src/html/Footer.html b/apps/site/src/html/Footer.html index dfc287a..54f4ddf 100644 --- a/apps/site/src/html/Footer.html +++ b/apps/site/src/html/Footer.html @@ -1 +1 @@ - + diff --git a/apps/site/src/html/Pricing.html b/apps/site/src/html/Pricing.html index e4cf52d..5b80c2e 100644 --- a/apps/site/src/html/Pricing.html +++ b/apps/site/src/html/Pricing.html @@ -1 +1 @@ -

Pilih paket

Free

Rp0

Coba dulu, tanpa bayar

  • 3 bot aktif
  • Managed AI termasuk — pemakaian bulanan terbatas
  • 3 jadwal otomatis · 1 integrasi
Mulai gratis

Plus

Rp79.000

per bulan

  • Semua yang ada di Free
  • Kuota AI sudah termasuk
  • 10 bot aktif
  • Tidak ada tindakan tanpa izin Anda
Ajukan akses beta

Pro

Rp199.000

per bulan

  • Semua yang ada di Plus
  • Kuota AI lebih besar
  • 30 bot aktif
  • Bantuan lewat email
Ajukan akses beta

Business

Rp499.000

per bulan

  • Semua yang ada di Pro
  • 100 bot aktif
  • Beberapa anggota tim
  • Bantuan prioritas
Hubungi kami

Semua harga sudah termasuk pajak yang berlaku.

Kuota AI habis sebelum akhir bulan? Top-up Rp25rb–250rb, langsung aktif. Punya kunci AI sendiri? Pasang BYOK dan jalankan seluruh Sentra Bot di komputer atau server Anda.

Kunjungilogo

“Tiap Minggu malam saya pusing mikir menu seminggu. Sekarang daftar belanjanya sudah jadi, tinggal saya coret yang tidak perlu.”

Sari Wijaya
\ No newline at end of file +

Pilih paket

Free

Rp0

Coba dulu, tanpa bayar

  • 3 bot aktif
  • Managed AI termasuk — pemakaian bulanan terbatas
  • 3 jadwal otomatis · 1 integrasi
Mulai gratis

Plus

Rp79.000

per bulan

  • Semua yang ada di Free
  • Kuota AI sudah termasuk
  • 10 bot aktif
  • Tidak ada tindakan tanpa izin Anda
Ajukan akses beta

Pro

Rp199.000

per bulan

  • Semua yang ada di Plus
  • Kuota AI lebih besar
  • 30 bot aktif
  • Bantuan lewat email
Ajukan akses beta

Business

Rp499.000

per bulan

  • Semua yang ada di Pro
  • 100 bot aktif
  • Beberapa anggota tim
  • Bantuan prioritas
Hubungi kami

Semua harga sudah termasuk pajak yang berlaku.

Kuota AI habis sebelum akhir bulan? Top-up Rp25rb–250rb, langsung aktif. Punya kunci AI sendiri? Pasang BYOK dan jalankan seluruh Sentra Bot di komputer atau server Anda.

Kunjungilogo

“Tiap Minggu malam saya pusing mikir menu seminggu. Sekarang daftar belanjanya sudah jadi, tinggal saya coret yang tidak perlu.”

Sari Wijaya
\ No newline at end of file From b2563a51e4333d5aae4cd4f1266529fa3ec7f713 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 00:05:47 +0000 Subject: [PATCH 2/4] feat(site): add favicon, share preview, sitemap, 404, and optional analytics Canonical, Open Graph, Twitter Card, and sitemap URLs use bot.sentrahai.com. Privacy and terms stay noindex. Analytics stays off until SITE_* env is set. Co-authored-by: Gaffer --- apps/site/DOMAIN.md | 2 + apps/site/README.md | 27 ++++ apps/site/index.html | 30 +++- apps/site/package.json | 5 +- apps/site/public/404.html | 69 +++++++++ apps/site/public/analytics.json | 6 + apps/site/public/apple-touch-icon.png | Bin 0 -> 4997 bytes apps/site/public/daftar.html | 24 ++- apps/site/public/favicon.ico | Bin 0 -> 3971 bytes apps/site/public/favicon.svg | 1 + apps/site/public/js/site-analytics.js | 85 +++++++++++ apps/site/public/ketentuan.html | 26 +++- apps/site/public/ketentuan/index.html | 26 +++- apps/site/public/og-image.png | Bin 0 -> 45787 bytes apps/site/public/privasi.html | 26 +++- apps/site/public/privasi/index.html | 26 +++- apps/site/public/robots.txt | 6 + apps/site/public/site.webmanifest | 26 ++++ apps/site/public/sitemap.xml | 18 +++ apps/site/public/tentang.html | 24 ++- apps/site/scripts/check-seo.mjs | 148 +++++++++++++++++++ apps/site/scripts/write-analytics-config.mjs | 23 +++ apps/site/vercel.json | 22 ++- apps/site/vite.config.js | 30 +++- 24 files changed, 632 insertions(+), 18 deletions(-) create mode 100644 apps/site/public/404.html create mode 100644 apps/site/public/analytics.json create mode 100644 apps/site/public/apple-touch-icon.png create mode 100644 apps/site/public/favicon.ico create mode 100644 apps/site/public/favicon.svg create mode 100644 apps/site/public/js/site-analytics.js create mode 100644 apps/site/public/og-image.png create mode 100644 apps/site/public/robots.txt create mode 100644 apps/site/public/site.webmanifest create mode 100644 apps/site/public/sitemap.xml create mode 100644 apps/site/scripts/check-seo.mjs create mode 100644 apps/site/scripts/write-analytics-config.mjs diff --git a/apps/site/DOMAIN.md b/apps/site/DOMAIN.md index 3871133..17ebfee 100644 --- a/apps/site/DOMAIN.md +++ b/apps/site/DOMAIN.md @@ -12,6 +12,8 @@ Canonical production host for this package (`apps/site`): DNS untuk `sentrahai.com` sudah di Vercel (`ns1`/`ns2.vercel-dns.com`). Subdomain `bot` harus di-assign ke project **`sentrabot-site`** (team yang deploy site ini). Apex **jangan** dipindah ke project ini — apex masih dipakai produk lain. +Share preview (WhatsApp / X / LinkedIn) and search tags (`canonical`, `og:*`, `twitter:*`, `sitemap.xml`, `robots.txt`) must use **`https://bot.sentrahai.com`**, not the apex. HTTPS is terminated by Vercel; `vercel.json` adds HSTS. + ## Assign domain (Vercel Dashboard) 1. Buka project **`sentrabot-site`** → **Settings** → **Domains**. diff --git a/apps/site/README.md b/apps/site/README.md index 45f9570..6d3c6cb 100644 --- a/apps/site/README.md +++ b/apps/site/README.md @@ -25,6 +25,33 @@ Deploy-critical images under `public/` and `src/` are stored as normal git blobs (not Git LFS). Vercel otherwise ships LFS pointer text (~130 bytes) as broken PNGs unless Project Settings → Git → Git LFS is enabled. +HTTPS is provided by Vercel. `vercel.json` adds HSTS and does **not** use the +Vite SPA fallback, so unknown paths serve `public/404.html` instead of the +homepage. + +## Favicon, share preview, SEO, analytics + +Browser tabs use `public/favicon.ico` plus `favicon.svg` and +`apple-touch-icon.png`. WhatsApp, X/Twitter, and LinkedIn read Open Graph and +Twitter Card tags (`og:image` is `public/og-image.png`, 1200×630). Indexed +pages also have `title`, `description`, and a canonical URL on +`https://bot.sentrahai.com`. Apex `sentrahai.com` remains the company hub, not this landing. + +`public/sitemap.xml` lists only indexable URLs (home, Tentang, Daftar). +Privacy and terms stay `noindex` because those drafts are not ready for search +results. `public/robots.txt` points crawlers at the sitemap. + +Analytics is optional and off by default. `public/js/site-analytics.js` loads +`/analytics.json` and enables **one** vendor if a key is present, in this +order: PostHog, Plausible, Google Analytics 4. Empty strings mean no third-party +script. Do not commit real keys. On Vercel, set any of +`SITE_POSTHOG_KEY` / `SITE_POSTHOG_HOST`, `SITE_PLAUSIBLE_DOMAIN`, or +`SITE_GA4_MEASUREMENT_ID`; `npm run build` writes them into `analytics.json`. + +```bash +pnpm --filter cora check:seo +``` + ## Layout - `original/` — untouched HTML capture (pre-rebrand archive) diff --git a/apps/site/index.html b/apps/site/index.html index 7f9f275..62e5463 100644 --- a/apps/site/index.html +++ b/apps/site/index.html @@ -1,9 +1,33 @@ - + - + Sentra Bot + + + + + + + + + + + + + + + + + + + + + + + + @@ -13,7 +37,7 @@ - Sentra Bot +
diff --git a/apps/site/package.json b/apps/site/package.json index 9cd45ae..aeb69bc 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -8,8 +8,9 @@ }, "scripts": { "dev": "vite", - "build": "vite build", - "preview": "vite preview" + "build": "node scripts/write-analytics-config.mjs && node scripts/check-seo.mjs && vite build", + "preview": "vite preview", + "check:seo": "node scripts/check-seo.mjs" }, "dependencies": { "react": "^19.1.0", diff --git a/apps/site/public/404.html b/apps/site/public/404.html new file mode 100644 index 0000000..cd0b48e --- /dev/null +++ b/apps/site/public/404.html @@ -0,0 +1,69 @@ + + + + + + + Halaman tidak ditemukan | Sentra Bot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Sentra Bot + +
Menu
+
+
+

404

+

Halaman tidak ditemukan.

+

Tautan ini tidak mengarah ke halaman yang ada. Kembali ke beranda, atau buka halaman Tentang bila Anda mencari Sentra Artificial Intelligence.

+

+ Beranda + Tentang + Daftar +

+
+ + diff --git a/apps/site/public/analytics.json b/apps/site/public/analytics.json new file mode 100644 index 0000000..507aa9e --- /dev/null +++ b/apps/site/public/analytics.json @@ -0,0 +1,6 @@ +{ + "posthogKey": "", + "posthogHost": "", + "plausibleDomain": "", + "ga4MeasurementId": "" +} diff --git a/apps/site/public/apple-touch-icon.png b/apps/site/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a658bf55d356c8b6868cb0252cd8d52ecd031a GIT binary patch literal 4997 zcmb_gXH-*BmqieygOM(R^j?w(NRdzi(glKm0i+jcB0+kQ5;{bRLMQ@KLZ!y+H%AQw0 z(W8e{m;u8$W}<_gunGcI1~L1asOX>Z0-{LE&`6wGK^Dm#cuU|B&2qY9Qr|iwcF43w z!`_-Zi))c5u`4(|hPI5#HWL#{gow_|<93|>NfH=Cu(9!bJ@+j&m0G$q2fGY|I-QVN z?B2$hL!nru+;A-!WZ0JT!DGgUyQmz_PK)L6#XfMci3qwBi-%!N>eXn{>1mOtTN_l= zX<2vgNlW&{L#+tK^I`cEQPEoIQZ1+%n}?E)ZvXV8I{#UWCUq3+` z+90H_@71ZTN_14~Bsd#uY5dKY+!7I}H_(+CuNagJ_G9{^wOXaVQWDO)CW2IkAVTRZ zR~GgcH?1&5sgI@~sBbrYE-nrQc2HJ^(G80q`kDP0vrRn9`o;@o3+*~N?9^mPMgFXw z-_iLysfgOW&x}*)Xb#MPcB!bth##*~F; zqYNlPckjHxhqIzW)Nfj^9gddI_IR0hq^YSnxkQh5{pQln*?D{ScPfyzK}TCJCKLh# zJ~lSq2s~G$W}pe0{w4)N#;cQ8Qw&Tl{JaT(n8zSu+89)s&Y^YvmM8u<%Hwq)kP;}8 zQMxP7Gx92TEnXQTPV68;M|BLLZ>?{z6FR!=S!iMz0vup)2d{8#5L%r=2ZO!y!w$uB(?PWTv5p!Z8oD2M?$_-YIzEOCPs}k~0TJK@DYlhbe!{3wE?`Cd znRVkWhQGcvEj`rnR_1oxoQTK?0ntJvX9mWV-~32x>w_3PeQF+4N?7H%4~pnp$?JTG zO%>9|-Y`Qiy23+BV_TP^pHQ$*W1-H+HWeo4n+YW4@9{bgq03B_;+LmoT*TAWx9sih z`@l?0Os^U$XsVs(TiU`fyDOTs%fEgNH3bBapIO}RjAlVUk!5UVz8?EBk(?tz0OSo% zRtLJEfWK7%ztl5J78Pw&y<>KWgvRxmY!~Je2f$bXX6TWL+&h?p-XsiT6A)Ns#W*bO zc{1b`p~aL1%p)WFUd+ ztF@7^E#s!MGoqT8Kcer|!J(WIwnx?7IE^;5&TGfRLkh#ypONWvjrq2Qyt_xU(#$f6 zgt8eJz674i=kK-BGjno65T62a$S=X#`ufUmGDP%?tV~%B7g`hx#mZ@VeppLNO4c~J zI8PWfogcF{Pinj=%aC3l(MD>-Q|5%YV6kZqjhq?KQW{IKgLX zZy;C?69P!HmCMqDXD27aV^XWzCP#HS@9cfj81I;BW!)Vc<=grudk1=>;L8{3gXxsi z)Wy;l0x2me$XVr=I4cm6hL?|zjn_=XBD*zt;_Q*%5qZ>mCU!ygf+gvs!LZI=_LzHxT~H}UCGst^-4>PYZyLFzr6Z9jA*C7I$itV3Zq&aAe_u1J^~Bojt58z=ZY9|Jx8BSn`ADDG9$2HN z`ZRCnc>i`ud8tM0-f|CX3Ce|T%GcEB?v46}D(GNGetuy{1a={NFdfiQQiZINr%mpE z{>Wn?kYqa}zjbY-TV7WFgT-$|#P8dv`+DCA9 z>~L5Y&Wn41?H+P9^uKZ=VvP@A>=5uu#$2bnebo278Tk8hm}; zh8vgX?5&<|H88F8e?Ol9`w%}Q$jBip4*Di_XXfyurG+zBeC@4*f&${HrmrSk%CCB8 zlaYof3+Irl`r4wa8HWjV)wL>!?@5x1`XC(X&5bxe3-z{QmJy8qrI@W8Bj>BZx!=x{ zr5(O7MC|G9g|hG(q3&;SVv4Kv-jcFK=Q)v2caygLBBuOfD-51%^dobS=2>Qe^~5Cc z%Cb&{L6ML4i;EjsQji>yKAE?j2**P~mh*G7ThsJs(}%jr$wrIA*8m_$jkfT~9CP7l_o8b{Z|10B-0yY)n~T zp~+Sv@!dP@w1I&^+wc@^7$xiCs9%w>`J2l?f51RyjF1k}Wz{T`k_biQ=jY!anx_{a zPl9&w3JC?H-wCb8exNo?G5lebYSlvI#k*dCxMm-Dqz9qL>H&|bHm ztRgWQv=hgc6)FbO(+B35&ArHK2)Yj24A9yZY%wl1x z^T(qilu{mmrja?X|DXAi^|>g8ai;BOl&jiZyl%C3c?)|lh-_x&5NW~#EN8v{Whmm< zhD)?-JN-l7x!=Xn`J-eq$K8=9F!fnflG8e2b9&yU;fY35>=C*d>TJ^baa$@ zVU<8J*t#zlv2o1kGE~qDk}NeO1yx< z-eF;_u1NW))!K0OV|Iuj0{~j`w1l{LtL2D(dpnowM&sGxR#Z-i-I|8gOk))g3ZF}Y z(g~Qh1vh<9{SaV2cBi&FWayVB?n;4S0{dm z-CI5~mbj(9w){Ub+c4riIzH~Y(wHda(Wy*j@Nn`ObO-a7*VP?->U~d1%R=F#ML;Iq zieJJWcjOm+2~It?;mpkemPAB(!Aw#b#~^L<0C64~s_Zzo5|O|5u$v!rsN21|Z*FUe z-nZRPwiXsRCq(4=w@G3ril3uD295MV=4;RIa<6M9y})wuD|-AMr!Cc4JaHeF(AYvN zuzmjyU96v@sC@kzARKFJ>rJZ3UYRQLi`V`O<6F{7u4zOhLioxtOo7WM3@hdW#asFZ zfqP4x>|A`Yr!|oq8}^fvMT!HZ#?&Bf*WFWJ&zhpHsSDsw=|2dy-kkmVkPC|G3QhAYoR4OG7sauCwsW9Ra)E_b2%aIbWrn462%-QmH9nk)^=L36>kl@@?l8{tSYFhW zYHZzQVk#XQA&b)~)vYvp7xG?iW#E)@kxhPW&LsWwCE#9wda60Su1UvMw}(v@C>}}% z2kBqj0%5J_NYTVm;jI(pEAf4aSslGYY$+;FG%e<8Rt|qlxJ)e!)ZSC)Q|x5`r1bp? zca64Qv*7+IS(4~;1B_K3xzO_m-p8G?Fjm2#jIPnf($d?hRo?8};(-Vszqdn>@PT#` z71c;3AZ&R@zkWS2vvTBqCru0ZanoOZ zR9_rz8F@O+g%W&TAH@PQQN#R-#em=SMj?F%{Wc=@SX zq?P^S@Z_Y*-x`$)AkDW|evthmViA7+lJRy>k}y5u9|WE|NnwCYzQ-u!L!ao>?{Rp(~ z4cf#%LxY%oh`_@GIK|>c8y+5?XQ-U8ifHYZAA%G>t%8QEX8yg zmhVqbA`pPV5B>+FlmI`w%lz{~Ls=T&y=wT%C8xkpuQt*V+d{Vp`FF^I1 yd4QIa{eO3!{#TC)`0<}!)PF|*Z!M^23#Ktqi{y>|7hynq4Np}`3tp^X7W7|CYmqMi literal 0 HcmV?d00001 diff --git a/apps/site/public/daftar.html b/apps/site/public/daftar.html index 1efdb12..b0aefaa 100644 --- a/apps/site/public/daftar.html +++ b/apps/site/public/daftar.html @@ -7,14 +7,36 @@ Cara memulai | Sentra Bot + + + + - + + + + + + + + + + + + + + + + + + + diff --git a/apps/site/public/favicon.ico b/apps/site/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..10e5bbfad76f31ea9bca1cf9c5a9aeb7a117015a GIT binary patch literal 3971 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 \ No newline at end of file diff --git a/apps/site/public/js/site-analytics.js b/apps/site/public/js/site-analytics.js new file mode 100644 index 0000000..e7c99d3 --- /dev/null +++ b/apps/site/public/js/site-analytics.js @@ -0,0 +1,85 @@ +(() => { + const ANALYTICS_URL = "/analytics.json"; + + function isNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0; + } + + function loadScript(src, attributes) { + const script = document.createElement("script"); + script.src = src; + script.async = true; + if (attributes) { + for (const [key, value] of Object.entries(attributes)) { + script.setAttribute(key, value); + } + } + document.head.appendChild(script); + return script; + } + + function enablePostHog(key, host) { + const script = loadScript("https://us-assets.i.posthog.com/static/array.js"); + script.addEventListener("load", () => { + if (typeof window.posthog?.init !== "function") { + return; + } + const apiHost = isNonEmptyString(host) ? host.trim() : "https://us.i.posthog.com"; + window.posthog.init(key.trim(), { + api_host: apiHost, + autocapture: false, + capture_pageview: true, + disable_session_recording: true, + persistence: "memory", + }); + }); + } + + function enablePlausible(domain) { + loadScript("https://plausible.io/js/script.js", { + defer: "true", + "data-domain": domain.trim(), + }); + } + + function enableGa4(measurementId) { + const id = measurementId.trim(); + loadScript(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`); + window.dataLayer = window.dataLayer || []; + function gtag() { + window.dataLayer.push(arguments); + } + window.gtag = gtag; + gtag("js", new Date()); + gtag("config", id, { anonymize_ip: true }); + } + + async function start() { + let config; + try { + const response = await fetch(ANALYTICS_URL, { credentials: "same-origin" }); + if (!response.ok) { + return; + } + config = await response.json(); + } catch { + return; + } + if (!config || typeof config !== "object") { + return; + } + if (isNonEmptyString(config.posthogKey)) { + enablePostHog(config.posthogKey, config.posthogHost); + return; + } + if (isNonEmptyString(config.plausibleDomain)) { + enablePlausible(config.plausibleDomain); + return; + } + if (isNonEmptyString(config.ga4MeasurementId)) { + enableGa4(config.ga4MeasurementId); + } + } + + void start(); +})(); diff --git a/apps/site/public/ketentuan.html b/apps/site/public/ketentuan.html index 69c3dcf..74b59de 100644 --- a/apps/site/public/ketentuan.html +++ b/apps/site/public/ketentuan.html @@ -3,14 +3,36 @@ - + Ketentuan Layanan | Sentra Bot + + + + - + + + + + + + + + + + + + + + + + + + diff --git a/apps/site/scripts/check-seo.mjs b/apps/site/scripts/check-seo.mjs new file mode 100644 index 0000000..b144a54 --- /dev/null +++ b/apps/site/scripts/check-seo.mjs @@ -0,0 +1,148 @@ +import { readFileSync, statSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const siteOrigin = "https://bot.sentrahai.com"; +const errors = []; + +function fail(message) { + errors.push(message); +} + +function read(relativePath) { + return readFileSync(join(root, relativePath), "utf8"); +} + +function assertContains(relativePath, snippet) { + if (!read(relativePath).includes(snippet)) { + fail(`${relativePath} missing ${JSON.stringify(snippet)}`); + } +} + +function assertNotLfsPointer(relativePath, minBytes) { + const path = join(root, relativePath); + const size = statSync(path).size; + const head = readFileSync(path, { encoding: "utf8" }).slice(0, 80); + if (head.startsWith("version https://git-lfs.github.com/spec/v1")) { + fail(`${relativePath} is a Git LFS pointer`); + } + if (size < minBytes) { + fail(`${relativePath} is ${size} bytes; expected at least ${minBytes}`); + } +} + +function pngSize(relativePath) { + const buffer = readFileSync(join(root, relativePath)); + if (buffer.length < 24 || buffer.toString("ascii", 1, 4) !== "PNG") { + fail(`${relativePath} is not a PNG`); + return { width: 0, height: 0 }; + } + return { + width: buffer.readUInt32BE(16), + height: buffer.readUInt32BE(20), + }; +} + +const pages = [ + { + file: "index.html", + canonical: `${siteOrigin}/`, + title: "Sentra Bot", + indexed: true, + }, + { + file: "public/tentang.html", + canonical: `${siteOrigin}/tentang.html`, + title: "Mengenai Sentra Artificial Intelligence", + indexed: true, + }, + { + file: "public/daftar.html", + canonical: `${siteOrigin}/daftar.html`, + title: "Cara memulai | Sentra Bot", + indexed: true, + }, + { + file: "public/privasi.html", + canonical: `${siteOrigin}/privasi.html`, + title: "Kebijakan Privasi | Sentra Bot", + indexed: false, + }, + { + file: "public/ketentuan.html", + canonical: `${siteOrigin}/ketentuan.html`, + title: "Ketentuan Layanan | Sentra Bot", + indexed: false, + }, + { + file: "public/404.html", + canonical: `${siteOrigin}/404.html`, + title: "Halaman tidak ditemukan | Sentra Bot", + indexed: false, + }, +]; + +for (const page of pages) { + const html = read(page.file); + if (!html.includes(page.title)) { + fail(`${page.file} missing ${page.title}`); + } + assertContains(page.file, `rel="canonical" href="${page.canonical}"`); + assertContains(page.file, 'name="description"'); + assertContains(page.file, `property="og:url" content="${page.canonical}"`); + assertContains(page.file, 'property="og:title"'); + assertContains(page.file, 'property="og:description"'); + assertContains(page.file, `property="og:image" content="${siteOrigin}/og-image.png"`); + assertContains(page.file, 'name="twitter:card" content="summary_large_image"'); + assertContains(page.file, `name="twitter:image" content="${siteOrigin}/og-image.png"`); + assertContains(page.file, 'href="/favicon.ico"'); + assertContains(page.file, 'href="/favicon.svg"'); + assertContains(page.file, 'href="/apple-touch-icon.png"'); + assertContains(page.file, 'src="/js/site-analytics.js"'); + if (page.indexed) { + if (html.includes('name="robots" content="noindex')) { + fail(`${page.file} should be indexable`); + } + } else if (!html.includes('name="robots" content="noindex')) { + fail(`${page.file} should be noindex`); + } +} + +assertContains("public/privasi/index.html", `rel="canonical" href="${siteOrigin}/privasi.html"`); +assertContains("public/ketentuan/index.html", `rel="canonical" href="${siteOrigin}/ketentuan.html"`); + +const sitemap = read("public/sitemap.xml"); +for (const loc of [`${siteOrigin}/`, `${siteOrigin}/tentang.html`, `${siteOrigin}/daftar.html`]) { + if (!sitemap.includes(`${loc}`)) { + fail(`sitemap.xml missing ${loc}`); + } +} +if (sitemap.includes("privasi.html") || sitemap.includes("ketentuan.html") || sitemap.includes("404.html")) { + fail("sitemap.xml should not list noindex pages"); +} + +assertContains("public/robots.txt", `Sitemap: ${siteOrigin}/sitemap.xml`); +assertNotLfsPointer("public/favicon.ico", 1000); +assertNotLfsPointer("public/favicon.svg", 1000); +assertNotLfsPointer("public/og-image.png", 10_000); +assertNotLfsPointer("public/apple-touch-icon.png", 1000); + +const og = pngSize("public/og-image.png"); +if (og.width !== 1200 || og.height !== 630) { + fail(`og-image.png is ${og.width}x${og.height}; expected 1200x630`); +} + +const analytics = JSON.parse(read("public/analytics.json")); +for (const key of ["posthogKey", "posthogHost", "plausibleDomain", "ga4MeasurementId"]) { + if (!(key in analytics)) { + fail(`analytics.json missing ${key}`); + } +} + +if (errors.length > 0) { + console.error(errors.map((error) => ` - ${error}`).join("\n")); + process.exit(1); +} + +console.log("site SEO checks passed"); diff --git a/apps/site/scripts/write-analytics-config.mjs b/apps/site/scripts/write-analytics-config.mjs new file mode 100644 index 0000000..68cdfa8 --- /dev/null +++ b/apps/site/scripts/write-analytics-config.mjs @@ -0,0 +1,23 @@ +import { writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); + +function envValue(name) { + const value = process.env[name]; + return typeof value === "string" ? value.trim() : ""; +} + +const config = { + posthogKey: envValue("SITE_POSTHOG_KEY"), + posthogHost: envValue("SITE_POSTHOG_HOST"), + plausibleDomain: envValue("SITE_PLAUSIBLE_DOMAIN"), + ga4MeasurementId: envValue("SITE_GA4_MEASUREMENT_ID"), +}; + +if (!Object.values(config).some((value) => value.length > 0)) { + process.exit(0); +} + +writeFileSync(join(root, "public/analytics.json"), `${JSON.stringify(config, null, 2)}\n`); diff --git a/apps/site/vercel.json b/apps/site/vercel.json index 556a306..f6d8a75 100644 --- a/apps/site/vercel.json +++ b/apps/site/vercel.json @@ -1,7 +1,25 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "framework": "vite", "installCommand": "npm ci", "buildCommand": "npm run build", - "outputDirectory": "dist" + "outputDirectory": "dist", + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Strict-Transport-Security", + "value": "max-age=63072000; includeSubDomains; preload" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "strict-origin-when-cross-origin" + } + ] + } + ] } diff --git a/apps/site/vite.config.js b/apps/site/vite.config.js index 8c0852c..8b4c546 100644 --- a/apps/site/vite.config.js +++ b/apps/site/vite.config.js @@ -1,8 +1,36 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +function customNotFoundPage() { + const html = readFileSync(resolve("public/404.html"), "utf8"); + const send = (_req, res) => { + if (res.writableEnded) { + return; + } + res.statusCode = 404; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + res.end(html); + }; + return { + name: "sentra-site-404", + configureServer(server) { + return () => { + server.middlewares.use(send); + }; + }, + configurePreviewServer(server) { + return () => { + server.middlewares.use(send); + }; + }, + }; +} + export default defineConfig({ - plugins: [react()], + appType: "mpa", + plugins: [react(), customNotFoundPage()], server: { // PORT lets the editor preview assign a free port; 5173 otherwise. port: Number(process.env.PORT) || 5173, From 879116444922200a04fef3deb5c8b14ebce073fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 00:11:56 +0000 Subject: [PATCH 3/4] fix(site): keep Vite MPA 404 without intercepting the homepage The custom preview middleware served 404.html for /, which hid the landing page. Co-authored-by: Gaffer --- apps/site/vite.config.js | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/apps/site/vite.config.js b/apps/site/vite.config.js index 8b4c546..30c6267 100644 --- a/apps/site/vite.config.js +++ b/apps/site/vite.config.js @@ -1,36 +1,11 @@ -import { readFileSync } from "node:fs"; -import { resolve } from "node:path"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; -function customNotFoundPage() { - const html = readFileSync(resolve("public/404.html"), "utf8"); - const send = (_req, res) => { - if (res.writableEnded) { - return; - } - res.statusCode = 404; - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(html); - }; - return { - name: "sentra-site-404", - configureServer(server) { - return () => { - server.middlewares.use(send); - }; - }, - configurePreviewServer(server) { - return () => { - server.middlewares.use(send); - }; - }, - }; -} - export default defineConfig({ + // Marketing site is multi-page (landing + static HTML). Do not SPA-fallback + // unknown paths to the homepage; Vercel serves public/404.html in production. appType: "mpa", - plugins: [react(), customNotFoundPage()], + plugins: [react()], server: { // PORT lets the editor preview assign a free port; 5173 otherwise. port: Number(process.env.PORT) || 5173, From a6685867884d7b680644a2c5ff7241e93d2f2ad2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 00:18:20 +0000 Subject: [PATCH 4/4] fix(site): satisfy Biome format and drop arguments in analytics loader Co-authored-by: Gaffer --- apps/site/public/js/site-analytics.js | 6 +++--- apps/site/scripts/check-seo.mjs | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/site/public/js/site-analytics.js b/apps/site/public/js/site-analytics.js index e7c99d3..ce78b3a 100644 --- a/apps/site/public/js/site-analytics.js +++ b/apps/site/public/js/site-analytics.js @@ -46,9 +46,9 @@ const id = measurementId.trim(); loadScript(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`); window.dataLayer = window.dataLayer || []; - function gtag() { - window.dataLayer.push(arguments); - } + const gtag = (...args) => { + window.dataLayer.push(args); + }; window.gtag = gtag; gtag("js", new Date()); gtag("config", id, { anonymize_ip: true }); diff --git a/apps/site/scripts/check-seo.mjs b/apps/site/scripts/check-seo.mjs index b144a54..3b9b545 100644 --- a/apps/site/scripts/check-seo.mjs +++ b/apps/site/scripts/check-seo.mjs @@ -110,7 +110,10 @@ for (const page of pages) { } assertContains("public/privasi/index.html", `rel="canonical" href="${siteOrigin}/privasi.html"`); -assertContains("public/ketentuan/index.html", `rel="canonical" href="${siteOrigin}/ketentuan.html"`); +assertContains( + "public/ketentuan/index.html", + `rel="canonical" href="${siteOrigin}/ketentuan.html"`, +); const sitemap = read("public/sitemap.xml"); for (const loc of [`${siteOrigin}/`, `${siteOrigin}/tentang.html`, `${siteOrigin}/daftar.html`]) { @@ -118,7 +121,11 @@ for (const loc of [`${siteOrigin}/`, `${siteOrigin}/tentang.html`, `${siteOrigin fail(`sitemap.xml missing ${loc}`); } } -if (sitemap.includes("privasi.html") || sitemap.includes("ketentuan.html") || sitemap.includes("404.html")) { +if ( + sitemap.includes("privasi.html") || + sitemap.includes("ketentuan.html") || + sitemap.includes("404.html") +) { fail("sitemap.xml should not list noindex pages"); }