Framework Readiness Investigation: Next.js, Eleventy, Nuxt
Context
Refrakt targets SvelteKit as its only framework. The Astro readiness investigation confirmed that the architecture is ready for a second framework. This document extends the analysis to three more: Next.js (React), Eleventy (vanilla SSG), and Nuxt (Vue) — covering the three largest remaining ecosystems for content-heavy sites.
Verdicts
| Framework | Verdict | Difficulty | Estimated New Code |
|---|---|---|---|
| Next.js (App Router + RSC) | Ready | Low | ~170 lines |
| Eleventy (11ty v3) | Ready | Low | ~145 lines |
| Nuxt (Vue 3 + Nitro) | Ready | Low-Medium | ~220 lines |
No blockers for any framework. No core architectural changes needed. Compare ~170-220 lines of new adapter code to ~15,000+ lines of framework-agnostic code being reused.
What's Already Framework-Agnostic (reusable by all three)
Same table as the Astro investigation — nothing has changed:
| Package | Status | Key Exports |
|---|---|---|
@refrakt-md/types | Fully agnostic | SerializedTag, RendererNode, ThemeConfig, RefraktConfig |
@refrakt-md/transform | Fully agnostic | createTransform(), layoutTransform(), renderToHtml() |
@refrakt-md/runes | Fully agnostic | 52 rune schemas, Markdoc tag specs |
@refrakt-md/content | Fully agnostic | loadContent(), ContentTree, Router, layout cascade |
@refrakt-md/behaviors | Fully agnostic | initRuneBehaviors(), initLayoutBehaviors(), registerElements(), 4 web components, RfContext |
@refrakt-md/highlight | Fully agnostic | Shiki-based syntax highlighting transform |
@refrakt-md/theme-base | Core agnostic | baseConfig (74 rune configs), defaultLayout, docsLayout, blogArticleLayout |
@refrakt-md/lumina CSS | Fully agnostic | Design tokens (base.css), 48 per-rune CSS files, index.css bundle |
Key architectural wins already in place
Empty component registry — All 74 runes render through identity transform. Interactive runes use web components or vanilla JS behaviors. Zero Svelte components needed for rendering.
renderToHtml()exists (packages/transform/src/html.ts) — A pure-JS function that rendersSerializedTag→ HTML string. Handles void elements,data-codeblockraw HTML, attribute escaping. This is the rendering strategy for all three frameworks.Layout transform implemented (
packages/transform/src/layout.ts) — DeclarativeLayoutConfigobjects replace Svelte layout components.layoutTransform()produces a complete page tree — any framework can render it.Behaviors are vanilla JS —
initRuneBehaviors()discovers elements by[data-rune]attributes, wires DOM event listeners, returns cleanup functions. No framework lifecycle required.Web components are standard custom elements —
rf-diagram,rf-nav,rf-map,rf-sandboxuseconnectedCallback, read fromRfContextstatic properties. Work in any HTML environment.
Shared Prerequisites (all frameworks)
Before building any adapter, extract two utilities from @refrakt-md/svelte that are already framework-agnostic:
1. serialize() → @refrakt-md/transform
Currently at packages/svelte/src/serialize.ts (24 lines). Converts Markdoc Tag class instances to plain SerializedTag objects. Zero Svelte imports — depends only on @markdoc/markdoc (already a transform dependency).
Move to packages/transform/src/serialize.ts. Re-export from @refrakt-md/svelte for backward compatibility.
2. matchRouteRule() → @refrakt-md/transform
Currently at packages/svelte/src/route-rules.ts (31 lines). Pure pattern matching against RouteRule[]. Zero Svelte imports — depends only on RouteRule type from @refrakt-md/types.
Move to packages/transform/src/route-rules.ts. Re-export from @refrakt-md/svelte for backward compatibility.
Estimated effort: ~20 lines of re-export glue
Eleventy (11ty v3)
Verdict: Ready
Eleventy is architecturally the most different — no Vite, no bundler, template-driven. But renderToHtml() was practically designed for template engines. This is the simplest integration of all three frameworks.
1. Integration Model: Global Data + Pagination
Eleventy's data cascade is the natural integration point. A global data file calls the entire refrakt pipeline at build time:
// _data/refrakt.js
import { loadContent } from '@refrakt-md/content';
import { createTransform, layoutTransform, renderToHtml } from '@refrakt-md/transform';
import { matchRouteRule } from '@refrakt-md/transform';
import { baseConfig, defaultLayout, docsLayout } from '@refrakt-md/theme-base';
const layouts = { default: defaultLayout, docs: docsLayout };
const routeRules = [
{ pattern: 'docs/**', layout: 'docs' },
{ pattern: '**', layout: 'default' },
];
export default async function () {
const site = await loadContent('./content');
const transform = createTransform(baseConfig);
return {
pages: site.pages.filter(p => !p.draft).map(page => {
const layoutName = matchRouteRule(page.url, routeRules);
const tree = layoutTransform(layouts[layoutName], page, 'rf');
return {
url: page.url,
title: page.title,
description: page.description,
seo: page.seo,
html: renderToHtml(tree),
pages: site.pages.map(p => ({ url: p.url, title: p.title })),
};
}),
};
}
Eleventy pagination creates one page per content item:
---
pagination:
data: refrakt.pages
size: 1
alias: page
permalink: "{{ page.url }}/index.html"
layout: base.njk
---
2. Rendering: Template Injection
The template receives pre-rendered HTML from the data file. No recursive component, no framework renderer:
{# _includes/base.njk #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ page.seo.og.title or page.title }}</title>
{% if page.description %}
<meta name="description" content="{{ page.description }}" />
{% endif %}
<link rel="stylesheet" href="/css/refrakt.css" />
</head>
<body>
{{ page.html | safe }}
<script type="module">
import { initRuneBehaviors, initLayoutBehaviors, registerElements, RfContext }
from '/js/behaviors.js';
RfContext.pages = {{ page.pages | dump | safe }};
RfContext.currentUrl = '{{ page.url }}';
registerElements();
initRuneBehaviors();
initLayoutBehaviors();
</script>
</body>
</html>