Blog Automation Process and Experience Optimization – Part 2

✍🏼 Written on Aug 14, 2022    💡 Updated on Aug 25, 2022
❗️ Note: it has been days since this article was written, please be aware of its timeliness
🖥  Note:The blog experience has reached new heights!
📚  Also published on Craft: https://www.craft.do/s/BJ8gIm5fmtdOCB

Preface

Previously, I set up a blog automation workflow and later a 2022 blog automation workflow. However, there were still several pain points:

  1. Uploading images pulled from Craft to Tencent Cloud via Github CI often encountered network errors, making the process unreliable.

  2. When images were uploaded to Tencent Cloud for the blog, they were forcibly converted to png, but more formats like gif should be supported.

  3. The Markdown output converted from Craft had many inconsistencies with standard Markdown, such as:

    1. Nested lists were not supported.

    2. When lists were nested inside blockquotes, the rendered result placed the lists outside the blockquote.

    3. In Craft, “focus” blocks and “block” (focus) were treated differently—only the latter was recognized as a blockquote, while both should have been.

    4. Craft’s bookmarks contained rich metadata like page descriptions and titles, but after conversion to Markdown, only a link wrapped in a paragraph remained, resulting in information loss.

    5. Craft itself did not support image captions.

    6. Code blocks generated by Craft did not automatically include the language identifier after backticks, causing some languages to be misidentified as plaintext without syntax highlighting.

To address these issues, I’ve made yet another round of optimizations.

Optimization 1: Using a Custom CraftBlockToMarkdown Function

After reporting to Craft that their craft.markdown.craftBlockToMarkdown conversion was incorrect:

Wrong render of markdown with Decoration focus and image question Decoration block ‘focus’ should be rendered as ‘blockquote,’ but currently, the markdown API `craftBlockToMarkdown` (flavor ‘common’) ignores this block type and renders it as plain text. Additionally, the image URIs returned by `craftBlockToMarkdown` are accessible everywhere, which may cause issues. I hope there could be a site whitelist setting to restrict image usage in documents to only approved sites. This would also help reduce AWS data transfer costs. https://forum.developer.craft.do/t/wrong-render-of-markdown-with-decoration-focus-and-image-question/235

However, the official team showed no intention of fixing it, so I wrote a simple function to handle the conversion manually:

craftBlockToMarkdown GitHub Gist: instantly share code, notes, and snippets. https://gist.github.com/Xheldon/036d9b187bd83303205001e8af97eda7

Note that this function is tailored for my Jekyll workflow (see Optimization 3 below), so it specially processes imageBlock and urlBlock block types:

Image

I’ve shared this function on the official plugin developer forum for discussion:

A better implementation of craft blocks to Markdown transformation methods Craft’s markdown API has the following issues: 1. Nested lists are not supported. 2. The order of nested blockquotes is incorrect. 3. Only the latter is recognized as a reference block. 4. `urlBlock` (possibly called ‘bookmark’?) is rendered as a plain link (in this function, you can customize the Jekyll render tag as needed). Note: 1. Toggle/formula blocks are unsupported since Markdown doesn’t support them either, but you can implement it. 2. I use this function for my Jekyll blog... https://forum.developer.craft.do/t/a-better-implementation-of-craft-blocks-to-markdown-transformation-methods/554

Optimization 2: Moving the Github CI Workflow Locally

In the previous workflow, clicking the plugin’s publish button (after filling in the GitHub Token, COS details, etc.) would trigger the process, and then you’d just wait.

For this optimization, to maintain consistency with the previous workflow, I created another Craft plugin. When clicked, it fetches the document content, performs simple processing (such as extracting image copyright information from headerImg into the meta), and then calls a specific URL with the document information passed as parameters:

1
2
3
craft.editorApi.openURL(
`xhelper://${btoa(unescape(encodeURIComponent(content)))}`
);

The purpose of this specific URL is to invoke an Application written in Apple Script. For details on how to write an Application and respond to URL calls, refer to this document I wrote. The Application then calls Node.js to execute the same code previously run in Github CI. Here’s a simple screenshot:

Xhelper截图👆🏻

Result:

Image

In the previous workflow, for simplicity, I converted all images to the png format because Craft-uploaded images might not always have extensions (e.g., images uploaded via drag-and-drop or file upload have extensions, while those pasted or web-uploaded do not). This time, I removed that logic: if an image has an extension, it retains it; if not, it’s forcibly converted to png. I highly recommend using the Sharp library—it’s extremely handy:

sharp - High performance Node.js image processing "Resize large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF, and AVIF images of varying dimensions" https://sharp.pixelplumbing.com/

So… here’s a GIF!

支持gif啦

Note: Previously, for convenience (no authentication, no need for a fixed-IP server), I used WeChat Cloud Hosting and even implemented logic to send summaries to a WeChat public account. However, the catch is that this server was only set up for publishing to the public account, which I typically use once every three months. Each time, it’s a cold start (their server shuts down instances by default after 30 minutes of inactivity since billing is based on instance runtime), leading to frequent failures. Debugging this took more time than manually copying and pasting content into WeChat’s editor. So, I gave up on the public account and opted for manual pasting… which works just fine!

Optimization 3: Implementing Bookmark and Image Caption in Craft

Achieving this effect in a Jekyll-based blog system is possible because I don’t directly use Github’s Jekyll service. Instead, I build the HTML with Jekyll locally and then push it to the repository. For the rationale and process, see here.

Bookmark and image caption rendering leverage special Jekyll tags generated in the previous step, combined with custom Jekyll plugins. Bookmarks are rendered using render_bookmark, and image captions are implemented with render_caption (written in Ruby, which I learned in just two hours):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module Jekyll
class RenderBookMarkBlock < Liquid::Block
def initialize(tag_name, attr, tokens)
super
attrs = attr.scan(/url\=\"(.*)\"\stitle\=\"(.*)\"\simg\=\"(.*)\"/)
@url = attrs[0][0]
@title = attrs[0][1]
@img = attrs[0][2]
@error = ""
end
def render(context)
@desc = super
"<p><a class='link-bookmark' href='#{@url}'><span><img src='#{@img}'/></span><span><span>#{@title}</span><span>#{@desc}</span><span>#{@url}</span></span></a></p>"
end

end
end

module Jekyll
class RenderImageCaptionBlock < Liquid::Block
def initialize(tag_name, attr, tokens)
super
attrs = attr.scan(/caption\=\"(.*)\"\simg\=\"(.*)\"/)
@caption = attrs[0][0]
@img = attrs[0][1]
end
def render(context)
text = super
"<p caption='#{@caption}'><img src='#{@img}' alt='#{@caption}' title='#{@caption}' /></p>"
end

end
end

Liquid::Template.register_tag('render_caption', Jekyll::RenderImageCaptionBlock)
Liquid::Template.register_tag('render_bookmark', Jekyll::RenderBookMarkBlock)

Using plugins in Jekyll is straightforward: create a _plugins directory in the root, place the Jekyll plugin written in ruby inside, and it will load during Jekyll builds.

Other Details

The previous content was standard Markdown-generated HTML, so RSS readers fetched it with proper formatting. However, after adding Bookmarks, the RSS format broke (screenshot from Reeder 5):

Reeder5中Bookmark格式错乱

To fix this, I wrote another Ruby plugin to filter the tags and convert them into regular HTML links:

1
2
3
4
5
6
7
8
9
module Jekyll
module BookmarkFilter
def bookmark_filter(input)
input.gsub(/^\<p\>\<a\s+class=\"link-bookmark\"\shref=(.*)\starget=\"_blank\"\>\<span\>(.*)\<\/span\>\<span\>\<span\>(.*)\<\/span\>\<span\>\n(.*)\n\<\/span\>\<span\>(.*)\<\/span\>\<\/span\>\<\/a\>\<\/p\>$/, '<p><a href=\1 target="_blank">\4</a></p>');
end
end
end

Liquid::Template.register_filter(Jekyll::BookmarkFilter)

Then, simply use it in feed.xml: post.content | bookmark_filter.

Feel free to discuss!

- EOF -
Originally published at: Blog Automation Process and Experience Optimization – Part 2 - Xheldon Blog