Hugo is great in many ways. However as a static site generator it doesn’t offer much to embed external content in your pages. As of Hugo 93 this is still an unsolved “problem”.

Luckily the one tool it does provide, getJson, is pretty powerful. I’ll be using this function to call a proxy which fetches the page and returns it as JSON.

Why

Ultimately remote content won’t be critical to a static site, however it’s useful to embed the latest versions of code snippets. This is especially useful when you’re building your site in CI.

To demonstrate this solution in action, here’s the weather in Vienna, AT today (from wttr.in) – [Vienna: ⛅️ +21°C ]

This is done in my content as a shortcode:

{{ remote "http://wttr.in/Vienna?format=3" }}

Of course, it isn’t truly dynamic. The weather is only rendered at build time (for my site, that’s once per day). Also this example isn’t so compelling since wttr.in provides a JSON api anyway. Instead, here’s some markdown from my site’s GitLab.

{{ remote "https://git.quinncasey.com/qcasey/garden.quinncasey.com/-/raw/master/docs/contrived-example-markdown.md" }}

I removed the %% percents within the brackets to show this as code. This renders as:

This is a L3 header loaded from a remote URL (contrived-example-markdown.md)

Hello, remote world!

Hugo Shortcode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{/*
  remote
  Fetch and render remote content from a JSON proxy

  @author @qcasey

  @access public

  @example - Go Template
    {{< remote "https://git.quinncasey.com/qcasey/lineageos-patches/-/raw/main/README.md" >}}
*/}}
{{- $remote_url := (.Get 0) -}}
{{- $remote_data := getJSON "https://hugo-remote-url-proxy.quinn934.workers.dev/fetch?url=" $remote_url -}}
{{- with $remote_data -}}
    {{ $remote_data.body | safeHTML }}
{{- end -}}

The first “variadic argument” is my Cloudflare Worker url. You’re welcome to use my worker but it would leave you susceptible to Rick Rolls.

Deploying Your Own

I used Cloudflare Workers to accomplish this without deploying anything, since it’s free and the code is believably simple. You could also deploy this locally since it’s only required when building hugo.

Cloudflare Serverless Function

 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
addEventListener("fetch", (event) => {
  event.respondWith(
    handleRequest(event.request).catch(
      (err) => new Response(err.stack, { status: 500 })
    )
  );
});

/**
 * Many more examples available at:
 *   https://developers.cloudflare.com/workers/examples
 * @param {Request} request
 * @returns {Promise<Response>}
 */
async function handleRequest(request) {
  const url = new URL(request.url);

  if (url.pathname.startsWith("/fetch")) {
    const proxy_url = url.searchParams.get("url");
    if (!proxy_url || !proxy_url.length) return new Response("Must have a ?url= parameter.", { status: 500 })
    const response = await fetch(proxy_url);
    const body = await response.text();
    return new Response(JSON.stringify({ response: response, body: body }), {
      headers: { "Content-Type": "application/json" },
    });
  }

  return fetch("https://quinncasey.com");
}

As a response, it returns both the proxied request’s response (status code, etc) and the body:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "response": {
        "webSocket": null,
        "url": "https://git.quinncasey.com/qcasey/beets-config/-/raw/main/beets-config.yaml",
        "redirected": false,
        "ok": true,
        "headers": {},
        "statusText": "OK",
        "status": 200,
        "bodyUsed": true,
        "body": {
            "locked": true
        }
    },
    "body": "..."
}

I simply display the body, with a few filters depending on the expected content.

Markdown and Code

See Hugo’s notes about shortcodes with and without future rendering. If your remote should be rendered as markdown, use %% instead of <> inside the shortcode.

If the remote is code, you can highlight it by wrapping the remote shortcode with a highlight.

Final thoughts

You should also trust the URL you’re rendering, since any arbitrary code could be added maliciously.

This could probably be done as a Hugo module, but this took 5 minutes instead. This hack is only needed until hugo implements a similar replacement.

Although with this shortcode you could add some neat frontend tricks, like a little badge or card that shows where the content is coming from.