Drafts, Scheduled Publishing, Multilingual — an Operator-Friendly Flow

Scheduled publishing that removes midnight releases, and AI translation into four languages

scheduled publishingdraftsmultilingualAI translationcache invalidation
6 min read

Introduction

Being able to write an article isn't enough to make day-to-day operation work. Requests like "I want to save this half-finished," "I want it live the moment the campaign starts," and "we need an English version too" always show up.

This article covers the operational details around publishing. It's unglamorous territory, but if this part is awkward to use, the operators simply stop updating the content.

Managing articles with three states

Draft, published, unpublished

Articles have three states: draft, published, and unpublished.

We keep draft and unpublished separate because they mean completely different things. A draft is "not out yet"; unpublished is "it was out, and we pulled it back." Being able to tell them apart in the dashboard list makes the situation much easier to read at a glance.

Scheduled publishing without adding a state

One option would have been a fourth state called "scheduled." Instead, we expressed it as a combination: published state plus a future publish date.

Publish date in the future means scheduled; in the past means live. That's all it takes. Adding a state multiplies the possible transitions and the complexity along with them, so if an existing field can express the idea, that's the safer route.

In the dashboard list, articles that are in the published state with a future date get a "Scheduled" badge, so it's obvious to the operator what's going on.

Dates and times are handled in Japan time

The date-and-time input uses the browser's built-in control, which by default follows the clock settings on the user's computer. Being nine hours off because someone is working from abroad is not acceptable.

So any entered date and time is always interpreted as Japan time, and converted back to Japan time when displayed. When the operator types "8:00 PM," they mean 8:00 PM in Japan. Obvious, perhaps — but it falls apart if you don't specify it explicitly.

Making four languages workable

Editing four languages on one screen

Articles can be held in four languages: Japanese, English, Traditional Chinese, and Korean. The editing screen splits them across tabs, but there's only one save button.

Switching tabs preserves what you've typed, and saving registers all four languages together. Per-language save buttons make it far too easy to have the "I forgot to save the English one" accident.

Only the Japanese title is required. Conversely, if a language's title, summary, and body are all empty, that language's version of the article is deleted. So when you decide "actually, let's not publish the English version," clearing the fields is all it takes.

AI translation only runs when you press the button

The heaviest burden in going multilingual is translation. So we put a "Generate AI translation" button in the dashboard that produces the other three languages from the Japanese article.

The AI translation flow
Write the article in Japanese

Get the Japanese version finished first

Press the translate button

It never runs automatically on save; the operator asks for it explicitly

Generate three languages at once

English, Traditional Chinese, and Korean are generated in parallel. If one fails, the others still complete

Review, then save

The generated text is editable. If something reads wrong, fix it before publishing

There are two reasons we don't translate automatically on every save. One is cost: if translation ran on every save, API charges would pile up fast. The other is that hand-corrected translations would get overwritten by the next save.

Translating without breaking the formatting

The crucial thing during translation is not to break the article's HTML structure. Headings should stay headings, and shop cards should come through with their identifiers intact.

So the instructions we give the AI explicitly require that the HTML structure be preserved, and the generated result goes through exactly the same sanitization as any other article before it's saved. The policy is simple: being AI output is not a reason to skip inspection.

For the titles and descriptions that appear in search results, we use a separate instruction asking for phrasing that reads naturally in the target language rather than a literal translation.

Untranslated languages fall back to Japanese

If someone arrives in English at an article that doesn't have an English version yet, we show the Japanese version rather than saying the article doesn't exist. Serving something readable felt kinder than serving an empty page.

Publishing shows up immediately

Caching and instant updates, together

To keep the site fast, page content is cached for a set period. But from the perspective of an operator who just hit publish, "I published it and it's not on the site" is unnerving.

So when an article is saved or published, we explicitly invalidate the cache for the related pages. The listing page, the article page, the homepage, the sitemap — all of them update immediately.

Pages stay up even if the database goes down

If the database becomes unreachable, does the blog listing page just show an error? That won't do.

We built in a fallback: if articles can't be fetched from the database, prepared sample articles are shown instead, along with a note saying "this is a sample view." It's a far better state to be in than the whole site going down.

Wrapping up

Three things mattered most in designing the publishing flow.

  1. Don't add states — scheduled publishing is expressed as "published state plus a future date," avoiding extra complexity
  2. AI translation runs explicitly — never automatically, which keeps cost down and protects hand-edited text
  3. Two tracks: instant and time-based — manual updates invalidate the cache, scheduled posts go live with the clock