diff --git a/.gitignore b/.gitignore
index f3f3bdf..d9969c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@
/composer.lock
/vendor
/.php_cs.cache
+/static/[0-9]*
/tests/compiler.jar
diff --git a/HISTORY.md b/HISTORY.md
index 641efe0..9153d96 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,6 @@
## Version 3.0.0 (unreleased)
* The project root is now what is deployed as `min`
+* Adds feature to serve static files directly
* Installation requires use of Composer to install dependencies
* Removes JSMin+ (unmaintained, high memory usage)
* Removes DooDigestAuth
diff --git a/README.md b/README.md
index 83a22af..406b2c8 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@ The stats above are from a [brief walkthrough](http://mrclay.org/index.php/2008/
Relative URLs in CSS files are rewritten to compensate for being served from a different directory.
+## Static file serving
+
+Version 3 allows [serving files directly from the filesystem](static/README.md) for much better performance. We encourage you to try this feature.
+
## Support
Post to the [Google Group](http://groups.google.com/group/minify).
diff --git a/config.php b/config.php
index 1c1cd4f..02f01ca 100644
--- a/config.php
+++ b/config.php
@@ -7,6 +7,12 @@
*/
+/**
+ * Enable the static serving feature
+ */
+$min_enableStatic = false;
+
+
/**
* Allow use of the Minify URI Builder app. Only set this to true while you need it.
*/
diff --git a/docs/FAQ.wiki.md b/docs/FAQ.wiki.md
index 8142e6e..a091aab 100644
--- a/docs/FAQ.wiki.md
+++ b/docs/FAQ.wiki.md
@@ -4,7 +4,9 @@ The simple JSMin algorithm is the most reliable in PHP, but check the [CookBook]
## How fast is it?
-Certainly not as fast as an HTTPd serving flat files. On a high-traffic site:
+If you [serve static files](https://github.com/mrclay/minify/blob/master/static/README.md), it's as fast as your web server, and you should do this for high-traffic sites.
+
+The PHP-based server is not as fast, but still performs well thanks to an internal cache. Tips:
* **Use a reverse proxy** to cache the Minify URLs. This is by far the most important tip.
* Revision your Minify URIs (so far-off Expires headers will be sent). One way to do this is using [groups](UserGuide.wiki.md#using-groups-for-nicer-urls) and the [Minify_groupUri()](UserGuide.wiki.md#far-future-expires-headers) utility function. Without this, clients will re-request Minify URLs every 30 minutes to check for updates.
diff --git a/lib/Minify/Config.php b/lib/Minify/Config.php
index 9f1e8b8..ae5b58c 100644
--- a/lib/Minify/Config.php
+++ b/lib/Minify/Config.php
@@ -11,6 +11,11 @@ class Config
*/
public $enableBuilder = false;
+ /**
+ * @var bool
+ */
+ public $enableStatic = false;
+
/**
* @var bool
*/
diff --git a/static/.htaccess b/static/.htaccess
new file mode 100644
index 0000000..f9cc303
--- /dev/null
+++ b/static/.htaccess
@@ -0,0 +1,40 @@
+
+ ExpiresActive On
+ ExpiresDefault "access plus 1 year"
+
+
+
+ FileETag MTime Size
+
+
+
+ mod_gzip_on yes
+ mod_gzip_dechunk yes
+ mod_gzip_keep_workfiles No
+ mod_gzip_minimum_file_size 1000
+ mod_gzip_maximum_file_size 1000000
+ mod_gzip_maximum_inmem_size 1000000
+ mod_gzip_item_include mime ^text/.*
+ mod_gzip_item_include mime ^application/javascript$
+ mod_gzip_item_include mime ^application/x-javascript$
+ # Exclude old browsers and images since IE has trouble with this
+ mod_gzip_item_exclude reqheader "User-Agent: .*Mozilla/4\..*\["
+ mod_gzip_item_exclude mime ^image/.*
+
+
+
+ AddOutputFilterByType DEFLATE text/css text/javascript application/javascript application/x-javascript
+ BrowserMatch ^Mozilla/4 gzip-only-text/html
+ BrowserMatch ^Mozilla/4\.[0678] no-gzip
+ BrowserMatch \bMSIE !no-gzip
+
+
+
+RewriteEngine on
+
+# You may need RewriteBase on some servers
+#RewriteBase /min/static
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ gen.php [QSA,L]
+
diff --git a/static/README.md b/static/README.md
new file mode 100644
index 0000000..9b84527
--- /dev/null
+++ b/static/README.md
@@ -0,0 +1,85 @@
+
+# Static file serving
+
+**Note:** This feature is new and not extensively tested.
+
+Within this folder, Minify creates minified files on demand, serving them without the overhead of PHP at all.
+
+For example, when a visitor requests a URL like `/min/static/1467089473/f=js/my-script.js`, Minify creates the directories `1467089473/f=js`, and saves the minified file `my-script.js` in it. On following requests, the file is served directly.
+
+## Getting started
+
+1. Make sure the `static` directory is writable by your server.
+
+2. In `minify/config.php`, set `$min_enableStatic = true;`
+
+3. Request the test script http://example.org/min/static/0/f=min/quick-test.js
+
+ This will create a new cache directory within `static` and redirect the browser to the new location, e.g. http://example.org/min/static/1467089473/f=min/quick-test.js.
+
+ You should see the minified script and on the server the `static` directory should contain a new subdirectory tree with the static file. Following requests will serve the file directly.
+
+4. Delete the new subdirectory (e.g. `1467089473`) and refresh the browser.
+
+You should be redirected to the new location where the file and cache directory has been recreated.
+
+## Site integration
+
+You don't want to hardcode any URLs. Instead we'll use functions in `lib.php`:
+
+```php
+require_once __DIR__ . '/path/to/static/lib.php';
+
+$static_uri = "/min/static";
+$query = "b=scripts&f=1.js,2.js";
+$type = "js";
+
+$uri = Minify\StaticService\build_uri($static_uri, $query, $type);
+```
+
+If you release a new build (or change any source file), you *must* clear the cache by deleting the entire directory:
+
+```php
+require_once __DIR__ . '/path/to/static/lib.php';
+
+Minify\StaticService\flush_cache();
+```
+
+## URL rules
+
+As URLs result in files being created, they are more strictly formatted.
+
+* Arbitrary parameters (e.g. to bust a cache) are not permitted.
+* URLs must end with `.js` or `.css`.
+
+If your URL does not end with `.js` or `.css`, you'll need to append `&z=.js` or `&z=.css` to the URL. E.g.:
+
+* http://example.org/min/static/1467089473/g=home-scripts&z=.js
+* http://example.org/min/static/1467089473/f=styles.less&z=.css
+
+Note that `Minify\StaticService\build_uri` handles this automatically for you.
+
+URLs aren't canonical, so these URLs are all valid and will produce separate files:
+
+* http://example.org/min/static/1467089473/f=one/two/three.js
+* http://example.org/min/static/1467089473/b=one/two&f=three.js
+* http://example.org/min/static/1467089473/f=three.js&b=one/two&z=.js
+
+## Disable caching
+
+You can easily switch to use the regular `min/` endpoint during development:
+
+```php
+config->enableStatic) {
+ die('Minify static serving is not enabled. Set $min_enableStatic = true; in config.php');
+}
+
+require __DIR__ . '/lib.php';
+
+if (!is_writable(__DIR__)) {
+ http_response_code(500);
+ die('Directory is not writable.');
+}
+
+// parse request
+// SCRIPT_NAME = /path/to/minify/static/gen.php
+// REQUEST_URI = /path/to/minify/static/1467084520/b=path/to/minify&f=quick-test.js
+
+// "/path/to/minify/static"
+$root_uri = dirname($_SERVER['SCRIPT_NAME']);
+
+// "/1467084520/b=path/to/minify&f=quick-test.js"
+$uri = substr($_SERVER['REQUEST_URI'], strlen($root_uri));
+
+if (!preg_match('~^/(\d+)/(.*)$~', $uri, $m)) {
+ http_response_code(404);
+ die('File not found');
+}
+
+// "1467084520"
+$requested_cache_dir = $m[1];
+
+// "b=path/to/minify&f=quick-test.js"
+$query = $m[2];
+
+// we basically want canonical querystrings because we make a file for each one.
+// manual parsing is the only way to do this. The MinApp controller will validate
+// these parameters anyway.
+$get_params = array();
+foreach (explode('&', $query) as $piece) {
+ if (false === strpos($piece, '=')) {
+ $send_400();
+ }
+
+ list($key, $value) = explode('=', $piece, 2);
+ if (!in_array($key, array('f', 'g', 'b', 'z'))) {
+ $send_400();
+ }
+
+ if (isset($get_params[$key])) {
+ // already used
+ $send_400();
+ }
+
+ if ($key === 'z' && !preg_match('~^\.(css|js)$~', $value, $m)) {
+ $send_400();
+ }
+
+ $get_params[$key] = urldecode($value);
+}
+
+$cache_time = Minify\StaticService\get_cache_time();
+if (!$cache_time) {
+ http_response_code(500);
+ die('Directory is not writable.');
+}
+
+$app->env = new Minify_Env(array(
+ 'get' => $get_params,
+));
+$ctrl = $app->controller;
+$options = $app->serveOptions;
+$sources = $ctrl->createConfiguration($options)->getSources();
+if (!$sources) {
+ http_response_code(404);
+ die('File not found');
+}
+if ($sources[0]->getId() === 'id::missingFile') {
+ $send_400("Bad URL: missing file");
+}
+
+// we need URL to end in appropriate extension
+$type = $sources[0]->getContentType();
+$ext = ($type === Minify::TYPE_JS) ? '.js' : '.css';
+if (substr($query, - strlen($ext)) !== $ext) {
+ $send_301("$root_uri/$cache_time/{$query}&z=$ext");
+}
+
+// fix the cache dir in the URL
+if ($cache_time !== $requested_cache_dir) {
+ $send_301("$root_uri/$cache_time/$query");
+}
+
+$content = $app->minify->combine($sources);
+
+// save and send file
+$file = __DIR__ . "/$cache_time/$query";
+if (!is_dir(dirname($file))) {
+ mkdir(dirname($file), 0777, true);
+}
+
+file_put_contents($file, $content);
+
+header("Content-Type: $type;charset=utf-8");
+header("Cache-Control: max-age=31536000");
+echo $content;
diff --git a/static/lib.php b/static/lib.php
new file mode 100644
index 0000000..665bc13
--- /dev/null
+++ b/static/lib.php
@@ -0,0 +1,68 @@
+