Inline CSS+JS assets

This commit is contained in:
Tobe O 2018-07-09 02:14:25 -04:00
parent 60cac8afe0
commit 83f1f39fbd
12 changed files with 195 additions and 1 deletions

2
CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
# 1.0.0
* Initial release.

View file

@ -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}');
}
```

3
analysis_options.yaml Normal file
View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-cast: false

25
example/main.dart Normal file
View file

@ -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}');
}

18
example/web/index.html Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="site.css">
<link rel="stylesheet" href="not-inlined.css" data-no-inline>
<script src="site.js"></script>
<script src="not-inlined.js" data-no-inline></script>
<title>Angel SEO</title>
</head>
<body>
<h1>Angel SEO</h1>
<p>Embrace the power of inlined styles, etc.</p>
</body>
</html>

View file

@ -0,0 +1,3 @@
p {
font-style: italic;
}

View file

@ -0,0 +1,3 @@
window.addEventListener('load', function() {
console.log('THIS message was not from an inlined file.');
});

3
example/web/site.css Normal file
View file

@ -0,0 +1,3 @@
h1 {
color: pink;
}

3
example/web/site.js Normal file
View file

@ -0,0 +1,3 @@
window.addEventListener('load', function() {
console.log('Hello, inline world!');
});

1
lib/angel_seo.dart Normal file
View file

@ -0,0 +1 @@
export 'src/inline_assets.dart';

View file

@ -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<bool> 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') ??
<html.Element>[];
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);
}
}
}

9
pubspec.yaml Normal file
View file

@ -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