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