Introduction
Websites, blogs, and e-commerce platforms need to be dynamic, offering tools and components that enable rapid content creation with minimal effort. WordPress excels in this area by providing various tools, including plugins and shortcodes, which allow users to create dynamic content using these components.
However, the Rust ecosystem and the web platforms built with Rust generally lack tools for dynamic content creation without recompiling the entire application. The goal of this article is to bring the functionality of WordPress shortcodes to Rust, enabling them to be inserted into templates to display content or functionality provided by the shortcode.
Shortcodes
A WordPress shortcode is a simple code enclosed in square brackets, like "[shortcode]", that allows users to easily insert dynamic content into posts, pages, or widgets. Shortcodes can perform various functions, such as displaying galleries, embedding videos, or pulling in data from plugins like WooCommerce. They help users add complex features without needing to write or understand code, making content management more flexible and user-friendly.
For example, the shortcode below, taken from the Wordpress API page, shows a shortcode with a name ("myshortcode") and two attributes ("foo" and "bar"):
[myshortcode foo="bar" bar="bing"]
While it's possible to spend time implementing something similar in Rust by creating a template engine from scratch, we can achieve similar functionality by using an existing tool like Tera and building a custom function. Here's an example:
{{ shortcode(display="myshortcode", foo="bar", bar="bing") | safe }}
Example
Let's create a small prototype in Rust to demonstrate the concept using Tera and Axum.
cargo new app --bin
cd app
cargo add tokio -F rt-multi-thread
cargo add axum
cargo add tera -F builtins
mkdir templates
The structure of our project:
├── app/
├── plugins/
├── src/
└── templates/
Inside the templates folder, we'll now create a template to include our shortcode.
templates/test_shortcode.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Shortcode Test</title>
</head>
<body>
{{ shortcode(display="myshortcode", foo="bar", bar="bing") | safe }}
</body>
</html>
The following code demonstrates the implementation of our shortcode in an Axum web server. When run, it displays the shortcode's output.
src/main.rs
use axum::{
extract::{Extension, Request},
response::Html,
routing::get,
Router,
ServiceExt,
};
use tera::{Tera, Context, Result, Function};
use std::collections::HashMap;
struct Shortcodes;
impl Function for Shortcodes {
fn call(&self,
args: &HashMap<String, tera::Value>,
) -> Result<tera::Value> {
// Extract attributes
let display = args.get("display").unwrap().as_str().unwrap();
let fragment = match display {
"myshortcode" => {
let foo = match args.get("foo") {
Some(value) => value
.as_str()
.unwrap()
.trim_matches(|c| c == '"' || c == '\''),
None => "no foo",
};
let bar = match args.get("bar") {
Some(value) => value
.as_str()
.unwrap()
.trim_matches(|c| c == '"' || c == '\''),
None => "no bar",
};
format!("bar: {} foo: {}", foo, bar)
},
_ => panic!("Unknown shortcode display name: {:?}", display),
};
Ok(tera::Value::String(fragment))
}
}
async fn test(
Extension(tera): Extension<Tera>,
) -> Html<String> {
let context = Context::new();
// Render the template with the context
let rendered = tera
.render("test_shortcode.html", &context)
.unwrap();
Html(rendered)
}
#[tokio::main]
async fn main() {
let mut tera = Tera::new("templates/**/*").unwrap();
// Register the custom function
tera.register_function("shortcode", Shortcodes);
// Build our application with a route
let app = Router::new()
.route("/", get(|| async {
"Hello world!"
}))
.route("/test", get(test))
.layer(Extension(tera));
// Run the server
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
.await
.unwrap();
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}
Now, let's run the axum web server from the terminal and view the result.
cargo run
In a separate terminal, we'll use the curl command to test the shortcode, as demonstrated below.
curl http://127.0.0.1:8080/test
The output:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Shortcode Test</title>
</head>
<body>
bar: bar foo: bing
</body>
</html>
The example above demonstrates how to use a Tera function to simulate a shortcode in Axum. However, in most cases, shortcodes are used to display data from a database, similar to how WooCommerce shortcodes display products based on the shortcode's attributes.
A challenge arises when using the template engines available in Rust. Many of these engines, often based on Jinja, only support custom functions that work synchronously. This creates a conflict with Axum, which operates on an asynchronous, Tokio-based runtime.
Fortunately, there is a way to work around this issue. While it may not be a perfect solution, it's the best approach I've found so far, and I’ll explain it below.
The solution is to create a route in Axum that handles asynchronous communication with the database. We can pass the database pool to Axum as an extension within a layer, allowing it to process inputs asynchronously.
This route receives inputs from the Tera custom function, processes the request to retrieve the data, and then collects the results.
To handle asynchronous code in this context, we use the "tokio::task::block_in_place" function to run the asynchronous operation within a blocking context.
Note:
I tried using "reqwest::blocking::Client::new()" without async in "fetch_data" function and without "block_in_place" in the "call" function, but I received the following error message:
thread 'tokio-runtime-worker' panicked at /../../.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.39.3/src/runtime/blocking/shutdown.rs:51:21:
Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
src/main.rs
use axum::{
extract::{Extension, Request, Json},
response::Html,
routing::{get, post},
Router,
ServiceExt,
};
use serde::{Serialize, Deserialize};
use tera::{Tera, Context, Result, Function};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct DataTest {
foo: String,
bar: String,
}
const ADDRESS: &str = "127.0.0.1:8080";
struct Shortcodes;
impl Shortcodes {
async fn fetch_data(&self, data: &DataTest) -> String {
let url = format!("http://{}/data", ADDRESS);
// Convert the struct to json compact string
let json_body = serde_json::to_string(data).unwrap();
let client = reqwest::Client::new();
let response = client.post(url)
.header("Content-Type", "application/json")
.body(json_body)
.send()
.await
.unwrap();
// Check the response status
if response.status().is_success() {
response.text().await.unwrap()
} else {
format!("Request failed with status: {}", response.status())
}
}
}
impl Function for Shortcodes {
fn call(&self,
args: &HashMap<String, tera::Value>,
) -> Result<tera::Value> {
// Extract attributes
let display = args.get("display").unwrap().as_str().unwrap();
let fragment = match display {
"myshortcode" => {
let foo = match args.get("foo") {
Some(value) => value
.as_str()
.unwrap()
.trim_matches(|c| c == '"' || c == '\''),
None => "no foo",
};
let bar = match args.get("bar") {
Some(value) => value
.as_str()
.unwrap()
.trim_matches(|c| c == '"' || c == '\''),
None => "no bar",
};
// Use `block_in_place` to run the async function
// within the blocking context
let result = tokio::task::block_in_place(|| {
// We need to access the current runtime to
// run the async function
tokio::runtime::Handle::current()
.block_on(self.fetch_data(&DataTest {
foo: foo.to_string(),
bar: bar.to_string(),
}))
});
result
},
_ => panic!("Unknown shortcode display name: {:?}", display),
};
Ok(tera::Value::String(fragment))
}
}
// Handler function that returns JSON content
async fn data(
Json(payload): Json<DataTest>,
) -> Json<DataTest> {
let data = DataTest {
foo: format!("ok {}", payload.foo),
bar: format!("ok {}", payload.bar),
};
// Return the JSON response
Json(data)
}
async fn test(
Extension(tera): Extension<Tera>,
) -> Html<String> {
let context = Context::new();
// Render the template with the context
let rendered = tera
.render("test_shortcode.html", &context)
.unwrap();
Html(rendered)
}
#[tokio::main]
async fn main() {
let mut tera = Tera::new("templates/**/*").unwrap();
// Register the custom function
tera.register_function("shortcode", Shortcodes);
// Build our application with a route
let app = Router::new()
.route("/", get(|| async {
"Hello world!"
}))
.route("/test", get(test))
.route("/data", post(data))
.layer(Extension(tera));
// Run the server
let listener = tokio::net::TcpListener::bind(ADDRESS)
.await
.unwrap();
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}
Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7.5"
reqwest = { version = "0.12.7", features = ["json"] }
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127"
tera = { version = "1.20.0", features = ["builtins"] }
tokio = { version = "1.39.3", features = ["rt-multi-thread", "bytes"] }
To run the application and view the result:
cargo run
Open another terminal and use the curl command to view the result.
curl http://127.0.0.1:8080/test
The output:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Shortcode Test</title>
</head>
<body>
{"foo":"ok bar","bar":"ok bing"}
</body>
</html>
Conclusion
The advantage of shortcodes is that they can be placed anywhere in templates where their content needs to be displayed, without requiring the application to be recompiled. Additionally, they can be customized through attributes.
For instance, on a platform built with Axum and Tera, we could create a shortcode library with various functionalities and an API, enabling others to extend it with additional features.
Thank you for reading!
Top comments (0)