I would like to share my journey on building a self-sustainable content management system that does not require a content database in a traditional sense.
The Problem
The content (blog posts and bookmarks) of this website is stored in a Notion database:
The problem that I was trying to solve was to not have to deploy the website manually after each bookmark that I add there. And on top of that – keep the hosting as cheap as possible, because for me it does not really matter how fast the bookmarks that I add to my Notion database end up online.
So, after some research I came up with the following setup:
The system consists of several components:
- The “Push to Main” action that deploys the changes
- The “Update Content” action that downloads content from Notion API and commits the changes
- The “Update Content on Schedule” action runs once in a while and triggers the “Update Content” action
Let us look into each one of them from the inside out in detail.
The “Push to Main” Workflow
There is not a lot to say here, pretty standard setup, – when there is a push to the main
branch, this workflow builds the app and deploys it to Cloudflare Pages using the Wrangler CLI:
name: Push to Main
on:
push:
branches: [main]
workflow_dispatch: {}
jobs:
deploy-cloudflare-pages:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- name: Install node modules
run: |
pnpm --version
pnpm install --frozen-lockfile
- name: Build the App
run: |
pnpm build
- name: Publish Cloudflare Pages
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
pnpm wrangler pages deploy ./out --project-name ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
The “Update Content” Workflow
This Workflow can only be triggered “manually”… but also automatically because you can trigger it using a GitHub Personal Access Token, a.k.a. PAT. I initially wrote it because I wanted to deploy changes from my phone. It downloads the posts and bookmarks using the Notion API and then – if there are any change to the codebase – creates a commit and pushes it. In order to function properly, this workflow must be provided with a PAT that has “Read and Write access to code” of the repository:
name: Update Content
on:
workflow_dispatch: {}
jobs:
download-content:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# A Github Personal Access Token with access to the repository
# that has the follwing permissions:
# ✅ Read and Write access to code
token: ${{ secrets.GITHUB_PAT_CONTENT }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- name: Install node modules
run: |
pnpm --version
pnpm install --frozen-lockfile
- name: Download articles content from Notion
env:
NOTION_KEY: "${{ secrets.NOTION_KEY }}"
NOTION_ARTICLES_DATABASE_ID: "${{ secrets.NOTION_ARTICLES_DATABASE_ID }}"
run: |
pnpm download-articles
- name: Download bookmarks content from Notion
env:
NOTION_KEY: ${{ secrets.NOTION_KEY }}
NOTION_BOOKMARKS_DATABASE_ID: ${{ secrets.NOTION_BOOKMARKS_DATABASE_ID }}
run: |
pnpm download-bookmarks
- name: Configure Git
run: |
git config --global user.email "${{ secrets.GIT_USER_EMAIL }}"
git config --global user.name "${{ secrets.GIT_USER_NAME }}"
- name: Check if anything changed
id: check-changes
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "There are changes"
echo "HAS_CHANGED=true" >> $GITHUB_OUTPUT
else
echo "There are no changes"
echo "HAS_CHANGED=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes
if: steps.check-changes.outputs.HAS_CHANGED == 'true'
run: |
git add ./src/content
git add ./public
git commit -m "Automatic content update commit"
git push
The “Update Content on Schedule” Workflow
This one is pretty simple: it just runs every once in a while and triggers the workflow above. In order to function properly, this workflow must be provided with a GitHub PAT that has “Read and Write access to actions” of the repository. In my case it’s a different PAT:
name: Update Content on Schedule
on:
schedule:
- cron: "13 0,12 * * *"
workflow_dispatch: {}
jobs:
trigger-update-content:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dispatch the Update Content workflow
env:
# A Github Personal Access Token with access to the repository
# that has the follwing permissions:
# ✅ Read and Write access to actions
GH_TOKEN: ${{ secrets.GITHUB_PAT_ACTIONS }}
run: |
gh workflow run "Update Content" --ref main
Conclusion
For me this setup has proven to be really good and flexible. Because of the modular structure, the “Update Content” action can be triggered manually – e.g. from my phone while travelling. To me this was another valuable experience of progressive enhancement of a workflow.
Hope you find this helpful 😉
Top comments (0)