Blogging in Markdown syntax is both a writing pleasure and a performance gain. The declarative structure of Markdown allows for intent to be expressed succinctly.
However, one can sometimes desire to include raw HTML to fill the gaps not readily expressed by Markdown.
Today we will demonstrate how easy it is to create custom Markdown elements using Shortcodes.
Click to Tweet
The option to allow visitors to share snippets of a blog via a Click to Tweet
button is prevalent. Indeed, there are readily available plugins on WordPress and other blogging frameworks to streamline this task, as well as the clicktotweet website itself.
But what can you do if you are rolling your blog and don’t wish your visitors to be tracked via 3rd-party plugins?
The short answer is to go to the source and build the functionality yourself.
A quick search of Twitter’s documentation shows the underlying Intent API that powers this feature.
Essentially, what is needed is to attach a well-formed link to the HTML snippet to be tweeted. Twitter suggests using their javascript widget.js
to aid in this task, but it can be omitted, as you will see next.
http://twitter.com/intent/tweet?url=<permalink>&text=<content>&hashtags=<tags>
But remember, we are using Markdown syntax, and while it is true that you can embed raw HTML, this is not the authoring flow that fits my fancy.
A better option would be to express the click-to-tweet intent in Markdown.
Shortcodes
I first became aware of Shortcodes while experimenting with the static blogging framework Hugo.
"Shortcodes are snippets inside your content files calling built-in or custom templates." — Hugo documentation
Clicking the Tweet button results in the familiar submission form below.
Let us derive a solution to use a Tweet shortcode in our Markdown.
{{< tweet text="<text>" >}}
This Markdown element will be detected and be expanded to an HTML blockquote during publishing.
Detection & Substitution
The plan is to find and replace all occurrences of the custom shortcode with an appropriate HTML block of the form:
<blockquote>
<p>
text...
</p>
<p>
<a href="https://twitter.com/intent/tweet?url=<url>&text=<text>&hashtags=<tags>" target="_blank">
<img src="/twitter_logo.png" title="Click to tweet" />
</a>
</p>
</blockquote>
I think you would agree that embedding one or more instances of this blockquote in your Markdown would not lend itself to an elegant writing experience.
The remainder of this tutorial builds on the publish workflow, which you can find in a previous article introducing our blog.
Let’s first define a helper function to build the Tweet intent. Notice that both the text and hashtags are URI encoded.
@doc false
def tweet_intent(text, slug, tags) when is_list(tags) do
"https://twitter.com/intent/tweet?" <>
"url=https://gocassava.com/blog/#{slug}" <>
"&" <>
"text=#{URI.encode(truncate(text, 280))}" <>
"&" <>
"hashtags=#{Enum.join(Enum.map(tags, &URI.encode(String.replace(&1, " ", ""))), ",")}" <>
"&" <>
"via=cassavaexp"
end
Next, modify the publish function to account for the shortcode detection and expansion.
The only change is the to_html
function. The slug
and keywords
are included in the processing of the content.
@doc false
def publish(content) when is_bitstring(content) do
{frontmatter, body} = content |> split_file_content()
slug = get_value(frontmatter, "slug")
keywords = get_value(frontmatter, "keywords")
Multi.new()
|> Multi.insert(
:post,
Post.changeset(
%Post{},
%{
author: get_value(frontmatter, "author"),
banner_image: get_value(frontmatter, "banner_image"),
content: to_html(body, slug: slug, keywords: keywords),
description: get_value(frontmatter, "description"),
featured: get_value(frontmatter, "featured"),
published_date: get_value(frontmatter, "date") |> DateTime.from_iso8601() |> elem(1),
title: get_value(frontmatter, "title")
}
)
)
end
defp to_html(body, opts) do
body
|> expand_shortcodes(:tweet, opts)
|> Earmark.as_html(compact_output: true)
|> case do
{:ok, data, _} ->
data
{:error, reason} ->
nil
end
end
A regex scan is performed to identify all the Tweet custom codes. The text in each code is captured and used to build the HTML blockquote. Finally, the two results are zipped, and a replacement operation is performed on the original content.
defp expand_shortcodes(content, :tweet, opts) do
snippets =
~r/{{< tweet text=(?:".+") >}}/
|> Regex.scan(content)
|> List.flatten()
block_quotes =
snippets
|> Enum.map(&Regex.named_captures(~r/text=(?<text>.+) >}}/, &1))
|> Enum.map(&(blockquote_tweet(&1, opts) |> String.replace(" ", "")))
content
|> replace_shortcode(Enum.zip(snippets, block_quotes))
end
defp blockquote_tweet(%{"text" => text}, opts) do
pruned_text =
text
|> String.replace_prefix("\"", "")
|> String.replace_suffix("\"", "")
|> String.replace("\\", "")
"""
<blockquote>
<p>#{pruned_text}</p>
<p>
<a href="#{StringHelper.tweet_intent(pruned_text, opts[:slug], opts[:keywords])}" target="_blank">
<img src="#{@assets_url}/assets/twitter_logo.png" title="Click to tweet" />
</a>
</p>
</blockquote>
"""
end
defp replace_shortcode(content, [{snippet, block_quote} | remainder]),
do:
content
|> String.replace(snippet, block_quote)
|> replace_shortcode(remainder)
defp replace_shortcode(content, []), do: content
So there you have it.
A quick demonstration of how you can roll your custom Markdown shortcodes to encapsulate the complexity of raw HTML and maintain elegant-looking and concise Markdown.
This template makes it straightforward to support other shortcodes by adding to the expand_shortcodes
function.
Your feedback is welcomed as always.
Michael
You no doubt have an opinion bubbling to the surface.