DEV Community

Cover image for Axum/Tera & Real Shortcodes in Rust: A WordPress-Like Implementation
Henrique Dias
Henrique Dias

Posted on • Updated on

Axum/Tera & Real Shortcodes in Rust: A WordPress-Like Implementation

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"]
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The structure of our project:

├── app/
    ├── plugins/
    ├── src/
    └── templates/
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Now, let's run the axum web server from the terminal and view the result.

cargo run
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The output:

<!DOCTYPE html>
<html lang="en">
<head>
<title>Shortcode Test</title>
</head>
<body>

bar: bar foo: bing

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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.

Diagram of how it works

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();
}
Enter fullscreen mode Exit fullscreen mode

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"] }
Enter fullscreen mode Exit fullscreen mode

To run the application and view the result:

cargo run
Enter fullscreen mode Exit fullscreen mode

Open another terminal and use the curl command to view the result.

curl http://127.0.0.1:8080/test
Enter fullscreen mode Exit fullscreen mode

The output:

<!DOCTYPE html>
<html lang="en">
<head>
<title>Shortcode Test</title>
</head>
<body>

{"foo":"ok bar","bar":"ok bing"}

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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)