Inline CSS+JS assets
This commit is contained in:
parent
60cac8afe0
commit
83f1f39fbd
12 changed files with 195 additions and 1 deletions
2
CHANGELOG.md
Normal file
2
CHANGELOG.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# 1.0.0
|
||||
* Initial release.
|
45
README.md
45
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}');
|
||||
}
|
||||
|
||||
```
|
3
analysis_options.yaml
Normal file
3
analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-cast: false
|
25
example/main.dart
Normal file
25
example/main.dart
Normal 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
18
example/web/index.html
Normal 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>
|
3
example/web/not-inlined.css
Normal file
3
example/web/not-inlined.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
p {
|
||||
font-style: italic;
|
||||
}
|
3
example/web/not-inlined.js
Normal file
3
example/web/not-inlined.js
Normal 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
3
example/web/site.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
h1 {
|
||||
color: pink;
|
||||
}
|
3
example/web/site.js
Normal file
3
example/web/site.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
window.addEventListener('load', function() {
|
||||
console.log('Hello, inline world!');
|
||||
});
|
1
lib/angel_seo.dart
Normal file
1
lib/angel_seo.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/inline_assets.dart';
|
81
lib/src/inline_assets.dart
Normal file
81
lib/src/inline_assets.dart
Normal 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
9
pubspec.yaml
Normal 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
|
Loading…
Reference in a new issue