From 83f1f39fbd63b817433002eb852ef529a52aa71f Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 02:14:25 -0400 Subject: [PATCH] Inline CSS+JS assets --- CHANGELOG.md | 2 + README.md | 45 ++++++++++++++++++++- analysis_options.yaml | 3 ++ example/main.dart | 25 ++++++++++++ example/web/index.html | 18 +++++++++ example/web/not-inlined.css | 3 ++ example/web/not-inlined.js | 3 ++ example/web/site.css | 3 ++ example/web/site.js | 3 ++ lib/angel_seo.dart | 1 + lib/src/inline_assets.dart | 81 +++++++++++++++++++++++++++++++++++++ pubspec.yaml | 9 +++++ 12 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 analysis_options.yaml create mode 100644 example/main.dart create mode 100644 example/web/index.html create mode 100644 example/web/not-inlined.css create mode 100644 example/web/not-inlined.js create mode 100644 example/web/site.css create mode 100644 example/web/site.js create mode 100644 lib/angel_seo.dart create mode 100644 lib/src/inline_assets.dart create mode 100644 pubspec.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..66bca2d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 1.0.0 +* Initial release. \ No newline at end of file diff --git a/README.md b/README.md index 09970539..4c365cc7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ # seo -Helpers for building SEO-friendly Web pages in Angel. +Helpers for building SEO-friendly Web pages in Angel. The goal of +`package:angel_seo` is to speed up perceived client page loads, prevent +the infamous +[flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content), +and other SEO optimizations that can easily become tedious to perform by hand. + +## `inlineAssets` +This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends +`.html` files. + +In any `.html` file sent down, `link` and `script` elements that point to internal resources +will have the contents of said file read, and inlined into the HTML page itself. + +In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or +`foo/bar/baz.js`. + +```dart +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + +main() async { + var app = new Angel()..lazyParseBodies = true; + var fs = const LocalFileSystem(); + var http = new AngelHttp(app); + + var vDir = inlineAssets( + new VirtualDirectory( + app, + fs, + source: fs.directory('web'), + ), + ); + + app.use(vDir.handleRequest); + + app.use(() => throw new AngelHttpException.notFound()); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} + +``` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..0ac20d6b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-cast: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..8d7d82d1 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,25 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + +main() async { + var app = new Angel()..lazyParseBodies = true; + var fs = const LocalFileSystem(); + var http = new AngelHttp(app); + + var vDir = inlineAssets( + new VirtualDirectory( + app, + fs, + source: fs.directory('web'), + ), + ); + + app.use(vDir.handleRequest); + + app.use(() => throw new AngelHttpException.notFound()); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 00000000..50161197 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + Angel SEO + + +

Angel SEO

+

Embrace the power of inlined styles, etc.

+ + \ No newline at end of file diff --git a/example/web/not-inlined.css b/example/web/not-inlined.css new file mode 100644 index 00000000..ab2a12a3 --- /dev/null +++ b/example/web/not-inlined.css @@ -0,0 +1,3 @@ +p { + font-style: italic; +} \ No newline at end of file diff --git a/example/web/not-inlined.js b/example/web/not-inlined.js new file mode 100644 index 00000000..6a8ad492 --- /dev/null +++ b/example/web/not-inlined.js @@ -0,0 +1,3 @@ +window.addEventListener('load', function() { + console.log('THIS message was not from an inlined file.'); +}); \ No newline at end of file diff --git a/example/web/site.css b/example/web/site.css new file mode 100644 index 00000000..acbbda4b --- /dev/null +++ b/example/web/site.css @@ -0,0 +1,3 @@ +h1 { + color: pink; +} \ No newline at end of file diff --git a/example/web/site.js b/example/web/site.js new file mode 100644 index 00000000..283b1eff --- /dev/null +++ b/example/web/site.js @@ -0,0 +1,3 @@ +window.addEventListener('load', function() { + console.log('Hello, inline world!'); +}); \ No newline at end of file diff --git a/lib/angel_seo.dart b/lib/angel_seo.dart new file mode 100644 index 00000000..8288ada9 --- /dev/null +++ b/lib/angel_seo.dart @@ -0,0 +1 @@ +export 'src/inline_assets.dart'; \ No newline at end of file diff --git a/lib/src/inline_assets.dart b/lib/src/inline_assets.dart new file mode 100644 index 00000000..2e33b36f --- /dev/null +++ b/lib/src/inline_assets.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:dart2_constant/convert.dart'; +import 'package:file/file.dart'; +import 'package:html/dom.dart' as html; +import 'package:html/parser.dart' as html; +import 'package:path/path.dart' as p; + +VirtualDirectory inlineAssets(VirtualDirectory vDir) => new _InlineAssets(vDir); + +class _InlineAssets extends VirtualDirectory { + final VirtualDirectory inner; + + _InlineAssets(this.inner) + : super(inner.app, inner.fileSystem, + source: inner.source, + indexFileNames: inner.indexFileNames, + publicPath: inner.publicPath, + callback: inner.callback, + allowDirectoryListing: inner.allowDirectoryListing); + + @override + Future serveFile( + File file, FileStat stat, RequestContext req, ResponseContext res) async { + if (p.extension(file.path) == '.html') { + var contents = await file.readAsString(); + var doc = html.parse(contents, sourceUrl: file.uri.toString()); + + var linksWithRel = doc.head + ?.getElementsByTagName('link') + ?.where((link) => link.attributes['rel'] == 'stylesheet') ?? + []; + + for (var link in linksWithRel) { + if (link.attributes.containsKey('data-no-inline')) { + link.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(link.attributes['href']); + + if (uri.scheme.isEmpty) { + var styleFile = inner.source.childFile(uri.path); + + if (await styleFile.exists()) { + var style = new html.Element.tag('style') + ..innerHtml = await styleFile.readAsString(); + link.replaceWith(style); + } + } + } + } + + var scripts = doc + .getElementsByTagName('script') + .where((script) => script.attributes.containsKey('src')); + + for (var script in scripts) { + if (script.attributes.containsKey('data-no-inline')) { + script.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(script.attributes['src']); + + if (uri.scheme.isEmpty) { + var scriptFile = inner.source.childFile(uri.path); + if (await scriptFile.exists()) { + script.attributes.remove('src'); + script.innerHtml = await scriptFile.readAsString(); + } + } + } + } + + res + ..headers['content-type'] = 'text/html; charset=utf8' + ..buffer.add(utf8.encode(doc.outerHtml)); + return false; + } else { + return await inner.serveFile(file, stat, req, res); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..781443aa --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,9 @@ +name: angel_seo +dependencies: + angel_framework: ^1.0.0-dev + dart2_constant: ^1.0.0 + file: ^2.0.0 + html: ^0.13.0 + path: ^1.0.0 +dev_dependencies: + angel_static: ^1.3.0 \ No newline at end of file