├── .gitignore ├── README.md ├── composer.json ├── public ├── .htaccess └── index.php └── views ├── footer.twig ├── header.twig ├── index.twig └── nav.twig /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.swp 4 | /.vagrant 5 | /.idea 6 | /vendor 7 | composer.lock 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Develop cacheable sites by levering HTTP 2 | This piece of example code uses the [Silex](http://silex.sensiolabs.org/) framework to illustrate how you can leverage HTTP to develop cacheable sites. 3 | 4 | The code uses the following HTTP concepts: 5 | 6 | * The use of `Cache-Control` headers using directives like `Public`, `Private` to decide which HTTP responses are cacheable and which are not 7 | * The use of `Cache-Control` headers using directives like `Max-Age` and `S-Maxage` to determine how long HTTP responses can be cached 8 | * `Expires` headers as a fallback to control the *time-to-live* 9 | * Cache variations based on the `Vary` header 10 | * Conditional requests based on the `Etag` header 11 | * Returning an `HTTP 304` status code when content was successfully revalidated 12 | * Content negotiation and language selection based on the `Accept-Language` header 13 | * Block caching using [Edge Side Includes](https://www.w3.org/TR/esi-lang) and [HInclude](http://mnot.github.io/hinclude/) 14 | 15 | ## Cacheable 16 | 17 | The output that this example code generates is highly cacheable. We don't keep track of state using cookies and the proper `Cache-Control` headers are used to store the output in an HTTP cache. 18 | 19 | If a reverse caching proxy (like [Varnish](https://www.varnish-cache.org/)) is installed in front of this application, it will respect the *time-to-live* that was set by the application. 20 | 21 | Reverse caching proxies will also create cache variations by respecting the `Vary` header. A separate version of the response is stored in cache per language. 22 | 23 | Non-cacheable content blocks will not cause a full miss on the page. These content blocks are loaded separately using *ESI* or *HInclude*. Either of those techniques load blocks as a subrequest. 24 | 25 | *ESI* tags are rendered by the reverse proxy, *HInclude* tags are loaded client-side by the browser. If the code notices that there's no *reverse caching proxy* in front of the application, it will render the output inline, without ESI. 26 | 27 | 28 | > When you test this code, please have a look at the HTTP request headers and HTTP response headers. 29 | > That's where the magic happens. 30 | 31 | ## How to install 32 | 33 | The minimum PHP version requirement is `PHP 5.5.9`. All dependencies are loaded via [Composer](https://getcomposer.org), the PHP package manager. Dependency definition happens in the [composer.json](/composer.json) file: 34 | 35 | ```json 36 | { 37 | "require": { 38 | "silex/silex": "^2.0", 39 | "twig/twig": "^1.27", 40 | "symfony/twig-bridge": "^3.1", 41 | "symfony/translation": "^3.1" 42 | } 43 | } 44 | ``` 45 | 46 | Run `composer install` to install these dependencies. They will be stored in the *vendor* folder. They are bootstrapped in [index.php](/public/index.php) via `require_once __DIR__ . '/../vendor/autoload.php'; 47 | ` 48 | 49 | If you use *Apache* as your webserver, there's an [.htaccess](/public/.htaccess) file that routes all traffic for non-existent files and directories to [index.php](/public/index.php). 50 | 51 | Please make sure that your webserver's *document root* points to the [public](/public) folder where the [index.php](/public/index.php) file is located. 52 | 53 | ## Key components 54 | 55 | The [index.php](/public/index.php) file is the controller of the application. It reads HTTP input and generates HTTP output. 56 | Routes are matched via `$app->get()` callbacks. 57 | 58 | Output is formatted using the [Twig](http://twig.sensiolabs.org) templating language. The [views](/views) folder contains the template file for each route: 59 | 60 | * [footer.twig](/views/footer.twig) contains the footer template which returns a translated string and a timestamp 61 | * [header.twig](/views/header.twig) contains the header template which also returns a translated string and a timestamp 62 | * [index.twig](/views/index.twig) contains the main template where the header, footer, and navigation are loaded, either via *ESI* or via *HInclude* 63 | * [nav.twig](/views/nav.twig) contains the navigation template 64 | 65 | ## Varnish 66 | 67 | To see the impact of this code, I would advise you to install [Varnish](https://www.varnish-cache.org/). Varnish will respect the *HTTP response headers* that were set and will cache the output. 68 | 69 | This is the minimum amount of [VCL code](https://www.varnish-cache.org/docs/4.1/reference/vcl.html#varnish-configuration-language) you need to make this work: 70 | 71 | ``` 72 | vcl 4.0; 73 | 74 | backend default { 75 | .host = "localhost"; 76 | .port = "8080"; 77 | } 78 | 79 | sub vcl_recv { 80 | set req.http.Surrogate-Capability="key=ESI/1.0"; 81 | if ((req.method != "GET" && req.method != "HEAD") || req.http.Authorization) { 82 | return (pass); 83 | } 84 | return(hash); 85 | } 86 | 87 | sub vcl_backend_response { 88 | if(beresp.http.Surrogate-Control~"ESI/1.0") { 89 | unset beresp.http.Surrogate-Control; 90 | set beresp.do_esi=true; 91 | return(deliver); 92 | } 93 | } 94 | ``` 95 | 96 | **This piece of *VCL* code assumes that Varnish is installed on port 80 and your webserver on port 8080 on the same machine.** 97 | 98 | ## Disclaimer 99 | 100 | This repository and its code are part of the code examples that are featured in my book **"Getting Started With Varnish Cache"**. 101 | It serves as a set of best practices that should encourage developers to control the cacheability of their sites themselves, instead of relying on infrastructure configuration. 102 | 103 | More information about me: 104 | 105 | * Visit my website: https://feryn.eu 106 | * Learn more about my book: https://book.feryn.eu 107 | * Follow me on Twitter: [@ThijsFeryn](https://twitter.com/ThijsFeryn) 108 | * Follow me on Instagram: [@ThijsFeryn](https://instagram.com/ThijsFeryn) 109 | * View my LinkedIn profile: http://linkedin.com/in/thijsferyn 110 | * View my public speaking track record: https://talks.feryn.eu 111 | * Read my blog: https://blog.feryn.eu 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "silex/silex": "^2.0", 4 | "twig/twig": "^1.27", 5 | "symfony/twig-bridge": "^3.1", 6 | "symfony/translation": "^3.1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} -s [OR] 3 | RewriteCond %{REQUEST_FILENAME} -l [OR] 4 | RewriteCond %{REQUEST_FILENAME} -d 5 | RewriteRule ^.*$ - [NC,L] 6 | RewriteRule ^.*$ index.php [NC,L] -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | register(new Silex\Provider\TwigServiceProvider(), ['twig.path' => __DIR__.'/../views']); 14 | $app->register(new HttpFragmentServiceProvider()); 15 | $app->register(new HttpCacheServiceProvider()); 16 | $app->register(new Silex\Provider\TranslationServiceProvider(), ['locale_fallbacks' => ['en']]); 17 | 18 | $app['locale'] = 'en'; 19 | $app['translator.domains'] = [ 20 | 'messages' => [ 21 | 'en' => [ 22 | 'welcome' => 'Welcome to the site', 23 | 'rendered' => 'Rendered at %date%', 24 | 'example' => 'An example page' 25 | ], 26 | 'nl' => [ 27 | 'welcome' => 'Welkom op de site', 28 | 'rendered' => 'Samengesteld op %date%', 29 | 'example' => 'Een voorbeeldpagina' 30 | ] 31 | ] 32 | ]; 33 | 34 | $app->before(function (Request $request) use ($app){ 35 | $app['translator']->setLocale($request->getPreferredLanguage()); 36 | }); 37 | 38 | $app->after(function(Request $request, Response $response) use ($app){ 39 | $date = new DateTime(); 40 | $date->add(new DateInterval('PT'.$response->getTtl().'S')); 41 | $response 42 | ->setExpires($date) 43 | ->setVary('Accept-Language') 44 | ->setETag(md5($response->getContent())) 45 | ->isNotModified($request); 46 | }); 47 | 48 | $app->get('/', function () use($app) { 49 | $response = new Response($app['twig']->render('index.twig'),200); 50 | $response 51 | ->setSharedMaxAge(5) 52 | ->setPublic(); 53 | return $response; 54 | })->bind('home'); 55 | 56 | $app->get('/header', function () use($app) { 57 | $response = new Response($app['twig']->render('header.twig'),200); 58 | $response 59 | ->setPrivate() 60 | ->setSharedMaxAge(0); 61 | return $response; 62 | })->bind('header'); 63 | 64 | $app->get('/footer', function () use($app) { 65 | $response = new Response($app['twig']->render('footer.twig'),200); 66 | $response 67 | ->setSharedMaxAge(10) 68 | ->setPublic(); 69 | return $response; 70 | })->bind('footer'); 71 | 72 | $app->get('/nav', function () use($app) { 73 | $response = new Response($app['twig']->render('nav.twig'),200); 74 | $response 75 | ->setSharedMaxAge(20) 76 | ->setPublic(); 77 | return $response; 78 | })->bind('nav'); 79 | 80 | $app->run(); 81 | -------------------------------------------------------------------------------- /views/footer.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/header.twig: -------------------------------------------------------------------------------- 1 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin, non ultrices turpis mollis. Aliquam sit amet tempus elit. Ut viverra risus enim, ut venenatis justo accumsan nec. Praesent a dolor tellus. Maecenas non mauris leo. Pellentesque lobortis turpis at dapibus laoreet. Mauris rhoncus nulla et urna mollis, et lobortis magna ornare. Etiam et sapien consequat, egestas felis sit amet, dignissim enim.
22 |Quisque quis mollis justo, imperdiet fermentum velit. Aliquam nulla justo, consectetur et diam non, luctus commodo metus. Vestibulum fermentum efficitur nulla non luctus. Nunc lorem nunc, mollis id efficitur et, aliquet sit amet ante. Sed ipsum turpis, vehicula eu semper eu, malesuada eget leo. Vestibulum venenatis dui id pulvinar suscipit. Etiam nec massa pharetra justo pharetra dignissim quis non magna. Integer id convallis lectus. Nam non ullamcorper metus. Ut vestibulum ex ut massa posuere tincidunt. Vestibulum hendrerit neque id lorem rhoncus aliquam. Duis a facilisis metus, a faucibus nulla.
23 |