diff --git a/README.md b/README.md index 1df2e23..7e7d51d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,11 @@ A lightweight all-in-one Docker deployment that combines reverse proxy managemen - Accessible only via domain through NPM proxy - 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 diff --git a/blog-starter/.forgejo/workflows/create-post.yml b/blog-starter/.forgejo/workflows/create-post.yml new file mode 100644 index 0000000..7de34dc --- /dev/null +++ b/blog-starter/.forgejo/workflows/create-post.yml @@ -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 diff --git a/blog-starter/.forgejo/workflows/deploy.yml b/blog-starter/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..d07b98d --- /dev/null +++ b/blog-starter/.forgejo/workflows/deploy.yml @@ -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/" diff --git a/blog-starter/.gitignore b/blog-starter/.gitignore new file mode 100644 index 0000000..2ebfeb4 --- /dev/null +++ b/blog-starter/.gitignore @@ -0,0 +1,4 @@ +/public/ +/resources/ +/themes/LoveIt/ +.hugo_build.lock diff --git a/blog-starter/README.md b/blog-starter/README.md new file mode 100644 index 0000000..31c0d73 --- /dev/null +++ b/blog-starter/README.md @@ -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/.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. diff --git a/blog-starter/archetypes/default.md b/blog-starter/archetypes/default.md new file mode 100644 index 0000000..29b8caa --- /dev/null +++ b/blog-starter/archetypes/default.md @@ -0,0 +1,10 @@ ++++ +title = "{{ replace .Name "-" " " | title }}" +date = {{ .Date }} +draft = true +slug = "{{ .Name }}" +tags = [] +categories = [] ++++ + +Write your post here. diff --git a/blog-starter/content/about/index.md b/blog-starter/content/about/index.md new file mode 100644 index 0000000..cdb2c77 --- /dev/null +++ b/blog-starter/content/about/index.md @@ -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. diff --git a/blog-starter/content/posts/_index.md b/blog-starter/content/posts/_index.md new file mode 100644 index 0000000..e423935 --- /dev/null +++ b/blog-starter/content/posts/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Posts" +date = 2026-03-19T12:00:00+07:00 +draft = false ++++ + +Latest posts from the blog. diff --git a/blog-starter/content/posts/hello-world.md b/blog-starter/content/posts/hello-world.md new file mode 100644 index 0000000..50fcaf0 --- /dev/null +++ b/blog-starter/content/posts/hello-world.md @@ -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 diff --git a/blog-starter/hugo.toml b/blog-starter/hugo.toml new file mode 100644 index 0000000..3a51a11 --- /dev/null +++ b/blog-starter/hugo.toml @@ -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 diff --git a/blog-starter/scripts/bootstrap-theme.sh b/blog-starter/scripts/bootstrap-theme.sh new file mode 100644 index 0000000..86900bf --- /dev/null +++ b/blog-starter/scripts/bootstrap-theme.sh @@ -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}" diff --git a/blog-starter/scripts/new-post.sh b/blog-starter/scripts/new-post.sh new file mode 100644 index 0000000..52ca15c --- /dev/null +++ b/blog-starter/scripts/new-post.sh @@ -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" diff --git a/blog-starter/static/.gitkeep b/blog-starter/static/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/blog-starter/static/.gitkeep @@ -0,0 +1 @@ +