mirror of
https://github.com/nekudo/shiny_blog.git
synced 2025-09-16 07:42:02 +02:00
Add create sitemap action
This commit is contained in:
@@ -15,6 +15,7 @@ out there.
|
||||
* __RSS feeds:__ Separate RSS feeds for all articles and article-categories are available.
|
||||
* __Pagination:__ Set how many articles you want to show per page.
|
||||
* __Basic SEO support:__ Title-Tags,Meta-Description, and index/follow stuff can be adjusted via config-file.
|
||||
* __XML sitemap:__ Sitemap of all articles and pages is automatically generated.
|
||||
* __Excerpts:__ Using a _read-more_ tag in your articles you can define the excerpt to appear on the blog-page.
|
||||
* __Clean Code:__ Well documented, PSR-0 and PSR-2 compatible PHP code.
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"metaTitle": "ShinyBlog - A shiny blog :)",
|
||||
"description": "A dummy homepage...",
|
||||
"slug": "home",
|
||||
"date" : "2016-01-23",
|
||||
"layout": "fullpage"
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,8 @@
|
||||
"title": "Imprint",
|
||||
"metaTitle": "Imprint of ShinyBlog",
|
||||
"description": "Imprint description",
|
||||
"slug": "imprint"
|
||||
"slug": "imprint",
|
||||
"date" : "2016-01-23"
|
||||
}
|
||||
|
||||
::METAEND::
|
||||
|
79
src/Action/ShowSitemapAction.php
Normal file
79
src/Action/ShowSitemapAction.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace Nekudo\ShinyBlog\Action;
|
||||
|
||||
use Nekudo\ShinyBlog\Domain\ShowSitemapDomain;
|
||||
use Nekudo\ShinyBlog\Responder\ShowSitemapResponder;
|
||||
|
||||
class ShowSitemapAction extends BaseAction
|
||||
{
|
||||
/** @var ShowSitemapDomain $domain */
|
||||
protected $domain;
|
||||
|
||||
/** @var ShowSitemapResponder $responder */
|
||||
protected $responder;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
$this->domain = new ShowSitemapDomain($config);
|
||||
$this->responder = new ShowSitemapResponder($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect sitemap elements and render sitemap.
|
||||
*
|
||||
* @param array $arguments
|
||||
*/
|
||||
public function __invoke(array $arguments)
|
||||
{
|
||||
$this->addBlogToSitemap();
|
||||
$this->addArticlesToSitemap();
|
||||
$this->addPagesToSitemap();
|
||||
$this->responder->__invoke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds blog page to sitemap.
|
||||
*/
|
||||
protected function addBlogToSitemap()
|
||||
{
|
||||
$urlPath = $this->config['routes']['blog']['buildPattern'];
|
||||
$lastmod = $this->domain->getBlogLastmod();
|
||||
$this->responder->addBlogItem($urlPath, $lastmod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds article items to sitemap.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function addArticlesToSitemap() : bool
|
||||
{
|
||||
$articlesData = $this->domain->getArticlesData();
|
||||
if (empty($articlesData)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($articlesData as $itemData) {
|
||||
$this->responder->addArticleItem($itemData['urlPath'], $itemData['lastmod']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds page items to sitemap.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function addPagesToSitemap() : bool
|
||||
{
|
||||
$pagesData = $this->domain->getPagesData();
|
||||
if (empty($pagesData)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($pagesData as $itemData) {
|
||||
$this->responder->addPageItem($itemData['urlPath'], $itemData['lastmod']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -15,10 +15,12 @@ class ContentDomain extends BaseDomain
|
||||
/** @var ParsedownExtra $markdownParser */
|
||||
protected $markdownParser;
|
||||
|
||||
protected $articleData = [];
|
||||
|
||||
/** @var array $articleMeta */
|
||||
protected $articleMeta = [];
|
||||
|
||||
/** @var array $pageMeta */
|
||||
protected $pageMeta = [];
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
@@ -28,15 +30,54 @@ class ContentDomain extends BaseDomain
|
||||
/**
|
||||
* Loads metadata of articles.
|
||||
*
|
||||
* @param string $keyName Key to use as array index.
|
||||
* @param string $keyName Name of value to use as array key.
|
||||
* @param bool $forceReload Reload metadata even if already loaded
|
||||
* @return bool
|
||||
*/
|
||||
protected function loadArticleMeta(string $keyName)
|
||||
protected function loadArticleMeta(string $keyName, bool $forceReload = false) : bool
|
||||
{
|
||||
if (!empty($this->articleMeta) && $forceReload === false) {
|
||||
return true;
|
||||
}
|
||||
$pathToArticleContents = $this->config['contentsFolder'] . 'articles/';
|
||||
if (!is_dir($pathToArticleContents)) {
|
||||
throw new RuntimeException('Articles folder not found.');
|
||||
}
|
||||
$iterator = new DirectoryIterator($pathToArticleContents);
|
||||
$this->articleMeta = $this->getContentMeta($pathToArticleContents, $keyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads metadata of pages.
|
||||
*
|
||||
* @param string $keyName Name of value to use as array key.
|
||||
* @param bool $forceReload Reload metadata even if already loaded
|
||||
* @return bool
|
||||
*/
|
||||
protected function loadPageMeta(string $keyName, bool $forceReload = false) : bool
|
||||
{
|
||||
if (!empty($this->pageMeta) && $forceReload === false) {
|
||||
return true;
|
||||
}
|
||||
$pathToPageContents = $this->config['contentsFolder'] . 'pages/';
|
||||
if (!is_dir($pathToPageContents)) {
|
||||
throw new RuntimeException('Pages folder not found.');
|
||||
}
|
||||
$this->pageMeta = $this->getContentMeta($pathToPageContents, $keyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches content metadata from given folder.
|
||||
*
|
||||
* @param string $contentFolder
|
||||
* @param string $keyName Name of value to use as array key. (e.g. "slug")
|
||||
* @return array
|
||||
*/
|
||||
protected function getContentMeta(string $contentFolder, string $keyName) : array
|
||||
{
|
||||
$metadata = [];
|
||||
$iterator = new DirectoryIterator($contentFolder);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDot()) {
|
||||
continue;
|
||||
@@ -44,14 +85,15 @@ class ContentDomain extends BaseDomain
|
||||
if ($file->getExtension() !== 'md') {
|
||||
continue;
|
||||
}
|
||||
$articleMeta = $this->parseContentFile($file->getPathname(), false);
|
||||
if (empty($articleMeta[$keyName])) {
|
||||
throw new RuntimeException('Key not found in article meta.');
|
||||
$itemMeta = $this->parseContentFile($file->getPathname(), false);
|
||||
if (empty($itemMeta[$keyName])) {
|
||||
throw new RuntimeException('Key not found in items metadata.');
|
||||
}
|
||||
$key = $articleMeta[$keyName];
|
||||
$this->articleMeta[$key] = $articleMeta;
|
||||
$this->articleMeta[$key]['file'] = $file->getPathname();
|
||||
$key = $itemMeta[$keyName];
|
||||
$metadata[$key] = $itemMeta;
|
||||
$metadata[$key]['file'] = $file->getPathname();
|
||||
}
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -6,10 +6,7 @@ class ArticleEntity extends BaseEntity
|
||||
{
|
||||
/** @var string $author */
|
||||
protected $author = '';
|
||||
|
||||
/** @var string $date */
|
||||
protected $date;
|
||||
|
||||
|
||||
/** @var array $categories */
|
||||
protected $categories = [];
|
||||
|
||||
@@ -41,26 +38,6 @@ class ArticleEntity extends BaseEntity
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets date property.
|
||||
*
|
||||
* @param string $date
|
||||
*/
|
||||
public function setDate(string $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns date property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDate() : string
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first part of an article if "more separator" is found. Otherwise the whole content is returned.
|
||||
*
|
||||
|
@@ -29,6 +29,9 @@ abstract class BaseEntity
|
||||
/** @var string $description Meta description */
|
||||
protected $description = '';
|
||||
|
||||
/** @var string $date */
|
||||
protected $date;
|
||||
|
||||
/** @var string $content */
|
||||
protected $content;
|
||||
|
||||
@@ -178,6 +181,26 @@ abstract class BaseEntity
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets date property.
|
||||
*
|
||||
* @param string $date
|
||||
*/
|
||||
public function setDate(string $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns date property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDate() : string
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content property.
|
||||
*
|
||||
|
@@ -14,11 +14,6 @@ class ShowBlogDomain extends ContentDomain
|
||||
/** @var int $articleCount */
|
||||
protected $articleCount = 0;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of articles ordered by date.
|
||||
*
|
||||
|
@@ -14,11 +14,6 @@ class ShowFeedDomain extends ContentDomain
|
||||
/** @var int $articleCount */
|
||||
protected $articleCount = 0;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of articles ordered by date.
|
||||
*
|
||||
|
@@ -14,11 +14,12 @@ class ShowPageDomain extends ContentDomain
|
||||
public function getPageSlug() : string
|
||||
{
|
||||
$uri = rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
$pageName = trim($uri, '/');
|
||||
if (empty($pageName)) {
|
||||
$pageName = 'home';
|
||||
$pathParts = explode('/', trim($uri, '/'));
|
||||
$pageSlug = array_pop($pathParts);
|
||||
if (empty($pageSlug)) {
|
||||
$pageSlug = 'home';
|
||||
}
|
||||
return $pageName;
|
||||
return $pageSlug;
|
||||
}
|
||||
|
||||
/**
|
||||
|
64
src/Domain/ShowSitemapDomain.php
Normal file
64
src/Domain/ShowSitemapDomain.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace Nekudo\ShinyBlog\Domain;
|
||||
|
||||
class ShowSitemapDomain extends ContentDomain
|
||||
{
|
||||
/**
|
||||
* Fetches data required to add pages to sitemap.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPagesData() : array
|
||||
{
|
||||
$pagesData = [];
|
||||
$this->loadPageMeta('slug');
|
||||
foreach ($this->pageMeta as $pageMeta) {
|
||||
// skip page if slug does not match a route:
|
||||
if (!isset($this->config['routes'][$pageMeta['slug']])) {
|
||||
continue;
|
||||
}
|
||||
array_push($pagesData, [
|
||||
'urlPath' => $this->config['routes'][$pageMeta['slug']]['buildPattern'],
|
||||
'lastmod' => date('c', strtotime($pageMeta['date'])),
|
||||
]);
|
||||
}
|
||||
return $pagesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data required to add articles to sitemap.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArticlesData() : array
|
||||
{
|
||||
$articlesData = [];
|
||||
$this->loadArticleMeta('date');
|
||||
krsort($this->articleMeta, SORT_NATURAL);
|
||||
$articleRouteBuildPattern = $this->config['routes']['article']['buildPattern'];
|
||||
foreach ($this->articleMeta as $articleMeta) {
|
||||
array_push($articlesData, [
|
||||
'urlPath' => sprintf($articleRouteBuildPattern, $articleMeta['slug']),
|
||||
'lastmod' => date('c', strtotime($articleMeta['date'])),
|
||||
]);
|
||||
}
|
||||
return $articlesData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns date of latest blog article.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBlogLastmod() : string
|
||||
{
|
||||
$this->loadArticleMeta('date');
|
||||
krsort($this->articleMeta, SORT_NATURAL);
|
||||
if (empty($this->articleMeta)) {
|
||||
return '';
|
||||
}
|
||||
$firstItemData = reset($this->articleMeta);
|
||||
return date('c', strtotime($firstItemData['date']));
|
||||
}
|
||||
}
|
110
src/Responder/ShowSitemapResponder.php
Normal file
110
src/Responder/ShowSitemapResponder.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace Nekudo\ShinyBlog\Responder;
|
||||
|
||||
use SimpleXMLElement;
|
||||
|
||||
class ShowSitemapResponder extends HttpResponder
|
||||
{
|
||||
/**
|
||||
* @var array $items Holds items to be listed in sitemap.
|
||||
*/
|
||||
protected $items = [];
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setHeader();
|
||||
$sitemapXml = $this->renderSitemap();
|
||||
$this->found($sitemapXml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds blog-url to sitemap.
|
||||
*
|
||||
* @param string $urlPath
|
||||
* @param string $lastmod
|
||||
*/
|
||||
public function addBlogItem(string $urlPath, string $lastmod)
|
||||
{
|
||||
$settings = $this->config['sitemap']['blog'];
|
||||
$this->addItem($urlPath, $lastmod, $settings['changefreq'], $settings['priority']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds article-url to sitemap.
|
||||
*
|
||||
* @param string $urlPath
|
||||
* @param string $lastmod
|
||||
*/
|
||||
public function addArticleItem(string $urlPath, string $lastmod)
|
||||
{
|
||||
$settings = $this->config['sitemap']['article'];
|
||||
$this->addItem($urlPath, $lastmod, $settings['changefreq'], $settings['priority']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds page-url to sitemap.
|
||||
*
|
||||
* @param string $urlPath
|
||||
* @param string $lastmod
|
||||
*/
|
||||
public function addPageItem(string $urlPath, string $lastmod)
|
||||
{
|
||||
$settings = $this->config['sitemap']['page'];
|
||||
$this->addItem($urlPath, $lastmod, $settings['changefreq'], $settings['priority']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new url/item to sitemap.
|
||||
*
|
||||
* @param string $urlPath
|
||||
* @param string $lastmod
|
||||
* @param string $changefreq
|
||||
* @param string $priority
|
||||
*/
|
||||
public function addItem(string $urlPath, string $lastmod, string $changefreq, string $priority)
|
||||
{
|
||||
array_push($this->items, [
|
||||
'loc' => $this->getUrlFromPath($urlPath),
|
||||
'lastmod' => $lastmod,
|
||||
'changefreq' => $changefreq,
|
||||
'priority' => $priority
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets XML header.
|
||||
*/
|
||||
protected function setHeader()
|
||||
{
|
||||
header('Content-Type: text/xml', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders sitemap items to XML.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderSitemap() : string
|
||||
{
|
||||
$urlset = new SimpleXMLElement(
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||
LIBXML_NOERROR|LIBXML_ERR_NONE|LIBXML_ERR_FATAL
|
||||
);
|
||||
|
||||
// add urls:
|
||||
foreach ($this->items as $itemData) {
|
||||
$url = $urlset->addChild('url');
|
||||
$url->addChild('loc', $itemData['loc']);
|
||||
$url->addChild('lastmod', $itemData['lastmod']);
|
||||
$url->addChild('changefreq', $itemData['changefreq']);
|
||||
$url->addChild('priority', $itemData['priority']);
|
||||
}
|
||||
|
||||
// return xml:
|
||||
return $urlset->asXML();
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ use Nekudo\ShinyBlog\Action\ShowArticleAction;
|
||||
use Nekudo\ShinyBlog\Action\ShowBlogAction;
|
||||
use Nekudo\ShinyBlog\Action\ShowFeedAction;
|
||||
use Nekudo\ShinyBlog\Action\ShowPageAction;
|
||||
use Nekudo\ShinyBlog\Action\ShowSitemapAction;
|
||||
use Nekudo\ShinyBlog\Responder\HttpResponder;
|
||||
use Nekudo\ShinyBlog\Responder\NotFoundResponder;
|
||||
|
||||
@@ -121,6 +122,9 @@ class ShinyBlog
|
||||
case 'feed':
|
||||
$action = new ShowFeedAction($this->config);
|
||||
break;
|
||||
case 'sitemap':
|
||||
$action = new ShowSitemapAction($this->config);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException('Invalid action.');
|
||||
break;
|
||||
|
@@ -57,10 +57,26 @@ return [
|
||||
|
||||
'feed' => [
|
||||
|
||||
// Defines how many artciles will show up in RSS feed:
|
||||
// Defines how many articles will show up in RSS feed:
|
||||
'limit' => 20,
|
||||
],
|
||||
|
||||
// Defines sitemap priority and changefreq settings for different page types:
|
||||
'sitemap' => [
|
||||
'blog' => [
|
||||
'changefreq' => 'daily',
|
||||
'priority' => '1.0',
|
||||
],
|
||||
'article' => [
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.5',
|
||||
],
|
||||
'page' => [
|
||||
'changefreq' => 'monthly',
|
||||
'priority' => '0.4',
|
||||
],
|
||||
],
|
||||
|
||||
/* Routing
|
||||
*
|
||||
* In this section you can define the URLs for every page-type.
|
||||
@@ -71,11 +87,18 @@ return [
|
||||
'home' => [
|
||||
'method' => 'GET',
|
||||
'route' => '/',
|
||||
'buildPattern' => '/',
|
||||
'action' => 'page',
|
||||
],
|
||||
'sitemap' => [
|
||||
'method' => 'GET',
|
||||
'route' => '/sitemap.xml',
|
||||
'action' => 'sitemap',
|
||||
],
|
||||
'imprint' => [
|
||||
'method' => 'GET',
|
||||
'route' => '/imprint',
|
||||
'route' => '/foo/imprint',
|
||||
'buildPattern' => '/foo/imprint',
|
||||
'action' => 'page',
|
||||
],
|
||||
'blog' => [
|
||||
|
Reference in New Issue
Block a user