From Code to Pages: Building a Clean Author Website with Quarkus Roq
Learn how Java developers can use Roq, Qute templates, and Bulma CSS to design a fast, static author site without touching Node.js.
Quarkus Roq is a static site generator on top of Quarkus and Qute. It gives you type-safe templates, a fast dev loop, and a zero-Node build. That’s perfect for product sites, blogs, or an author homepage. We’ll start from “no-code,” add a clean Bulma layout via Web Bundler, create a books showcase from YAML data, and finish by exporting a static site you can host anywhere. Roq includes the Web Bundler, so you don’t install Node or NPM.
Why Roq for static sites in Java teams:
Quarkus dev experience, speed, and CI/CD friendliness.
Qute templates with type safety, so template errors fail at build time.
No Node toolchain to keep in sync. (Roq)
We’ll build:
Home page with author bio.
“Books” page that lists titles from a YAML file.
Bulma styling bundled by Web Bundler.
Static export to
target/roq/
for GitHub Pages, Netlify, or any CDN.
Prerequisites
Java 17+
Quarkus CLI (optional but convenient)
Roq runs on Quarkus; you don’t need Node. The Web Bundler uses esbuild under the hood and is already wired by Roq. (Quarkiverse Docs)
Bootstrap the project
quarkus create app org.acme:roq-author-website \
--extension='roq' \
--no-code
cd roq-author-website
--no-code
gives you a clean slate. The Roq extension brings Qute, Markdown, and Web Bundler integration. In dev mode you’ll see the site on http://localhost:8080
If you can’t wait, the full source code with a little more Java Script magic for the navigation on smaller view-ports is on my Github repository.
Add Bulma via mvnpm
Add Bulma as an mvnpm dependency so Web Bundler can import it directly in SCSS. Roq already brings Web Bundler transitively. Just add the following to your pom.xml
<!-- Bulma via mvnpm (no Node). Web Bundler will import it --> <dependency>
<groupId>org.mvnpm</groupId>
<artifactId>bulma</artifactId>
<version>1.0.4</version>
<scope>provided</scope>
</dependency>
Bulma 1.0.4 is published as an mvnpm artifact and can be imported from SCSS.
Project structure for Roq
Roq scans a simple site tree under the project root (or src/main
).
Layouts and Partials: Building Blocks for Pages
Roq sites are structured around layouts and partials. Understanding their relationship is essential to keeping templates clean and reusable.
Layouts are the skeleton of your page. They define the common structure: the
<html>
wrapper,<head>
, navigation, and footer.Partials are smaller reusable fragments. Think of them as Lego bricks you can slot into layouts or pages.
In your project:
templates/layouts/base.html
is the outer shell with the<html>
tag and main sections.templates/layouts/page.html
extendsbase.html
and adds a content wrapper with a page title.templates/partials/head.html
,navbar.html
, andfooter.html
are included into layouts to avoid duplication, despite some more advantages.
When a page like content/books.html
is rendered:
Roq loads the layout defined in its FrontMatter (
layout: page
).That layout pulls in partials for head, navbar, and footer.
The page content is inserted where
{#insert/}
is declared in the layout.
This hierarchy keeps pages lightweight. Instead of repeating navigation in every file, you just reference the layout and focus on what’s unique for that page. For this example project, the file system layout looks like this:
roq-author-website/
├── content/
│ ├── books.html
│ └── index.html
├── data/
│ └── books.yml
├── public/
│ └── images/*
└── src/
└── main/
└── resources/
├── application.properties
├── templates/
│ ├── layouts/
│ │ ├── base.html
│ │ └── page.html
│ └── partials/
│ ├── footer.html
│ ├── head.html
│ └── navbar.html
└── web/
└── app/
├── index.js
└── style.scss
Roq expects content/
for pages, templates/
for Qute layouts/partials, data/
for JSON/YAML you can inject with type safety, and public/
for static assets.
If you follow along with this tutorial, make sure to grab the public directoy from the Github repository as it contains some assets.
FrontMatter: Metadata for Content Pages
If layouts are the skeleton and partials the bricks, FrontMatter is the label on the box. It’s a short block of metadata at the top of each content file, written in YAML. Roq reads this metadata and makes it available to templates.
Take a look at content/books.html
:
---
title: Books
layout: page
description: Selected works and publications.
---
title
is used inpage.html
to display a heading.layout
tells Roq which layout template to wrap around this page.description
for SEO meta tags.
Other examples of useful FrontMatter:
date
for blog posts.tags
for categorization.
You can define anything. Roq injects those values into the page
object inside your template, so {page.title}
or {page.description}
work out of the box.
The best practice is:
Use FrontMatter for per-page metadata.
Use
data/
YAML or JSON for shared structured data (like books).Let layouts and partials decide how metadata is displayed.
Wire Bulma with Web Bundler
Create src/main/resources/web/app/style.scss
: This is enough so the web bundler knows to create a bundle.
(the example in my Github repository has some more styles. Just as a warning.)
/* Import Bulma from mvnpm */
@import "bulma/css/bulma.css";
/* Your tiny customization */
.section {
padding-top: 2rem;
padding-bottom: 2rem;
}
We will also add src/main/resources/web/app/index.js
because I want to add some more Java Script features for the full demo (compare Github).
// Import the stylesheet so Web Bundler includes it in the build output.
import './style.scss';
// Optional place for small enhancements later.
console.log('Roq Author Website loaded');
Web Bundler discovers entry files under src/main/resources/web/app/
and bundles what you import. We’ll inject the resulting JS/CSS into our HTML with the {#bundle/}
tag.
Qute templates and Bulma layout
We’ve now set up our project structure, and it’s time to wire Qute templates with Bulma styling. Each file plays a role in shaping the look and feel of your author website.
templates/partials/head.html
This partial holds everything that belongs inside the <head>
of the page: the title, meta tags, and asset includes. Notice the {#bundle/}
tag. That’s Roq’s way of injecting Web Bundler’s output—hashed CSS and JS files that browsers can cache safely. By isolating the <head>
into a partial, you avoid duplication across multiple layouts.
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{page.title ?: site.title}</title>
<meta name="description" content="{page.description ?: 'Author website'}">
{#bundle /}
</head>
templates/partials/navbar.html
Navigation is a classic candidate for a partial. Instead of copying the same <nav>
into every layout, you place it once here and include it where needed. We use Bulma’s navbar classes to get a responsive menu with minimal markup. When you add new sections, you’ll only update this one file.
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{site.url('/')}">Author</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
data-target="navMenu"><span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span></a>
</div>
<div id="navMenu" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{site.url('/books')}">Books</a>
</div>
</div>
</nav>
templates/partials/footer.html
The footer is another reusable element. It’s simple now, but it’s a great place for copyright notices, dates, or even social links. We’ll extend it later with Qute virtual methods so the year updates automatically. Keeping it as a partial lets every page stay in sync.
<footer class="footer">
<div class="content has-text-centered">
<p>© {site.title ?: 'Author Website'} — Built with Roq</p>
</div>
</footer>
templates/layouts/base.html
Think of this file as the skeleton of your site. It defines the outer HTML structure, includes the head and navbar, and provides a {#insert/}
placeholder where child layouts or pages can drop their content. By separating “structure” from “content,” you gain consistency and flexibility.
<!DOCTYPE html>
<html lang="en">
{#include partials/head /}
<body>
{#include partials/navbar /}
{#insert /}
{#include partials/footer /}
</body>
</html>
templates/layouts/page.html
This layout builds on top of base.html
. It adds a title section and a dedicated content wrapper styled with Bulma’s content
class. When a page specifies layout: page
in its FrontMatter, Roq knows to use this layout. That way you can define other specialized layouts later (for blog posts, landing pages, etc.) without touching the base skeleton.
---
layout: base
---
<section class="hero is-small">
<div class="hero-body">
<div class="container">
<h1 class="title">{page.title}</h1>
{#if page.description}
<p class="subtitle">{page.description}</p>
{/if}
</div>
</div>
</section>
<section class="section">
<div class="container content">
{#insert /}
</div>
</section>
Content pages
With the building blocks in place, we can finally write the pages visitors will see. In Roq, content lives under the content/
folder, and each file represents a page on your site. These pages are lightweight by design: they carry just enough metadata in the FrontMatter to describe themselves, and then they focus entirely on the unique content they need to show.
content/index.html
---
title: Markus Eisele — Author
layout: page
description: Writing on enterprise Java, Quarkus, and the future of AI-powered software.
---
<div class="columns is-vcentered">
<div class="column is-two-thirds">
<p class="subtitle">
Exploring the craft of modern software — from enterprise Java and Quarkus to AI-infused applications.
</p>
<p>
Find selected work below, or browse the <a href="{site.url('/books')}">books</a>.
</p>
<div class="buttons">
<a class="button is-link" href="{site.url('/books')}">Explore Books</a>
</div>
</div>
<div class="column">
<figure class="image is-3by4 book-cover">
<img src="{site.image('applied_ai_for_enterprise_java.jpeg')}" alt="Author">
</figure>
</div>
</div>
content/books.html
---
title: Books
layout: page
description: Selected works and publications.
---
<div class="columns is-multiline is-variable is-books-grid">
{#for b in cdi:books.list}
<div class="column is-6-mobile is-4-tablet is-3-desktop">
<div class="card is-compact">
<div class="card-image">
<!-- 2:3-ish shape via Bulma helpers and our cover CSS -->
<figure class="image is-2by3 book-cover">
<img src="{site.image(b.cover ?: 'images/cover-default.png')}" alt="{b.title}">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-6">{b.title}</p>
<p class="subtitle is-7">{b.pyear}</p>
</div>
</div>
<div class="content">
{b.tagline}
</div>
{#if b.buy}
<a class="button is-link is-light is-fullwidth" href="{b.buy}" target="_blank" rel="noopener">
Buy
</a>
{/if}
</div>
</div>
</div>
{/for}
</div>
For the homepage (index.html
), that means a short author bio and a clean layout with an image. For the books page (books.html
), the real work is looping over the YAML data we defined earlier and rendering each book as a Bulma card. Thanks to Qute, the templates stay readable. Loops, conditions, and variable expressions blend naturally into the HTML without breaking its structure.
By combining layouts, partials, and content pages, we now have a site that feels cohesive and professional, while still being easy to extend with new sections or styles.
Book data
Static sites are strongest when content and presentation are kept separate. Instead of hardcoding book titles into HTML, we store them in a YAML file under data/
. This way the data is structured, reusable, and easy to update. Roq automatically exposes YAML and JSON files as CDI beans inside templates, so looping over a list of books feels natural in Qute. If you add a new book later, you only update the YAML file without template changes. It’s a clean separation that scales nicely as your catalog grows.
data/books.yml
list:
- title: "Applied AI for Enterprise Java Development"
year: 2025
tagline: "Enter this clear-cut, no-nonsense guide to integrating generative AI into your Java enterprise ecosystem."
cover: "applied_ai_for_enterprise_java.jpeg"
buy: "https://www.oreilly.com/library/view/applied-ai-for/9781098174491/"
- title: "Modernizing Enterprise Java"
year: 2021
tagline: "This practical book helps developers examine long-established Java-based models and demonstrates how to bring these monolithic applications successfully into the future."
cover: "modernizing_enterprise_java.jpeg"
buy: "https://www.amazon.com/Modernizing-Enterprise-Java-Concise-Developers/dp/1098102142"
- title: "Modern Java EE Design Pattern"
year: 2016
tagline: "Bridge that gap by building microservice-based architectures on top of Java EE.."
cover: "modern_javaee_designpattern.png"
buy: "https://www.oreilly.com/library/view/modern-java-ee/9781492042266/"
- title: "Developing Reactive Microservices"
year: 2016
tagline: "People, resilience, and the cost of downtime."
cover: "developing_reactive_microservices.jpeg"
buy: "https://www.oreilly.com/library/view/developing-reactive-microservices/9781491975640/"
Site configuration
Not everything belongs in templates or content. Some settings, like which paths should be exported when generating a static site, are best handled in configuration. Roq respects Quarkus conventions, so you put these settings in application.properties
. Most defaults will work out of the box, but having a central place for adjustments means you can refine the build later without touching templates. It’s the glue that connects your content, layouts, and the final static export process. For this example, we really don’t need to do anything here. Just some suggestions for now:
src/main/resources/application.properties
# Optional: customize the Web Bundler bundle name if you introduce multiple entry-points later
# quarkus.web-bundler.bundle.main=true
# Roq Generator defaults are fine; we’ll use them in the static export section
# quarkus.roq.generator.paths=/, /static/**, /books/
Web Bundler auto-bundles everything under web/app/
and Roq Generator can export any configured paths. (Quarkiverse Docs)
Run and verify
./mvnw quarkus:dev
Open: http://localhost:8080/ and browse books http://localhost:8080/books
You should see Bulma styling and a four-column card grid. If styles don’t load, confirm the {#bundle/}
tag is present in head.html
and that style.scss
imports Bulma correctly. (Quarkiverse Docs)
Adding Qute Global Variables for Date and Year
Static sites often need dynamic touches: the current year in a footer, or today’s date on a page. Hardcoding these values means revisiting your templates every year. With Qute you can define global variables, Java functions that templates can call as if they were built-in.
Why virtual methods
Keep templates clean and declarative.
Eliminate repetitive boilerplate.
Leverage Java’s type safety instead of scattering logic in HTML.
Define the extension
Create src/main/java/org/acme/qute/GlobalVariables.java
:
package org.acme;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import io.quarkus.qute.TemplateGlobal;
@TemplateGlobal
public class GlobalVariables {
/**
* Return the current date formatted as yyyy-MM-dd
*/
static String date() {
return LocalDate.now().format(DateTimeFormatter.ISO_DATE);
}
/**
* Return the current year
*/
static int year() {
return LocalDate.now().getYear();
}
}
Key detail: Global variables must be static.
Use in templates
Open templates/partials/footer.html
and update it:
<footer class="footer">
<div class="content has-text-centered">
<p>© {year} — Built with Roq</p>
<p>Generated on {date}</p>
</div>
</footer>
When the page renders, {year}
expands to the current year and {date}
prints today’s date.
Run and verify again
./mvnw quarkus:dev
Open: http://localhost:8080/ and browse books http://localhost:8080/books
Generate a static site
Roq includes the “Roq Generator.” It renders your pages and copies assets to a static directory. That’s ideal for GitHub Pages or any CDN.
./mvnw package quarkus:run -DskipTests
Output goes to target/roq/
. You can serve it locally or push to Pages/Netlify. You can also configure which paths to export through quarkus.roq.generator.paths
and programmatically add dynamic ones with RoqSelection
if needed. (Quarkiverse Docs)
Extend the site
Add author posts under
content/posts/
(Roq supports Markdown out of the box). (Roq)Split bundles per page with Web Bundler entry-points when you add heavier JS. (Quarkiverse Docs)
Use Roq plugins (tags, series, etc.) if you grow into a blog. (Quarkus)
Theme later by adding a Roq theme dependency and overriding layouts. (Roq)
Roq gives Java teams a clean, type-safe path to static sites without a separate web stack. Ship your words, not a toolchain.