What problem does this post solve

Many personal blogs do not stall because the site is hard to build. They stall because publishing feels heavy. Every new post means touching config, rebuilding by hand, uploading files manually, and repeating deployment steps over and over.

I wanted a lighter workflow: write Markdown locally, push to GitHub, let GitHub Actions build and publish the site automatically. That turns publishing into a repeatable habit instead of a small ops task every time.

When this setup fits

  • You run a personal blog, technical publication, or lightweight docs-style site
  • You want your content stored as Markdown files in Git
  • You do not want WordPress or a heavy CMS backend
  • You want bilingual content without maintaining manual translation maps

If you need a visual editorial backend, multi-author approval flow, or online editing first, this is probably not the best model.

Final result

Once the workflow is set up, publishing a new post looks like this:

  1. Generate a bilingual post skeleton locally
  2. Edit the Chinese and English Markdown files
  3. Preview the site locally
  4. git push origin main
  5. Let GitHub Actions deploy the update

That separation matters. Writing stays focused on content, while deployment becomes automation.

Environment checklist

The current stack behind this blog is:

  • Framework: Astro
  • Content layer: astro:content
  • Content format: Markdown
  • Deployment model: static artifact release
  • Web server: Nginx
  • CI/CD: GitHub Actions

Posts live in a bilingual directory structure:

src/content/posts/zh-cn/*.md
src/content/posts/en-us/*.md

Each language gets its own file, but both files share the same path, so the site generates:

  • Chinese: /posts/<path>/
  • English: /en/posts/<path>/

Step-by-step instructions

1. Lock down the content model early

If the content model keeps changing, every new article becomes a code task instead of a writing task.

In this project, posts, pages, categories, and resources all live in astro:content. The most important rule is:

  • posts use path
  • pages and categories use key

That means adding a new article should only require adding Markdown files, not touching route mapping logic.

2. Add a post scaffold command

Copying old files by hand works for a while, but it always gets messy.

I added a simple script so a new article can be generated like this:

npm run new:post -- "markdown-blog-publish-workflow" "本地写 Markdown,git push 后自动上线:我的双语博客发布流程" "Write Markdown Locally, Deploy on git push: My Bilingual Blog Publishing Workflow" writing

That creates two draft files immediately:

src/content/posts/zh-cn/markdown-blog-publish-workflow.md
src/content/posts/en-us/markdown-blog-publish-workflow.md

They are created with:

draft: true

That default matters, because it prevents half-written content from being published automatically.

3. Keep local work focused on writing and previewing

While writing, I only care about two things:

  1. editing Markdown
  2. previewing with npm run dev

The local checks are straightforward:

  • does the post appear on the homepage
  • do both language versions render correctly
  • do categories and tags aggregate correctly
  • do the title and description work for SEO

If this stage is solid, there is no good reason to keep doing manual SSH deployment.

4. Hand deployment over to GitHub Actions

This is the part that turns the workflow into something sustainable.

The current production workflow is:

  1. push to main
  2. run npm ci
  3. run npm run build
  4. archive dist
  5. upload through SSH
  6. extract to a new releases/<timestamp>-<sha>
  7. switch current
  8. run a public availability check

At that point, publishing a post is no longer a manual release task. It is standard CI/CD.

5. Let the server host only static files

One principle I care about is simple: the server should serve, not build.

That gives several immediate advantages:

  • fewer moving parts on production
  • a more consistent build environment
  • easier rollback
  • clearer debugging when something fails

The live directory layout looks like this:

/var/www/aitokenhacks.com/releases/<release-id>
/var/www/aitokenhacks.com/current

Each release is isolated, and publishing is basically just switching a symlink.

6. Add diagnostics to the deployment workflow

Automation is not finished when the YAML file exists. It becomes useful only when failures are easy to understand.

So I added deployment diagnostics for things like:

  • which deploy secret is missing
  • whether SSH access works
  • whether remote directories exist
  • which release current was switched to

That way, a failed deployment does not end with a vague red badge. It tells you whether the problem is GitHub Secrets, SSH auth, directory permissions, or public verification.

Common issues

Why not just use an online backend

Because the main goal of this site is content accumulation and SEO, not editor UX.

Markdown files managed in Git give me several things I value more:

  • full version history
  • rollback
  • reviewable changes
  • content that lives beside code
  • no dependency on a third-party admin panel

Why not keep both languages in one file

Because a single file with duplicated frontmatter and duplicated body content becomes harder to maintain very quickly.

Using two files with the same path turned out to be much cleaner:

  • it fits Astro content collections naturally
  • body content stays easy to edit
  • no extra translation mapping layer is needed

What fails most often in automated deployment

In practice, I ran into two recurring classes of problems:

  • GitHub Actions billing or quota issues, where the job never really starts
  • missing or incorrect deploy secrets, where SSH setup fails immediately

So my normal debugging order is:

  1. did the workflow actually start
  2. are the deploy secrets present
  3. does the SSH user and key work
  4. does the remote directory allow publishing

Closing thought

If you are building a personal technical blog, I strongly recommend separating writing from release mechanics.

While writing, focus on the quality of the post. For publishing, let CI/CD handle the repetitive steps.

Once your workflow becomes “write Markdown locally, then git push,” the blog finally becomes something you can keep publishing to consistently.