Business websites tend to have familiar requirements: introduce the company, present products and customer stories, publish news, and provide contact information. Yet finishing the pages is only part of delivering the website.
Who updates the content? Can an unfinished edit appear on the public site? Does replacing a cover image require a developer? Can someone recover from an interrupted installation? What must they back up before moving to another server?
These are the questions behind HHY CMS. I am building a product that packages the recurring work of content management, publication, and website delivery, so other people can install it in their own environment and maintain a business website themselves.
This article follows the current 0.1.2 source code. It explains the boundaries I chose and why seemingly small constraints deserve attention in the first release.
Delivering a website means handing over the ability to update it, an understandable set of operating boundaries, and a way to recover when something goes wrong.
Define the smallest complete product
The first release includes a public business website and its administration interface: home, about, products, cases, news, and contact pages, supported by categories, media, site settings, and homepage sections.
That scope shapes the data model. Products, cases, news, and pages share fields for titles, slugs, summaries, bodies, categories, covers, and SEO metadata, with kind distinguishing the content types. Brand information, contact details, navigation, and footer content belong to site settings. Homepage sections have their own ordering, visibility, and copy.
I have kept arbitrary content modeling, freeform layout editing, multiple tenants, and a plugin marketplace outside this release. Every additional degree of freedom needs validation, compatibility rules, and maintenance. A focused product helps reveal which differences between websites deserve to become reusable capabilities.
Reuse therefore has a concrete meaning: companies can change their brand, content, and homepage organization while sharing the same implementation of saving, publishing, authentication, and media management.
Give HHY the full server-side responsibility
HHY CMS runs on HHY Web and MySQL. HHY implements installation, routing, permissions, database access, and server-rendered pages. CSS and JavaScript improve the browser experience. Deploying the CMS itself requires neither Node.js nor a frontend build step.
The current documented environment is HHY 1.5.0, the official database 1.0.0 extension, MySQL 8.x or a compatible version, OpenSSL 3, and the system file utility. An explicit dependency contract is more useful to someone receiving the software than a vague promise of effortless setup.
The code follows business responsibilities:
| Module | Responsibility |
|---|---|
lib/application.hhy | Route registration, dispatch, and shared error responses |
lib/install.hhy | Installation validation, migration records, initialization, and recovery |
lib/auth.hhy | Login, logout, and login rate limiting |
lib/content.hhy | Editing, saving, publishing, unpublishing, and preview |
lib/manage.hhy | Site settings, homepage sections, categories, and media |
lib/site.hhy | Public page data and sitemap generation |
themes/default/theme.hhy | Default theme presentation |
This makes the system readable through the actions it supports. Publication changes lead to the content module; presentation changes lead to the theme; installation failures lead to the installation state. The same map helps the next developer find their way.
The application entry point registers static resources with HHY Web. This excerpt comes from the implementation:
let mut app = hhyweb.minimal()
|> hhyweb.static_files("/assets",path("public"))
|> hhyweb.static_files("/media",c.file("uploads"))
These mappings also describe a deployment boundary. Assets and uploaded images may be public. Private configuration, installation tokens, and temporary files must stay outside those public mappings.
Treat installation as a recoverable workflow
A personal project can often start with manually created tables and a configuration file. Software delivered to someone else needs an installation experience that stands on its own.
HHY CMS generates an installation token on first startup. The wizard collects database, site, and administrator information. The deployer prepares the database in advance, and a fresh installation requires it to be empty. The submitted database endpoint must also appear in the environment's CMS_DB_ALLOW setting.
The form collects configuration; the deployer decides which database endpoints the application may contact.
Installation must also survive partial progress. The implementation writes install-pending.json before migrations and initialization. The cms_migrations table records completed steps, while a database lock prevents concurrent execution of the migration phase. A retry uses the original pending installation details. The final configuration is written after initialization succeeds.
I distinguish resuming the same installation from overwriting an existing site. An installation identity in the database checks ownership, and the installation endpoint closes once the site is installed. Recovery should not quietly become reinstallation.
These mechanisms support the current installation workflow. They are not a complete upgrade system: schema evolution, compatibility with older releases, and upgrade rollback still need their own design.
Separate drafts from the public version in the data
Few behaviors undermine an editor's confidence faster than an unfinished change going live when they click Save.
HHY CMS keeps editable fields and a public_json snapshot in the same content record. Saving a draft updates the editable state. Publishing replaces the snapshot. Public pages read the snapshot, while authenticated preview reads the current editable content.
| Action | Editable content | Public website |
|---|---|---|
| Create and save a draft | Store the new content | Keep it private |
| Edit published content and save a draft | Store the changes | Keep the previous publication |
| Publish | Save content and replace its snapshot | Show the new public version |
| Unpublish | Keep the editable content | Remove the public version |
This is the publication update in lib/content.hhy:
if f.intent == "publish" {
let snapshot = put(item,"revision",c.str(revision+1))
c.exec(tx,"UPDATE cms_content SET public_json=?,public_category_id=NULLIF(?,0),public_cover_id=NULLIF(?,0),published_at=UTC_TIMESTAMP() WHERE id=?",[encode_json(snapshot),item.category_id,item.cover_id,saved_id])
}
The separate public_category_id and public_cover_id matter too. Keeping the old body while allowing category filtering or cover references to follow a new draft would leave the publication boundary incomplete.
This snapshot applies to content records. Site settings and homepage sections have separate save paths, so this is not a site-wide draft system. It also does not provide a complete history of revisions with rollback.
One administrator can still create concurrent edits
The first release supports one administrator. That person can still open two editing tabs and accidentally overwrite a newer change from an older form.
Saving reads the record with SELECT ... FOR UPDATE inside a transaction, then compares the submitted revision with the stored revision. A mismatch produces an explicit conflict message instead of silently overwriting content.
if current != null and (to_int(current.revision) != revision or current.kind != item.kind) {
throw("内容已被其他操作修改,请重新打开编辑页")
}
The message asks the editor to reopen the page because another operation changed the content. The code excerpt retains the application's original Chinese validation text.
Field validation, category and media existence checks, content updates, publication snapshots, and audit records are organized around the transaction. Failures enter the rollback path; successful saves redirect to the editing page.
Published content also restricts slug changes. This protects existing links within the current feature set. Supporting public URL changes properly would require additional behavior, such as redirects, rather than simply unlocking an input.
Keep publication rules outside the theme
The default theme presents company information as a readable website. site.hhy assembles public data for presentation. The theme does not authenticate administrators or decide when a draft becomes public.
That separation matters because visual identity and information organization vary between companies. Reusing the CMS becomes difficult if every new appearance requires a new permission or publication implementation.
The homepage uses predefined sections with ordering, visibility, and copy controls. Editors get understandable ways to adapt the site while layout combinations remain manageable. There is currently one default theme. The boundary leaves room for future extension, but it is not yet a mature theme ecosystem.
Build editing around everyday actions
Version 0.1.2 adds a searchable, paginated media picker and a visual body editor. The purpose is continuity: an editor should be able to finish a content update without unnecessary detours.
Cover images and logos can be selected or uploaded from the current editing page, with an immediate preview. Selection updates the form; saving makes the association effective. Choosing an image should preserve the text in progress and should not require remembering a media ID.
Media also has a lifecycle beyond the page that first uploaded it. Uploads are limited to PNG and JPEG files of at most 2 MiB, with the system file utility checking the actual type. Deletion checks references from drafts, public versions, and the site logo, protecting images that are still in use.
The body editor uses a local copy of Quill 2.0.3. Its Delta representation is serialized into restricted HTML for headings, bold, italic, lists, and quotations. The server independently applies an allowlist of tags without attributes.
The toolbar therefore does not define the security boundary. Requests that bypass the browser editor still encounter server-side output rules. If the editor fails to load, an HTML textarea remains available, preserving a basic editing path.
The implementation also avoids rewriting untouched legacy content merely because its editing page was opened. Displaying stored content and changing it should be distinct operations.
Delivery quality lives in failure paths
Authentication, MySQL-backed sessions, CSRF checks, parameterized SQL, field limits, login rate limiting, and keeping low-level database errors out of public responses are part of the current implementation. They matter only when applied along actual request paths.
The repository's validation cases cover HTML escaping, rich-text event attributes, invalid slugs, CSRF mismatches, and oversized fields. Editor serialization has separate tests. Navigation newline regression cases capture another practical lesson: something as small as browser form CRLF line endings can become a user's unexplained failure to save.
These checks do not replace complete delivery acceptance. The scenarios I care about include interrupted installation, stale editing forms, draft changes leaking onto the website, and attempts to delete images still referenced by published content. They are directly connected to whether someone can trust the system with their work.
Documentation must explain operating boundaries too. The server listens locally by default; public deployment needs a reverse proxy and HTTPS. Login rate limiting uses the directly connected IP, so shared proxy addresses need consideration. A recoverable backup includes the database, private configuration, and uploaded files. Copying source code alone cannot restore an operating website.
Make each capability understandable and maintainable
HHY CMS remains a deliberately scoped early release: one administrator and one default theme, without multi-tenancy, commerce, multilingual content management, or a plugin marketplace. Publishing this article in two languages does not imply that the CMS already supports multilingual content.
As development continues, I want to prioritize upgrade and recovery paths, validation through more delivery scenarios, and stable boundaries between content and presentation. Before adding a role or a content model, I need to understand how it changes permissions, publication, migrations, and compatibility.
This project also connects HHY Language, the Web Runtime, and Database in a concrete application. A running server and a committed transaction eventually have to support an ordinary person editing a paragraph, saving a draft, and confirming publication.
The HHY CMS I want to deliver is a website product that other people can understand, use, and take over. My work as its developer is to handle the complexity behind those everyday actions.