From 3dda082ccdb517ef092c29bfaf5ac91503d8f8b0 Mon Sep 17 00:00:00 2001 From: Cory LaNou Date: Sun, 15 Mar 2026 22:16:31 -0500 Subject: [PATCH 1/5] feat(marketing): add blog-to-X distribution pipeline Add a lightweight workflow for generating X/Twitter post variants from blog posts with UTM tracking, hashtag generation, and thread templates. Closes #4 --- Makefile | 5 +- marketing/distribute.sh | 180 +++++++++++++++++++++++++++++ marketing/distribution-workflow.md | 90 +++++++++++++++ marketing/templates.md | 83 +++++++++++++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100755 marketing/distribute.sh create mode 100644 marketing/distribution-workflow.md create mode 100644 marketing/templates.md diff --git a/Makefile b/Makefile index 53f3c9f..e291b2b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build serve dev clean +.PHONY: build serve dev clean distribute build: hype blog build @@ -17,3 +17,6 @@ docker-build: docker-run: docker run -p 3000:3000 hypemd-dev + +distribute: + @./marketing/distribute.sh $(SLUG) diff --git a/marketing/distribute.sh b/marketing/distribute.sh new file mode 100755 index 0000000..4e8e92d --- /dev/null +++ b/marketing/distribute.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +set -euo pipefail + +SITE_URL="https://hypemd.dev" +HANDLE="@hype_markdown" +MAX_CHARS=280 + +usage() { + echo "Usage: $0 " + echo "" + echo "Generate X/Twitter post variants for a blog post." + echo "" + echo "Examples:" + echo " $0 getting-started" + echo " $0 ai-authoring-workflow" + exit 1 +} + +if [[ $# -lt 1 ]]; then + usage +fi + +SLUG="$1" +FILE="content/${SLUG}/module.md" + +if [[ ! -f "$FILE" ]]; then + echo "Error: $FILE not found" >&2 + exit 1 +fi + +title=$(grep -m1 '^# ' "$FILE" | sed 's/^# //') +details=$(awk '/
/{found=1; next} /<\/details>/{if(found) exit} found{print}' "$FILE") +slug=$(echo "$details" | grep '^slug:' | head -1 | sed 's/^slug: *//') +seo_desc=$(echo "$details" | grep '^seo_description:' | head -1 | sed 's/^seo_description: *//') +tags=$(echo "$details" | grep '^tags:' | head -1 | sed 's/^tags: *//') +author=$(echo "$details" | grep '^author:' | head -1 | sed 's/^author: *//') + +if [[ -z "$title" ]]; then + echo "Error: could not parse title from $FILE" >&2 + exit 1 +fi + +if [[ "$slug" == docs/* || "$slug" == "docs" ]]; then + echo "WARNING: This appears to be a documentation page (slug: ${slug}). Docs pages are typically not promoted on social media." >&2 + echo "" >&2 +fi + +is_tutorial=false +if echo "$tags" | grep -qi 'tutorial'; then + is_tutorial=true +fi + +build_url() { + local variant="$1" + echo "${SITE_URL}/${slug}/?utm_source=twitter&utm_medium=social&utm_content=${variant}" +} + +print_variant() { + local label="$1" + local text="$2" + local chars=${#text} + echo "=== ${label} ===" + echo "$text" + echo "" + echo "Characters: ${chars}" + if [[ $chars -gt $MAX_CHARS ]]; then + echo "WARNING: Exceeds ${MAX_CHARS} character limit by $((chars - MAX_CHARS)) characters" + fi + echo "" +} + +build_hashtags() { + local -a all=("#HypeMarkdown" "#Golang" "#OpenSource") + + IFS=',' read -ra tag_arr <<< "$tags" + for tag in "${tag_arr[@]}"; do + tag=$(echo "$tag" | sed 's/^ *//;s/ *$//') + case "$tag" in + tutorial|getting-started|hype) ;; + docker) all+=("#Docker") ;; + ai|claude) all+=("#AI") ;; + workflow) all+=("#DevWorkflow") ;; + authoring) all+=("#TechWriting") ;; + training) all+=("#Training") ;; + documentation|docs) all+=("#Documentation") ;; + release*) all+=("#ReleaseNotes") ;; + handbook) all+=("#EngineeringHandbook") ;; + *) all+=("#${tag^}") ;; + esac + done + + local seen="" + local result="" + for h in "${all[@]}"; do + if [[ "$seen" != *"$h"* ]]; then + result="$result $h" + seen="$seen $h" + fi + done + echo "${result# }" +} + +url_technical=$(build_url "technical") +url_founder=$(build_url "founder") +url_hook=$(build_url "hook") + +if [[ "$is_tutorial" == true ]]; then + technical="${seo_desc} ${url_technical}" + founder="We built Hype because documentation shouldn't lie. Here's how to get started with dynamic Markdown that validates everything at build time: ${url_founder}" + hook="Your Markdown can run code now. ${url_hook}" + + case "$slug" in + getting-started) + technical="Learn how to install Hype and create your first dynamic Markdown document with build-time code execution. ${url_technical}" + founder="We built Hype because documentation shouldn't lie. Here's a quick guide to get started: ${url_founder}" + hook="What if your Markdown could execute code and catch errors before publish? ${url_hook}" + ;; + deploying-with-docker) + technical="Deploy a Hype-powered blog with Docker โ€” from Dockerfile to production with auto-rebuilds. ${url_technical}" + founder="We wanted deploying a Hype blog to be as simple as 'docker build && docker run'. Here's how: ${url_founder}" + hook="Ship your Hype blog in a container. ${url_hook}" + ;; + *) + technical="${seo_desc} ${url_technical}" + founder="We built this with Hype because ${title,,} shouldn't be harder than it needs to be: ${url_founder}" + hook="${title} โ€” powered by dynamic Markdown. ${url_hook}" + ;; + esac +else + technical="${seo_desc} ${url_technical}" + founder="We've been using Hype for ${title,,} and it's been a game changer. Here's how: ${url_founder}" + hook="${title} โ€” see how teams are using Hype. ${url_hook}" +fi + +echo "========================================" +echo "X/Twitter Posts for: ${title}" +echo "Post type: $(if $is_tutorial; then echo 'Tutorial'; else echo 'Usage Scenario'; fi)" +echo "========================================" +echo "" + +print_variant "TECHNICAL VARIANT" "$technical" +print_variant "FOUNDER VOICE VARIANT" "$founder" +print_variant "SHORT HOOK VARIANT" "$hook" + +hashtags=$(build_hashtags) +echo "=== HASHTAGS ===" +echo "$hashtags" +echo "" + +if [[ "$is_tutorial" == true ]]; then + url_thread=$(build_url "thread") + echo "=== THREAD VARIANT (3 posts) ===" + echo "" + echo "1/3:" + echo "${seo_desc}" + echo "" + echo "A thread on ${title,,} with @hype_markdown ๐Ÿงต" + echo "" + echo "2/3:" + first_section=$(sed -n '/^## /{s/^## //;p;q;}' "$FILE") + if [[ -n "$first_section" ]]; then + echo "It starts with ${first_section,,} โ€” Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time." + else + echo "Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time." + fi + echo "" + echo "3/3:" + echo "Full walkthrough here: ${url_thread}" + echo "" + echo "${hashtags}" + echo "" +fi + +echo "=== UTM URLS ===" +echo "Technical: ${url_technical}" +echo "Founder: ${url_founder}" +echo "Hook: ${url_hook}" +if [[ "$is_tutorial" == true ]]; then + echo "Thread: $(build_url "thread")" +fi diff --git a/marketing/distribution-workflow.md b/marketing/distribution-workflow.md new file mode 100644 index 0000000..aec2ac9 --- /dev/null +++ b/marketing/distribution-workflow.md @@ -0,0 +1,90 @@ +# Distribution Workflow + +## Quick Start + +```bash +./marketing/distribute.sh +# or +make distribute SLUG= +``` + +Example: +```bash +make distribute SLUG=getting-started +``` + +## Process + +1. **Publish** the blog post (merge to main, auto-deploys via Dokploy) +2. **Generate** post variants: `make distribute SLUG=` +3. **Review and edit** the generated variants โ€” tweak tone, add context +4. **Post** to X (manually, via X scheduler, or via Buffer) +5. **Cross-post** to LinkedIn if appropriate (rewrite for longer format) + +## UTM Conventions + +All generated URLs include UTM parameters for tracking. + +| Parameter | Value | Purpose | +|-----------|-------|---------| +| `utm_source` | `twitter`, `linkedin`, `newsletter` | Where the click came from | +| `utm_medium` | `social`, `email` | Channel type | +| `utm_content` | `technical`, `founder`, `hook`, `thread` | Which variant was clicked | + +Example URL: +``` +https://hypemd.dev/getting-started/?utm_source=twitter&utm_medium=social&utm_content=technical +``` + +## Which Posts to Promote + +| Post Type | Promote? | Example | +|-----------|----------|---------| +| Tutorial posts | Yes | getting-started, deploying-with-docker | +| Usage scenario posts | Yes | ai-authoring-workflow, release-notes-pipeline | +| Documentation posts (docs-*) | No | These are reference material | + +## Post Timing Guidelines + +| Content Type | Best Time | Best Days | +|-------------|-----------|-----------| +| Tutorials | 9-11am ET | Weekdays | +| Usage scenarios | 10am-1pm ET | Tue-Thu | +| Announcements | 9am-12pm ET | Any weekday | + +## Scheduler Integration + +### X Built-in Scheduler + +1. Compose your tweet on X +2. Click the calendar icon +3. Pick date and time +4. Schedule + +Best for one-off posts. + +### Buffer (Free Tier) + +- 3 channels, 10 scheduled posts per channel +- Paste generated text, set schedule +- Best for batching a week of posts + +### X API v2 (Advanced) + +For automated posting, use the X API directly: + +```bash +curl -X POST "https://api.x.com/2/tweets" \ + -H "Authorization: Bearer $X_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"text": "Your tweet text here"}' +``` + +Store the bearer token in an environment variable. Never commit tokens to the repo. + +## Measuring Results + +- Check UTM parameters in your analytics tool +- `utm_content` tells you which variant performed best +- Compare `technical` vs `founder` vs `hook` click-through rates +- Iterate on templates based on what resonates diff --git a/marketing/templates.md b/marketing/templates.md new file mode 100644 index 0000000..4383399 --- /dev/null +++ b/marketing/templates.md @@ -0,0 +1,83 @@ +# X/Twitter Post Templates for hypemd.dev + +## Variables + +| Variable | Source | Example | +|----------|--------|---------| +| `{title}` | First `# ` heading | Getting Started with Hype | +| `{slug}` | Frontmatter | getting-started | +| `{seo_description}` | Frontmatter | Learn how to install Hype... | +| `{tags}` | Frontmatter | tutorial, getting-started, hype | +| `{author}` | Frontmatter | Gopher Guides | +| `{url}` | Generated with UTM | https://hypemd.dev/getting-started/?utm_source=twitter&... | + +## Single Post Templates + +### Tutorial Posts + +**Technical:** +> {seo_description} {url} + +**Founder Voice:** +> We built this with Hype because {title} shouldn't be harder than it needs to be: {url} + +**Short Hook:** +> {title} โ€” powered by dynamic Markdown. {url} + +### Usage Scenario Posts + +**Technical:** +> {seo_description} {url} + +**Founder Voice:** +> We've been using Hype for {title} and it's been a game changer. Here's how: {url} + +**Short Hook:** +> {title} โ€” see how teams are using Hype. {url} + +## Thread Templates + +### Tutorial Thread (3 posts) + +**Post 1 (Hook):** +> {seo_description} +> +> A thread on {title} with @hype_markdown + +**Post 2 (Key insight):** +> It starts with {first_section} โ€” Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time. + +**Post 3 (CTA):** +> Full walkthrough here: {url} +> +> {hashtags} + +### Announcement Thread (4 posts) + +**Post 1:** Bold claim about the problem being solved. + +**Post 2:** Why existing solutions fall short. + +**Post 3:** How Hype solves it differently (with a concrete example). + +**Post 4:** Link + CTA + hashtags. + +## Hashtag Reference + +**Always include:** +- `#HypeMarkdown` +- `#Golang` +- `#OpenSource` + +**Conditional (based on tags):** + +| Tag | Hashtag | +|-----|---------| +| docker | #Docker | +| ai, claude | #AI | +| workflow | #DevWorkflow | +| authoring | #TechWriting | +| training | #Training | +| documentation, docs | #Documentation | +| release* | #ReleaseNotes | +| handbook | #EngineeringHandbook | From 8612097bbb18230e74f7b0b9d15e9ac2e4c8dc9f Mon Sep 17 00:00:00 2001 From: Cory LaNou Date: Mon, 16 Mar 2026 06:46:33 -0500 Subject: [PATCH 2/5] feat(marketing): add automated blog-to-X tweet pipeline Replace the template-based distribution system with an automated tweet pipeline that actually posts to X via the API. - marketing/tweet.sh: reads `tweet` field from blog post frontmatter, appends the post URL, and posts via X API v2 (OAuth 1.0a) - .github/workflows/tweet.yml: auto-tweets when blog posts with a `tweet` field are merged to main - marketing/SETUP.md: step-by-step guide for X API keys, .envrc setup, and gh secret set for CI - Add `tweet` frontmatter field to all 7 blog posts - Makefile: `make tweet` and `make tweet-dry` targets Closes #4 --- .github/workflows/tweet.yml | 44 ++++++ .gitignore | 1 + Makefile | 9 +- content/ai-authoring-workflow/module.md | 1 + content/deploying-with-docker/module.md | 1 + content/engineering-handbook/module.md | 1 + content/getting-started/module.md | 1 + content/release-notes-pipeline/module.md | 1 + content/single-source-docs/module.md | 1 + content/training-materials/module.md | 1 + marketing/SETUP.md | 91 ++++++++++++ marketing/distribute.sh | 180 ----------------------- marketing/distribution-workflow.md | 90 ------------ marketing/templates.md | 83 ----------- marketing/tweet.sh | 130 ++++++++++++++++ 15 files changed, 279 insertions(+), 356 deletions(-) create mode 100644 .github/workflows/tweet.yml create mode 100644 marketing/SETUP.md delete mode 100755 marketing/distribute.sh delete mode 100644 marketing/distribution-workflow.md delete mode 100644 marketing/templates.md create mode 100755 marketing/tweet.sh diff --git a/.github/workflows/tweet.yml b/.github/workflows/tweet.yml new file mode 100644 index 0000000..701a55d --- /dev/null +++ b/.github/workflows/tweet.yml @@ -0,0 +1,44 @@ +name: Tweet New Posts + +on: + push: + branches: [main] + paths: + - "content/*/module.md" + +jobs: + tweet: + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Find changed blog posts + id: changed + run: | + changed=$(git diff --name-only HEAD~1 HEAD -- 'content/*/module.md' | grep -v '^content/docs' || true) + echo "files<> "$GITHUB_OUTPUT" + echo "$changed" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Tweet changed posts + if: steps.changed.outputs.files != '' + env: + X_API_KEY: ${{ secrets.X_API_KEY }} + X_API_SECRET: ${{ secrets.X_API_SECRET }} + X_ACCESS_TOKEN: ${{ secrets.X_ACCESS_TOKEN }} + X_ACCESS_TOKEN_SECRET: ${{ secrets.X_ACCESS_TOKEN_SECRET }} + run: | + echo "${{ steps.changed.outputs.files }}" | while IFS= read -r file; do + if [[ -z "$file" ]]; then continue; fi + slug=$(basename "$(dirname "$file")") + tweet_field=$(awk '/
/{f=1;next} /<\/details>/{if(f)exit} f' "$file" | grep '^tweet:' | head -1) + if [[ -z "$tweet_field" ]]; then + echo "Skipping $slug โ€” no tweet field" + continue + fi + echo "Tweeting for: $slug" + ./marketing/tweet.sh "$slug" + done diff --git a/.gitignore b/.gitignore index 0c376ea..a15757a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ public/ .DS_Store +.envrc diff --git a/Makefile b/Makefile index e291b2b..8e8ac74 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build serve dev clean distribute +.PHONY: build serve dev clean tweet tweet-dry build: hype blog build @@ -18,5 +18,8 @@ docker-build: docker-run: docker run -p 3000:3000 hypemd-dev -distribute: - @./marketing/distribute.sh $(SLUG) +tweet: + @./marketing/tweet.sh $(SLUG) + +tweet-dry: + @./marketing/tweet.sh --dry-run $(SLUG) diff --git a/content/ai-authoring-workflow/module.md b/content/ai-authoring-workflow/module.md index 38c4b75..da79bce 100644 --- a/content/ai-authoring-workflow/module.md +++ b/content/ai-authoring-workflow/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Learn how to combine AI coding assistants like Claude with Hype's build-time validation to write technical documentation faster while keeping every code example correct. tags: tutorial, ai, authoring, workflow, claude, hype +tweet: AI can write docs fast, but how do you trust the code examples? Combine Claude with Hype's build-time validation โ€” every snippet is verified before publish.
AI assistants are transforming how we write code. But when it comes to technical documentation, there's a trust problem: AI can generate plausible-looking code examples that don't actually work. Hype solves this by validating everything at build time โ€” making AI-generated content trustworthy through automated verification. diff --git a/content/deploying-with-docker/module.md b/content/deploying-with-docker/module.md index 7616c1f..7094c16 100644 --- a/content/deploying-with-docker/module.md +++ b/content/deploying-with-docker/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Deploy a Hype-powered blog site with Docker. Covers Dockerfile setup, Dokploy, Heroku, and generic VPS deployment with Docker Compose. tags: tutorial, docker, deployment, blog, hype +tweet: Deploy a Hype-powered blog with Docker โ€” from Dockerfile to production. Covers Dokploy, Heroku, and Docker Compose setups.
Hype builds and serves your blog in a single binary. That makes it a natural fit for Docker โ€” one container that builds your site from source and serves it, with no external web server required. diff --git a/content/engineering-handbook/module.md b/content/engineering-handbook/module.md index 58ede42..c22a70c 100644 --- a/content/engineering-handbook/module.md +++ b/content/engineering-handbook/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Use Hype to build an internal engineering handbook that stays in sync with your codebase. Executable examples, automated validation, and single-source documentation for your team. tags: tutorial, engineering, handbook, automation, hype +tweet: Your engineering handbook is probably wrong. Not because it was written badly โ€” the code just changed. Here's how to make docs a build artifact with Hype. Every engineering team has internal documentation. Style guides, onboarding docs, architecture decision records, runbook procedures. And almost universally, that documentation is wrong. Not because someone wrote it badly, but because the code changed and nobody updated the docs. diff --git a/content/getting-started/module.md b/content/getting-started/module.md index 624b2bd..e9e4c87 100644 --- a/content/getting-started/module.md +++ b/content/getting-started/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Gopher Guides seo_description: Learn how to install Hype, create your first dynamic Markdown document, and execute code blocks at build time. tags: tutorial, getting-started, hype +tweet: What if your Markdown could execute code and catch errors before you publish? Get started with Hype โ€” dynamic Markdown for Go developers. Hype is a content engine that makes Markdown dynamic. You can execute code, include files, and validate everything at build time. This guide walks you through installation and your first hype document. diff --git a/content/release-notes-pipeline/module.md b/content/release-notes-pipeline/module.md index 1dc6e69..d0e7e5b 100644 --- a/content/release-notes-pipeline/module.md +++ b/content/release-notes-pipeline/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Build a release notes pipeline using Hype with executable code snippets, automated validation, and version-aware documentation that ships with every release. tags: tutorial, release-notes, ci-cd, pipeline, hype +tweet: Bad release notes erode trust. Build a pipeline where every code example compiles, every command runs, and every API ref is verified at build time. Release notes are the first thing users read after updating. Bad release notes โ€” outdated examples, broken migration commands, incorrect API signatures โ€” erode trust and generate support tickets. Hype lets you build release notes where every code example compiles, every command runs, and every API reference reflects the actual release. diff --git a/content/single-source-docs/module.md b/content/single-source-docs/module.md index 0d1de4b..7ad50d7 100644 --- a/content/single-source-docs/module.md +++ b/content/single-source-docs/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Learn how to use Hype to maintain a single Markdown source that generates both your GitHub README and website documentation, keeping them permanently in sync. tags: tutorial, docs, workflow, hype +tweet: Your README says one thing, your docs site says another. Write it once with Hype and generate both from a single source. Every open source project has the same problem: documentation lives in too many places. Your README says one thing, your docs site says another, and the install instructions in your wiki are three versions behind. You end up maintaining the same content in multiple locations, and they inevitably drift apart. diff --git a/content/training-materials/module.md b/content/training-materials/module.md index 926ba01..e7713da 100644 --- a/content/training-materials/module.md +++ b/content/training-materials/module.md @@ -6,6 +6,7 @@ published: 03/15/2026 author: Cory LaNou seo_description: Learn how to use Hype to build reusable training and course materials with executable code examples, file includes, and modular content that stays in sync with your codebase. tags: tutorial, training, education, includes, hype +tweet: Training materials go stale the moment code changes. Use Hype to build courses with executable examples that stay in sync with your codebase. If you've ever written training materials for a programming course, you know the pain: code examples go stale, exercises drift out of sync with the slides, and updating one module means checking every other module that references it. Hype was built to solve exactly this problem. diff --git a/marketing/SETUP.md b/marketing/SETUP.md new file mode 100644 index 0000000..7d03757 --- /dev/null +++ b/marketing/SETUP.md @@ -0,0 +1,91 @@ +# X/Twitter API Setup + +## 1. Create an X Developer Account + +1. Go to https://developer.x.com and sign in with the @hype_markdown account +2. Sign up for the **Free** tier (1,500 tweets/month, write-only) +3. Create a new **Project** and **App** + +## 2. Configure App Permissions + +1. In your app settings, go to **User authentication settings** +2. Set app permissions to **Read and Write** +3. After changing permissions, **regenerate** your Access Token and Access Token Secret (old tokens won't have the new permissions) + +## 3. Generate Credentials + +From your app's **Keys and Tokens** page, you need 4 values: + +| Credential | Environment Variable | +|-----------|---------------------| +| API Key (Consumer Key) | `X_API_KEY` | +| API Secret (Consumer Secret) | `X_API_SECRET` | +| Access Token | `X_ACCESS_TOKEN` | +| Access Token Secret | `X_ACCESS_TOKEN_SECRET` | + +## 4. Local Setup + +Copy the credentials into `.envrc` at the project root: + +```bash +export X_API_KEY="your-api-key" +export X_API_SECRET="your-api-secret" +export X_ACCESS_TOKEN="your-access-token" +export X_ACCESS_TOKEN_SECRET="your-access-token-secret" +``` + +Then activate with direnv: + +```bash +direnv allow +``` + +Test with a dry run: + +```bash +make tweet-dry SLUG=getting-started +``` + +## 5. CI Setup (GitHub Actions) + +Set the repository secrets using the `gh` CLI: + +```bash +gh secret set X_API_KEY --body "your-api-key" +gh secret set X_API_SECRET --body "your-api-secret" +gh secret set X_ACCESS_TOKEN --body "your-access-token" +gh secret set X_ACCESS_TOKEN_SECRET --body "your-access-token-secret" +``` + +Once set, the `tweet.yml` workflow will automatically tweet when blog posts with a `tweet` field are merged to main. + +## 6. Manual Tweeting + +To tweet an existing post manually: + +```bash +make tweet SLUG=getting-started +``` + +To preview without posting: + +```bash +make tweet-dry SLUG=getting-started +``` + +## Adding Tweets to Blog Posts + +Add a `tweet` field to the `
` frontmatter block in any blog post: + +```markdown +
+slug: my-article +published: 03/16/2026 +author: Gopher Guides +seo_description: Full SEO description here +tags: tutorial, hype +tweet: Short punchy text for X (max ~250 chars, URL is appended automatically) +
+``` + +The script automatically appends the post URL, so keep the tweet text under ~250 characters. diff --git a/marketing/distribute.sh b/marketing/distribute.sh deleted file mode 100755 index 4e8e92d..0000000 --- a/marketing/distribute.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SITE_URL="https://hypemd.dev" -HANDLE="@hype_markdown" -MAX_CHARS=280 - -usage() { - echo "Usage: $0 " - echo "" - echo "Generate X/Twitter post variants for a blog post." - echo "" - echo "Examples:" - echo " $0 getting-started" - echo " $0 ai-authoring-workflow" - exit 1 -} - -if [[ $# -lt 1 ]]; then - usage -fi - -SLUG="$1" -FILE="content/${SLUG}/module.md" - -if [[ ! -f "$FILE" ]]; then - echo "Error: $FILE not found" >&2 - exit 1 -fi - -title=$(grep -m1 '^# ' "$FILE" | sed 's/^# //') -details=$(awk '/
/{found=1; next} /<\/details>/{if(found) exit} found{print}' "$FILE") -slug=$(echo "$details" | grep '^slug:' | head -1 | sed 's/^slug: *//') -seo_desc=$(echo "$details" | grep '^seo_description:' | head -1 | sed 's/^seo_description: *//') -tags=$(echo "$details" | grep '^tags:' | head -1 | sed 's/^tags: *//') -author=$(echo "$details" | grep '^author:' | head -1 | sed 's/^author: *//') - -if [[ -z "$title" ]]; then - echo "Error: could not parse title from $FILE" >&2 - exit 1 -fi - -if [[ "$slug" == docs/* || "$slug" == "docs" ]]; then - echo "WARNING: This appears to be a documentation page (slug: ${slug}). Docs pages are typically not promoted on social media." >&2 - echo "" >&2 -fi - -is_tutorial=false -if echo "$tags" | grep -qi 'tutorial'; then - is_tutorial=true -fi - -build_url() { - local variant="$1" - echo "${SITE_URL}/${slug}/?utm_source=twitter&utm_medium=social&utm_content=${variant}" -} - -print_variant() { - local label="$1" - local text="$2" - local chars=${#text} - echo "=== ${label} ===" - echo "$text" - echo "" - echo "Characters: ${chars}" - if [[ $chars -gt $MAX_CHARS ]]; then - echo "WARNING: Exceeds ${MAX_CHARS} character limit by $((chars - MAX_CHARS)) characters" - fi - echo "" -} - -build_hashtags() { - local -a all=("#HypeMarkdown" "#Golang" "#OpenSource") - - IFS=',' read -ra tag_arr <<< "$tags" - for tag in "${tag_arr[@]}"; do - tag=$(echo "$tag" | sed 's/^ *//;s/ *$//') - case "$tag" in - tutorial|getting-started|hype) ;; - docker) all+=("#Docker") ;; - ai|claude) all+=("#AI") ;; - workflow) all+=("#DevWorkflow") ;; - authoring) all+=("#TechWriting") ;; - training) all+=("#Training") ;; - documentation|docs) all+=("#Documentation") ;; - release*) all+=("#ReleaseNotes") ;; - handbook) all+=("#EngineeringHandbook") ;; - *) all+=("#${tag^}") ;; - esac - done - - local seen="" - local result="" - for h in "${all[@]}"; do - if [[ "$seen" != *"$h"* ]]; then - result="$result $h" - seen="$seen $h" - fi - done - echo "${result# }" -} - -url_technical=$(build_url "technical") -url_founder=$(build_url "founder") -url_hook=$(build_url "hook") - -if [[ "$is_tutorial" == true ]]; then - technical="${seo_desc} ${url_technical}" - founder="We built Hype because documentation shouldn't lie. Here's how to get started with dynamic Markdown that validates everything at build time: ${url_founder}" - hook="Your Markdown can run code now. ${url_hook}" - - case "$slug" in - getting-started) - technical="Learn how to install Hype and create your first dynamic Markdown document with build-time code execution. ${url_technical}" - founder="We built Hype because documentation shouldn't lie. Here's a quick guide to get started: ${url_founder}" - hook="What if your Markdown could execute code and catch errors before publish? ${url_hook}" - ;; - deploying-with-docker) - technical="Deploy a Hype-powered blog with Docker โ€” from Dockerfile to production with auto-rebuilds. ${url_technical}" - founder="We wanted deploying a Hype blog to be as simple as 'docker build && docker run'. Here's how: ${url_founder}" - hook="Ship your Hype blog in a container. ${url_hook}" - ;; - *) - technical="${seo_desc} ${url_technical}" - founder="We built this with Hype because ${title,,} shouldn't be harder than it needs to be: ${url_founder}" - hook="${title} โ€” powered by dynamic Markdown. ${url_hook}" - ;; - esac -else - technical="${seo_desc} ${url_technical}" - founder="We've been using Hype for ${title,,} and it's been a game changer. Here's how: ${url_founder}" - hook="${title} โ€” see how teams are using Hype. ${url_hook}" -fi - -echo "========================================" -echo "X/Twitter Posts for: ${title}" -echo "Post type: $(if $is_tutorial; then echo 'Tutorial'; else echo 'Usage Scenario'; fi)" -echo "========================================" -echo "" - -print_variant "TECHNICAL VARIANT" "$technical" -print_variant "FOUNDER VOICE VARIANT" "$founder" -print_variant "SHORT HOOK VARIANT" "$hook" - -hashtags=$(build_hashtags) -echo "=== HASHTAGS ===" -echo "$hashtags" -echo "" - -if [[ "$is_tutorial" == true ]]; then - url_thread=$(build_url "thread") - echo "=== THREAD VARIANT (3 posts) ===" - echo "" - echo "1/3:" - echo "${seo_desc}" - echo "" - echo "A thread on ${title,,} with @hype_markdown ๐Ÿงต" - echo "" - echo "2/3:" - first_section=$(sed -n '/^## /{s/^## //;p;q;}' "$FILE") - if [[ -n "$first_section" ]]; then - echo "It starts with ${first_section,,} โ€” Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time." - else - echo "Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time." - fi - echo "" - echo "3/3:" - echo "Full walkthrough here: ${url_thread}" - echo "" - echo "${hashtags}" - echo "" -fi - -echo "=== UTM URLS ===" -echo "Technical: ${url_technical}" -echo "Founder: ${url_founder}" -echo "Hook: ${url_hook}" -if [[ "$is_tutorial" == true ]]; then - echo "Thread: $(build_url "thread")" -fi diff --git a/marketing/distribution-workflow.md b/marketing/distribution-workflow.md deleted file mode 100644 index aec2ac9..0000000 --- a/marketing/distribution-workflow.md +++ /dev/null @@ -1,90 +0,0 @@ -# Distribution Workflow - -## Quick Start - -```bash -./marketing/distribute.sh -# or -make distribute SLUG= -``` - -Example: -```bash -make distribute SLUG=getting-started -``` - -## Process - -1. **Publish** the blog post (merge to main, auto-deploys via Dokploy) -2. **Generate** post variants: `make distribute SLUG=` -3. **Review and edit** the generated variants โ€” tweak tone, add context -4. **Post** to X (manually, via X scheduler, or via Buffer) -5. **Cross-post** to LinkedIn if appropriate (rewrite for longer format) - -## UTM Conventions - -All generated URLs include UTM parameters for tracking. - -| Parameter | Value | Purpose | -|-----------|-------|---------| -| `utm_source` | `twitter`, `linkedin`, `newsletter` | Where the click came from | -| `utm_medium` | `social`, `email` | Channel type | -| `utm_content` | `technical`, `founder`, `hook`, `thread` | Which variant was clicked | - -Example URL: -``` -https://hypemd.dev/getting-started/?utm_source=twitter&utm_medium=social&utm_content=technical -``` - -## Which Posts to Promote - -| Post Type | Promote? | Example | -|-----------|----------|---------| -| Tutorial posts | Yes | getting-started, deploying-with-docker | -| Usage scenario posts | Yes | ai-authoring-workflow, release-notes-pipeline | -| Documentation posts (docs-*) | No | These are reference material | - -## Post Timing Guidelines - -| Content Type | Best Time | Best Days | -|-------------|-----------|-----------| -| Tutorials | 9-11am ET | Weekdays | -| Usage scenarios | 10am-1pm ET | Tue-Thu | -| Announcements | 9am-12pm ET | Any weekday | - -## Scheduler Integration - -### X Built-in Scheduler - -1. Compose your tweet on X -2. Click the calendar icon -3. Pick date and time -4. Schedule - -Best for one-off posts. - -### Buffer (Free Tier) - -- 3 channels, 10 scheduled posts per channel -- Paste generated text, set schedule -- Best for batching a week of posts - -### X API v2 (Advanced) - -For automated posting, use the X API directly: - -```bash -curl -X POST "https://api.x.com/2/tweets" \ - -H "Authorization: Bearer $X_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"text": "Your tweet text here"}' -``` - -Store the bearer token in an environment variable. Never commit tokens to the repo. - -## Measuring Results - -- Check UTM parameters in your analytics tool -- `utm_content` tells you which variant performed best -- Compare `technical` vs `founder` vs `hook` click-through rates -- Iterate on templates based on what resonates diff --git a/marketing/templates.md b/marketing/templates.md deleted file mode 100644 index 4383399..0000000 --- a/marketing/templates.md +++ /dev/null @@ -1,83 +0,0 @@ -# X/Twitter Post Templates for hypemd.dev - -## Variables - -| Variable | Source | Example | -|----------|--------|---------| -| `{title}` | First `# ` heading | Getting Started with Hype | -| `{slug}` | Frontmatter | getting-started | -| `{seo_description}` | Frontmatter | Learn how to install Hype... | -| `{tags}` | Frontmatter | tutorial, getting-started, hype | -| `{author}` | Frontmatter | Gopher Guides | -| `{url}` | Generated with UTM | https://hypemd.dev/getting-started/?utm_source=twitter&... | - -## Single Post Templates - -### Tutorial Posts - -**Technical:** -> {seo_description} {url} - -**Founder Voice:** -> We built this with Hype because {title} shouldn't be harder than it needs to be: {url} - -**Short Hook:** -> {title} โ€” powered by dynamic Markdown. {url} - -### Usage Scenario Posts - -**Technical:** -> {seo_description} {url} - -**Founder Voice:** -> We've been using Hype for {title} and it's been a game changer. Here's how: {url} - -**Short Hook:** -> {title} โ€” see how teams are using Hype. {url} - -## Thread Templates - -### Tutorial Thread (3 posts) - -**Post 1 (Hook):** -> {seo_description} -> -> A thread on {title} with @hype_markdown - -**Post 2 (Key insight):** -> It starts with {first_section} โ€” Hype makes this straightforward because your Markdown is dynamic. Code blocks execute, files get included, and everything is validated at build time. - -**Post 3 (CTA):** -> Full walkthrough here: {url} -> -> {hashtags} - -### Announcement Thread (4 posts) - -**Post 1:** Bold claim about the problem being solved. - -**Post 2:** Why existing solutions fall short. - -**Post 3:** How Hype solves it differently (with a concrete example). - -**Post 4:** Link + CTA + hashtags. - -## Hashtag Reference - -**Always include:** -- `#HypeMarkdown` -- `#Golang` -- `#OpenSource` - -**Conditional (based on tags):** - -| Tag | Hashtag | -|-----|---------| -| docker | #Docker | -| ai, claude | #AI | -| workflow | #DevWorkflow | -| authoring | #TechWriting | -| training | #Training | -| documentation, docs | #Documentation | -| release* | #ReleaseNotes | -| handbook | #EngineeringHandbook | diff --git a/marketing/tweet.sh b/marketing/tweet.sh new file mode 100755 index 0000000..73f42f4 --- /dev/null +++ b/marketing/tweet.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +SITE_URL="https://hypemd.dev" +API_URL="https://api.x.com/2/tweets" + +usage() { + echo "Usage: $0 [--dry-run] " + echo "" + echo "Post a tweet for a blog post using the 'tweet' field from its frontmatter." + echo "" + echo "Options:" + echo " --dry-run Preview the tweet without posting" + echo "" + echo "Examples:" + echo " $0 getting-started" + echo " $0 --dry-run ai-authoring-workflow" + echo "" + echo "Environment variables (required unless --dry-run):" + echo " X_API_KEY Consumer API key" + echo " X_API_SECRET Consumer API secret" + echo " X_ACCESS_TOKEN Access token" + echo " X_ACCESS_TOKEN_SECRET Access token secret" + exit 1 +} + +DRY_RUN=false +SLUG="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + -h|--help) usage ;; + -*) echo "Unknown option: $1" >&2; usage ;; + *) SLUG="$1"; shift ;; + esac +done + +if [[ -z "$SLUG" ]]; then + usage +fi + +FILE="content/${SLUG}/module.md" +if [[ ! -f "$FILE" ]]; then + echo "Error: $FILE not found" >&2 + exit 1 +fi + +details=$(awk '/
/{found=1; next} /<\/details>/{if(found) exit} found{print}' "$FILE") +slug=$(echo "$details" | grep '^slug:' | head -1 | sed 's/^slug: *//') +tweet_text=$(echo "$details" | grep '^tweet:' | head -1 | sed 's/^tweet: *//') + +if [[ -z "$tweet_text" ]]; then + echo "Error: no 'tweet' field found in $FILE frontmatter" >&2 + exit 1 +fi + +post_url="${SITE_URL}/${slug}/" +full_tweet="${tweet_text} ${post_url}" + +char_count=${#full_tweet} +if [[ $char_count -gt 280 ]]; then + echo "WARNING: Tweet is ${char_count} chars (limit 280). URLs count as 23 chars on X, so actual count may differ." >&2 +fi + +echo "=== Tweet Preview ===" +echo "$full_tweet" +echo "" +echo "Characters: ${char_count} (URLs count as 23 on X)" +echo "" + +if [[ "$DRY_RUN" == true ]]; then + echo "[Dry run โ€” not posted]" + exit 0 +fi + +for var in X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET; do + if [[ -z "${!var:-}" ]]; then + echo "Error: $var is not set. See marketing/SETUP.md for instructions." >&2 + exit 1 + fi +done + +urlencode() { + local string="$1" + python3 -c "import urllib.parse; print(urllib.parse.quote('$string', safe=''))" 2>/dev/null \ + || printf '%s' "$string" | curl -Gso /dev/null -w '%{url_effective}' --data-urlencode @- '' | cut -c3- +} + +generate_nonce() { + openssl rand -hex 16 +} + +oauth_timestamp=$(date +%s) +oauth_nonce=$(generate_nonce) + +oauth_consumer_key="$X_API_KEY" +oauth_token="$X_ACCESS_TOKEN" +oauth_signature_method="HMAC-SHA1" +oauth_version="1.0" + +escaped_tweet=$(urlencode "$full_tweet") + +param_string="oauth_consumer_key=${oauth_consumer_key}&oauth_nonce=${oauth_nonce}&oauth_signature_method=${oauth_signature_method}&oauth_timestamp=${oauth_timestamp}&oauth_token=${oauth_token}&oauth_version=${oauth_version}" + +signature_base="POST&$(urlencode "$API_URL")&$(urlencode "$param_string")" + +signing_key="$(urlencode "$X_API_SECRET")&$(urlencode "$X_ACCESS_TOKEN_SECRET")" + +oauth_signature=$(printf '%s' "$signature_base" | openssl dgst -sha1 -hmac "$signing_key" -binary | base64) + +auth_header="OAuth oauth_consumer_key=\"${oauth_consumer_key}\", oauth_nonce=\"${oauth_nonce}\", oauth_signature=\"$(urlencode "$oauth_signature")\", oauth_signature_method=\"${oauth_signature_method}\", oauth_timestamp=\"${oauth_timestamp}\", oauth_token=\"${oauth_token}\", oauth_version=\"${oauth_version}\"" + +response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \ + -H "Authorization: ${auth_header}" \ + -H "Content-Type: application/json" \ + -d "{\"text\": $(printf '%s' "$full_tweet" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}") + +http_code=$(echo "$response" | tail -1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" == "201" ]]; then + tweet_id=$(echo "$body" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['data']['id'])" 2>/dev/null || echo "unknown") + echo "Tweet posted successfully!" + echo "https://x.com/hype_markdown/status/${tweet_id}" +else + echo "Error posting tweet (HTTP ${http_code}):" >&2 + echo "$body" >&2 + exit 1 +fi From cd3c749325f16527b02ecad6a4363fcbc653d5f9 Mon Sep 17 00:00:00 2001 From: Cory LaNou Date: Mon, 16 Mar 2026 12:21:13 -0500 Subject: [PATCH 3/5] feat(seo): add Open Graph and Twitter Card support to all blog posts - Update config.yaml with twitter handle (@hype_markdown) - Add author_twitter frontmatter to all 7 blog posts - Pin Dockerfile to hype v0.8.0 which includes enhanced OG/Twitter tags - Fix tweet.sh OAuth signing to use Python for reliability New meta tags now generated: og:site_name, article:published_time, article:author, article:tag, twitter:site, twitter:creator. Ref #4 --- Dockerfile | 2 +- config.yaml | 2 +- content/ai-authoring-workflow/module.md | 1 + content/deploying-with-docker/module.md | 1 + content/engineering-handbook/module.md | 1 + content/getting-started/module.md | 1 + content/release-notes-pipeline/module.md | 1 + content/single-source-docs/module.md | 1 + content/training-materials/module.md | 1 + marketing/tweet.sh | 108 +++++++++++++---------- 10 files changed, 72 insertions(+), 47 deletions(-) diff --git a/Dockerfile b/Dockerfile index 048cbc6..0b35513 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM golang:1.25 AS builder -RUN go install github.com/gopherguides/hype/cmd/hype@main +RUN go install github.com/gopherguides/hype/cmd/hype@v0.8.0 FROM golang:1.25 COPY --from=builder /go/bin/hype /usr/local/bin/hype diff --git a/config.yaml b/config.yaml index 57150b0..eb828e1 100644 --- a/config.yaml +++ b/config.yaml @@ -4,7 +4,7 @@ baseURL: "https://hypemd.dev" author: name: "Gopher Guides" email: "" - twitter: "" + twitter: "@hype_markdown" theme: "developer" highlight: style: "monokai" diff --git a/content/ai-authoring-workflow/module.md b/content/ai-authoring-workflow/module.md index da79bce..a33775e 100644 --- a/content/ai-authoring-workflow/module.md +++ b/content/ai-authoring-workflow/module.md @@ -4,6 +4,7 @@ slug: ai-authoring-workflow published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Learn how to combine AI coding assistants like Claude with Hype's build-time validation to write technical documentation faster while keeping every code example correct. tags: tutorial, ai, authoring, workflow, claude, hype tweet: AI can write docs fast, but how do you trust the code examples? Combine Claude with Hype's build-time validation โ€” every snippet is verified before publish. diff --git a/content/deploying-with-docker/module.md b/content/deploying-with-docker/module.md index 7094c16..7c9d22b 100644 --- a/content/deploying-with-docker/module.md +++ b/content/deploying-with-docker/module.md @@ -4,6 +4,7 @@ slug: deploying-with-docker published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Deploy a Hype-powered blog site with Docker. Covers Dockerfile setup, Dokploy, Heroku, and generic VPS deployment with Docker Compose. tags: tutorial, docker, deployment, blog, hype tweet: Deploy a Hype-powered blog with Docker โ€” from Dockerfile to production. Covers Dokploy, Heroku, and Docker Compose setups. diff --git a/content/engineering-handbook/module.md b/content/engineering-handbook/module.md index c22a70c..5dbaa41 100644 --- a/content/engineering-handbook/module.md +++ b/content/engineering-handbook/module.md @@ -4,6 +4,7 @@ slug: engineering-handbook published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Use Hype to build an internal engineering handbook that stays in sync with your codebase. Executable examples, automated validation, and single-source documentation for your team. tags: tutorial, engineering, handbook, automation, hype tweet: Your engineering handbook is probably wrong. Not because it was written badly โ€” the code just changed. Here's how to make docs a build artifact with Hype. diff --git a/content/getting-started/module.md b/content/getting-started/module.md index e9e4c87..edf894e 100644 --- a/content/getting-started/module.md +++ b/content/getting-started/module.md @@ -4,6 +4,7 @@ slug: getting-started published: 03/15/2026 author: Gopher Guides +author_twitter: @hype_markdown seo_description: Learn how to install Hype, create your first dynamic Markdown document, and execute code blocks at build time. tags: tutorial, getting-started, hype tweet: What if your Markdown could execute code and catch errors before you publish? Get started with Hype โ€” dynamic Markdown for Go developers. diff --git a/content/release-notes-pipeline/module.md b/content/release-notes-pipeline/module.md index d0e7e5b..7c97b3f 100644 --- a/content/release-notes-pipeline/module.md +++ b/content/release-notes-pipeline/module.md @@ -4,6 +4,7 @@ slug: release-notes-pipeline published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Build a release notes pipeline using Hype with executable code snippets, automated validation, and version-aware documentation that ships with every release. tags: tutorial, release-notes, ci-cd, pipeline, hype tweet: Bad release notes erode trust. Build a pipeline where every code example compiles, every command runs, and every API ref is verified at build time. diff --git a/content/single-source-docs/module.md b/content/single-source-docs/module.md index 7ad50d7..a4e5a41 100644 --- a/content/single-source-docs/module.md +++ b/content/single-source-docs/module.md @@ -4,6 +4,7 @@ slug: single-source-docs published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Learn how to use Hype to maintain a single Markdown source that generates both your GitHub README and website documentation, keeping them permanently in sync. tags: tutorial, docs, workflow, hype tweet: Your README says one thing, your docs site says another. Write it once with Hype and generate both from a single source. diff --git a/content/training-materials/module.md b/content/training-materials/module.md index e7713da..b116c4a 100644 --- a/content/training-materials/module.md +++ b/content/training-materials/module.md @@ -4,6 +4,7 @@ slug: training-materials published: 03/15/2026 author: Cory LaNou +author_twitter: @caborundrum seo_description: Learn how to use Hype to build reusable training and course materials with executable code examples, file includes, and modular content that stays in sync with your codebase. tags: tutorial, training, education, includes, hype tweet: Training materials go stale the moment code changes. Use Hype to build courses with executable examples that stay in sync with your codebase. diff --git a/marketing/tweet.sh b/marketing/tweet.sh index 73f42f4..40f4859 100755 --- a/marketing/tweet.sh +++ b/marketing/tweet.sh @@ -81,50 +81,68 @@ for var in X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET; do fi done -urlencode() { - local string="$1" - python3 -c "import urllib.parse; print(urllib.parse.quote('$string', safe=''))" 2>/dev/null \ - || printf '%s' "$string" | curl -Gso /dev/null -w '%{url_effective}' --data-urlencode @- '' | cut -c3- +python3 - "$full_tweet" << 'PYEOF' +import urllib.parse, hashlib, hmac, base64, time, os, json, secrets, sys, subprocess + +tweet = sys.argv[1] +api_key = os.environ["X_API_KEY"] +api_secret = os.environ["X_API_SECRET"] +access_token = os.environ["X_ACCESS_TOKEN"] +access_token_secret = os.environ["X_ACCESS_TOKEN_SECRET"] + +url = "https://api.x.com/2/tweets" + +oauth_nonce = secrets.token_hex(16) +oauth_timestamp = str(int(time.time())) + +params = { + "oauth_consumer_key": api_key, + "oauth_nonce": oauth_nonce, + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": oauth_timestamp, + "oauth_token": access_token, + "oauth_version": "1.0", } -generate_nonce() { - openssl rand -hex 16 -} - -oauth_timestamp=$(date +%s) -oauth_nonce=$(generate_nonce) - -oauth_consumer_key="$X_API_KEY" -oauth_token="$X_ACCESS_TOKEN" -oauth_signature_method="HMAC-SHA1" -oauth_version="1.0" - -escaped_tweet=$(urlencode "$full_tweet") - -param_string="oauth_consumer_key=${oauth_consumer_key}&oauth_nonce=${oauth_nonce}&oauth_signature_method=${oauth_signature_method}&oauth_timestamp=${oauth_timestamp}&oauth_token=${oauth_token}&oauth_version=${oauth_version}" - -signature_base="POST&$(urlencode "$API_URL")&$(urlencode "$param_string")" - -signing_key="$(urlencode "$X_API_SECRET")&$(urlencode "$X_ACCESS_TOKEN_SECRET")" - -oauth_signature=$(printf '%s' "$signature_base" | openssl dgst -sha1 -hmac "$signing_key" -binary | base64) - -auth_header="OAuth oauth_consumer_key=\"${oauth_consumer_key}\", oauth_nonce=\"${oauth_nonce}\", oauth_signature=\"$(urlencode "$oauth_signature")\", oauth_signature_method=\"${oauth_signature_method}\", oauth_timestamp=\"${oauth_timestamp}\", oauth_token=\"${oauth_token}\", oauth_version=\"${oauth_version}\"" - -response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL" \ - -H "Authorization: ${auth_header}" \ - -H "Content-Type: application/json" \ - -d "{\"text\": $(printf '%s' "$full_tweet" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}") - -http_code=$(echo "$response" | tail -1) -body=$(echo "$response" | sed '$d') - -if [[ "$http_code" == "201" ]]; then - tweet_id=$(echo "$body" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['data']['id'])" 2>/dev/null || echo "unknown") - echo "Tweet posted successfully!" - echo "https://x.com/hype_markdown/status/${tweet_id}" -else - echo "Error posting tweet (HTTP ${http_code}):" >&2 - echo "$body" >&2 - exit 1 -fi +param_string = "&".join( + f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(v, safe='')}" + for k, v in sorted(params.items()) +) +signature_base = f"POST&{urllib.parse.quote(url, safe='')}&{urllib.parse.quote(param_string, safe='')}" +signing_key = f"{urllib.parse.quote(api_secret, safe='')}&{urllib.parse.quote(access_token_secret, safe='')}" +signature = base64.b64encode( + hmac.new(signing_key.encode(), signature_base.encode(), hashlib.sha1).digest() +).decode() + +auth_header = ( + f'OAuth oauth_consumer_key="{urllib.parse.quote(api_key, safe="")}", ' + f'oauth_nonce="{urllib.parse.quote(oauth_nonce, safe="")}", ' + f'oauth_signature="{urllib.parse.quote(signature, safe="")}", ' + f'oauth_signature_method="HMAC-SHA1", ' + f'oauth_timestamp="{oauth_timestamp}", ' + f'oauth_token="{urllib.parse.quote(access_token, safe="")}", ' + f'oauth_version="1.0"' +) + +result = subprocess.run( + ["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url, + "-H", f"Authorization: {auth_header}", + "-H", "Content-Type: application/json", + "-d", json.dumps({"text": tweet})], + capture_output=True, text=True +) + +output = result.stdout.strip() +http_code = output.split("\n")[-1] +body = "\n".join(output.split("\n")[:-1]) + +if http_code == "201": + data = json.loads(body) + tweet_id = data["data"]["id"] + print("Tweet posted successfully!") + print(f"https://x.com/hype_markdown/status/{tweet_id}") +else: + print(f"Error posting tweet (HTTP {http_code}):", file=sys.stderr) + print(body, file=sys.stderr) + sys.exit(1) +PYEOF From d992da14b088f26e4968e96bed13bf01e35eb9b5 Mon Sep 17 00:00:00 2001 From: Cory LaNou Date: Mon, 16 Mar 2026 12:25:44 -0500 Subject: [PATCH 4/5] fix: correct author_twitter handle to @corylanou --- content/ai-authoring-workflow/module.md | 2 +- content/deploying-with-docker/module.md | 2 +- content/engineering-handbook/module.md | 2 +- content/release-notes-pipeline/module.md | 2 +- content/single-source-docs/module.md | 2 +- content/training-materials/module.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/content/ai-authoring-workflow/module.md b/content/ai-authoring-workflow/module.md index a33775e..f7e20b8 100644 --- a/content/ai-authoring-workflow/module.md +++ b/content/ai-authoring-workflow/module.md @@ -4,7 +4,7 @@ slug: ai-authoring-workflow published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Learn how to combine AI coding assistants like Claude with Hype's build-time validation to write technical documentation faster while keeping every code example correct. tags: tutorial, ai, authoring, workflow, claude, hype tweet: AI can write docs fast, but how do you trust the code examples? Combine Claude with Hype's build-time validation โ€” every snippet is verified before publish. diff --git a/content/deploying-with-docker/module.md b/content/deploying-with-docker/module.md index 7c9d22b..7aac676 100644 --- a/content/deploying-with-docker/module.md +++ b/content/deploying-with-docker/module.md @@ -4,7 +4,7 @@ slug: deploying-with-docker published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Deploy a Hype-powered blog site with Docker. Covers Dockerfile setup, Dokploy, Heroku, and generic VPS deployment with Docker Compose. tags: tutorial, docker, deployment, blog, hype tweet: Deploy a Hype-powered blog with Docker โ€” from Dockerfile to production. Covers Dokploy, Heroku, and Docker Compose setups. diff --git a/content/engineering-handbook/module.md b/content/engineering-handbook/module.md index 5dbaa41..0644ed3 100644 --- a/content/engineering-handbook/module.md +++ b/content/engineering-handbook/module.md @@ -4,7 +4,7 @@ slug: engineering-handbook published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Use Hype to build an internal engineering handbook that stays in sync with your codebase. Executable examples, automated validation, and single-source documentation for your team. tags: tutorial, engineering, handbook, automation, hype tweet: Your engineering handbook is probably wrong. Not because it was written badly โ€” the code just changed. Here's how to make docs a build artifact with Hype. diff --git a/content/release-notes-pipeline/module.md b/content/release-notes-pipeline/module.md index 7c97b3f..33437f1 100644 --- a/content/release-notes-pipeline/module.md +++ b/content/release-notes-pipeline/module.md @@ -4,7 +4,7 @@ slug: release-notes-pipeline published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Build a release notes pipeline using Hype with executable code snippets, automated validation, and version-aware documentation that ships with every release. tags: tutorial, release-notes, ci-cd, pipeline, hype tweet: Bad release notes erode trust. Build a pipeline where every code example compiles, every command runs, and every API ref is verified at build time. diff --git a/content/single-source-docs/module.md b/content/single-source-docs/module.md index a4e5a41..ec74c16 100644 --- a/content/single-source-docs/module.md +++ b/content/single-source-docs/module.md @@ -4,7 +4,7 @@ slug: single-source-docs published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Learn how to use Hype to maintain a single Markdown source that generates both your GitHub README and website documentation, keeping them permanently in sync. tags: tutorial, docs, workflow, hype tweet: Your README says one thing, your docs site says another. Write it once with Hype and generate both from a single source. diff --git a/content/training-materials/module.md b/content/training-materials/module.md index b116c4a..ccb3306 100644 --- a/content/training-materials/module.md +++ b/content/training-materials/module.md @@ -4,7 +4,7 @@ slug: training-materials published: 03/15/2026 author: Cory LaNou -author_twitter: @caborundrum +author_twitter: @corylanou seo_description: Learn how to use Hype to build reusable training and course materials with executable code examples, file includes, and modular content that stays in sync with your codebase. tags: tutorial, training, education, includes, hype tweet: Training materials go stale the moment code changes. Use Hype to build courses with executable examples that stay in sync with your codebase. From bcf0020b47cbb2755fbe0a1dcfc3209fa76aaf1d Mon Sep 17 00:00:00 2001 From: Cory LaNou Date: Mon, 16 Mar 2026 12:38:08 -0500 Subject: [PATCH 5/5] fix: address codex review findings (pass 1) - Use full push range instead of HEAD~1 to catch multi-commit pushes - Skip deleted files before reading frontmatter - Use --diff-filter=A to only tweet newly added posts, not edits --- .github/workflows/tweet.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tweet.yml b/.github/workflows/tweet.yml index 701a55d..dfaac72 100644 --- a/.github/workflows/tweet.yml +++ b/.github/workflows/tweet.yml @@ -13,17 +13,17 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 - - name: Find changed blog posts + - name: Find new blog posts id: changed run: | - changed=$(git diff --name-only HEAD~1 HEAD -- 'content/*/module.md' | grep -v '^content/docs' || true) + changed=$(git diff --name-only --diff-filter=A ${{ github.event.before }}..${{ github.sha }} -- 'content/*/module.md' | grep -v '^content/docs' || true) echo "files<> "$GITHUB_OUTPUT" echo "$changed" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" - - name: Tweet changed posts + - name: Tweet new posts if: steps.changed.outputs.files != '' env: X_API_KEY: ${{ secrets.X_API_KEY }} @@ -33,6 +33,10 @@ jobs: run: | echo "${{ steps.changed.outputs.files }}" | while IFS= read -r file; do if [[ -z "$file" ]]; then continue; fi + if [[ ! -f "$file" ]]; then + echo "Skipping $file โ€” file does not exist" + continue + fi slug=$(basename "$(dirname "$file")") tweet_field=$(awk '/
/{f=1;next} /<\/details>/{if(f)exit} f' "$file" | grep '^tweet:' | head -1) if [[ -z "$tweet_field" ]]; then