hydraline_server 0.0.10
hydraline_server: ^0.0.10 copied to clipboard
SSR server for Hydraline - streaming, caching, HTMX helpers. Shelf/Dart Frog middleware, bot-aware transport, ETag/304, byte-identical anti-cloaking bodies. Pairs with hydraline core. No Flutter dependency.
hydraline_server #
Part of Hydraline - real crawlable HTML for Flutter Web. Three packages, one toolkit.
hydraline(core) ·hydraline_server(SSR, you are here) ·hydraline_flutter(widgets)
Pure-Dart SSR - streaming, caching, and HTMX helpers for shelf and Dart Frog. No Flutter dependency. Works on any Dart server.
Turn a DocumentNode tree into an HTTP response: real HTML in view-source
for crawlers, chunked streaming for humans, byte-identical bodies (anti-cloaking
by architecture). Add SEO to one route, keep the rest on your existing server.
Why #
SEO without a server is SSG-only — you ship static files. Add this package and
you get server-side rendering: the same DocumentNode tree streams from a
shelf/Dart Frog handler, with real HTTP semantics.
- Anti-cloaking, proven. Builders physically cannot see
User-Agent. Bots and humans get byte-identical HTML bodies — verified by CI test. - ETag, 304, cache-control. Deterministic FNV-1a hash over rendered HTML,
If-None-Matchrevalidation, configurable TTL, cache-key query normalisation. - Bot-aware transport. Buffered (Content-Length) for crawlers when cache is on; chunked streaming for humans. HEAD semantics, redirects (301/302/303/307/ 308/410), X-Robots-Tag per route type.
- HTMX helpers. First-class
HtmxResponsewith trigger/retarget/reswap headers, CRLF validation, fragment rendering, client-side redirect support. - Zero Flutter dependency. Works on any Dart server, not just behind a Flutter Web app. Use it for API doc sites, landing pages, micro-frontends.
What's inside #
| Module | Description |
|---|---|
hydralineMiddleware() |
Route-matching shelf middleware - document / hybrid / app modes, builder dispatch, redirect handling |
HydralineConfig |
Configuration: manifest, builders, cache, TTL, bot UA pattern |
DocumentBuilder |
Builder typedef - receives Request + matched RouteEntry as data. Architecturally UA-blind |
ResponseDelivery |
Buffered (bots) and chunked streaming (users) from a DocumentNode tree |
HydralineCache |
Pluggable cache interface + InMemoryCache with configurable maxSize and maxEntryBytes byte cap |
Http |
Status helpers: redirect() (301/302/303/307/308), notFound(), gone(), withRobots(), path canonicalization |
RedirectException |
Redirect from inside a builder - .gone() (410) and custom status |
Htmx / HtmxResponse |
Fragment rendering, HX-Trigger, HX-Retarget, HX-Reswap, HX-Redirect with CRLF validation |
HtmxTrigger |
Trigger helper: bare event name or {event: detail} JSON |
Assets |
robots.txt, sitemap.xml, L0-L1 JS serving, Flutter asset injection with escaped baseHref |
DartFrogAdapter |
Drop-in adapter for Dart Frog servers |
Automatic behaviours #
- HEAD handling - same status + headers as GET, empty body
- ETag / 304 - deterministic 64-bit FNV-1a hash over the rendered HTML,
If-None-Matchrevalidation (RFC 9110) - Cache-Control / Vary -
max-agefrom TTL,Vary: Accept-Encoding - X-Robots-Tag -
noindexforapproutes,nofollowfrom route metadata - Cache-key normalisation -
?a=1&b=2and?b=2&a=1share one entry - Bot-aware transport - buffered (Content-Length) when cache is configured or bot UA matched; chunked for humans
- Anti-cloaking - builders physically cannot see
User-Agent. Byte-identical bodies verified by CI test - HTMX header safety - CRLF rejected at construction (response-splitting prevention)
Quick start #
import 'package:hydraline/hydraline.dart';
import 'package:hydraline_server/hydraline_server.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
DocumentNode home(Request req, Object? data) => DocumentRootNode(
head: buildHead(const SeoMeta(title: 'Home')),
body: const [
HeadingNode(level: 1, children: [TextNode('Home')]),
],
);
Future<void> main() async {
final handler = const Pipeline()
.addMiddleware(hydralineMiddleware(HydralineConfig(
manifest: RouteManifest.builder()
.route(const RouteEntry(path: '/', mode: RouteMode.document))
.build(),
builders: {'/': home},
botUserAgentPattern: RegExp(r'Googlebot|bingbot'),
cache: HydralineCache.inMemory(maxSize: 500),
cacheTtl: const Duration(minutes: 5),
)))
.addHandler((req) => Response.ok('app shell'));
// Asset endpoints: robots.txt, sitemap.xml, vanilla/htmx JS
final assets = Assets.serveCoreAssets();
await io.serve((req) {
final path = req.url.path;
if (path == 'robots.txt' || path == 'sitemap.xml' || path.endsWith('.js')) {
return assets(req);
}
return handler(req);
}, 'localhost', 8080);
}
curl -N http://localhost:8080/ # chunked streaming (humans)
curl -A Googlebot http://localhost:8080/ # buffered (bots) - same bytes
curl -I http://localhost:8080/ # HEAD → 200, etag, content-type
curl -H "If-None-Match: \"...\"" ... # 304 Not Modified
Proven #
- 107 unit/integration tests - route matching, cache lifecycle, ETag/304, HEAD, redirects, SSR invariants (anti-cloaking byte-identity), HTMX headers
- SSR-invariant CI: bot vs human bodies are byte-identical - proven by test
- Cache-key normalisation: query-parameter ordering doesn't fragment the cache
- HEAD semantics: same status and headers as GET, empty body
Runnable example: example/main.dart - SSR, streaming,
bot-aware delivery, caching and HTMX endpoint in one file.
Documentation #
- Server Guide - full setup, streaming, HTMX, caching
- Configuration - route manifest, SEO
- Architecture - SSR flow, bot-aware delivery
- Security - cloaking prevention, CSP, header injection
- Getting Started
License #
MIT - Yevhen Leonidov