Production website for TechNext Pte. Ltd. — Odoo Partner, Singapore. Static HTML/CSS/JS generated by a small Python build. No framework, no build step on the host.
Live: https://technext.asia · Hosting: Vercel project technext-website (auto-deploys master)
# 1. edit a page partial (or assets/css/site.css, assets/js/*.js, _src/sitedata.py)
# 2. rebuild everything: pages, sitemap.xml, Nexi (nexi-app.html + assets/js/nexi-app.js) AND vercel.json
python _src/build.py
# 3. preview locally, exactly as Vercel serves it (clean URLs + the real security headers)
python _src/serve.py 3960 # http://127.0.0.1:3960
# 4. static audit: links, SEO, headings
python _src/audit.py
# 5. commit the partial AND the generated files, open a PR, merge -> Vercel deploysNever edit the generated .html files at the repo root or in solutions/ industries/ odoo/
directly — the next build overwrites them. Edit the partial in _src/pages/.
| What | Where |
|---|---|
Pages (one partial per page, <!--meta {...}--> header) |
_src/pages/ |
| Layout, header, mega menus, footer, Google tags | _src/build.py |
| Navigation, company facts, icons, tracking IDs | _src/sitedata.py |
| Redirects + security headers (generated) | _src/make_vercel.py → vercel.json |
| Design tokens and all styles | assets/css/site.css |
| Menus, forms, reveals, image fallbacks | assets/js/site.js |
| Cookie consent + first-party visitor context | assets/js/consent.js |
| Nexi, the AI companion: the full chatbot on /nexi (knowledge base, 3D robot, tours) | _src/nexi/app.html → _src/make_nexi.py → nexi-app.html + assets/js/nexi-app.js |
| Nexi roaming the home hero (desktop only) | assets/js/nexi.js (loader) + assets/js/nexi-bot.js |
| Odoo module pages content | python _src/fetch_odoo.py → _src/apps_content.json |
Page meta: {"title", "desc", "out", "nav", "head"?, "body"?, "scripts"?} — scripts is a
list of files. {{ROOT}} is the relative path to the site root; {{icon:name}} inlines an
icon from sitedata.ICONS (an unknown name fails the build on purpose).
- The CSP allows inline scripts by hash, not
'unsafe-inline'.vercel.jsonis regenerated on every build, so changing an inline snippet is fine — as long as you rebuild before committing. Do not add inlineon*=handlers anywhere: the CSP blocks them. Use listeners in a.jsfile. - Google tags must stay in the homepage
<head>in their standard form. Search Console verifies site ownership through the GA4 (G-E7P849N9JY) and Tag Manager snippets. Removing or rewriting them can cost you ownership of the Search Console property. - New third-party resources need a CSP entry in
_src/make_vercel.py, or the browser blocks them. Test withpython _src/serve.py— it applies the real headers. - Clean URLs. Links are extensionless (
/solutions/ai), canonicals too.build.pystrips.htmlfrom internal links; Vercel'scleanUrlsresolves them. - Retired URLs (the previous site's blog, careers, gallery,
/ai-article/…,/contact,/lp/, …) redirect to the closest v2 page viamake_vercel.py. Never delete a redirect: those URLs are indexed and linked. CNAMEis a leftover from GitHub Pages hosting and is kept deliberately; it is not deployed.- App page URLs use Odoo's app names (
/odoo/apps/inventory, not the modulestock): the map isAPP_SLUGin_src/sitedata.py. The build rewrites every internal link,make_vercel.py308-redirects the old module-name URLs, andassets/js/app-slugs.js(generated) gives the scripts the same map. A new app whose Odoo name differs from its module needs an entry there. - The intro plays on the home page only (
INTRO_HEAD/INTRO_BODYin_src/build.py), so visitors who land on any other page from search see it straight away.
Every page is the same shell (LAYOUT in _src/build.py): the header with the mega menus, <main> with the
page partial, the footer, then the fixed side tabs (Set up meeting · Let's Talk · Ask Nexi) and the Let's Talk
panel. One <h1> per page; decorative layers (canvases, SVG scenes, the 3D robot) are aria-hidden.
The home hero is layered, bottom to top:
| z | Layer | What it is |
|---|---|---|
| 0 | .hero-bg |
grid, three drifting auroras, the pointer spotlight, the particles canvas (hero.js) |
| 1 | .hero-track |
the three slides, which share one grid cell so the hero never changes height; the active one tilts toward the cursor |
| 3 | .nexi |
Nexi, the roaming companion (desktop only): a small WebGL window that travels with the robot |
| 4 | .hero-marquee |
the app marquee |
| 5 | .hero-ctl |
arrows and progress pills |
Slide scenes (orbit, process map, launch path) are drawn by assets/js/hero-scenes.js; the carousel, camera
moves, tilt and particles by assets/js/hero.js. Inner pages use a data-stage-hero hero with its own
illustration (assets/css/stage.css, assets/js/stage.js) and interactive demos (assets/js/demo-*.js,
all driven by demo-kit.js).
Nexi exists twice. On the home hero it only roams and introduces itself (nexi.js is a 2.8 KB loader that
fetches Three.js and nexi-bot.js on desktop only; phones download nothing else). The full chatbot is /nexi:
a normal page that frames nexi-app.html (noindex; opened directly it forwards to /nexi), built from
_src/nexi/app.html by _src/make_nexi.py with the site's page index written in, so Nexi knows every page.
Its Let's Talk button asks the page around it to open the form (postMessage, same origin only).
- Animate
transformandopacityonly. Afilterorbackdrop-filterin the same animation keeps it off the compositor: the camera moves keep their blur in separate keyframes on the parent.slide. - No
backdrop-filterover moving content (it re-blurs every frame); use a slightly more opaque background. - No big blur filters on moving blobs: the auroras bake their blur into a radial gradient.
- Everything stops off screen: the hero gets
.is-off(all its animations pause, particles and autoplay stop), demos get.is-offscreen, scenes and rAF loops check an IntersectionObserver. - Frame loops never read layout: paths are sampled into point tables once (no
getPointAtLengthper frame), pointer handlers cache the element's box and work once per frame, the spotlight moves bytransform. - No lazy loading (owner's decision): every image loads with the page. Images below the fold carry
fetchpriority="low"so they never delay the first screen. - Nexi renders into a canvas three times its own size that follows it, compiles its shaders one per frame before its first frame, parks only where it covers no text or button, and fades and lets clicks through while flying across text.
Forms → sales@technext.asia
The Let's Talk panel (Nexi on /nexi opens it too) and the quotation builder post to FormSubmit's AJAX
endpoint for sales@technext.asia. An accepted submission fires the Google Ads lead conversion
and a GA4 generate_lead event (only if the visitor accepted cookies — Consent Mode).
- Brand blue
#3167CA. Partner wording is Odoo Ready Partner, never "Certified". - Approved figures only: 10+ countries, 11+ enterprise clients, 4 core AI disciplines. No founding year, no project counts, no ISO claims.
- Case studies are placeholders (
noindex) until each client approves publication.