Skip to content

Commit 3b528e8

Browse files
committed
feat: add website card embeds and fix URL generation
- Add optional embed_url input to bluesky-post workflow - Fetch OG tags (title, description, image) from embed URL - Upload image as blob and create website card embed - Fix slug extraction to only use post name (not year)
1 parent d06c3a3 commit 3b528e8

File tree

2 files changed

+188
-20
lines changed

2 files changed

+188
-20
lines changed

.github/workflows/bluesky-post.yml

Lines changed: 157 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
description: 'The text content of the post. Supports markdown links [text](url) and hashtags.'
88
required: true
99
type: string
10+
embed_url:
11+
description: 'Optional URL to create a website card embed from (fetches OG tags)'
12+
required: false
13+
type: string
1014
secrets:
1115
BLUESKY_USERNAME:
1216
required: true
@@ -20,6 +24,7 @@ jobs:
2024
- name: Post to BlueSky
2125
env:
2226
POST_INPUT: ${{ toJSON(inputs.post_text) }}
27+
EMBED_URL: ${{ inputs.embed_url }}
2328
run: |
2429
# Authenticate with BlueSky
2530
echo "Authenticating with BlueSky..."
@@ -111,26 +116,160 @@ jobs:
111116
echo "Post text: $POST_TEXT"
112117
echo "Facets: $FACETS"
113118
119+
# Build embed if URL provided
120+
EMBED=""
121+
if [ -n "$EMBED_URL" ]; then
122+
echo "Fetching OG tags from: $EMBED_URL"
123+
124+
# Fetch page and extract OG tags using Python
125+
EMBED_DATA=$(python3 << 'PYEOF'
126+
import urllib.request
127+
import re
128+
import json
129+
import os
130+
import sys
131+
132+
url = os.environ.get('EMBED_URL', '')
133+
if not url:
134+
print(json.dumps({}))
135+
sys.exit(0)
136+
137+
try:
138+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
139+
with urllib.request.urlopen(req, timeout=10) as response:
140+
html = response.read().decode('utf-8', errors='ignore')
141+
142+
# Extract OG tags
143+
og_title = re.search(r'<meta[^>]*property=["\']og:title["\'][^>]*content=["\']([^"\']+)["\']', html, re.I)
144+
og_title = og_title.group(1) if og_title else ''
145+
146+
og_desc = re.search(r'<meta[^>]*property=["\']og:description["\'][^>]*content=["\']([^"\']+)["\']', html, re.I)
147+
og_desc = og_desc.group(1) if og_desc else ''
148+
149+
og_image = re.search(r'<meta[^>]*property=["\']og:image["\'][^>]*content=["\']([^"\']+)["\']', html, re.I)
150+
og_image = og_image.group(1) if og_image else ''
151+
152+
print(json.dumps({'title': og_title, 'description': og_desc, 'image': og_image}))
153+
except Exception as e:
154+
print(json.dumps({'error': str(e)}), file=sys.stderr)
155+
print(json.dumps({}))
156+
PYEOF
157+
)
158+
159+
OG_TITLE=$(echo "$EMBED_DATA" | jq -r '.title // empty')
160+
OG_DESC=$(echo "$EMBED_DATA" | jq -r '.description // empty')
161+
OG_IMAGE=$(echo "$EMBED_DATA" | jq -r '.image // empty')
162+
163+
echo "OG Title: $OG_TITLE"
164+
echo "OG Description: $OG_DESC"
165+
echo "OG Image: $OG_IMAGE"
166+
167+
if [ -n "$OG_TITLE" ]; then
168+
# Upload image as blob if present
169+
THUMB_JSON=""
170+
if [ -n "$OG_IMAGE" ]; then
171+
echo "Downloading image: $OG_IMAGE"
172+
curl -s -L -o /tmp/og_image "$OG_IMAGE"
173+
174+
if [ -f /tmp/og_image ] && [ -s /tmp/og_image ]; then
175+
# Detect mime type
176+
MIME_TYPE=$(file --mime-type -b /tmp/og_image)
177+
echo "Image MIME type: $MIME_TYPE"
178+
179+
# Upload blob
180+
echo "Uploading image blob..."
181+
BLOB_RESPONSE=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.uploadBlob \
182+
-H "Content-Type: $MIME_TYPE" \
183+
-H "Authorization: Bearer $ACCESS_TOKEN" \
184+
--data-binary @/tmp/og_image)
185+
186+
BLOB_REF=$(echo "$BLOB_RESPONSE" | jq -c '.blob')
187+
if [ "$BLOB_REF" != "null" ] && [ -n "$BLOB_REF" ]; then
188+
echo "Blob uploaded successfully"
189+
THUMB_JSON="$BLOB_REF"
190+
else
191+
echo "Failed to upload blob: $BLOB_RESPONSE"
192+
fi
193+
fi
194+
fi
195+
196+
# Build embed object
197+
if [ -n "$THUMB_JSON" ]; then
198+
EMBED=$(jq -n \
199+
--arg uri "$EMBED_URL" \
200+
--arg title "$OG_TITLE" \
201+
--arg desc "$OG_DESC" \
202+
--argjson thumb "$THUMB_JSON" \
203+
'{
204+
"$type": "app.bsky.embed.external",
205+
"external": {
206+
"uri": $uri,
207+
"title": $title,
208+
"description": $desc,
209+
"thumb": $thumb
210+
}
211+
}')
212+
else
213+
EMBED=$(jq -n \
214+
--arg uri "$EMBED_URL" \
215+
--arg title "$OG_TITLE" \
216+
--arg desc "$OG_DESC" \
217+
'{
218+
"$type": "app.bsky.embed.external",
219+
"external": {
220+
"uri": $uri,
221+
"title": $title,
222+
"description": $desc
223+
}
224+
}')
225+
fi
226+
echo "Embed created"
227+
fi
228+
fi
229+
114230
# Create the post using jq to properly escape JSON
115231
echo "Creating BlueSky post..."
116-
POST_RESPONSE=$(jq -n \
117-
--arg did "$DID" \
118-
--arg text "$POST_TEXT" \
119-
--arg timestamp "$TIMESTAMP" \
120-
--argjson facets "$FACETS" \
121-
'{
122-
repo: $did,
123-
collection: "app.bsky.feed.post",
124-
record: {
125-
text: $text,
126-
facets: $facets,
127-
createdAt: $timestamp,
128-
"$type": "app.bsky.feed.post"
129-
}
130-
}' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
131-
-H "Content-Type: application/json" \
132-
-H "Authorization: Bearer $ACCESS_TOKEN" \
133-
-d @-)
232+
if [ -n "$EMBED" ]; then
233+
POST_RESPONSE=$(jq -n \
234+
--arg did "$DID" \
235+
--arg text "$POST_TEXT" \
236+
--arg timestamp "$TIMESTAMP" \
237+
--argjson facets "$FACETS" \
238+
--argjson embed "$EMBED" \
239+
'{
240+
repo: $did,
241+
collection: "app.bsky.feed.post",
242+
record: {
243+
text: $text,
244+
facets: $facets,
245+
embed: $embed,
246+
createdAt: $timestamp,
247+
"$type": "app.bsky.feed.post"
248+
}
249+
}' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
250+
-H "Content-Type: application/json" \
251+
-H "Authorization: Bearer $ACCESS_TOKEN" \
252+
-d @-)
253+
else
254+
POST_RESPONSE=$(jq -n \
255+
--arg did "$DID" \
256+
--arg text "$POST_TEXT" \
257+
--arg timestamp "$TIMESTAMP" \
258+
--argjson facets "$FACETS" \
259+
'{
260+
repo: $did,
261+
collection: "app.bsky.feed.post",
262+
record: {
263+
text: $text,
264+
facets: $facets,
265+
createdAt: $timestamp,
266+
"$type": "app.bsky.feed.post"
267+
}
268+
}' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
269+
-H "Content-Type: application/json" \
270+
-H "Authorization: Bearer $ACCESS_TOKEN" \
271+
-d @-)
272+
fi
134273
135274
POST_URI=$(echo "$POST_RESPONSE" | jq -r '.uri')
136275

.github/workflows/detect-new-blog-post.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ on:
4444
post_hashtags:
4545
description: 'Categories as hashtags (e.g., #cat1 #cat2)'
4646
value: ${{ jobs.detect.outputs.post_hashtags }}
47+
post_description:
48+
description: 'Description of the new post'
49+
value: ${{ jobs.detect.outputs.post_description }}
50+
post_image_path:
51+
description: 'Local path to the cover image file'
52+
value: ${{ jobs.detect.outputs.post_image_path }}
4753

4854
jobs:
4955
detect:
@@ -53,6 +59,8 @@ jobs:
5359
post_title: ${{ steps.check.outputs.post_title }}
5460
post_url: ${{ steps.check.outputs.post_url }}
5561
post_hashtags: ${{ steps.check.outputs.post_hashtags }}
62+
post_description: ${{ steps.check.outputs.post_description }}
63+
post_image_path: ${{ steps.check.outputs.post_image_path }}
5664
steps:
5765
- name: Checkout
5866
uses: actions/checkout@v4
@@ -141,16 +149,37 @@ jobs:
141149
CATEGORIES=$(sed -n '/^---$/,/^---$/p' "$NEW_POST" | grep -E "^${CATEGORIES_FIELD}:" | sed "s/^${CATEGORIES_FIELD}:[[:space:]]*//" | tr -d '[]"' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/^/#/' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
142150
echo "Hashtags: $CATEGORIES"
143151
144-
# Extract slug from path: {content_path}/{year}/{slug}/{filename} -> {year}/{slug}
145-
SLUG=$(echo "$NEW_POST" | sed "s|${CONTENT_PATH}/||" | sed "s|/${POST_FILENAME}$||")
152+
# Extract slug from path: {content_path}/{year}/{slug}/{filename} -> {slug}
153+
SLUG=$(dirname "$NEW_POST" | xargs basename)
146154
147155
# Build full URL (remove trailing slash from site_url if present, ensure url_prefix starts with /)
148156
SITE_URL="${SITE_URL%/}"
149157
POST_URL="${SITE_URL}${URL_PREFIX}/${SLUG}/"
150158
151159
echo "URL: $POST_URL"
152160
161+
# Extract description from frontmatter (optional)
162+
DESCRIPTION=$(sed -n '/^---$/,/^---$/p' "$NEW_POST" | grep -E '^description:' | sed "s/^description:[[:space:]]*['\"]*//" | sed "s/['\"][[:space:]]*$//")
163+
echo "Description: $DESCRIPTION"
164+
165+
# Extract image path from frontmatter (optional, e.g., ./cover.png)
166+
IMAGE_REL=$(sed -n '/^---$/,/^---$/p' "$NEW_POST" | grep -E '^image:' | sed "s/^image:[[:space:]]*['\"]*//" | sed "s/['\"][[:space:]]*$//" | sed 's|^\./||')
167+
if [ -n "$IMAGE_REL" ]; then
168+
POST_DIR=$(dirname "$NEW_POST")
169+
IMAGE_PATH="$POST_DIR/$IMAGE_REL"
170+
if [ -f "$IMAGE_PATH" ]; then
171+
echo "Image path: $IMAGE_PATH"
172+
else
173+
echo "Image file not found: $IMAGE_PATH"
174+
IMAGE_PATH=""
175+
fi
176+
else
177+
IMAGE_PATH=""
178+
fi
179+
153180
echo "has_new_post=true" >> $GITHUB_OUTPUT
154181
echo "post_title=$TITLE" >> $GITHUB_OUTPUT
155182
echo "post_url=$POST_URL" >> $GITHUB_OUTPUT
156183
echo "post_hashtags=$CATEGORIES" >> $GITHUB_OUTPUT
184+
echo "post_description=$DESCRIPTION" >> $GITHUB_OUTPUT
185+
echo "post_image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT

0 commit comments

Comments
 (0)