hongming 731d42486b
ci / lint (push) Successful in 15s
ci / brand-drift (push) Successful in 12s
ci / build (push) Successful in 30s
Lead with governed agent organizations in the hero (#31)
2026-08-02 07:23:58 +00:00

enteros-landing

SEO-first marketing site for Enter OS — the org-native operating system for AI agent teams. Next.js (App Router, TypeScript), statically prerendered with output: "standalone" for our container deploy (mirrors the JRS/Minori sites).

  • Canonical host: https://enteros.ai
  • Routes: / (home) and /pricing (Platform page, the design's legacy pricing route; /platform 308-redirects there)

Design lineage

The DOM, layout, animations, and interactions are a 1:1 port of the internal design at github.com/EnterOS-AI/Landingpage-Redesign (static "Steep-inspired" HTML/CSS/JS), componentized into React without changing the rendered markup:

  • src/app/globals.css — the design's styles.css, verbatim except brand tokens and /assets/... URL roots.
  • public/script.js — the design's interaction script (scroll-hero timeline, split-title, Rive demo tabs, reveals), verbatim except absolute asset paths and the split-title word list (fixed to animate the EnterOS headline). Loaded per page by src/components/ScriptLoader.tsx; in-site navigation uses plain <a> full page loads so the script re-runs exactly like the static site.
  • src/components/* — one component per section, markup identical to the source.

Brand SSOT

Visual brand comes from @enteros/brand — the Enter OS visual SSOT, in molecule-ai/molecule-ai-sdk under brand/. It owns the logo, the icon and the webfonts (Chillax / Inter Tight / JetBrains Mono, declared in src/app/fonts.css, woff2 under public/assets/fonts/, logo + icon under public/assets/brand/). Token colors are applied in globals.css (midnight #010120, sky/blush/peach/mint washes).

The assets are committed, not pulled at build time: @enteros/brand is not published to our internal npm registry (only @enteros/contracts, @enteros/skill-* and @molecule-ai/* are), and this site's output: "standalone" Docker build has to work offline from the tree. scripts/sync-brand.mjs is what puts them here, and what stops them drifting:

ENTEROS_BRAND_DIR=<path-to>/molecule-ai-sdk/brand npm run brand:sync    # copy from the SSOT
ENTEROS_BRAND_DIR=<path-to>/molecule-ai-sdk/brand npm run brand:check   # fail if anything differs

brand:check runs in CI (the brand-drift context), so a re-vendored or hand-edited brand asset is a red build. The script derives its manifest — it parses the @font-face rules out of both the SSOT's css/fonts.css and this app's src/app/fonts.css and reconciles them by (family, weight), and takes the logo/icon filenames from brand.json. Nothing is typed out, so adding a weight to the SSOT is picked up automatically.

If the package is ever published, sync-brand.mjs already prefers node_modules/@enteros/brand, so moving to a pinned registry dependency is a one-line change.

One declared divergence: Chillax ships here in the 400/500/600/700 cut from the "EnterOS Free Font Pack" (see public/assets/fonts/README.md), while the SSOT ships 300/400/500/600 — differently-subset cuts of the same family, so no weight is byte-identical. Reconciling them changes rendered type, so it is tracked in #22 and recorded in DECLARED_DIVERGENCE in scripts/sync-brand.mjs. That list is the only escape hatch in the drift gate; everything else is hard-gated.

SEO

Metadata API with metadataBase https://enteros.ai, per-page canonical + OpenGraph/Twitter cards, Organization + WebSite JSON-LD (src/components/JsonLd.tsx), src/app/sitemap.ts, src/app/robots.ts.

Develop

npm ci
npm run dev        # http://localhost:3000
npm run build
npm test           # content-migration regression (needs a prior build)

Container

docker build -t enteros-landing .
docker run --rm -p 3000:3000 enteros-landing

Blog content policy (scaling contract)

  • Post URLs are flat and permanent: /blog/<slug> — never section-scoped. Sections and tags are index layers on top; changing post URLs later means mass redirects, so we do not.
  • Frontmatter is schema-gated (content-collections.ts): section (IA category) and optional updatedDate exist from day one so the corpus never needs a backfill migration.
  • /blog/tag/<tag> (+ /page/N) are the crawl/index layers; section routes (/insights, …) go live when their sections have content.
  • Deferred by design until scale demands: ISR (config change), search (Pagefind on static output).

Contact form

/contact posts to a server action that delivers to team@enteros.ai over Google Workspace SMTP. The domain already routes MX to Workspace and publishes Google's DKIM key, so the notification stays inside the tenant and needs no third-party sender DNS.

Variable Required Purpose
SMTP_USER yes Workspace account that authenticates and sends, e.g. noreply@enteros.ai.
SMTP_PASSWORD yes 16-character App Password for that account (not its login password). Requires 2-Step Verification on the account, and that the admin has not disabled App Passwords.
CONTACT_FROM_EMAIL no Full From header override. Only works if the address is a configured "Send mail as" alias of SMTP_USER — Gmail rewrites From to the authenticated account otherwise.
SMTP_HOST / SMTP_PORT no Default smtp.gmail.com / 587 (STARTTLS).

Generate the App Password at https://myaccount.google.com/apppasswords while signed in as SMTP_USER. Gmail caps SMTP sending per user per day (2,000 at time of writing — check current Google limits); a contact form is nowhere near it. Unset credentials fail closed: the form tells visitors to email the inbox directly rather than silently dropping the message.

Nothing is needed at build time — the transport is created on first use, so the image still builds with no secrets present. The recipient lives in CONTACT.inbox (src/lib/site.ts), not in an env var, because it is public information already printed on the page.

Spam handling is a hidden honeypot field plus zod validation. There is no rate limit: the single-container deploy has no shared store to hold counters, so add one at the proxy if the inbox starts taking abuse.

Mail DNS

enteros.ai publishes all four records a receiver looks for. Mail sent as SMTP_USER through Gmail SMTP passes SPF (Google's IPs, aligned domain) and DKIM (Google signing the domain), so it passes DMARC on both counts.

Host Type Value
@ MX 1 smtp.google.com
google._domainkey TXT v=DKIM1; k=rsa; p=… (Workspace-managed)
@ TXT v=spf1 include:_spf.google.com ~all
_dmarc TXT v=DMARC1; p=none; rua=mailto:dmarc@enteros.ai; fo=1

Two follow-ups on DMARC:

  • rua=mailto:dmarc@enteros.ai only produces something if that address receives mail. Create it as a user or a group, or reports go nowhere.
  • p=none asks for reports and changes nothing about delivery. Once the aggregate reports show legitimate mail passing, tighten to p=quarantine and then p=reject. That is the step that actually stops someone spoofing the domain.

Publish exactly one SPF record — a second invalidates both. The apex also carries a google-site-verification TXT, which is unrelated and fine.

Rendering: ISR (self-hosted)

Every route is incrementally static (revalidate in src/lib/posts.ts, default 1h). The build seeds a bounded set of pages — newest PRERENDER_POSTS, first PRERENDER_LIST_PAGES listing pages, busiest PRERENDER_TAGS tags — and everything else is rendered on first request and then served from the ISR cache. Below those thresholds this is byte-identical to a full static build; above them it keeps build time and memory flat as the corpus grows.

  • Cache location: .next/cache inside the container. The Dockerfile creates it and declares a volume — mount one to keep the cache warm across restarts (optional; it repopulates on demand).
  • Replicas: each replica keeps its own cache, which is correct for the single-container self-host deploy. Running several replicas behind a load balancer wants a shared cacheHandler so they cannot serve different vintages of the same page.
  • On-demand invalidation: POST /api/revalidate with authorization: Bearer $REVALIDATE_SECRET and {"path":"/blog"}. The route returns 404 unless REVALIDATE_SECRET is set, so a default deploy exposes no mutation surface.
  • Note on content freshness: posts are compiled into the bundle at build time, so a revalidation re-renders identical HTML until the image is rebuilt. The win today is lazy generation and a flat build; the revalidate window starts mattering when content or data comes from outside the bundle.
S
Description
SEO-first Next.js marketing site for Enter OS (enteros.ai).
Readme 5.1 MiB
Languages
MDX 41.4%
TypeScript 26.9%
CSS 23.4%
JavaScript 8%
Dockerfile 0.3%