Skip to content

Commit

Permalink
Merge pull request #6 from vormkracht10/feature/route-based-build
Browse files Browse the repository at this point in the history
Feature/route based build
  • Loading branch information
markvaneijk authored Oct 27, 2023
2 parents 4433221 + e2a85f7 commit ba5b3ff
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 38 deletions.
35 changes: 21 additions & 14 deletions config/static.php
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
<?php

use Spatie\Crawler\CrawlProfiles\CrawlInternalUrls;

return [
/**
* Path within storage disk to save files in.
* The driver that will be used to cache your pages.
* This can be either 'crawler' or 'routes'.
*/
'path' => storage_path('public/static'),
'driver' => 'crawler',

'build' => [
/**
* Use a web crawler to find all links to cache
* Clear static files before building static cache.
* When disabled, the cache is warmed up rather by updating and overwriting files instead of starting without an existing cache.
*/
'crawler' => true,
'clear_before_start' => true,

/**
* Build cache for non-dynamic routes
* Number of concurrent http requests to build static cache.
*/
'routes' => false,
'concurrency' => 5,

/**
* Build cache for these defined URLs
* Ideally when using dynamic routes, like /posts/{slug}
* Whether to follow links on pages.
*/
'urls' => [],
'accept_no_follow' => true,

/**
* Clear static files before building static cache.
* When disabled, the cache is warmed up rather by updating and overwriting files instead of starting without an existing cache.
* The default scheme the crawler will use.
*/
'clear_before_start' => false,
'default_scheme' => 'https',

/**
* Number of concurrent http requests to build static cache.
* The crawl profile that will be used by the crawler.
*/
'concurrency' => 5,
'crawl_profile' => new CrawlInternalUrls(config('app.url')),

/**
* HTTP header that can be used to bypass the cache. Useful for updating the cache without needing to clear it first,
Expand All @@ -44,6 +46,11 @@
],

'files' => [
/**
* The filesystem disk that will be used to cache your pages.
*/
'disk' => env('STATIC_FILESYSTEM_DISK', 'local'),

/**
* Different caches per domain.
*/
Expand Down
69 changes: 61 additions & 8 deletions src/Commands/StaticBuildCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

namespace Vormkracht10\LaravelStatic\Commands;

use Exception;
use GuzzleHttp\RequestOptions;
use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Route;
use Spatie\Crawler\Crawler;
use Spatie\Crawler\CrawlProfiles\CrawlInternalUrls;
use Vormkracht10\LaravelStatic\Crawler\StaticCrawlObserver;
use Vormkracht10\LaravelStatic\LaravelStatic;
use Vormkracht10\LaravelStatic\Middleware\StaticResponse;

class StaticBuildCommand extends Command
{
Expand All @@ -29,25 +34,73 @@ public function __construct(Repository $config, LaravelStatic $static)

public function handle(): void
{
if ($this->config->get('static.build.clear_before_start')) {
if ($this->config->get('static.build.clear_before_start', true)) {
$this->call(StaticClearCommand::class);
}

match ($driver = $this->config->get('static.driver', 'routes')) {
'crawler' => $this->cacheWithCrawler(),
'routes' => $this->cacheWithRoutes(),
default => throw new Exception("Static driver [{$driver}] is not supported"),
};
}

public function cacheWithRoutes(): void
{
/**
* @var Collection<\Illuminate\Routing\Route> $routes
*/
$routes = collect(Route::getRoutes()->get('GET'))->filter(
fn ($route) => in_array(StaticResponse::class, Route::gatherRouteMiddleware($route)),
);

$failed = 0;

foreach ($routes as $route) {
$request = Request::create($route->uri());

$request->headers->set('User-Agent', 'LaravelStatic/1.0');

$response = Route::dispatchToRoute($request);

if (! $response->isOk()) {
$this->components->error("✘ failed to cache page on route \"{$route->uri()}\"");
$failed++;
}

$this->components->info("✔ page on route \"{$route->uri()}\" has been cached");
}

if ($failed > 0) {
$this->components->warn("FAILED TO CACHE {$failed} PAGES");
}
}

public function cacheWithCrawler(): void
{
$bypassHeader = $this->config->get('static.build.bypass_header');

Crawler::create([
RequestOptions::VERIFY => ! app()->environment('local'),
$profile = $this->config->get('static.build.crawl_profile');

$crawler = Crawler::create([
RequestOptions::VERIFY => ! app()->environment('local', 'testing'),
RequestOptions::ALLOW_REDIRECTS => true,
RequestOptions::HEADERS => [
array_key_first($bypassHeader) => array_shift($bypassHeader),
'User-Agent' => 'LaravelStatic/1.0',
],
])
->setCrawlProfile(new CrawlInternalUrls($this->config->get('app.url')))
->acceptNofollowLinks()
->setCrawlObserver(new StaticCrawlObserver($this->components))
->setCrawlProfile($profile)
->setConcurrency($this->config->get('static.build.concurrency'))
->setDefaultScheme('https')
// ->setParseableMimeTypes(['text/html', 'text/plain'])
->startCrawling($this->config->get('app.url'));
->setDefaultScheme($this->config->get('static.build.default_scheme'));
// ->setParseableMimeTypes(['text/html', 'text/plain'])

if ($this->config->get('static.build.accept_no_follow')) {
$crawler->acceptNofollowLinks();
}

$crawler->startCrawling($this->config->get('app.url'));
}
}
57 changes: 57 additions & 0 deletions src/Crawler/StaticCrawlObserver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Vormkracht10\LaravelStatic\Crawler;

use GuzzleHttp\Exception\RequestException;
use Illuminate\Console\View\Components\Factory as ComponentFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Crawler\CrawlObservers\CrawlObserver;

class StaticCrawlObserver extends CrawlObserver
{
protected ComponentFactory $components;

public function __construct(ComponentFactory $components)
{
$this->components = $components;
}

/**
* Called when the crawler will crawl the url.
*/
public function willCrawl(UriInterface $url): void
{
//
}

/**
* Called when the crawler has crawled the given url successfully.
*/
public function crawled(
UriInterface $url,
ResponseInterface $response,
UriInterface $foundOnUrl = null
): void {
$this->components->info("{$url} has been crawled");
}

/**
* Called when the crawler had a problem crawling the given url.
*/
public function crawlFailed(
UriInterface $url,
RequestException $requestException,
UriInterface $foundOnUrl = null
): void {
$this->components->error("✘ failed to crawl path ({$url})");
}

/**
* Called when the crawl has ended.
*/
public function finishedCrawling(): void
{
$this->components->info('✔ Static build completed');
}
}
18 changes: 16 additions & 2 deletions src/LaravelStatic.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Vormkracht10\LaravelStatic;

use Illuminate\Config\Repository;
use Illuminate\Contracts\Filesystem\Filesystem as FilesystemContract;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;

class LaravelStatic
{
Expand All @@ -19,13 +21,25 @@ public function __construct(Repository $config, Filesystem $files)

public function clear(): bool
{
$files = $this->files->allFiles(directory: $this->config->get('static.path'), hidden: false);
$disk = $this->disk();

return $this->files->delete($files);
$files = $disk->allFiles();

return $disk->delete($files);
}

public function forget(string $path): bool
{
return $this->files->delete($path);
}

public function disk(string $override = null): FilesystemContract
{
$disk = $override ?? $this->config->get(
'static.files.disk',
'static',
);

return Storage::disk($disk);
}
}
30 changes: 16 additions & 14 deletions src/Middleware/StaticResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@

use Closure;
use Illuminate\Config\Repository;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use voku\helper\HtmlMin;
use Vormkracht10\LaravelStatic\Facades\LaravelStatic;

class StaticResponse
{
protected Repository $config;

protected Filesystem $files;

protected array $bypassHeader;

public function __construct(Repository $config, Filesystem $files)
public function __construct(Repository $config)
{
$this->config = $config;
$this->files = $files;

$this->bypassHeader = $this->config->get('static.build.bypass_header');
}

Expand Down Expand Up @@ -91,7 +89,7 @@ public function minifyResponse($response)
return $response;
}

if (! starts_with($response->headers->get('Content-Type'), 'text/html')) {
if (! str_starts_with($response->headers->get('Content-Type'), 'text/html')) {
return $response;
}

Expand Down Expand Up @@ -119,14 +117,16 @@ public function createStaticFile(Request $request, $response): void
return;
}

$this->files->makeDirectory($path, 0775, true, true);
$disk = LaravelStatic::disk();

if (! $this->files->exists($this->config->get('static.path').'/.gitignore')) {
$this->files->put($this->config->get('static.path').'/.gitignore', '*'.PHP_EOL.'!.gitignore');
$disk->makeDirectory($path);

if (! $disk->exists($this->config->get('static.path').'/.gitignore')) {
$disk->put($this->config->get('static.path').'/.gitignore', '*'.PHP_EOL.'!.gitignore');
}

if ($response->getContent()) {
$this->files->put($filepath, $response->getContent(), true);
$disk->put($filepath, $response->getContent(), true);
}
}

Expand Down Expand Up @@ -186,8 +186,8 @@ protected function getFileExtension($filename, $response): ?string
}

if (
starts_with($contentType, 'text/xml') ||
starts_with($contentType, 'application/xml')
str_starts_with($contentType, 'text/xml') ||
str_starts_with($contentType, 'application/xml')
) {
$extension = 'xml';
}
Expand All @@ -206,7 +206,7 @@ public function generateFilepath(Request $request, $response): array
{
$parts = $this->getUriParts($request);

$filename = '__index__';
$filename = '';

if (! str_ends_with($request->getPathInfo(), '/')) {
$filename = array_pop($parts);
Expand All @@ -217,11 +217,13 @@ public function generateFilepath(Request $request, $response): array
$parts,
]);

$filename .= '?';

if (
$this->config->get('static.include_query_string') &&
! blank($request->server('QUERY_STRING'))
) {
$filename .= '?'.$request->server('QUERY_STRING');
$filename .= $request->server('QUERY_STRING');
}

$filename .= $this->getFileExtension($filename, $response);
Expand Down
Loading

0 comments on commit ba5b3ff

Please sign in to comment.