Write Markdown Locally, Deploy on git push: My Bilingual Blog Publishing Workflow
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:
- Generate a bilingual post skeleton locally
- Edit the Chinese and English Markdown files
- Preview the site locally
git push origin main- 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:
- editing Markdown
- 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:
- push to
main - run
npm ci - run
npm run build - archive
dist - upload through SSH
- extract to a new
releases/<timestamp>-<sha> - switch
current - 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
currentwas 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:
- did the workflow actually start
- are the deploy secrets present
- does the SSH user and key work
- 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.