1
0
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:
Simon Samtleben
2016-03-25 14:18:40 +01:00
parent d2c6d9729d
commit 22c28f18d6
14 changed files with 368 additions and 52 deletions

View File

@@ -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.

View File

@@ -3,6 +3,7 @@
"metaTitle": "ShinyBlog - A shiny blog :)",
"description": "A dummy homepage...",
"slug": "home",
"date" : "2016-01-23",
"layout": "fullpage"
}

View File

@@ -2,7 +2,8 @@
"title": "Imprint",
"metaTitle": "Imprint of ShinyBlog",
"description": "Imprint description",
"slug": "imprint"
"slug": "imprint",
"date" : "2016-01-23"
}
::METAEND::

View 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;
}
}

View File

@@ -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;
}
/**

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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;
}
/**

View 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']));
}
}

View 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();
}
}

View File

@@ -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;

View File

@@ -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' => [