Hello. I'm trying to put together some pieces of code to make a basic Poor man CMS. I'm looking for these features: Pagination. Full Friendly URLs. Articles. Tags. Flat files instead of database driven. Some doubts There is a problem in the pagination list: Element 1 has url /articles/0 instead of /articles. Element 2 has url /articles/1 instead of /articles/2. What would be the best approach to add a tag system? the articles have an ID. I have thought about creating a file named tags.txt assigning tags to each article ID in this way: 1 tag1,tag2,tag3 2 tag2 3 tag4,tag5 Code (markup): And then processing it with PHP. Is it a good idea? My programming knowledge is very limited - only a bit of basic C. Any help is appreciated. Actual code Most of it is taken from @deathshadow 's contribution to several forums and his website Cutcodedown. Pagination is taken from the modification done to @Jeremy Benson 's code. I made some modifications. index.php <?php // start compression first since it does a heading and starts buffering define('CONTENT_ENCODING', (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== false) ? 'x-gzip' : (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) ? 'gzip' : false ); if (CONTENT_ENCODING) include('libs/gzip.lib.php'); function cleanPath($path) { return trim(str_replace(['\\','%5C'],'/',$path),'/'); } function bomb($title, $message) { template_header('ERROR - ' . $title); echo ' <div id="fatalError"> <h2> ',$title,' </h2> <p>',$message,'</p> <p><strong>EXECUTION HALTED</strong></p> <!-- #fatalError --></div>'; template_footer(); die; } final class request { private static $data = []; public static function value($index = 0, $clean = false) { if ($index === true) { $index = 0; $clean = true; } if (count(self::$data) == 0) { $path = cleanPath(parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH)); if (strpos($path,'..')) bomb('hackingDetected', 'uptreeFail'); $path = substr($path, strlen(HTTP_ROOT) - 1); self::$data = empty($path) ? ['default'] : explode('/',$path); foreach (self::$data as &$p) $p = urldecode($p); } return isset(self::$data[$index]) ? ( $clean ? cleanName(self::$data[$index]) : self::$data[$index] ) : false; } } // class request. request::value(0) return first parameter. /* define('SCRIPT_PATH', cleanPath(pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME))); define('ROOT_HTTP', '/' . SCRIPT_PATH . (SCRIPT_PATH == '' ? '' : '/')); define('ROOT_LOCAL', pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_DIRNAME) . '/'); define('HOST_PROTOCOL', 'http' . ( isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off') ? 's' : '' ) . '://'); define('HOST_HTTP', HOST_PROTOCOL . $_SERVER['HTTP_HOST']); define('BASE_HTTP', HOST_HTTP . ROOT_HTTP); define('PATH_HTTP', '/' . parse_url(cleanPath($_SERVER['REQUEST_URI']), PHP_URL_PATH) ); define('FULL_HTTP', HOST_HTTP . PATH_HTTP); define('CANONICAL_PATH', request::getPath()); define('CANONICAL_URI', HOST_PROTOCOL . WORKING_DOMAIN . '/' . CANONICAL_PATH); define('CANONICAL_URIe', urlencode(CANONICAL_URI)); */ define('SCRIPT_PATH', cleanPath(pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME))); define('ROOT_HTTP', '/' . SCRIPT_PATH . (SCRIPT_PATH == '' ? '' : '/')); define('SCRIPT_FILENAME',cleanPath($_SERVER['SCRIPT_FILENAME'])); define('HTTP_ROOT',str_ireplace('index.php','',$_SERVER['PHP_SELF'])); define('LOCAL_ROOT',str_ireplace('index.php','',SCRIPT_FILENAME)); define('HTTP_THEME', HTTP_ROOT . 'theme/'); require_once('setup.php'); switch(request::value()) { case 'article': require_once('theme/article.template.php'); break; default: // then it must be the index, a static or Not Found require_once( is_file($targetFile = 'statics/' . request::value() . '.php') ? $targetFile : 'statics/404.php' ); } ?> Code (markup): libs/articles.lib.php <?php function newsNumber() { global $article_dir,$users,$txt_convert; $articles=scandir($article_dir,1); return (count($articles)-2); // Number of files except . and .. } function news($mode,$start,$count) { global $article_dir,$users,$txt_convert; $articles=scandir($article_dir,1); $t=$start; $end=$t+$count; if ($end>count($articles)) { $end=count($articles); } while ($t<$end) { switch ($articles[$t]) { case '.': case '..': break; default: list($id, $date,$subject,$author)=explode('_',$articles[$t]); //$author=$users[str_replace('.article.txt','',$author)]; $author = str_replace('.article.txt','',$author); $date=date("d F Y", mktime(0,0,0, substr($date,4,2), substr($date,6,2), substr($date,0,4) ) ); $contents=str_ireplace($txt_convert['in'],$txt_convert['out'], trim(file_get_contents($article_dir.'/'.$articles[$t])) ); echo ' <div class="article"> <h3> '; echo ' ',$subject,' <span>',$date,'</span>'; echo ' </h3> '; echo '<div class="article_body">'; echo ' <p>',$contents,'</p> <div class="author">Author: ',$author,'</div> <div class="id">ID: ',$id,'</div> '; echo '</div>'; echo ' </div> '; break; } $t++; } } ?> Code (markup): libs/paginate.lib.php <?php class Paginate { private $perPage, $maxShow, $middle, $href, $current, $lastPage; public function __construct( $href = '/', $current = 0, $total = 0, $perPage = 5, $maxShow = 5 //, //$field = 'page' ) { //$this->href = ' href="' . $href . ( // strpos($href, '?') === FALSE ? '?' : '&' //) . $field . '='; $this->href = ' href="'; $this->current = $current; $this->total = $total; $this->perPage = $perPage; $this->maxShow = $maxShow; $this->middle = floor($maxShow / 2); $this->lastPage = floor(($total - 1) / $perPage); } // Paginate::__construct public function getLimits() { return [ ':offset' => $this->current * $this->perPage, ':limit' => $this->perPage ]; } private function anchorLine($page, $text, $title = false, $rel = false) { $tag = $page < 0 ? 'span' : 'a'; echo ' <li><', $tag, ( $page >= 0 ? $this->href . $page . '"' : '' ), ( $page == -2 ? ' class="disabled"' : '' ), ( $title ? ' title="' . $title . '"' : '' ), ( $rel ? ' rel="' . $rel . '"' : '' ), '>', $text, '</', $tag, '></li>'; } public function show() { if ($this->total <= $this->perPage) return; echo ' <ul class="pagination">'; if (($this->lastPage > 0) && ($this->current > 0)) { $this->anchorLine(0, 'First'); $this->anchorLine($this->current - 1, '◀', 'Previous Page', 'prev'); } else { $this->anchorLine(-2, 'First'); $this->anchorLine(-2, '◀'); } if ($this->lastPage >= $this->maxShow) { $counter = ($this->current <= $this->middle) ? 0 : $this->current - $this->middle; $endPage = $counter + $this->maxShow; if ($endPage > $this->lastPage) { $counter = $this->lastPage - $this->maxShow; if ($counter < 0) $counter = 0; $endPage = $this->lastPage; } } else { $counter = 0; $endPage = $this->lastPage; } while ($counter <= $endPage) $this->anchorLine( $counter == $this->current ? -1 : $counter, ++$counter ); if (($this->lastPage > 0) && ($this->current < $this->lastPage)) { $this->anchorLine($this->current + 1, '▶', 'Next Page', 'next'); $this->anchorLine($this->lastPage, 'Last'); } else { $this->anchorLine(-2, '▶'); $this->anchorLine(-2, 'Last'); } echo ' </ul>'; } // Paginate::show } // Paginate Code (markup): libs/gzip.lib.php <?php /* gzip.lib.php Version 1.0 Jason M. Knight, August 2009 Uses a proper exit handler to provide automation of gzip compression of our PHP output with little if any headaches. ASSUMES: CONTENT_ENCODING contains either 'x-gzip' or 'gzip' based on the value in HTTP_ACCEPT_ENCODING. See "defines.php" to see how this is set. If STRIP_WHITESPACE is defined whitespace between tags or at the start of lines will be stripped, as will comments. Whitespace between a tag and CDATA or between attributes will be left alone. */ ob_start(); ob_implicit_flush(0); register_shutdown_function(function() { header('Content-Encoding: ' . CONTENT_ENCODING); $contents = ob_get_contents(); if (defined('STRIP_WHITESPACE')) $contents = preg_replace( ['#<!--.*?-->#s', '#>\s+<#', '#\n\s+<#'], ['', '><', '<'], $data ); ob_end_clean(); echo "\x1f\x8b\x08\x00\x00\x00\x00\x00", substr(gzcompress($contents, 6), 0, -4); }); [/B] Code (markup): statics/articles.php <?php require_once('theme/index.template.php'); template_header( false, // don't show "pageTitle - Site title" 'seven,or,eight,words,that,exist,in,body', // keywords 'A short natural language description describing the site' ); require_once('libs/articles.lib.php'); require_once('libs/paginate.lib.php'); if (request::value(2)!='') { echo '<h1>Error 404</h1>'; http_response_code(404); template_footer(); die(); } echo ' <h2>Articles</h2> <p> This is where your page unique content would go! </p> '; //$page = isset($_GET['page']) && is_numeric($_GET['page']) ? $_GET['page'] : 0; $page = request::value(1); $paginator = new Paginate('', $page, newsNumber()); $paginator->show(); $limits = $paginator->getLimits(); news(false, $limits[':offset'], $limits[':limit']); template_footer(); ?> Code (markup): theme/index.template.php <?php function template_header( $pageTitle = false, $keywords = false, $description = false ) { echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" ><head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Language" content="en" /> <meta name="viewport" content="width=device-width; initial-scale=1.0" />'; if ($keywords) echo ' <meta name="keywords" content="', $keywords, '" />'; if ($description) echo ' <meta name="description" content="', $description, '" />'; echo ' <link type="text/css" rel="stylesheet" href="', ROOT_HTTP, 'theme/screen.css" media="screen,projection,tv" /> <title> ', ($pageTitle ? $pageTitle . ' - ' : ''), ' Site Title </title> </head><body> <div id="top" class="widthWrapper"> <h1> Site Title <span><!-- image replacement --></span> </h1> <ul id="mainMenu"> <li><a href="', ROOT_HTTP, '">Home</a></li> <li><a href="', ROOT_HTTP, 'articles">Articles</a></li> <li><a href="', ROOT_HTTP, 'about">About</a></li> <li><a href="', ROOT_HTTP, 'contact">Contact</a></li> </ul> <hr /><!-- remove if content starts with numbered heading --> <div id="contentWrapper"><div id="content">'; // using double-wrapper for fluid content-first columns } // template_header function template_footer($extrasFile = false) { echo ' <!-- #content, #contentWrapper --></div></div> <div id="extras">'; // any static / common to all pages sidebar stuff here if ($extrasFile) include($extrasFile . '.php'); echo ' <!-- #extras --></div> <div id="footer"> Disclaimer / Other footer stuff here <!-- #footer --></div> <!-- #top.widthWrapper --></div> '; } // template_footer ?> Code (markup): References for the code I'm using https://forums.digitalpoint.com/threads/pagination-trouble.2765688/#post-19241004 http://www.codingforums.com/php/376974-what-better.html http://www.webdeveloper.com/forum/showthread.php?294891-Maintaining-Large-Websites-(input-wanted) http://www.codingforums.com/php/377106-single-entry-point-php.html https://forums.digitalpoint.com/threads/what-is-the-need-of-php-in-a-website.506496/#post-4760020
This code finds the requested article scanning the directory and comparing the title. Is there a better way to do this without SQL? I'm afraid of bad performance when the number of articles is high. Please sorry for the bad indentation, when copying the content to this editor the lines appear misplaced. statics/article.php <?php require_once('theme/index.template.php'); template_header( false, // don't show "pageTitle - Site title" 'seven,or,eight,words,that,exist,in,body', // keywords 'A short natural language description describing the site' ); require_once('libs/articles.lib.php'); require_once('libs/paginate.lib.php'); $number_of_articles = newsNumber(); $number_of_pages = ceil($number_of_articles / 5); $articles=scandir($article_dir,1); $t=0; $found=0; while (!$found && $t < $number_of_articles ) { list($id,$date,$subject,$author)=explode('_',$articles[$t]); $slug = str_replace(' ', '_', strtolower($subject)); if ($slug == request::value(1)) $found++; $t++; } if (!$found) { echo '<h1>Error 404</h1>'; http_response_code(404); template_footer(); die(); } else { if ($t == $number_of_articles) // Sucky solution but it fixes a bug displaying the last article. $t--; $slug = str_replace(' ', '_', strtolower($subject)); $contents=str_ireplace($txt_convert['in'],$txt_convert['out'], trim(file_get_contents($article_dir.'/'.$articles[$t])) ); echo ' <div class="article"> <h3> ',$subject,' </h3> <div class="article_body"> ',$contents,' <div class="author">Author: ',$author,'</div> </div> </div> '; } template_footer(); ?> Code (markup): I have to fix details such as unique keywords & description for each article and clean up the code. Apart from that I'm happy with the results, I was using wok static generator and now I feel more free with PHP.
A legitimate concern, which can be slightly alleviated if you do something like break down months into their own subdirectories, but depending on your traffic levels that can very quickly result in diminishing returns if you post a LOT of date sorted articles. ScanDIr and glob both start to fall apart on high traffic levels in that regard -- and WHEN that becomes a concern, that's when you stop using a poor man's setup, bite the bullet, and dive into PHP. Poor man's is perfect for static page websites, but when it turns into a blog if you end up with a LOT of entries that you're sorting by date? That's when you'll start to have problems. You could use an intermediate step of a static file containing the directory entries as your own sort of "index", keeping say... 12 or so entries per file as a per-line item, but sooner or later you might have to man-up and use a database. Poor man's has it's flaws -- and while you CAN use it for a blog type system, it's really more of a education tool in that regard than a practical system. Eventually you do reach the point where it's time to put on the big boy pants. Again though, if all you have are static pages, it's bloody brilliant. NOT that adding a database to it is rocket science... Oh, and for the love of Christmas, if you have multiple echo in a row, only use one echo!
I'm very happy exploring PHP, it's much more powerful and pleasant than messing with frameworks or static site generators. I have cleaned the code a little and I've also corrected a bug in statics/article.php. It's now adapted to utf-8. Although I still need to polish the code, the basic functionality is working fine for now statics/article.php <?php require_once('libs/articles.lib.php'); require_once('libs/paginate.lib.php'); $number_of_articles = newsNumber(); $number_of_pages = ceil($number_of_articles / 5); // 5 articles per page $articles=scandir($article_dir,1); $t=0; $found=0; while (!$found && $t < $number_of_articles ) { list($date,$subject,$author)=explode('_',$articles[$t]); $slug = str_replace(' ', '_', strtolower($subject)); if (friendize_url(utf8_encode($slug)) == request::value(1)) $found++; $t++; } if (!$found) { require_once('theme/index.template.php'); not_found(); // 404 error. } else { $slug = str_replace(' ', '_', strtolower($subject)); $contents = explode('<!-- | -->',str_ireplace($txt_convert['in'],$txt_convert['out'], trim(file_get_contents($article_dir.'/'.$articles[$t-1]))) ); $author = str_replace('.article.txt','',$author); isset($contents[2]) ? $keywords=$contents[2] : $keywords=false; isset($contents[3]) ? $description=$contents[3] : $description=false; require_once('theme/index.template.php'); template_header( utf8_encode($subject), // if isset show "pageTitle - Site title" $keywords, // if isset show keywords $description ); echo ' <div class="article"> <h2> ',utf8_encode($subject),' <br> <small> por ',utf8_encode($author),' </small> </h2> <div class="article_body"> ',$contents[0].$contents[1],' </div> </div> '; } template_footer(); ?> Code (markup): libs/url.lib.php <?php function friendize_url($url) { $unwanted_array = array('Š'=>'S', 'š'=>'s', 'Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A', 'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E', 'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I', 'Ï'=>'I', 'Ñ'=>'N', 'Ò'=>'O', 'Ó'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'O', 'Ø'=>'O', 'Ù'=>'U', 'Ú'=>'U', 'Û'=>'U', 'Ü'=>'U', 'Ý'=>'Y', 'Þ'=>'B', 'ß'=>'Ss', 'à'=>'a', 'á'=>'a', 'â'=>'a', 'ã'=>'a', 'ä'=>'a', 'å'=>'a', 'æ'=>'a', 'ç'=>'c', 'è'=>'e', 'é'=>'e', 'ê'=>'e', 'ë'=>'e', 'ì'=>'i', 'í'=>'i', 'î'=>'i', 'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o', 'ö'=>'o', 'ø'=>'o', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y',' '=>'_' ); return(strtr($url, $unwanted_array)); } ?> Code (markup): I still have to generate 404 errors for every non-existant page in the site to avoid duplicate content issues.
The url-solution is really bad. First of all, you should do a strtolower() - there is never a valid reason to use caps in a link.
Not sure what you mean here. I checked some links on the net and when I changed them from caps to lc or vice versa, I get 404 errors OR the wrong page.
Yes... which is my point. All links should always be in lower case. Especially on your own site. Hence, you have a url-replacement array that contains values for caps - which shouldn't be needed, if the string you're doing the replacement on is already in all lower-case. Again, there shouldn't ever be a need to use caps in an URL - apart from maybe if you're uploading files, but still, you can just do a strtolower on the filename before it gets attached to the url, and placed in the file-system. However, you will then lose the ability to have different files with different type in the name - on filesystems where you can make such distinctions - ie, that there is a difference between ThisIsAFile.txt and thisisafile.txt. Hence my point that you shouldn't be needing the array-replacement. Also, that using such a replacement array is bound to fail, since you can't possibly cater for all potential valid filename characters. That's why you should use one of the built-in PHP functions instead, if the goal is to make normalised urls.
If you have access to Apache, you can enable mod_spelling. There is a tiny performance overhead, but wouldn't worry about it unless you have a famous website. Doing so will allow you to use both capital/lowercase without worrying.
Thank you all for the replies. I run nginx but anyway I prefer lowercase urls. This function is a fix for some special characters in the article titles. I already do strtolower in another part of the code. I'll move it to this function for better organization. What I find more difficult is to adapt the pagination library to count from 1 instead of starting from zero. Also, both / and /0 give the same contents. My programming experience is very limited and I get lost in this algorithm.
Thank you for the tip. I have this modification in my TODO list with low priority... I'm learning many things in parallel: writing, html, css and php basics. Both http://cutcodedown.com/ and /blogSummary/0 (also /blogSummary) have exactly the same contents, is it a problem? especially taking into account that both URLs are linked inside the site. Next thing I want to try is to make a basic shopping cart since all existing solutions are bloated, so I'm going to start learning PDO with sqlite and php sessions. Some times people ask me for an online shop but I refuse to use things like prestashop or magento, so learning this apart from useful is a great opportunity to improve my programming skills.
It's generally minor as it's an index of articles not the articles themselves, with both being variable content so I could give a flying **** if that page gets de-ranked compared to the ACTUAL article pages. Hence my use of rel="bookmark" on the title and read more links. If it were an actual issue, I'd use <link rel="canonical"> in the header of the "/" version.