pg_render is a PostgreSQL extension that lets you render Liquid templates with data from your queries. Generate HTML pages, emails, or any templated content directly in SQL.
select render(
'<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ text }}</p>
<strong>{{ author }}</strong>
</body>
</html>',
(select to_json(props) from (
select title, text, author
from posts where id = 1
) props)
);
→
<html>
<head>
<title>My First Post</title>
</head>
<body>
<h1>My First Post</h1>
<p>Hello world!</p>
<strong>Jane Doe</strong>
</body>
</html>
Installation
Download a pre-built PostgreSQL extension package from Releases for your PostgreSQL version (13–18).
# Ubuntu / Debian with PostgreSQL 18
wget https://github.com/mkaski/pg_render/releases/download/v0.1.3/pg_render-v0.1.3-pg18-amd64-linux-gnu.deb
dpkg -i pg_render-v0.1.3-pg18-amd64-linux-gnu.deb
Build from source
git clone https://github.com/mkaski/pg_render
cargo install --locked cargo-pgrx
cargo pgrx init
cargo pgrx run
Usage
-- PostgreSQL extension
create extension pg_render;
-- Serve /index using PostgREST
create function api.index() returns "text/html" as $$
select render(
'<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
<p>{{ text }}</p>
<strong>{{ author }}</strong>
</body>
</html>',
(select to_json(props) from (select title, text, author from posts where id = 1) props)
)
$$ language sql;
Docs
render(template, input)
Render a template with query results as context. Input can be a scalar value, a JSON object, or an array.
-- scalar value
select render('Total posts: {{ value }}', (select count(*) from posts));
->
Total posts: 42
-- row data
select render(
'{{ title }} by {{ author }}',
(select to_json(props) from (select title, author from posts where id = 1) props)
);
->
My First Post by Jane Doe
-- array
select render(
'{% for value in values %} {{ value }} {% endfor %}',
(select array(select title from posts))
);
-- saved template
select render(
(select template from templates where id = 'post'),
(select to_json(props) from (select title, author from posts where id = 1) props)
);
render_agg(template, input)
An aggregate function that renders a template per row and concatenates the results.
select render_agg('<li>{{ value }}</li>', title) from posts;
select render_agg(
'<article><h2>{{ title }}</h2><p>{{ text }}</p></article>',
props
) from (select title, text from posts) as props;
Template syntax
pg_render uses the Liquid templating language. Templates support variables, control flow, loops, and filters.
{{ title }}
{{ title | upcase }}
{{ content | truncate: 100 }}
{% if image_url != blank %}
<img src="{{ image_url }}">
{% endif %}
{% for row in rows %}
<li>{{ row.title }}</li>
{% endfor %}
See the full Liquid documentation for all available tags and filters.
Usage with PostgREST
pg_render works well with PostgREST to serve HTML pages directly from PostgreSQL.
Create a domain so PostgREST sets the correct Content-Type header:
create domain "text/html" as text;
Create a function that renders your page:
create function index() returns "text/html" as $$
select render(
(select template from templates where id = 'layout'),
json_build_object(
'title', 'My Site',
'content', (select render_agg(
(select template from templates where id = 'post'),
props
) from (select title, text from posts) as props)
)
);
$$ language sql;
# nginx.conf
location = / {
proxy_pass http://postgrest/rpc/index;
}
Examples
This is the pattern used to build this page and other pages
- pgrender.org — this page
- example.pgrender.org — demo blog with user interactivity and dynamic content (HTMX)