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..17ebfee --- /dev/null +++ b/apps/site/DOMAIN.md @@ -0,0 +1,36 @@ +# 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. + +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**. +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..6d3c6cb 100644 --- a/apps/site/README.md +++ b/apps/site/README.md @@ -18,10 +18,40 @@ 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. +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 0000000..a2a658b Binary files /dev/null and b/apps/site/public/apple-touch-icon.png differ 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 0000000..10e5bbf Binary files /dev/null and b/apps/site/public/favicon.ico differ diff --git a/apps/site/public/favicon.svg b/apps/site/public/favicon.svg new file mode 100644 index 0000000..6a3fe3c --- /dev/null +++ b/apps/site/public/favicon.svg @@ -0,0 +1 @@ + \ 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..ce78b3a --- /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 || []; + const gtag = (...args) => { + window.dataLayer.push(args); + }; + 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..3b9b545 --- /dev/null +++ b/apps/site/scripts/check-seo.mjs @@ -0,0 +1,155 @@ +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/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 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..30c6267 100644 --- a/apps/site/vite.config.js +++ b/apps/site/vite.config.js @@ -2,6 +2,9 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; 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()], server: { // PORT lets the editor preview assign a free port; 5173 otherwise.