跳到主要内容
新品云 ERP 正式上线,几分钟开通专属实例

技术分享

How we rebuilt the Openstone website on Next.js 16 and Odoo 20

Rebuilding a website is not a re-skin. Our old site froze in 2023 (Odoo 16); its blog and documentation had already moved to Hongzhai, leaving behind stale pages, dead entry points and a server that would need patching forever. The rebuild had three goals: content back where a parent-company site should keep it, a backend that does one thing reliably, and an answer for every legacy URL.

Why not retrofit the old site

The facts first: 27 page records (13 unique URLs), 13 blog posts, plus template leftovers from a shop, forum and courses that were never really run. The blog and docs already live in newer form on Hongzhai. Retrofitting would mean maintaining an Odoo instance purely for legacy pages — costlier than a rewrite, and backwards strategically: product content belongs to Hongzhai; the parent site goes back to being the company and services site.

Three trade-offs anchor everything:

  1. Static content lives in the repository. News and legal pages are MDX files (content/blog/*.mdx) reviewed and shipped with code, not rows in a database. Edits go through pull requests and build-time validation — no "someone broke production in an editor" class of problem.
  2. The backend keeps exactly one job: deliver the contact form into CRM reliably — allow-lists, rate limiting, human verification, idempotency included (detailed below).
  3. Documentation is not forked. The product manual is maintained once, on Hongzhai. Publishing the same content on two domains is self-competition that dilutes both.

A disposition for every legacy URL

The real risk in a migration is not building the new site badly — it is 404-ing a batch of old URLs and throwing away years of incoming links and search weight. We catalogued all 64 legacy URLs and sorted them into three dispositions:

Group Count Disposition Target
Home, about, contact, legal 9 On-site 301 Matching new pages
ERP product terms (pricing, saas-erp) 3 Cross-domain 301 Hongzhai product page (with UTM)
Blog posts 13 Cross-domain 301, one by one Hongzhai blog, same slugs
Docs entry 1+ Cross-domain 301 Hongzhai manual
Template leftovers (shop/forum/courses) 38 410 No successor; retired deliberately

The 13 blog posts were verified individually: exported from the old site via a read-only API, then matched against the Hongzhai versions — twelve slugs matched outright, one had been retitled during migration and got its own mapping. All rules live in one edge-nginx map file, versioned with the frontend repository; the old site stays up, and for two weeks after cut-over we keep adding still-requested paths from the access log into the map.

Frontend: zero backend dependency at build time

The new frontend is a bilingual Next.js 16 App Router site. This generation differs from most training material in ways worth flagging: params and searchParams are promises, middleware is now proxy.ts. We pinned the exact minor version and worked against the installed version's own docs rather than memory.

The critical piece is the build-time address contract. "No page is rendered by the backend" is a hard constraint:

  • News and other static content is read from the repository at build time — no runtime API;
  • Only the inquiry endpoint needs the backend, proxied through Next rewrites — and rewrites are baked into the route manifest at build time, so the backend address must be the fixed container DNS, never anything host-specific;
  • Acceptance therefore includes: with the backend completely unreachable, docker build must still succeed — proof that the build truly has no backend dependency.

Backend: a small, complete endpoint

The backend shrank to one custom module whose only write endpoint is the inquiry form. The design checklist (expanded in a companion article): an allow-list that rejects any extra field; 20 requests per IP per hour; Cloudflare Turnstile; an idempotency key plus content hash (retries get the same reference, changed content gets a 409); concurrent duplicates settled by a database unique constraint; notifications sent asynchronously after commit, never affecting the user's success. Reference numbers come from a sequence in a readable format (OSTN/year/month/number) that sales can quote directly.

One easily missed rule: privacy suppression — a lead landing in CRM must not trigger third-party data enrichment. Privacy promises belong in code behaviour, not only on a policy page.

How we accepted it

"Build passes" is not a standard. Ours was a tested matrix:

  • Type-check, unit tests and production build all clean;
  • Build succeeds with the backend down; the production image boots standalone with CMS reads and proxied form submission each exercised;
  • Inquiry end-to-end: a real browser submission lands in CRM with correct name/phone/topic/source page; same-key retries return the same reference; request 21 gets a 429;
  • Full bilingual × light/dark × 320px walkthrough on real pages;
  • A content gate that blocks release outright in strict mode if anything is unconfirmed.

Closing

The real cost of rebuilding a website is not pages — it is deciding where everything lives, who owns it, and how it retires. Get those three right, and the technology choices are the easy part.

相关产品与 ERP 实践文章在宏斋博客。 宏斋云ERP · Blog

返回列表