Add Hugo blog starter and Forgejo post workflows
This commit is contained in:
parent
fd7398be9f
commit
62593d8316
13 changed files with 387 additions and 0 deletions
|
|
@ -24,6 +24,11 @@ A lightweight all-in-one Docker deployment that combines reverse proxy managemen
|
||||||
- Accessible only via domain through NPM proxy
|
- Accessible only via domain through NPM proxy
|
||||||
- CI/CD via Forgejo Runner for automated Docker builds
|
- CI/CD via Forgejo Runner for automated Docker builds
|
||||||
|
|
||||||
|
### Blog Starter
|
||||||
|
- `blog-starter/` contains a ready-to-use Hugo + LoveIt starter
|
||||||
|
- includes a Forgejo Actions workflow that deploys generated files to `/opt/blog/public`
|
||||||
|
- intended to be used as the base of a separate blog repository
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
|
||||||
116
blog-starter/.forgejo/workflows/create-post.yml
Normal file
116
blog-starter/.forgejo/workflows/create-post.yml
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
name: Create Blog Post
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
title:
|
||||||
|
description: "Post title"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
slug:
|
||||||
|
description: "URL slug, for example my-new-post"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
summary:
|
||||||
|
description: "Short summary"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
description: "Comma-separated tags"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
categories:
|
||||||
|
description: "Comma-separated categories"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
draft:
|
||||||
|
description: "Create as draft"
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-post:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Generate post file
|
||||||
|
env:
|
||||||
|
INPUT_TITLE: ${{ inputs.title }}
|
||||||
|
INPUT_SLUG: ${{ inputs.slug }}
|
||||||
|
INPUT_SUMMARY: ${{ inputs.summary }}
|
||||||
|
INPUT_TAGS: ${{ inputs.tags }}
|
||||||
|
INPUT_CATEGORIES: ${{ inputs.categories }}
|
||||||
|
INPUT_DRAFT: ${{ inputs.draft }}
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def split_csv(value: str):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
return [item.strip() for item in value.split(",") if item.strip()]
|
||||||
|
|
||||||
|
title = os.environ["INPUT_TITLE"].strip()
|
||||||
|
slug = os.environ["INPUT_SLUG"].strip().lower()
|
||||||
|
summary = os.environ.get("INPUT_SUMMARY", "").strip()
|
||||||
|
draft = os.environ.get("INPUT_DRAFT", "true").strip().lower() == "true"
|
||||||
|
tags = split_csv(os.environ.get("INPUT_TAGS", ""))
|
||||||
|
categories = split_csv(os.environ.get("INPUT_CATEGORIES", ""))
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
raise SystemExit("Title is required")
|
||||||
|
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug):
|
||||||
|
raise SystemExit("Slug must use lowercase letters, numbers, and hyphens only")
|
||||||
|
|
||||||
|
content_dir = Path("content/posts")
|
||||||
|
content_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = content_dir / f"{slug}.md"
|
||||||
|
if target.exists():
|
||||||
|
raise SystemExit(f"Post already exists: {target}")
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).astimezone().replace(microsecond=0).isoformat()
|
||||||
|
|
||||||
|
def format_array(values):
|
||||||
|
if not values:
|
||||||
|
return "[]"
|
||||||
|
return "[{}]".format(", ".join(f'\"{value}\"' for value in values))
|
||||||
|
|
||||||
|
escaped_title = title.replace('"', '\\"')
|
||||||
|
escaped_summary = summary.replace('"', '\\"')
|
||||||
|
|
||||||
|
body = f"""++++
|
||||||
|
title = \"{escaped_title}\"
|
||||||
|
date = {now}
|
||||||
|
draft = {\"true\" if draft else \"false\"}
|
||||||
|
slug = \"{slug}\"
|
||||||
|
summary = \"{escaped_summary}\"
|
||||||
|
tags = {format_array(tags)}
|
||||||
|
categories = {format_array(categories)}
|
||||||
|
++++
|
||||||
|
|
||||||
|
Write your post here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
target.write_text(body, encoding="utf-8")
|
||||||
|
print(target)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Commit new post
|
||||||
|
env:
|
||||||
|
POST_SLUG: ${{ inputs.slug }}
|
||||||
|
run: |
|
||||||
|
git config user.name "forgejo-actions"
|
||||||
|
git config user.email "forgejo-actions@localhost"
|
||||||
|
git add "content/posts/${POST_SLUG}.md"
|
||||||
|
git commit -m "Create post: ${POST_SLUG}"
|
||||||
|
git push
|
||||||
68
blog-starter/.forgejo/workflows/deploy.yml
Normal file
68
blog-starter/.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
name: Deploy Blog
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y curl git rsync openssh-client tar
|
||||||
|
|
||||||
|
- name: Install Hugo Extended
|
||||||
|
env:
|
||||||
|
HUGO_VERSION: "0.145.0"
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o hugo.deb \
|
||||||
|
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
|
||||||
|
sudo dpkg -i hugo.deb
|
||||||
|
hugo version
|
||||||
|
|
||||||
|
- name: Bootstrap LoveIt theme
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/bootstrap-theme.sh
|
||||||
|
./scripts/bootstrap-theme.sh
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: |
|
||||||
|
hugo --gc --minify
|
||||||
|
test -f public/index.html
|
||||||
|
|
||||||
|
- name: Configure SSH
|
||||||
|
env:
|
||||||
|
BLOG_DEPLOY_KEY: ${{ secrets.BLOG_DEPLOY_KEY }}
|
||||||
|
BLOG_DEPLOY_KNOWN_HOSTS: ${{ secrets.BLOG_DEPLOY_KNOWN_HOSTS }}
|
||||||
|
run: |
|
||||||
|
install -d -m 700 ~/.ssh
|
||||||
|
printf '%s\n' "$BLOG_DEPLOY_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
if [ -n "${BLOG_DEPLOY_KNOWN_HOSTS}" ]; then
|
||||||
|
printf '%s\n' "$BLOG_DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||||
|
chmod 644 ~/.ssh/known_hosts
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
env:
|
||||||
|
BLOG_DEPLOY_HOST: ${{ secrets.BLOG_DEPLOY_HOST }}
|
||||||
|
BLOG_DEPLOY_PORT: ${{ secrets.BLOG_DEPLOY_PORT }}
|
||||||
|
BLOG_DEPLOY_USER: ${{ secrets.BLOG_DEPLOY_USER }}
|
||||||
|
BLOG_DEPLOY_PATH: ${{ secrets.BLOG_DEPLOY_PATH }}
|
||||||
|
run: |
|
||||||
|
test -n "$BLOG_DEPLOY_HOST"
|
||||||
|
test -n "$BLOG_DEPLOY_PORT"
|
||||||
|
test -n "$BLOG_DEPLOY_USER"
|
||||||
|
test -n "$BLOG_DEPLOY_PATH"
|
||||||
|
ssh -p "$BLOG_DEPLOY_PORT" "$BLOG_DEPLOY_USER@$BLOG_DEPLOY_HOST" "mkdir -p '$BLOG_DEPLOY_PATH/public'"
|
||||||
|
rsync -az --delete \
|
||||||
|
-e "ssh -p $BLOG_DEPLOY_PORT" \
|
||||||
|
public/ "$BLOG_DEPLOY_USER@$BLOG_DEPLOY_HOST:$BLOG_DEPLOY_PATH/public/"
|
||||||
4
blog-starter/.gitignore
vendored
Normal file
4
blog-starter/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/public/
|
||||||
|
/resources/
|
||||||
|
/themes/LoveIt/
|
||||||
|
.hugo_build.lock
|
||||||
85
blog-starter/README.md
Normal file
85
blog-starter/README.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Blog Starter
|
||||||
|
|
||||||
|
This folder is a standalone starter for a Hugo blog that uses the LoveIt theme
|
||||||
|
and deploys to the static blog host installed by `install.sh`.
|
||||||
|
|
||||||
|
## What This Starter Includes
|
||||||
|
|
||||||
|
- Hugo site structure
|
||||||
|
- LoveIt bootstrap script
|
||||||
|
- sample content
|
||||||
|
- a helper script to create new posts
|
||||||
|
- a Forgejo Actions workflow that builds and deploys to `/opt/blog/public`
|
||||||
|
|
||||||
|
## Recommended Setup
|
||||||
|
|
||||||
|
Create a separate Git repository for your blog, then copy this starter into that
|
||||||
|
repository root.
|
||||||
|
|
||||||
|
## Local Writing Workflow
|
||||||
|
|
||||||
|
1. Bootstrap the theme:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/bootstrap-theme.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a new post:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/new-post.sh my-first-post
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start the local preview server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hugo server
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Edit the generated file under `content/posts/`.
|
||||||
|
5. When ready to publish, set `draft = false`, commit, and push.
|
||||||
|
|
||||||
|
## Forgejo Web Workflow
|
||||||
|
|
||||||
|
If you want to create posts directly from the Forgejo web UI, use the manual
|
||||||
|
workflow in `.forgejo/workflows/create-post.yml`.
|
||||||
|
|
||||||
|
From the Actions page:
|
||||||
|
|
||||||
|
1. Run `Create Blog Post`
|
||||||
|
2. Fill in:
|
||||||
|
- `title`
|
||||||
|
- `slug`
|
||||||
|
- optional `summary`
|
||||||
|
- optional comma-separated `tags`
|
||||||
|
- optional comma-separated `categories`
|
||||||
|
- `draft`
|
||||||
|
3. The workflow creates `content/posts/<slug>.md`
|
||||||
|
4. The normal deploy workflow publishes the post on the next push
|
||||||
|
|
||||||
|
This keeps you out of front matter for most day-to-day writing.
|
||||||
|
|
||||||
|
## Forgejo Secrets
|
||||||
|
|
||||||
|
The workflow expects these repository secrets:
|
||||||
|
|
||||||
|
- `BLOG_DEPLOY_HOST`: server hostname or IP
|
||||||
|
- `BLOG_DEPLOY_PORT`: SSH port, usually `22`
|
||||||
|
- `BLOG_DEPLOY_USER`: deploy user on the server
|
||||||
|
- `BLOG_DEPLOY_KEY`: private SSH key for the deploy user
|
||||||
|
- `BLOG_DEPLOY_PATH`: target directory, usually `/opt/blog`
|
||||||
|
- `BLOG_DEPLOY_KNOWN_HOSTS`: optional `known_hosts` entry for stricter SSH
|
||||||
|
|
||||||
|
## Expected Server State
|
||||||
|
|
||||||
|
- Blog host installed from this repo's `install.sh`
|
||||||
|
- `d3v-blog` container running
|
||||||
|
- the deploy user can write to `/opt/blog/public`
|
||||||
|
- Nginx Proxy Manager forwards `blog.yourdomain.com` to `d3v-blog:80`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This starter fetches LoveIt into `themes/LoveIt` using Git.
|
||||||
|
- The workflow installs Hugo Extended before building.
|
||||||
|
- Deployment uses `rsync --delete` to keep `/opt/blog/public` in sync with the
|
||||||
|
latest generated `public/` output.
|
||||||
10
blog-starter/archetypes/default.md
Normal file
10
blog-starter/archetypes/default.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
+++
|
||||||
|
title = "{{ replace .Name "-" " " | title }}"
|
||||||
|
date = {{ .Date }}
|
||||||
|
draft = true
|
||||||
|
slug = "{{ .Name }}"
|
||||||
|
tags = []
|
||||||
|
categories = []
|
||||||
|
+++
|
||||||
|
|
||||||
|
Write your post here.
|
||||||
9
blog-starter/content/about/index.md
Normal file
9
blog-starter/content/about/index.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
+++
|
||||||
|
title = "About"
|
||||||
|
date = 2026-03-19T12:00:00+07:00
|
||||||
|
draft = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
This is the About page for your Hugo blog.
|
||||||
|
|
||||||
|
Update this page with your own bio, project information, or contact details.
|
||||||
7
blog-starter/content/posts/_index.md
Normal file
7
blog-starter/content/posts/_index.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
title = "Posts"
|
||||||
|
date = 2026-03-19T12:00:00+07:00
|
||||||
|
draft = false
|
||||||
|
+++
|
||||||
|
|
||||||
|
Latest posts from the blog.
|
||||||
17
blog-starter/content/posts/hello-world.md
Normal file
17
blog-starter/content/posts/hello-world.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
+++
|
||||||
|
title = "Hello World"
|
||||||
|
date = 2026-03-19T12:00:00+07:00
|
||||||
|
draft = false
|
||||||
|
slug = "hello-world"
|
||||||
|
tags = ["welcome", "hugo"]
|
||||||
|
categories = ["general"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
This is the first post published from the Hugo + LoveIt starter.
|
||||||
|
|
||||||
|
When you are ready to write your own content:
|
||||||
|
|
||||||
|
1. Create a new post with `./scripts/new-post.sh your-post-slug`
|
||||||
|
2. Edit the generated Markdown file
|
||||||
|
3. Preview with `hugo server`
|
||||||
|
4. Commit and push to trigger deployment
|
||||||
35
blog-starter/hugo.toml
Normal file
35
blog-starter/hugo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
baseURL = "https://blog.example.com/"
|
||||||
|
languageCode = "en-us"
|
||||||
|
title = "D3V Blog"
|
||||||
|
theme = "LoveIt"
|
||||||
|
defaultContentLanguage = "en"
|
||||||
|
enableRobotsTXT = true
|
||||||
|
|
||||||
|
[permalinks]
|
||||||
|
posts = "/posts/:slug/"
|
||||||
|
|
||||||
|
[params]
|
||||||
|
defaultTheme = "auto"
|
||||||
|
dateFormat = "2006-01-02"
|
||||||
|
description = "Notes, updates, and guides."
|
||||||
|
|
||||||
|
[params.author]
|
||||||
|
name = "D3V Team"
|
||||||
|
|
||||||
|
[markup]
|
||||||
|
[markup.goldmark]
|
||||||
|
[markup.goldmark.renderer]
|
||||||
|
unsafe = true
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
[[menu.main]]
|
||||||
|
identifier = "posts"
|
||||||
|
name = "Posts"
|
||||||
|
url = "/posts/"
|
||||||
|
weight = 1
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
identifier = "about"
|
||||||
|
name = "About"
|
||||||
|
url = "/about/"
|
||||||
|
weight = 2
|
||||||
19
blog-starter/scripts/bootstrap-theme.sh
Normal file
19
blog-starter/scripts/bootstrap-theme.sh
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
THEME_DIR="themes/LoveIt"
|
||||||
|
THEME_REPO="https://github.com/dillonzq/LoveIt.git"
|
||||||
|
THEME_REF="${LOVEIT_REF:-master}"
|
||||||
|
|
||||||
|
mkdir -p themes
|
||||||
|
|
||||||
|
if [ -d "${THEME_DIR}/.git" ]; then
|
||||||
|
echo "Updating LoveIt theme..."
|
||||||
|
git -C "${THEME_DIR}" fetch --depth 1 origin "${THEME_REF}"
|
||||||
|
git -C "${THEME_DIR}" checkout -f FETCH_HEAD
|
||||||
|
else
|
||||||
|
echo "Cloning LoveIt theme..."
|
||||||
|
git clone --depth 1 --branch "${THEME_REF}" "${THEME_REPO}" "${THEME_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "LoveIt theme is ready in ${THEME_DIR}"
|
||||||
11
blog-starter/scripts/new-post.sh
Normal file
11
blog-starter/scripts/new-post.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "${1:-}" = "" ]; then
|
||||||
|
echo "Usage: $0 my-post-slug" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
slug="$1"
|
||||||
|
hugo new "posts/${slug}.md"
|
||||||
|
echo "Created content/posts/${slug}.md"
|
||||||
1
blog-starter/static/.gitkeep
Normal file
1
blog-starter/static/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Loading…
Reference in a new issue