From 3121619fd8d7e079bc67e6db194414b12f123465 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 15:02:03 -0600 Subject: [PATCH 1/3] sitemap validated --- .env-example | 2 ++ README.md | 2 +- content/public/sitemap.xml | 3 +++ src/Content/FrontMatter.php | 8 ++++++++ src/Content/Markdown.php | 5 +++++ src/Documents/Sitemap.php | 41 +++++++++++++++++++++++++++++++++++++ src/File.php | 18 +++++++++++----- src/FileSystem.php | 38 ++++++++++++++++++++++++++++++++++ src/HttpRequest.php | 18 ++++++++++++++++ src/HttpResponse.php | 22 +++++++++++++++++--- src/ServerGlobals.php | 20 +++++++++++++++++- tests/FileTest.php | 34 ++++++++++++++++++++++++++++++ tests/IndexTest.php | 2 +- tests/SitemapTest.php | 36 ++++++++++++++++++++++++++++++++ 14 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 .env-example create mode 100644 content/public/sitemap.xml create mode 100644 src/Documents/Sitemap.php create mode 100644 tests/FileTest.php create mode 100644 tests/SitemapTest.php diff --git a/.env-example b/.env-example new file mode 100644 index 00000000..34453b5f --- /dev/null +++ b/.env-example @@ -0,0 +1,2 @@ +APP_ENV={production|local|test} +APP_URL={host with schema} diff --git a/README.md b/README.md index edb68cea..95b902e6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The content is also available in a [separate repository](https://github.com/josh - [MAMP](https://www.mamp.info/en/mamp-pro/windows/), - [XAMPP](https://www.apachefriends.org/download.html), or - custom build. -3. Point the locally hosted domain to the `public` directory. +3. Point the locally hosted domain to the `site-dynamic-php` directory. When you go to the locally hosted URL, it should throw a 500 server error. This is because you will need two things: the content folder and a `.env` file. diff --git a/content/public/sitemap.xml b/content/public/sitemap.xml new file mode 100644 index 00000000..87ba159a --- /dev/null +++ b/content/public/sitemap.xml @@ -0,0 +1,3 @@ +--- +template: 'sitemap' +--- diff --git a/src/Content/FrontMatter.php b/src/Content/FrontMatter.php index ca49655e..0ddb0918 100644 --- a/src/Content/FrontMatter.php +++ b/src/Content/FrontMatter.php @@ -63,4 +63,12 @@ public function original(): string } return ''; } + + public function template(): string + { + if ($this->hasMember('template')) { + return strval($this->frontMatter['template']); + } + return ''; + } } diff --git a/src/Content/Markdown.php b/src/Content/Markdown.php index ce5b7c9a..49429a04 100644 --- a/src/Content/Markdown.php +++ b/src/Content/Markdown.php @@ -131,6 +131,11 @@ public function pageTitle(): string return implode(' | ', $titles); } + public function canonicalURl(): string + { + return $this->file->canonicalUrl(); + } + private function fileContent(): string { if (strlen($this->fileContent) === 0 and $this->file->found()) { diff --git a/src/Documents/Sitemap.php b/src/Documents/Sitemap.php new file mode 100644 index 00000000..2cc565da --- /dev/null +++ b/src/Documents/Sitemap.php @@ -0,0 +1,41 @@ +name('content.md')->sortByName() + ->notContains('redirect:') + ->notContains('noindex:'); + + $markdown = []; + foreach ($finder as $file) { + $markdown[] = Markdown::for( + File::at($file->getPathname()) + ); + } + + $urls = []; + foreach ($markdown as $m) { + $urls[] = Element::url( + Element::loc($m->canonicalUrl()) + ); + } + + return Document::urlset( + ...$urls + )->props("xmlns http://www.sitemaps.org/schemas/sitemap/0.9")->build(); + } +} diff --git a/src/File.php b/src/File.php index fa5af6c1..934f49e4 100644 --- a/src/File.php +++ b/src/File.php @@ -10,6 +10,8 @@ class File { + private string $contentFileName = '/content.md'; + public static function at(string $localPath): File { return new File($localPath); @@ -66,7 +68,7 @@ public function path(bool $full = true): string public function canGoUp(): bool { - return $this->path(false) !== '/content.md'; + return $this->path(false) !== $this->contentFileName; } public function up(): File @@ -74,7 +76,7 @@ public function up(): File $parts = explode('/', $this->localPath); $parts = array_slice($parts, 0, -2); // remove file name and one folder. $localPath = implode('/', $parts); - return File::at($localPath . '/content.md'); + return File::at($localPath . $this->contentFileName); } public function contents(): string @@ -97,7 +99,8 @@ public function mimetype(): string $extensionMap = [ 'md' => 'text/html', 'css' => 'text/css', - 'js' => 'text/javascript' + 'js' => 'text/javascript', + 'xml' => 'application/xml' ]; $parts = explode('.', $this->path()); @@ -108,12 +111,17 @@ public function mimetype(): string return $type; } + public function canonicalUrl(): string + { + return str_replace($this->contentFileName, '', 'https://joshbruce.com' . $this->path(false)); + } + /** * @return File[] */ public function children(string $filesNamed): array { - $base = str_replace('/content.md', '', $this->path()); + $base = str_replace($this->contentFileName, '', $this->path()); $files = []; foreach (new DirectoryIterator($base) as $folder) { @@ -134,6 +142,6 @@ public function children(string $filesNamed): array private function contentRoot(): string { - return FileSystem::contentRoot() . '/public'; + return FileSystem::publicRoot(); } } diff --git a/src/FileSystem.php b/src/FileSystem.php index 9af3e1fe..0dce6c32 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -4,8 +4,17 @@ namespace JoshBruce\Site; +use SplFileInfo; + +use Symfony\Component\Finder\Finder; + class FileSystem { + public static function publicRoot(): string + { + return FileSystem::contentRoot() . '/public'; + } + public static function contentRoot(): string { $parts = explode('/', self::projectRoot()); @@ -24,4 +33,33 @@ public static function projectRoot(): string $parts = array_slice($parts, 0, -1); return implode('/', $parts); } + + public static function finder(): Finder + { + $finder = new Finder(); + return $finder->ignoreVCS(false) + ->ignoreUnreadableDirs() + ->ignoreDotFiles(false) + ->ignoreVCSIgnored(true) + ->files() + ->filter(fn($f) => self::isPublished($f)) + ->in(self::publicRoot()); + } + + private static function isPublished(SplFileInfo $finderFile): bool + { + return ! self::isDraft($finderFile); + } + + private static function isDraft(SplFileInfo $finderFile): bool + { + $filePath = (string) $finderFile; + $relativePath = self::relativePath($filePath); + return str_contains($relativePath, '_'); + } + + private static function relativePath(string $path): string + { + return str_replace(self::contentRoot(), '', $path); + } } diff --git a/src/HttpRequest.php b/src/HttpRequest.php index b32a13c3..6b5ecd1b 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -65,6 +65,16 @@ public function isFile(): bool return str_contains($this->possibleFileName(), '.'); } + public function isSitemap(): bool + { + return $this->isFile() and $this->possibleFileName() === 'sitemap.xml'; + } + + public function isNotSitemap(): bool + { + return ! $this->isSitemap(); + } + public function localFile(): File { return File::at(localPath: $this->localPath()); @@ -82,6 +92,14 @@ private function localPath(): string $relativePath = $this->uriPath(); if (empty($possibleFileName)) { $relativePath = $this->uriPath() . '/content.md'; + + // } elseif (str_contains($relativePath, '.xml')) { + // $relativePath = str_replace('.xml', '.md', $relativePath); + + } + + if (! str_starts_with($relativePath, '/')) { + $relativePath = "/{$relativePath}"; } $root = FileSystem::contentRoot(); diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 90eaa3ad..e216dcc6 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -21,6 +21,8 @@ use JoshBruce\Site\PageComponents\Navigation; +use JoshBruce\Site\Documents\Sitemap; + class HttpResponse { private ResponseInterface $psrResponse; @@ -69,7 +71,11 @@ public function headers(): array public function body(): string { $localFile = $this->request->localFile(); - if ($this->statusCode() === 200 and $localFile->isNotMarkdown()) { + if ( + $this->statusCode() === 200 and + $localFile->isNotMarkdown() and + $this->request->isNotSitemap($localFile) + ) { return $localFile->path(); } elseif ($this->statusCode() === 404) { @@ -82,15 +88,22 @@ public function body(): string } - $html = ''; + $template = ''; $pageTitle = ''; + $html = ''; if ($localFile->isMarkdown()) { $markdown = Markdown::for(file: $localFile); + $template = $markdown->frontMatter()->template(); $pageTitle = $markdown->pageTitle(); $html = $markdown->html(); } + if ($this->request->isSitemap()) { + return Sitemap::create(); + + } + return Document::create( $pageTitle )->head( @@ -153,7 +166,10 @@ public function psrResponse(): ResponseInterface $psr17Factory = new PsrFactory(); $body = $this->body(); $stream = $psr17Factory->createStream($body); - if ($this->request->isFile()) { + if ( + $this->request->isFile() and + $this->request->isNotSitemap() + ) { $stream = $psr17Factory->createStreamFromFile($body); } diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index 9b4ae757..0f11fd80 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -15,7 +15,6 @@ private function __construct() { } - public function appEnvIsNot(string $value): bool { return $this->appEnv() !== $value; @@ -26,6 +25,20 @@ public function isMissingAppEnv(): bool return ! $this->hasAppEnv(); } + public function isMissingAppUrl(): bool + { + return ! $this->hasAppUrl(); + } + + public function appUrl(): string + { + if ($this->hasAppUrl()) { + $globals = $this->globals(); + return strval($globals['APP_URL']); + } + return ''; + } + private function appEnv(): string { if ($this->hasAppEnv()) { @@ -40,6 +53,11 @@ private function hasAppEnv(): bool return array_key_exists('APP_ENV', $this->globals()); } + private function hasAppUrl(): bool + { + return array_key_exists('APP_URL', $this->globals()); + } + /** * @return array */ diff --git a/tests/FileTest.php b/tests/FileTest.php new file mode 100644 index 00000000..956a10c5 --- /dev/null +++ b/tests/FileTest.php @@ -0,0 +1,34 @@ +canonicalUrl() + )->toBe( + 'https://joshbruce.com' + ); + + expect( + File::at(FileSystem::publicRoot())->canonicalUrl() + )->toBe( + 'https://joshbruce.com' + ); + + expect( + File::at(FileSystem::publicRoot() . '/web-development')->canonicalUrl() + )->toBe( + 'https://joshbruce.com/web-development' + ); + + expect( + File::at(FileSystem::publicRoot() . '/web-development/content.md') + ->canonicalUrl() + )->toBe( + 'https://joshbruce.com/web-development' + ); +})->group('file'); diff --git a/tests/IndexTest.php b/tests/IndexTest.php index 524d006d..6393e1c4 100644 --- a/tests/IndexTest.php +++ b/tests/IndexTest.php @@ -29,4 +29,4 @@ 'Okay to fail in local.' )->toBeFalse(); } -})->group('index', 'focus'); +})->group('index'); diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php new file mode 100644 index 00000000..c37f0645 --- /dev/null +++ b/tests/SitemapTest.php @@ -0,0 +1,36 @@ +statusCode() + )->toBe( + 200 + ); + + expect( + $xml->headers() + )->toBe( + ['Content-Type' => 'application/xml'] + ); + + expect( + str_contains($xml->body(), 'toBeTrue(); + + expect( + $xml->body() + )->toBe(''); +})->group('sitemap', 'focus'); From 812fc14b575ef57869a2b9eec75448c35508c6e6 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 16:19:59 -0600 Subject: [PATCH 2/3] prod script passing --- phpstan.neon | 2 ++ src/File.php | 6 +++++- src/HttpResponse.php | 2 +- tests/SitemapTest.php | 6 +++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index c4149660..6f975930 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,3 +5,5 @@ parameters: - site-dynamic-php ignoreErrors: - '#Call to an undefined static method Eightfold\\HTMLBuilder\\Element::(.*)#' + - '#Call to an undefined static method Eightfold\\XMLBuilder\\Element::(.*)#' + - '#Call to an undefined static method Eightfold\\XMLBuilder\\Document::(.*)#' diff --git a/src/File.php b/src/File.php index 934f49e4..6c88eb21 100644 --- a/src/File.php +++ b/src/File.php @@ -113,7 +113,11 @@ public function mimetype(): string public function canonicalUrl(): string { - return str_replace($this->contentFileName, '', 'https://joshbruce.com' . $this->path(false)); + return str_replace( + $this->contentFileName, + '', + 'https://joshbruce.com' . $this->path(false) + ); } /** diff --git a/src/HttpResponse.php b/src/HttpResponse.php index e216dcc6..7f9e7d36 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -74,7 +74,7 @@ public function body(): string if ( $this->statusCode() === 200 and $localFile->isNotMarkdown() and - $this->request->isNotSitemap($localFile) + $this->request->isNotSitemap() ) { return $localFile->path(); diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php index c37f0645..edadd0b3 100644 --- a/tests/SitemapTest.php +++ b/tests/SitemapTest.php @@ -32,5 +32,9 @@ expect( $xml->body() - )->toBe(''); + )->toBe(<< + https://joshbruce.comhttps://joshbruce.com/design-your-lifehttps://joshbruce.com/design-your-life/motivatorshttps://joshbruce.com/finances/budgetinghttps://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210301https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210315https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210401https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210415https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210501https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210515https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210601https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210615https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210701https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210715https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210801https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210815https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210901https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210915https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211001https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211015https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211101https://joshbruce.com/finances/building-wealth-paycheck-to-paycheckhttps://joshbruce.com/financeshttps://joshbruce.com/finances/investment-policyhttps://joshbruce.com/health-and-wellnesshttps://joshbruce.com/legalhttps://joshbruce.com/software-developmenthttps://joshbruce.com/software-development/why-dont-you-usehttps://joshbruce.com/web-development/2021-site-in-depthhttps://joshbruce.com/web-developmenthttps://joshbruce.com/web-development/modern-web-developmenthttps://joshbruce.com/web-development/my-history-on-the-webhttps://joshbruce.com/web-development/on-constraintshttps://joshbruce.com/web-development/on-constraints/internet-bandwidthhttps://joshbruce.com/web-development/refactoring-re-engineering-and-rebuildinghttps://joshbruce.com/web-development/site-statshttps://joshbruce.com/web-development/static-dynamic-and-interactive + xml + ); })->group('sitemap', 'focus'); From 8193f2c049dab5ef3e24d0c163a44a91b36e5721 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 16:28:53 -0600 Subject: [PATCH 3/3] fix .gitignore ignoring itself --- .gitignore | 2 ++ content/public/.gitignore | 2 ++ src/FileSystem.php | 1 + 3 files changed, 5 insertions(+) create mode 100644 content/public/.gitignore diff --git a/.gitignore b/.gitignore index 55b6cd2f..892b868a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ yarn-error.log .nova site-static-html/ + +!site-dynamic-php/public/.gitignore diff --git a/content/public/.gitignore b/content/public/.gitignore new file mode 100644 index 00000000..5ca0973f --- /dev/null +++ b/content/public/.gitignore @@ -0,0 +1,2 @@ +.DS_Store + diff --git a/src/FileSystem.php b/src/FileSystem.php index 0dce6c32..e82af58d 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -41,6 +41,7 @@ public static function finder(): Finder ->ignoreUnreadableDirs() ->ignoreDotFiles(false) ->ignoreVCSIgnored(true) + ->notName('.gitignore') ->files() ->filter(fn($f) => self::isPublished($f)) ->in(self::publicRoot());