Use this if the user wants to convert a blog post from Google Docs markdown to the format used in the Home Assistant website.
Convert a draft markdown file into a properly formatted Home Assistant blog post.
There are two kinds of blog posts this skill handles:
Place your draft blog post markdown file in the project root create-blog-post/ directory (e.g., /workspaces/home-assistant.io/create-blog-post/), then run:
/create-blog-post
Automates conversion of a draft markdown file with metadata into a production-ready Home Assistant blog post:
### **ā Summary break / Read more ā** to <!--more--><a> tags with target="_blank"source/_posts/ with Jekyll front mattercreate-blog-post/ Directoryart.* - Hero/OG image (required, any common image format: .webp, .png, .jpg, .jpeg)image2.*, image3.*, etc. - Additional images (optional, any common image format)# Metadata
**Blog title:** Your Blog Title
**Author:** Author Name
**Publish date:** DD-MM-YYYY
**Category:** Category Name
**Social/OpenGraph title** (Usually same as the blog title, visibility mostly limited to 50-60 characters)**:**
A short title.
**Social/OpenGraph description** (120-158 characters):
Influences SEO ranking. Include the main keyword, describe what readers will find, and give them a clear reason to click.
# Blog notes/preparations
āļø Any lines with the pointer emoji can be removed during processing
# Blog content
![][image1]
Your intro paragraph here...
### **ā Summary break / Read more ā**
Rest of content...
Notes:
![][image1] reference should appear at the start of the "# Blog content" section. This will be replaced with the art.webp hero image.### **ā Summary break / Read more ā** marker will be converted to <!--more-->Creates a production-ready blog post at:
source/_posts/YYYY-MM-DD-slug.markdown - The formatted blog postsource/images/blog/YYYY-MM-slug/art.webp - OG/hero image (moved from create-blog-post/)source/images/blog/YYYY-MM-slug/image2.webp, image3.webp, etc. - Additional images (converted from PNGs)Before doing anything else, strip out embedded base64 image data from the draft file using a shell command. Do not read the draft file before this step ā the base64 data can make the file extremely large.
Google Docs markdown exports include image references like ![][image1] in the content body, with corresponding base64 definitions at the bottom of the file in the format:
[image1]: <data:image/png;base64,iVBORw0KGgo... (potentially megabytes of data)>
Run this sed command via the Bash tool to strip them in-place:
sed -i '/^\[image[0-9]*\]: <data:/d' "create-blog-post/draft.md"
![][image1] references in the content body are preserved ā they will be replaced with proper image paths later### **ā Summary break / Read more ā** marker to <!--more-->Before processing images, ensure the cwebp tool is installed. If not, install it:
# Check if cwebp is available, install if missing
which cwebp || sudo apt-get install -y webp
Hero image (art.*):
art image in create-blog-post/ (any extension: .webp, .png, .jpg, .jpeg).webp, copy it to source/images/blog/YYYY-MM-slug/art.webpcwebp -resize 1200 630 -q 85 input -o source/images/blog/YYYY-MM-slug/art.webp-resize 1200 630 to ensure correctness![][image1] reference in "# Blog content" section with: <img src="/images/blog/YYYY-MM-slug/art.webp" alt="Blog Title" style="border: 0;box-shadow: none;"><p> tag)Additional images (if any):
image2.*, image3.*, etc. in create-blog-post/ (any extension: .webp, .png, .jpg, .jpeg)cwebp -resize 900 0 -q 85 input -o output.webp (the 0 for height preserves the aspect ratio).webp, still re-encode it with the resize: cwebp -resize 900 0 -q 85 input.webp -o output.webpsource/images/blog/YYYY-MM-slug/image2.webp, image3.webp, etc.External links (different domains/subdomains):
<a href="URL" target="_blank" rel="noopener">text</a>my.home-assistant.io, partner.home-assistant.io, etc.Internal links (www.home-assistant.io only):
[text](/path)If the blog post category is Works-with-Home-Assistant, look for a section that lists certified/supported devices. Replace any manually written device list with the dynamic device list shortcode:
{% include integrations/device_list.html brand="brandname" %}
brand value must match a brand in source/_data/wwha_devices.json (for example: "eve", "heatit", "shelly", "zooz")## **Title** ā ## Title)#), demote all headings one level (content should start at H2)') and speech marks (") in prose ā including quoted statements, like in blockquotes ā to their curly equivalents (ā, ā/ā). Only apply this to body text ā never to HTML attribute values, URLs, code blocks/inline code, or Liquid/Jekyll syntax.source/_posts/YYYY-MM-DD-slug.markdown<!--more--> tag after first paragraphcreate-blog-post/:draft-partner-update.md - Your draft fileart.webp - OG/hero imageimage2.png, image3.png - Additional images (if any)/create-blog-postThis would create:
source/_posts/2026-01-13-partner-update.markdownsource/images/blog/2026-01-partner/art.webpsource/images/blog/2026-01-partner/image2.webp, image3.webp (if additional images exist)Image references:
![][image1] (at start of "# Blog content" section) ā Output: art.webp hero image (1200x630, OG image)![][image2] ā Look for image2.* (any format), convert to image2.webp (max 900px wide)![][image3] ā Look for image3.* (any format), convert to image3.webp (max 900px wide).webp, .png, .jpg, .jpeg) ā all are converted/re-encoded to .webpRequirements:
cwebp tool is required ā the skill will auto-install it via sudo apt-get install -y webp if not already presentContent processing:
### **ā Summary break / Read more ā** to <!--more-->Device lists (Works with Home Assistant posts):
Works-with-Home-Assistant) typically include a list of certified devices in the draft (as bullet points, tables, or similar){% include integrations/device_list.html brand="brandname" %}brand value must match an entry in source/_data/wwha_devices.json ā verify the brand exists in that file before using itOutput format:
YYYY-MM-DD-slug.markdownsource/images/blog/YYYY-MM-slug/Link handling:
www.home-assistant.io and home-assistant.io stay as Markdown links<a> tags with target="_blank" rel="noopener"A crosspost is a short blog entry that does not host the full article. Instead, it shows a teaser and redirects the reader to an article hosted on another site (for example, the Open Home Foundation blog). Use this path when the user asks to publish an externally linked blog, add a crosspost, or link out to an article on another site.
Unlike a standard blog post, a crosspost has no draft file and no local images to process:
post layout. It renders as a regular blog page showing the teaser, then redirects visitors to the external_url on page load with a small JavaScript redirect. The page also sets its canonical URL to the external_url, and comments are automatically disabled while an external_url is present.crosspost_og plugin derives og_image from the external_url using the Open Home Foundation dynamic Open Graph endpoint (https://assets.openhomefoundation.org/opengraph?url=<external_url>). This shows the social image and the blog archive thumbnail immediately ā even before the source article is live ā and it auto-updates once the article publishes, without a rebuild. Do not set og_image by hand unless you need to override it with a specific image.Before writing anything, gather the required details from the user with the ask-questions tool (vscode_askQuestions). Present these as a short wizard so the user can confirm each value. Ask for all of the following, pre-filling any values the user already provided in their request:
https://deploy-preview-87--open-home-foundation.netlify.app/blog/ā¦). A preview URL is useful for pulling the article details, but it must not be used as the final external_url. If they provide a preview URL, work out the final published URL (usually the same path on the live domain, such as https://www.openhomefoundation.org/blog/ā¦) and confirm it with the user before continuing.<!--more--> tag. If the user does not have one ready, offer to draft a short teaser from the article for their review.source/_data/people.yml. Verify it exists; if not, tell the user it must be added before publishing.YYYY-MM-DD format. Used for the filename, date, and date_formatted fields.Technology).external_url, so leave this blank in most cases. Ask only whether the user wants to override it with a specific image URL. If they provide one, set it as og_image in the front matter and the plugin will leave it untouched.If a source URL is available, fetch it to pre-fill as many of these details as possible (title, description, opening paragraph, author, date) so the user only has to confirm or correct them.
Confirm the collected values back to the user before creating the file.
source/_data/people.yml. If missing, stop and ask the user to add the author first.https:// (the OG image fetch only works over HTTPS).external_url is the final published URL, not a preview or deploy-preview link. If the user only supplied a preview URL, confirm the resolved live URL with them before using it. The og_image is derived from external_url by the plugin, so a preview URL here would produce the wrong image.Create source/_posts/YYYY-MM-DD-slug.markdown with this front matter and body:
---
layout: post
title: "Your crosspost title"
description: "Your Social/OpenGraph description."
date: YYYY-MM-DD 00:00:01
date_formatted: "Month DD, YYYY"
author: AuthorKey
comments: false
categories:
- Category
external_url: "https://example.com/full-article-url/"
external_source: "Source name"
# Optional: only add og_image to override the auto-generated social image.
# og_image: "https://example.com/custom-social-image.png"
---
Your opening teaser paragraph goes here, ending with the summary break tag.<!--more-->
Notes:
title, description, external_url, and external_source values in double quotes.date_formatted is the human-readable date, such as "June 18, 2026".author is the key from people.yml, not the display name.<!--more-->. Do not add the full article text ā the reader is redirected to the source.comments: false in the front matter. Comments are also disabled automatically whenever an external_url is set, but keeping the field makes the intent explicit.og_image field in the normal case. The crosspost_og plugin derives it from external_url automatically. Add og_image only when the user wants to override the generated image with a specific URL ā an explicit value always wins over the generated one.After creating the file, summarize for the user:
people.yml), date, and categoryexternal_url by the crosspost_og plugin, renders immediately (including the blog archive thumbnail), and auto-updates once the source article is live, with no rebuild requiredAfter the blog post has been created, output a summary to the user covering:
Metadata:
people.yml), date, categoryImages:
Content transformations:
Proposed text changes (requires user approval):