-
Notifications
You must be signed in to change notification settings - Fork 0
feat(marketing): add automated blog-to-X tweet pipeline #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3dda082
feat(marketing): add blog-to-X distribution pipeline
corylanou 8612097
feat(marketing): add automated blog-to-X tweet pipeline
corylanou cd3c749
feat(seo): add Open Graph and Twitter Card support to all blog posts
corylanou d992da1
fix: correct author_twitter handle to @corylanou
corylanou bcf0020
fix: address codex review findings (pass 1)
corylanou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <content-slug>" | ||
| 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 '/<details>/{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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # Distribution Workflow | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```bash | ||
| ./marketing/distribute.sh <slug> | ||
| # or | ||
| make distribute SLUG=<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=<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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.