├── src ├── templates │ ├── blog-page.html.twig │ ├── blog-authors.html.twig │ ├── blog-archives.html.twig │ ├── blog-tags.html.twig │ ├── blog-post.html.twig │ └── blog-listings.html.twig ├── Twig │ ├── Parser.php │ ├── Markdown.php │ ├── Object.php │ ├── Page.php │ ├── ExpressionParser.php │ └── Theme.php ├── Component.php └── Blog.php ├── CHANGELOG.md ├── LICENSE.md ├── composer.json └── README.md /src/templates/blog-page.html.twig: -------------------------------------------------------------------------------- 1 | {{ content }} 2 | -------------------------------------------------------------------------------- /src/Twig/Parser.php: -------------------------------------------------------------------------------- 1 | env = $env; 10 | $this->expressionParser = new ExpressionParser($this, $this->env); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/templates/blog-authors.html.twig: -------------------------------------------------------------------------------- 1 | 10 | 11 | {{ page.set('title', 'Authors @ ' ~ blog.name) }} 12 | 13 | {% for author in authors %} 14 |

{{ author.name }} {{ author.count }}

15 | {% endfor %} -------------------------------------------------------------------------------- /src/Twig/Markdown.php: -------------------------------------------------------------------------------- 1 | theme = $theme; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function transform($content) 23 | { 24 | return $this->theme->markdown($content); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getName() 31 | { 32 | return 'Blog\Markdown'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Twig/Object.php: -------------------------------------------------------------------------------- 1 | properties = $properties; 13 | $this->methods = $methods; 14 | } 15 | 16 | public function __get($name) 17 | { 18 | return (isset($this->properties[$name])) ? $this->properties[$name] : null; 19 | } 20 | 21 | public function __isset($name) 22 | { 23 | return (isset($this->methods[$name])) ? false : true; 24 | } 25 | 26 | public function __call($name, $arguments) 27 | { 28 | if (isset($this->methods[$name]) && is_callable($this->methods[$name])) { 29 | return call_user_func_array($this->methods[$name], $arguments); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Twig/Page.php: -------------------------------------------------------------------------------- 1 | methods[$name] = array($page, $name); 16 | } 17 | } 18 | 19 | public function __get($name) 20 | { 21 | return PageClone::html()->$name; 22 | } 23 | 24 | public function __isset($name) 25 | { 26 | return (isset($this->methods[$name])) ? false : true; 27 | } 28 | 29 | public function __call($name, $arguments) 30 | { 31 | if (isset($this->methods[$name]) && is_callable($this->methods[$name])) { 32 | return call_user_func_array($this->methods[$name], $arguments); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.4] - 2017-01-08 10 | ### Added 11 | - ParsedownExtra as default markdown provider 12 | - Custom callable markdown implementations 13 | 14 | ## [1.3] - 2017-01-04 15 | ### Added 16 | - Microtimes to Twig templates debug data 17 | - Static Theme dumper method 18 | 19 | ## [1.2] - 2016-11-29 20 | ### Added 21 | - Twig dir function for relative paths 22 | 23 | ### Changed 24 | - Only save html pages in database 25 | 26 | ### Fixed 27 | - Empty category undefined offset 28 | 29 | ## [1.1] - 2016-10-25 30 | ### Added 31 | - Optionally specify Twig array keys via '**:**' colon, similar to hashes 32 | - Improved Global Vars Dump 33 | 34 | ### Fixed 35 | - Relative asset paths for Twig v1.27.0 36 | 37 | ## [1.0] - 2016-10-13 38 | - Initial Release 39 | -------------------------------------------------------------------------------- /src/templates/blog-archives.html.twig: -------------------------------------------------------------------------------- 1 | 10 | 11 | {{ page.set('title', 'The Archives @ ' ~ blog.name) }} 12 | 13 | {% for Y, years in archives|reverse(true) %} 14 | 15 |

{{ Y }} {{ years.count }}

16 | 17 |
18 | {% set columns = [] %} 19 | {% for M, months in years.months %} 20 |
21 |

22 | {{ M }} 23 | {% if months.count > 0 %} 24 |
{{ months.count }} 25 | {% endif %} 26 |
27 |

28 |
29 | {% endfor %} 30 |
31 | 32 | {% endfor %} -------------------------------------------------------------------------------- /src/templates/blog-tags.html.twig: -------------------------------------------------------------------------------- 1 | 10 | 11 | {{ page.set('title', 'Tag Cloud @ ' ~ blog.name) }} 12 | 13 |

14 | {% for tag in tags %} 15 | {% if tag.rank == 1 %} 16 | {{ tag.name }} 17 | {% elseif tag.rank == 2 %} 18 | {{ tag.name }} 19 | {% elseif tag.rank == 3 %} 20 | {{ tag.name }} 21 | {% elseif tag.rank == 4 %} 22 | {{ tag.name }} 23 | {% elseif tag.rank == 5 %} 24 | {{ tag.name }} 25 | {% endif %} 26 | {% endfor %} 27 |

28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kyle Gadd 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/templates/blog-post.html.twig: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 | 15 |

16 | Published: 17 | {% if post.author %} 18 | by 19 | {% endif %} 20 |

21 | 22 |
{{ post.content }}
23 | 24 | {% for tag in post.tags %} 25 | {% if loop.first %} 26 |

Tagged: 27 | {% endif %} 28 | 29 |  {{ tag.name }} 30 | 31 | {% if loop.last %} 32 |

33 | {% endif %} 34 | {% endfor %} 35 | 36 |
37 | 38 | {{ pagination.pager(post.previous, post.next) }} 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootpress/blog", 3 | "description": "A flat file Blog and CMS that doesn't skimp on features, and can be utilized in any website.", 4 | "keywords": ["bootpress", "blog", "flat-file", "cms", "twig", "markdown"], 5 | "homepage": "https://www.bootpress.org/components/blog.html", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Kyle Gadd", 10 | "email": "kyle.gadd@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4", 15 | "bootpress/page": "^1.0", 16 | "bootpress/asset": "^1.0", 17 | "bootpress/sqlite": "^1.0", 18 | "bootpress/sitemap": "^1.0", 19 | "bootpress/hierarchy": "^1.0", 20 | "bootpress/pagination": "^1.0", 21 | "symfony/var-dumper": "^2.3||^3.0", 22 | "symfony/finder": "^2.3||^3.0", 23 | "symfony/yaml": "^2.3||^3.0", 24 | "jbroadway/urlify": "^1.0", 25 | "xsirix/phpuri": "^1.0", 26 | "spartz/text-formatter": "^1.0", 27 | "aptoma/twig-markdown": "^2.0", 28 | "erusev/parsedown-extra": "^0.7", 29 | "twig/twig": "^1.27" 30 | }, 31 | "require-dev": { 32 | "bootpress/htmlunit": "^1.0", 33 | "friendsofphp/php-cs-fixer": "^1.0", 34 | "squizlabs/php_codesniffer": "^2.5" 35 | }, 36 | "autoload": { 37 | "psr-4": { "BootPress\\Blog\\": "src" } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Twig/ExpressionParser.php: -------------------------------------------------------------------------------- 1 | parser->getStream(); 13 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); 14 | 15 | $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine()); 16 | $first = true; 17 | while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { 18 | if (!$first) { 19 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); 20 | 21 | // trailing ,? 22 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) { 23 | break; 24 | } 25 | } 26 | $first = false; 27 | 28 | // $node->addElement($this->parseExpression()); 29 | $value = $this->parseExpression(); 30 | if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) { 31 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ':'); 32 | $node->addElement($this->parseExpression(), $value); 33 | } else { 34 | $node->addElement($value); 35 | } 36 | } 37 | $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); 38 | 39 | return $node; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/templates/blog-listings.html.twig: -------------------------------------------------------------------------------- 1 | 14 | 15 | {% if search %} 16 | 17 | {% if blog.page == 'index' %} 18 | 19 | {{ page.set('title', 'Search: ' ~ search|escape('html_attr') ~ ' @ ' ~ blog.name) }} 20 | 21 | {% elseif blog.page == 'category' %} 22 | 23 | {{ page.set('title', 'Search: ' ~ search|escape('html_attr') ~ ' @ ' ~ category|join(' » ')) }} 24 | 25 | {% endif %} 26 | 27 | {% elseif blog.page == 'index' %} 28 | 29 | {{ page.set('title', blog.name) }} 30 | 31 | {% elseif blog.page == 'category' %} 32 | 33 | {{ page.set('title', category|join(' » ') ~ ' @ ' ~ blog.name) }} 34 | 35 | {% elseif blog.page == 'archives' %} 36 | 37 | {% if archive|length == 4 %} 38 | {% set date = archive.date|date('F j, Y') %} 39 | {% elseif archive|length == 3 %} 40 | {% set date = archive.date|date('F Y') %} 41 | {% elseif archive|length == 2 %} 42 | {% set date = archive.date|date('Y') %} 43 | {% endif %} 44 | 45 | {{ page.set('title', date ~ ' Archives @ ' ~ blog.name) }} 46 | 47 | {% elseif blog.page == 'authors' %} 48 | 49 | {{ page.set('title', author.name ~ ' @ ' ~ blog.name) }} 50 | 51 | {% elseif blog.page == 'tags' %} 52 | 53 | {{ page.set('title', tag.name ~ ' @ ' ~ blog.name) }} 54 | 55 | {% endif %} 56 | 57 | {% if not pagination.set('page', 10) %} 58 | {{ pagination.total(blog.query(listings, 'count')) }} 59 | {% endif %} 60 | 61 | {% set posts = blog.query(listings, pagination) %} 62 | 63 | {% for post in posts %} 64 | 65 |
66 | 67 |

{{ post.title }}

68 | 69 |

70 | {% if post.page.image %} 71 | {{ post.title }} 72 | {% endif %} 73 | {% if search %} 74 | {{ post.snippet }} 75 | {% else %} 76 | {{ post.content|striptags|length > 500 ? post.content|striptags|slice(0,500) ~ '…' : post.content|striptags }} 77 | {% endif %} 78 |

79 | 80 | Read more → 81 | 82 |
83 | 84 | {% endfor %} 85 | 86 | {{ pagination.pager() }} 87 | -------------------------------------------------------------------------------- /src/Component.php: -------------------------------------------------------------------------------- 1 | url['path']`` Twig template, a string if ``$page->url['format'] != 'html'``, or an array suitable for passing to ``$blog->theme->renderTwig($file)`` with the following keys: 18 | * 19 | * - '**file**' => The appropriate Twig template that is equipped to deal with these '**type**' of '**vars**'. 20 | * - '**type**' => The kind of Blog page you are working with. Either *'page'*, *'post'*, *'category'*, *'index'*, *'archives'*, *'authors'*, or *'tags'*. 21 | * - '**vars**' => For the Twig template to utilize. 22 | * - '**default**' => An alternate '**file**' to use if it's missing in your theme. 23 | * 24 | * ```php 25 | * if (is_array($file = $blog->page())) { // An 'index.html.twig' file 26 | * $html = $blog->theme->renderTwig($file); 27 | * } elseif ($file !== false) { // A 'txt', 'json', 'xml', 'rdf', 'rss', or 'atom' page 28 | * $page->send(Asset::dispatch($page->url['format'], $file)); 29 | * } 30 | * ``` 31 | */ 32 | public function page() 33 | { 34 | $page = Page::html(); 35 | $listings = $this->config('blog', 'listings'); 36 | if ($page->url['format'] != 'html') { 37 | $file = (preg_match('/\.(txt|json|xml|rdf|rss|atom)$/', $page->url['path'])) ? $this->folder.'content/'.$page->url['path'].'.twig' : null; 38 | 39 | return (is_file($file)) ? trim($this->theme->renderTwig($file)) : false; 40 | } elseif ($id = $this->file($page->url['path'])) { 41 | $params = array('id' => $id, 'path' => $page->url['path']); 42 | $method = 'blog'; 43 | } elseif ($route = $page->routes(array( 44 | $listings, 45 | $listings.'/[archives:path]/[i:year]?/[i:month]?/[i:day]?', 46 | $listings.'/[authors|tags:path]/[:name]?', 47 | '[**:category]', 48 | ))) { 49 | $params = $route['params']; 50 | $method = (isset($params['path'])) ? $params['path'] : 'listings'; 51 | if ($method == 'listings' && $search = $page->request->query->get('search')) { 52 | $params['search'] = $search; 53 | } 54 | } 55 | if (!isset($method) || (isset($params['category']) && !is_dir($this->folder.'content/'.$params['category']))) { 56 | $template = false; 57 | } elseif ($template = $this->$method($params)) { 58 | $template = array_combine(array('file', 'type', 'vars'), $template); 59 | $template['default'] = __DIR__.'/templates/'; 60 | $this->theme->vars['blog']->properties['page'] = $template['type']; 61 | } 62 | 63 | return $template; 64 | } 65 | 66 | private function blog($params, array $vars = array()) 67 | { 68 | $page = Page::html(); 69 | extract($params); // 'id' and 'path' 70 | $page->enforce($path); 71 | $info = $this->info($id); 72 | if (is_bool($info['published'])) { 73 | return array('blog-page.html.twig', 'post', $info); 74 | } 75 | $vars['post'] = $info; 76 | if ($search = $page->request->query->get('search')) { 77 | $sitemap = new Sitemap(); 78 | if ($docid = $sitemap->db->value('SELECT docid FROM sitemap WHERE path = ?', $path)) { 79 | $words = $sitemap->words($search, $docid); 80 | if (!empty($words)) { 81 | $vars['search'] = $words; 82 | } 83 | } 84 | unset($sitemap); 85 | } 86 | $vars['breadcrumbs'] = $this->breadcrumbs(); 87 | foreach ($vars['post']['categories'] as $category) { 88 | $vars['breadcrumbs'][$category['name']] = $category['url']; 89 | } 90 | $vars['breadcrumbs'][$vars['post']['title']] = $vars['post']['url']; 91 | 92 | return array('blog-post.html.twig', 'post', $vars); 93 | } 94 | 95 | private function listings($params, array $vars = array()) 96 | { 97 | $page = Page::html(); 98 | extract($params); // 'path'?, 'category'? and 'search'? 99 | if (isset($category)) { 100 | $url = $page->url('base', $category); 101 | } else { 102 | $url = $page->url('blog/listings'); 103 | } 104 | $page->enforce($url); 105 | $type = (isset($category)) ? 'category' : 'index'; 106 | $vars['listings'] = array(); 107 | $vars['breadcrumbs'] = $this->breadcrumbs(); 108 | if (isset($category)) { 109 | $hier = new Hierarchy($this->db, 'categories'); 110 | $path = $hier->path('path', $category, array('path', 'name')); 111 | if (empty($path)) { 112 | return false; 113 | } 114 | $tree = $hier->tree(array('path', 'name'), 'path', $category); 115 | $counts = $hier->counts('blog', 'category_id'); 116 | foreach ($tree as $id => $fields) { 117 | $tree[$id]['count'] = (isset($counts[$id])) ? $counts[$id] : 0; 118 | } 119 | foreach ($path as $row) { 120 | $vars['category'][] = $row['name']; 121 | $vars['breadcrumbs'][$row['name']] = $page->url('base', $row['path']); 122 | } 123 | $vars['categories'] = $this->query('categories', array('nest' => $hier->nestify($tree), 'tree' => $tree)); 124 | $vars['listings']['categories'] = array_keys($tree); 125 | } 126 | if (isset($search) && !empty($search)) { 127 | $vars['search'] = $search; 128 | $vars['breadcrumbs']['Search'] = $page->url('add', $url, 'search', $search); 129 | $vars['listings']['search'] = $search; 130 | } 131 | 132 | return array('blog-listings.html.twig', $type, $vars); 133 | } 134 | 135 | private function archives($params, array $vars = array()) 136 | { 137 | $page = Page::html(); 138 | list($path, $Y, $m, $d) = array_pad(array_values($params), 4, ''); 139 | if (!empty($d)) { 140 | list($from, $to) = $this->range($Y, $m, $d); 141 | $page->enforce($page->url('blog/listings', $path, date('/Y/m/d', $from))); 142 | $vars['archive'] = array_combine(array('date', 'year', 'month', 'day'), explode(' ', date($from.' Y F j', $from))); 143 | $vars['breadcrumbs'] = $this->breadcrumbs(array( 144 | 'Archives' => $path, 145 | $vars['archive']['year'] => $Y, 146 | $vars['archive']['month'] => $m, 147 | $vars['archive']['day'] => $d, 148 | )); 149 | } elseif (!empty($m)) { 150 | list($from, $to) = $this->range($Y, $m); 151 | $page->enforce($page->url('blog/listings', $path, date('/Y/m', $from))); 152 | $vars['archive'] = array_combine(array('date', 'year', 'month'), explode(' ', date($from.' Y F', $from))); 153 | $vars['breadcrumbs'] = $this->breadcrumbs(array( 154 | 'Archives' => $path, 155 | $vars['archive']['year'] => $Y, 156 | $vars['archive']['month'] => $m, 157 | )); 158 | } elseif (!empty($Y)) { 159 | list($from, $to) = $this->range($Y); 160 | $page->enforce($page->url('blog/listings', $path, date('/Y', $from))); 161 | $vars['archive'] = array_combine(array('date', 'year'), explode(' ', date($from.' Y', $from))); 162 | $vars['breadcrumbs'] = $this->breadcrumbs(array( 163 | 'Archives' => $path, 164 | $vars['archive']['year'] => $Y, 165 | )); 166 | } else { 167 | $page->enforce($page->url('blog/listings', $path)); 168 | $vars['archives'] = $this->query('archives'); 169 | $vars['breadcrumbs'] = $this->breadcrumbs(array( 170 | 'Archives' => $path, 171 | )); 172 | 173 | return array('blog-archives.html.twig', 'archives', $vars); 174 | } 175 | $vars['listings']['archives'] = array($from, $to); 176 | 177 | return array('blog-listings.html.twig', 'archives', $vars); 178 | } 179 | 180 | private function authors($params, array $vars = array()) 181 | { 182 | $page = Page::html(); 183 | extract($params); // 'path' and 'name'? 184 | $vars['breadcrumbs'] = $this->breadcrumbs(array('Authors' => $path)); 185 | if (!isset($name)) { // just authors, no posts 186 | $page->enforce($page->url('blog/listings', $path)); 187 | $vars['authors'] = $this->query('authors'); 188 | 189 | return array('blog-authors.html.twig', 'authors', $vars); 190 | } 191 | $vars['author'] = $this->query('authors', $name); 192 | if (empty($vars['author'])) { 193 | $page->eject($page->url('blog/listings', $path)); 194 | 195 | return false; 196 | } 197 | $vars['breadcrumbs'][$vars['author']['name']] = $page->url('blog/listings', $path, $vars['author']['path']); 198 | $vars['listings']['count'] = $vars['author']['count']; 199 | $vars['listings']['authors'] = $name; 200 | 201 | return array('blog-listings.html.twig', 'authors', $vars); 202 | } 203 | 204 | private function tags($params, array $vars = array()) 205 | { 206 | $page = Page::html(); 207 | extract($params); // 'path' and 'name'? 208 | $vars['breadcrumbs'] = $this->breadcrumbs(array('Tags' => $path)); 209 | if (!isset($name)) { // search all tags and get a frequency count 210 | $page->enforce($page->url('blog/listings', $path)); 211 | $vars['tags'] = $this->query('tags'); 212 | 213 | return array('blog-tags.html.twig', 'tags', $vars); 214 | } 215 | $vars['tag'] = $this->query('tags', $name); 216 | if (empty($vars['tag'])) { 217 | $page->eject($page->url('blog/listings', $path)); 218 | 219 | return false; 220 | } 221 | $vars['breadcrumbs'][$vars['tag']['name']] = $page->url('blog/listings', $path, $vars['tag']['path']); 222 | $vars['listings']['count'] = $vars['tag']['count']; 223 | $vars['listings']['tags'] = $vars['tag']['path']; 224 | 225 | return array('blog-listings.html.twig', 'tags', $vars); 226 | } 227 | 228 | private function breadcrumbs(array $links = array()) 229 | { 230 | $breadcrumbs = array(); 231 | $breadcrumbs[$this->config('blog', 'breadcrumb')] = Page::html()->url('blog/listings'); 232 | $previous = ''; 233 | foreach ($links as $name => $path) { 234 | $breadcrumbs[$name] = Page::html()->url('blog/listings', $previous.$path); 235 | $previous .= $path.'/'; 236 | } 237 | 238 | return $breadcrumbs; 239 | } 240 | 241 | private function range($Y, $m = null, $d = null) 242 | { 243 | if (!empty($d)) { 244 | $from = mktime(0, 0, 0, (int) $m, (int) $d, (int) $Y); 245 | $to = mktime(23, 59, 59, (int) $m, (int) $d, (int) $Y); 246 | } elseif (!empty($m)) { 247 | $from = mktime(0, 0, 0, (int) $m, 1, (int) $Y); 248 | $to = mktime(23, 59, 59, (int) $m + 1, 0, (int) $Y); 249 | } else { 250 | $from = mktime(0, 0, 0, 1, 1, (int) $Y); 251 | $to = mktime(23, 59, 59, 1, 0, (int) $Y + 1); 252 | } 253 | 254 | return array($from, $to); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use BootPress\Blog\Component as Blog; 2 | 3 | [![Packagist][badge-version]][link-packagist] 4 | [![License MIT][badge-license]](LICENSE.md) 5 | [![HHVM Tested][badge-hhvm]][link-travis] 6 | [![PHP 7 Supported][badge-php]][link-travis] 7 | [![Build Status][badge-travis]][link-travis] 8 | [![Code Climate][badge-code-climate]][link-code-climate] 9 | [![Test Coverage][badge-coverage]][link-coverage] 10 | 11 | A flat file Blog and CMS that doesn't skimp on features, and can be utilized in any website. 12 | 13 | ## Installation 14 | 15 | Add the following to your ``composer.json`` file. 16 | 17 | ``` bash 18 | { 19 | "require": { 20 | "bootpress/blog": "^1.0" 21 | } 22 | } 23 | ``` 24 | 25 | Create an ``.htaccess`` file in your website's public root folder to redirect everything that doesn't exist to an ``index.php`` file. 26 | 27 | ```htaccess 28 | # Prevent directory browsing 29 | Options All -Indexes 30 | 31 | # Turn on URL re-writing (remove 'example.com/' if not on localhost) 32 | RewriteEngine On 33 | RewriteBase /example.com/ 34 | 35 | # If the file exists, then that's all folks 36 | RewriteCond %{REQUEST_FILENAME} -f 37 | RewriteRule .+ - [L] 38 | 39 | # For everything else, there's BootPress 40 | RewriteRule ^(.*)$ index.php [L] 41 | ``` 42 | 43 | Your ``index.php`` file should then look something like this: 44 | 45 | ```php 46 | '../page', // a private (root) directory 58 | 'base' => 'http://localhost/example.com', 59 | 'suffix' => '.html', 60 | )); 61 | $html = ''; 62 | 63 | // Deliver sitemap and assets first 64 | if ($asset = Asset::cached('assets')) { 65 | $page->send($asset); 66 | } elseif ($xml = Sitemap::page()) { 67 | $page->send($xml); 68 | } 69 | 70 | // Implement a blog 71 | $blog = new Blog(); 72 | if (false !== $file = $blog->page()) { 73 | if (is_array($file)) { // An 'index.html.twig' file 74 | $html = $blog->theme->renderTwig($file); 75 | } else { // A 'txt', 'json', 'xml', 'rdf', 'rss', or 'atom' page 76 | $page->send(Asset::dispatch($page->url['format'], $file)); 77 | } 78 | } else { 79 | $page->send(404); 80 | } 81 | 82 | // Create the layout 83 | $html = $page->display($blog->theme->layout($html)); 84 | 85 | // Send to user 86 | $page->send(Asset::dispatch('html', $html)); 87 | ``` 88 | 89 | ## Setup Blog 90 | 91 | Create a ``../page/blog/config.yml`` file with the following information: 92 | 93 | ```yaml 94 | blog: 95 | name: Example # The name of your website 96 | image: logo.png # The main image relative to this directory 97 | listings: blog # The url base for all your listing pages - authors, archives, tags, etc. 98 | breadcrumb: Blog # How to reference the listings in your breadcrumbs array 99 | theme: default # The main theme for your site 100 | ``` 101 | 102 | You can access any of these in your Twig templates eg. ``{{ blog.name }}``, including the ``{{ blog.page }}`` you are on. Eventually this file will be full of authors, categories, and tags that you can easily manage as well. You can create a [Bootstrap list group](http://getbootstrap.com/components/#list-group) of categories by: 103 | 104 | ```twig 105 | 114 | ``` 115 | 116 | Other ``{{ blog.query(...) }}``'s include '**tags**', '**authors**', '**archives**', '**recent**', '**featured**', '**similar**', '**posts**', and **[...]** listings of every sort, otherwise known as "The Loop". 117 | 118 | ## Create Content 119 | 120 | A BootPress Blog is a flat-file CMS, which means you don't need any fancy admin interface to manage all of the content that is scattered througout a database. You simply create files. All of your blog's posts and pages will reside in the ``../page/blog/content/`` directory, and if you look at a URL, you will be able to follow the folders straight to your ``index.html.twig`` file. For example: 121 | 122 | | URL | File | 123 | | ------------------------------------- | ------------------------------------------------------------ | 124 | | / | blog/content/index.html.twig | 125 | | /feed.rss | blog/content/feed.rss.twig | 126 | | /about-me.html | blog/content/about-me/index.html.twig | 127 | | /category/post.html | blog/content/category/post/index.html.twig | 128 | | /category/subcategory/long-title.html | blog/content/category/subcategory/long-title/index.html.twig | 129 | 130 | Why not have the '**/about-me.html**' URL file at '**content/about-me.html.twig**' instead of '**content/about-me/index.html.twig**' instead, right? This is so you can have all of the assets that you want to use, right there where you want to use them. Linking to them is even easier. Place an '**image.jpg**' in the '**content/about-me/**' folder, and link to ``{{ 'image.jpg'|asset }}`` in the '**index.html.twig**' file. Would you like to resize that? Try an ``{{ 'image.jpg?w=300'|asset }}``. To see all the options, check out the [Quick Reference "Glide"](http://glide.thephpleague.com/1.0/api/quick-reference/). 131 | 132 | Non-HTML files are accessed according to the '**/feed.rss**' URL example above. 133 | 134 | ## Twig Templates 135 | 136 | Every ``index.html.twig`` file is a Twig template that receives the [BootPress Page Component](https://packagist.org/packages/bootpress/page), so that you can interact with your HTML Page. The methods available to you are: 137 | 138 | - ``{{ page.set() }}`` - Set HTML Page properties. Things like the title, keywords (tags), author, etc. 139 | - ``{{ page.url() }} `` - Either create a url, or manipulate it's query string and fragment. 140 | - ``{{ page.get() }} `` - Access $_GET parameters. 141 | - ``{{ page.post() }}`` - Access $_POST parameters. 142 | - ``{{ page.tag() }} `` - Generate an HTML tag programatically. 143 | - ``{{ page.meta() }} `` - Insert ```` tag(s) into the ```` section of your page. 144 | - ``{{ page.link() }} `` - Include js, css, ico, etc links in your page. 145 | - ``{{ page.style() }} `` - Add CSS ``