Zero-build privacy policies with Astro

OpenPolicy has updated its Astro integration to allow direct policy compilation in frontmatter, eliminating the need for intermediate files and complex plugin configurations.
When we launched the OpenPolicy Astro integration in March, it worked by generating Markdown files at build time. You added the plugin to astro.config.mjs, pointed it at an output directory, and Astro imported the generated .md files as components.
// astro.config.mjs — old approach
import { openPolicy } from "@openpolicy/astro";
export default defineConfig({
integrations: [
openPolicy({
formats: ["markdown"],
outDir: "src/generated/policies",
}),
],
});
It worked. But it added friction: an extra package, a .gitignore entry for the generated directory, and a file-watching step between your config and your page.
The core library now compiles policies directly. You can call it straight from Astro’s frontmatter — no integration, no generated files.
Install
bun add @openpolicy/sdk @openpolicy/core @openpolicy/renderers
No Astro integration needed. No Vite plugin. Three packages.
Define your config
Create src/lib/openpolicy.ts:
import { defineConfig } from "@openpolicy/sdk";
export default defineConfig({
company: {
name: "Acme",
legalName: "Acme, Inc.",
// ...
},
privacy: {
effectiveDate: "2026-04-01",
dataCollected: { /* ... */ },
jurisdictions: ["us", "eu"],
// ...
},
cookie: {
effectiveDate: "2026-04-01",
cookies: [ /* ... */ ],
// ...
},
terms: {
effectiveDate: "2026-04-01",
governingLaw: { jurisdiction: "Delaware, USA" },
// ...
},
});
The fastest way to fill this out is to paste your existing privacy page into Claude and ask it to generate the config. Because the output is deterministic — the same config always produces the same policy — Claude is only configuring, not writing legal text. You review the inputs, OpenPolicy handles the rest.
Render on a dedicated page
Each page finds its policy from the config, compiles it, and renders to HTML — all in the frontmatter:
---
// src/pages/privacy.astro
import { compile, expandOpenPolicyConfig } from "@openpolicy/core";
import { renderHTML } from "@openpolicy/renderers";
import openpolicy from "../lib/openpolicy";
const policies = expandOpenPolicyConfig(openpolicy);
const privacyPolicy = policies.find((p) => p.type === "privacy");
if (!privacyPolicy) {
throw new Error("Privacy policy not found in config");
}
const policy = renderHTML(compile(privacyPolicy));
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Privacy Policy</title>
</head>
<body>
<div set:html={policy} />
</body>
</html>
expandOpenPolicyConfig splits the unified config into individual policies. compile runs each through its section builders. renderHTML converts the result to an HTML string. set:html renders it without escaping.
Everything runs at build time. Zero JavaScript ships to the browser.
Why this is simpler than the old approach
The plugin approach generated intermediate files that lived in your src/ directory. This one doesn’t:
- No generated files. No
src/generated/directory to create, manage, or exclude from git. - No plugin configuration.
astro.config.mjsstays empty. NooutDir, noformats, no watched paths. - No extra package. Drop
@openpolicy/astro— onlycoreandrenderersare needed at runtime. - All three policy types. Privacy, terms, and cookie policies compile from one config.
The output is the same as before: statically rendered HTML with no client-side JavaScript. The difference is that the compilation happens inline, in the page that uses it, rather than as a side-effect of a plugin watching your filesystem.
Going further with OpenPolicy+
If you need more than static generation, OpenPolicy+ extends the core library with cloud-based consent tracking, PR automation (policy linting, compliance checks on every pull request), and hands-on onboarding from our team.
Source: Hacker News
















