Skip to content

Building a Bilingual Technical Blog with Codex, from Specifications to the First Article

At first, all I wanted was to publish the first article on my VitePress site.

The site was already running through GitHub and Cloudflare Pages, and the first setup guide was mostly written. In theory, I only needed to move it into the published area and add a basic entry point and navigation.

Then the scope slowly grew into this:

  • Bilingual home pages
  • Chinese and English article directories
  • About pages
  • Article categories
  • Tag archives
  • Local search
  • Rules for drafts and published articles
  • Image asset directories
  • Language switching and preference memory
  • Cloudflare Pages deployment settings

This article records how I worked with Codex to put all of that together. It also covers a problem I did not expect: sometimes AI does not do too little—it does too much, and the finished article no longer sounds like you.


Let Codex understand the current state first

I did not ask Codex to start changing the site immediately. I first asked it to read:

  • The existing VitePress project
  • draft/建站教學.md
  • The VitePress configuration
  • package.json
  • The Git and deployment state
  • The original site plan

This looks ordinary, but it matters a lot.

If AI does not know what the project currently looks like, it can easily drop a generic set of “VitePress blog best practices” on top. Those practices may not be wrong, but they may not match the actual project. The result is often a complete-looking answer that still needs to be rebuilt in several places.

Reading first made a few facts clear:

  • The project itself sits at the GitHub repository root.
  • The correct Cloudflare Pages output is .vitepress/dist.
  • The original article used docs/.vitepress/dist, which did not match the project.
  • The home page and navigation still contained VitePress examples.
  • The first article only existed in draft/.
  • The old .gitignore did not exclude the cache and dist paths actually used by this project.

These were all things we only found by looking at the real project first.


Decide how much version one really needs

I already had a larger plan for a bilingual technical blog, but if I followed every part of it, the first article might never be published.

So the goal became: build the smallest version that can properly publish the first article.

Version one includes:

  • Traditional Chinese and English home pages
  • Traditional Chinese and English article lists
  • Traditional Chinese and English About pages
  • Switching between both versions of the same article
  • Article categories
  • Tag archive pages
  • Local search
  • Basic brand colors and typography

Things left for later:

  • Comments
  • RSS
  • Full SEO work
  • Social sharing images
  • Complicated animation
  • A complete world-building-inspired visual interface
  • Databases and back-end APIs

This split is more practical for me. Get the articles online, make them readable, and make deployment work. The rest can wait.


Basic site specifications

Keep the brand and author separate

The site name is:

text
INN666

The author name is:

text
Hazime

These are different identities. INN666 is the site and brand; Hazime is the author.

Public content follows one rule as well: the site does not actively publish a real name, private email address, résumé link, or unreleased material. A technical blog can have some personality without putting everything under direct sunlight at once.

Bilingual URLs

The site uses matching language paths:

text
/zh/
/en/

Article URLs use:

text
/zh/articles/{category}/{slug}
/en/articles/{category}/{slug}

For example, the first article is available at:

text
/zh/articles/engineering/build-blog-with-vitepress
/en/articles/engineering/build-blog-with-vitepress

Both versions use the same slug and translationKey. This lets the language switch open the matching article instead of throwing the reader back to the other home page.

Detecting language at the root URL

When someone opens /, the site chooses a language in this order:

  1. Check whether the reader selected a language before.
  2. Otherwise, check the browser language.
  3. If it still cannot decide, use Traditional Chinese.

After a manual switch, the choice is saved in the browser so the reader does not need to select it again next time.


Use directories as article categories

Categories do not live in frontmatter. They come directly from directories.

The current categories are:

text
engineering
ai-workflows
games

The published structure is:

text
zh/
└─ articles/
   ├─ engineering/
   ├─ ai-workflows/
   └─ games/

en/
└─ articles/
   ├─ engineering/
   ├─ ai-workflows/
   └─ games/

The category also appears in the URL.

This is pleasantly straightforward. Where a file lives is its category. There is no chance of putting a file in engineering while frontmatter quietly says games, creating the kind of double configuration that makes your future self want to flip a table.

The article loader validates categories too. An unsupported directory makes the build fail instead of silently creating a new category from a typo.


A draft is not a published article

One important rule came out of this work:

Publishing an article does not move the original draft. It copies a finished version into the published article directory.

This file:

text
draft/建站教學.md

stays where it is. Publishing creates:

text
zh/articles/engineering/build-blog-with-vitepress.md
en/articles/engineering/build-blog-with-vitepress.md

This has several benefits:

  • Editing and translation do not erase the original.
  • I can compare how an article changed.
  • If the published version drifts too far, the original voice is still there.
  • Drafts can stay flexible without choosing a category and language immediately.

VitePress also excludes all of draft/** through srcExclude. This does more than hide drafts from the article list: VitePress does not build them into pages at all.

Without that exclusion, a draft could still be public if someone guessed a URL such as /draft/建站教學, even if no navigation linked to it. “Not linked” and “not public” are not the same thing.


Published article frontmatter

Published articles currently use:

yaml
title: Article title
description: Article summary
author: Hazime
date: 2026-06-20
updated: 2026-06-20
tags:
  - Codex
  - AI Workflow
lang: en-US
translationKey: article-slug
draft: false

The important parts are:

  • author is always Hazime.
  • Both language versions share a translationKey.
  • tags can be added freely.
  • The category comes from the directory, not frontmatter.
  • Articles with draft: true do not appear in article lists or Tag pages.

Published articles usually use draft: false, but keeping the field is still useful. It gives an article a waiting room after moving into the published directory but before appearing in the site index.


How article lists and Tags are generated

Article lists are not maintained by hand. VitePress createContentLoader scans:

text
zh/articles/*/*.md
en/articles/*/*.md

During loading it:

  • Excludes draft: true.
  • Validates category directories.
  • Checks titles, descriptions, and translationKey.
  • Normalizes authors, dates, tags, and category labels.
  • Sorts articles from newest to oldest.
  • Keeps Chinese and English in separate lists.

Tags come from each article's frontmatter.

For example:

yaml
tags:
  - VitePress
  - GitHub
  - Cloudflare

generates:

text
/en/tags/vitepress
/en/tags/github
/en/tags/cloudflare

Chinese gets its own Tag pages containing only Chinese articles.

Tag labels preserve their original capitalization, while URLs use lowercase kebab-case. Tags are deduplicated without regard to case, so VitePress and vitepress do not split into parallel universes.


Search articles only

Version one uses VitePress local search. There is no Algolia account or external search service.

The scope is deliberately limited to published articles:

  • Do not search home pages.
  • Do not search About pages.
  • Do not search article indexes.
  • Do not search Tag pages.
  • Do not search drafts.

Chinese pages search Chinese content; English pages search English content. Results do not mix languages.

Tags are included in the search index too. Searching for Codex, VitePress, or Cloudflare can find an article even when the useful keyword mainly lives in frontmatter.


Image assets follow categories too

Articles will eventually contain images, so the asset path was decided early:

text
public/assets/{category}/{article-slug}/

For example:

text
public/assets/ai-workflows/build-bilingual-blog-with-codex/

Both languages share ordinary images. Images containing text use language suffixes:

text
workflow.zh-TW.webp
workflow.en-US.webp

Markdown uses stable public paths:

markdown
![Workflow diagram](/assets/ai-workflows/build-bilingual-blog-with-codex/workflow.en-US.webp)

Draft images stay in:

text
draft/assets/

Approved assets are copied into public/assets/ only when publishing. Files in public/ are directly public, even if an article does not reference them.


Deployment settings

The project sits at the GitHub repository root, so Cloudflare Pages uses:

text
Root directory: /
Build command: pnpm docs:build
Build output directory: .vitepress/dist

Common local commands are:

bash
pnpm docs:dev
pnpm docs:build
pnpm docs:preview

pnpm docs:dev runs continuously. When it is stopped with Ctrl+C, pnpm may print:

text
[ELIFECYCLE] Command failed with exit code 1

If that only appears after pressing Ctrl+C, while the site started normally, it usually means pnpm treated the manual interruption as a non-normal exit. It does not mean the project is broken.


The actual workflow with Codex

The collaboration happened in a few stages.

1. Read first; do not rush into edits

I first asked Codex to read the draft, project structure, configuration, and original plan, then summarize the current state.

At this stage it only inspected the project. It did not modify files.

2. Make the choices explicit

Next, we decided:

  • Whether version one should be bilingual immediately
  • Which entry points the home page needed
  • How article URLs should be named
  • How the root URL should choose a language
  • Whether article lists should be manual or automatic
  • Whether Tags should only be displayed or open archive pages
  • Whether search should cross languages
  • Whether Chinese and English needed separate image copies

These are not purely technical questions. They change the way future work happens. Deciding them early avoids repeatedly tearing things down later.

3. Produce a complete plan before implementation

After the decisions were locked, we assembled an implementation plan covering:

  • Directory structure
  • URL structure
  • Frontmatter
  • Article indexes
  • Tags and search
  • Language switching
  • Image assets
  • Deployment settings
  • Acceptance checks

The value of the plan was not that it looked professional. Its value was that implementation did not need to keep guessing.

4. Actually run the build

Writing the files did not mean the work was finished.

The following problems were encountered by Codex while building and verifying its own implementation. They were not errors left behind by my original setup:

  • Missing "type": "module" for ESM: after Codex added the article data loader, the first pnpm docs:build treated VitePress as CommonJS and failed. Adding "type": "module" to package.json fixed it.
  • Calling createContentLoader at the wrong time for dynamic Tag routes: this was also a problem in Codex's first implementation. The route called the loader before VitePress configuration was active and produced content loader invoked without an active vitepress process. The final version reads article files and frontmatter directly while generating routes.
  • Assuming the wrong date shape: Codex assumed frontmatter dates would always remain YYYY-MM-DD strings. YAML could already parse them into date objects, causing Invalid time value during server rendering. The fix handles both strings and date objects.
  • Duplicate language switching: Codex first added a custom language button, then found during HTML inspection that VitePress had already generated one from the locale configuration. The build did not fail, but the interface had two identical controls. The custom button was removed, the built-in menu stayed, and language preference memory was added separately.

One existing project problem was also discovered during verification:

  • Cache and dist were already tracked by Git: the original .gitignore used docs/.vitepress/cache/ and docs/.vitepress/dist/, but this project lives at the repository root. Those rules never matched. Codex found the tracked build output while checking Git status, corrected the paths to .vitepress/cache/ and .vitepress/dist/, and removed the generated files from version control.

This is a good example of why “Codex finished writing” is not the same as “the work is complete.” Several of these bugs came from Codex's own first implementation. The code looked plausible; running pnpm docs:build made the problems real.

5. Check the public boundary

After the build succeeds, verify that:

  • draft/ is absent from the output.
  • Both language versions and Tag pages were generated.
  • Search indexes do not mix languages.
  • Navigation has no dead links.
  • Public content contains no accidental private information or unreleased material.

A site that runs is technically successful. That does not automatically mean it is ready to be public.


AI polishing an article too well can also be a problem

When the first article was finished, I ran into a subtle problem.

Codex organized it well. The sections were clear, the sentences flowed, and the technical explanation was more complete. It was so polished that I no longer knew whether I should accept it, because it did not really sound like me anymore.

The original included phrases with my own rhythm, such as:

  • “very discouraging for people with busy lives”
  • “start by writing a few articles”
  • “room to advance or retreat”
  • “learning it is still a win”
  • the casual Chinese nickname “little black window” for Command Prompt

Most of those casual, recognizable phrases disappeared during the first edit. The article became more formal, but it also started reading like standard technical documentation.

That led to a clearer set of editing rules:

  • Preserve the original context, examples, and wording.
  • A casual tone is fine.
  • Headings, paragraphs, and lists can be reorganized.
  • Technical errors and unclear steps can be corrected.
  • Do not replace the author's voice just to make the writing “better.”
  • Translate the finished Chinese faithfully; do not add arguments or conclusions in English.

This is why keeping the original draft matters.

Without it, it is difficult to answer the question, “Does this still sound like me?” An AI version can be a very useful editorial suggestion, but the published article should still belong to the author.

When Codex provides the English translation, the article ends with:

text
English translation provided by Codex.

This does not make AI the author of the article. It simply explains how the translation was produced.


The practical publishing steps from now on

Using this article as an example, the workflow is:

1. Write the Chinese draft in draft/

text
draft/用Codex協作建立雙語技術部落格.md

The draft can change freely. VitePress does not build it and the site does not expose it.

2. Organize it without losing the original tone

Check:

  • Are the sections clear?
  • Are commands and paths correct?
  • Are the lists easy to read?
  • Is anything unnecessarily repeated?
  • After editing, does it still sound like me?

3. Copy the finished Chinese article

This article belongs to ai-workflows, so the finished file goes to:

text
zh/articles/ai-workflows/build-bilingual-blog-with-codex.md

Do not move the original. Keep the version in draft/.

4. Create the English version

The English article goes to:

text
en/articles/ai-workflows/build-bilingual-blog-with-codex.md

Both versions must share:

  • The slug
  • translationKey
  • Publication date
  • Tag concepts
  • Article structure

Add the Codex translation statement at the end of the English version.

5. Add published images

If the article uses images, put them in:

text
public/assets/ai-workflows/build-bilingual-blog-with-codex/

Confirm that:

  • File names use lowercase and hyphens.
  • Shared images are not duplicated.
  • Images containing text have language suffixes.
  • Chinese and English Markdown use suitable alternative text.

6. Verify locally

bash
pnpm docs:build
pnpm docs:preview

Check that:

  • Both articles open.
  • Language switching opens the matching article.
  • The article appears in the correct language list.
  • Tags are clickable.
  • Search finds the title, body, and Tags.
  • Images work in light mode, dark mode, and on mobile.
  • Drafts were not built.

7. Commit and publish

bash
git add .
git commit -m "Publish Codex blog workflow article"
git push

Cloudflare Pages runs:

bash
pnpm docs:build

The new version goes live only after a successful build. If it fails, the previous successful deployment remains online.


Conclusion

This started as an attempt to publish one article and ended up defining the entire bilingual publishing workflow. The scope grew a little, but future articles no longer need to reinvent directories, Tags, search, translation, and deployment every time.

Codex helped with more than writing code. It turned scattered ideas into questions I could choose between, then turned those choices into specifications that could actually be verified.

Still, when AI produces something impressively complete, a human needs to tap the brakes. Tools can help organize the technical structure; the author still has to protect the voice of the article.

That may be the most interesting part of AI collaboration: the point is not to hand all the work away, but to understand what you actually want through the back-and-forth.


Appendix: Site specifications in Markdown

The current site specifications are extracted below without the collaboration story. Anyone building something similar can copy this Markdown and adjust it.

markdown
# INN666 Bilingual Technical Blog Specifications

## Project positioning

- Site name: `INN666`
- Author name: `Hazime`
- Site type: Traditional Chinese and English technical blog
- Stack: VitePress, Vue, Markdown, GitHub, Cloudflare Pages, Cloudflare DNS
- Content areas: Engineering, AI Workflows, Games
- Version one prioritizes reading and quick publishing over a complex back end or complete visual system

## Public content rules

- `INN666` is the site and brand name, not the author name.
- All articles use `Hazime` as the author.
- The site does not actively publish a real name, private email, résumé, or personal social accounts.
- Unapproved settings, character information, and other unreleased material stay private.
- Git metadata, page metadata, and public files must not accidentally expose private identity information.

## Site entry points and navigation

```text
/
├─ zh/
│  ├─ Home
│  ├─ Articles
│  └─ About
└─ en/
   ├─ Home
   ├─ Articles
   └─ About
```

- `/` first reads the saved browser language preference.
- Without a preference, choose from the browser language.
- Fall back to Traditional Chinese when the language cannot be determined.
- Save manual language changes in `localStorage`.
- Language switching should open the matching version of the current page.

## Public URLs

Home pages:

```text
/zh/
/en/
```

Article indexes:

```text
/zh/articles/
/en/articles/
```

Articles:

```text
/zh/articles/{category}/{slug}
/en/articles/{category}/{slug}
```

Tags:

```text
/zh/tags/{tag-slug}
/en/tags/{tag-slug}
```

About pages:

```text
/zh/about
/en/about
```

## Content directories

```text
draft/
├─ {article-draft}.md
└─ assets/

zh/
├─ index.md
├─ about.md
├─ articles/
│  ├─ index.md
│  ├─ engineering/
│  ├─ ai-workflows/
│  └─ games/
└─ tags/
   └─ [tag].md

en/
├─ index.md
├─ about.md
├─ articles/
│  ├─ index.md
│  ├─ engineering/
│  ├─ ai-workflows/
│  └─ games/
└─ tags/
   └─ [tag].md

public/
└─ assets/
   ├─ engineering/
   ├─ ai-workflows/
   └─ games/
```

## Category rules

| Directory | Display name |
| --- | --- |
| `engineering` | Engineering |
| `ai-workflows` | AI Workflows |
| `games` | Games |

- The article directory is the only category source.
- Do not duplicate `category` in frontmatter.
- Unsupported category directories must fail the production build.
- Categories appear directly in article URLs.

## Draft rules

- Drafts may live freely in `draft/` without choosing a language or category first.
- Publishing keeps the original draft and copies finished versions into published directories.
- Add `draft/**` to VitePress `srcExclude`.
- Article lists, Tags, and search never scan `draft/`.
- Draft assets stay in `draft/assets/` until copied into `public/assets/` for publication.

## Article frontmatter

English example:

```yaml
title: Article title
description: Article summary
author: Hazime
date: 2026-06-20
updated: 2026-06-20
tags:
  - Codex
  - AI Workflow
lang: en-US
translationKey: article-slug
draft: false
```

The Traditional Chinese version uses:

```yaml
lang: zh-TW
```

- `title`, `description`, `translationKey`, and valid dates are required.
- Both language versions use the same `translationKey` and slug.
- `author` defaults to `Hazime`.
- `tags` are freely extensible and deduplicated without regard to case.
- Articles with `draft: true` do not appear in lists or Tag pages.

## Article indexes

The content loader scans only:

```text
zh/articles/*/*.md
en/articles/*/*.md
```

Processing rules:

- Exclude `draft: true`.
- Derive language and category from the file path.
- Validate categories, required frontmatter, and dates.
- Build separate Chinese and English indexes.
- Sort by `date`, newest first.
- Display date, title, description, category, and Tags.

## Tags

- Tags come from article frontmatter.
- Labels preserve their original capitalization.
- URLs use lowercase kebab-case.
- Deduplicate Tags without regard to case.
- Tag pages list only matching, non-draft articles in the current language.
- Tags are clickable on article pages and article indexes.

## Search

- Use VitePress local search.
- Do not depend on Algolia or another external service.
- Chinese pages search only Chinese; English pages search only English.
- Index published articles only.
- Do not index home pages, About pages, article indexes, Tag pages, or drafts.
- Search titles, article text, and Tags.
- Provide localized Chinese and English search UI text.

## Images and assets

Published asset path:

```text
public/assets/{category}/{article-slug}/
```

Markdown references:

```markdown
![Alternative text](/assets/{category}/{article-slug}/{filename})
```

- Share ordinary images between both languages.
- Use `.zh-TW` and `.en-US` suffixes for images containing language-specific text.
- Use lowercase kebab-case file names.
- Provide suitable alternative text in each language.
- Put only approved public assets in `public/assets/`.

## Translation and editing

- Traditional Chinese is the source language.
- Editing preserves the author's context, examples, wording, and casual voice.
- Headings, paragraphs, lists, links, and step order may be reorganized.
- Technical errors may be corrected without replacing the author's voice or adding unrelated arguments.
- English faithfully translates the finished Chinese article without adding explanations, opinions, or conclusions.
- English translations produced by Codex end with:

```text
English translation provided by Codex.
```

## Local commands

Development:

```bash
pnpm docs:dev
```

Production build:

```bash
pnpm docs:build
```

Production preview:

```bash
pnpm docs:preview
```

## Cloudflare Pages

```text
Production branch: main
Root directory: /
Build command: pnpm docs:build
Build output directory: .vitepress/dist
```

- Git push triggers Cloudflare Pages automatically.
- Replace the production version only after a successful build.
- Keep the last successful deployment online when a build fails.

## `.gitignore`

```text
node_modules/
.vitepress/cache/
.vitepress/dist/
dist/
```

- Do not commit cache, dist, or `node_modules`.
- If `node_modules` is already tracked, run:

```bash
git rm -r --cached node_modules
```

## Publishing workflow

1. Write the Traditional Chinese source in `draft/`.
2. Organize the article, verify technical content, and preserve the author's voice.
3. Copy the finished Chinese article to `zh/articles/{category}/{slug}.md`.
4. Create the English version at `en/articles/{category}/{slug}.md`.
5. Copy approved images to `public/assets/{category}/{slug}/`.
6. Run `pnpm docs:build`.
7. Preview and verify both articles, Tags, search, images, and language switching.
8. Confirm drafts, private data, and unreleased material are absent from the build.
9. Commit and push; Cloudflare Pages publishes automatically.

## Acceptance criteria

- `/zh/` and `/en/` open successfully.
- Both home pages, article lists, About pages, and articles work.
- Language switching opens the corresponding version of the current page.
- Articles appear only in the correct language and category list.
- Tag pages contain only matching articles in the current language.
- Search returns only published articles in the current language.
- Search finds titles, body text, and Tags.
- `draft/` and draft assets are absent from build output.
- The site remains readable on mobile, desktop, light mode, and dark mode.
- Images and internal links work.
- Public content contains no private identity information or unreleased material.
- `pnpm docs:build` succeeds.

English translation provided by Codex.