Last active
May 21, 2023 17:27
-
-
Save designly1/77bd50d03cea977311fb43fbb14ad376 to your computer and use it in GitHub Desktop.
A static site generator (SSG) and templating engine built in PHP.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
try { | |
require_once(__DIR__ . '/config.inc.php'); | |
require_once(DIR . '/inc/build.inc.php'); | |
// Fetch posts | |
doLog('Fetching posts'); | |
define('URL', BLOG_BASE_URL . '?content=true'); | |
$json = json_decode(Helper::getUtf8(URL), true); | |
if (!$json || empty($json)) { | |
throw new Exception('Failed to fetch posts'); | |
} | |
doLog('Removing old files'); | |
$rmfiles = PUB_DIR . '/*.html'; | |
`rm -f $rmfiles`; | |
doLog('Building post pages'); | |
$postCards = Helper::buildPostCards($json, 9); | |
$meta = []; | |
foreach ($json as $post) { | |
$thisUrl = 'https://blog.designly.biz/' . $post['slug']; | |
$data = Helper::buildPostData($post); | |
$data['POSTS'] = implode("\n", $postCards); | |
$page = new Page(COMP_DIR . '/post.html', $data); | |
ob_start(); | |
$page->render(); | |
$content = ob_get_clean(); | |
$file = PUB_DIR . '/' . $post['slug'] . '.html'; | |
$putres = file_put_contents($file, $content); | |
doLog("Writing $file: " . ($putres ? "OK" : "FAIL")); | |
$meta[] = array_filter($post, function ($key) { | |
return $key !== 'content'; | |
}, ARRAY_FILTER_USE_KEY); | |
} | |
// Write meta data to json file | |
file_put_contents(DATA_DIR . '/postMeta.json', json_encode($meta)); | |
file_put_contents(DATA_DIR . '/posts.json', json_encode($json)); | |
// Create index page | |
doLog('Building index page'); | |
// Get tags list | |
$tags = Helper::getAllUniqueTags($json); | |
$tagList = Helper::buildTagList($tags); | |
// Generate page | |
$thisUrl = 'https://blog.designly.biz'; | |
$page = new Page(COMP_DIR . '/home.html', [ | |
'PAGE_TITLE' => 'Blog', | |
'POSTS' => implode("\n", $postCards), | |
'tagList' => $tagList, | |
]); | |
$page->write(PUB_DIR . '/index.html'); | |
// Generate sitemap | |
$sitemap = new DOMDocument('1.0', 'UTF-8'); | |
$urlset = $sitemap->createElement('urlset'); | |
$urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); | |
$urlset->setAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1'); | |
$sitemap->appendChild($urlset); | |
foreach ($json as $post) { | |
$thisUrl = 'https://blog.designly.biz/' . $post['slug']; | |
$url = $sitemap->createElement('url'); | |
$loc = $sitemap->createElement('loc'); | |
$loc->appendChild($sitemap->createTextNode($thisUrl)); | |
$url->appendChild($loc); | |
// Add image | |
$image = $sitemap->createElement('image:image'); | |
$imageUrl = $post['coverImage']; // Replace with your image URL | |
$imageLoc = $sitemap->createElement('image:loc'); | |
$imageLoc->appendChild($sitemap->createTextNode($imageUrl)); | |
$image->appendChild($imageLoc); | |
$url->appendChild($image); | |
$urlset->appendChild($url); | |
} | |
// Save the sitemap to a file | |
$sitemap->formatOutput = true; | |
$sitemap->save(PUB_DIR . '/sitemaps/posts.xml'); | |
} catch (Exception $e) { | |
doLog($e->getMessage()); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Helper | |
{ | |
static public function buildPostData($post) | |
{ | |
// get css files | |
$css = ""; | |
foreach (BLOG_CSS_FILES as $file) { | |
$css .= "<link rel=\"stylesheet\" href=\"$file\" />\n"; | |
} | |
// Configure image path | |
$coverImage = str_replace('cdn.designly.biz', 'cdn.designly.biz/imgr', $post['coverImage']); | |
$coverImageSrcSet = Helper::mkSrcSet($post['coverImage']); | |
// Create tags | |
$tags = []; | |
foreach ($post['tagsCollection']['items'] as $t) { | |
$tag = $t['tag']; | |
$slug = $t['slug']; | |
$tags[] = "<a href=\"/tag/$slug\">#$tag</a>"; | |
} | |
// Configure title | |
$title = $post['title'] . ' | Designly'; | |
$data = [ | |
...$post, | |
'CSS_FILES' => $css, | |
'PAGE_TITLE' => $title, | |
'postId' => $post['sys']['id'], | |
'coverImageThumb' => $coverImage . '?w=400&q=75', | |
'coverImagePost' => $coverImage . '?w=1200&q=75', | |
'coverImageSrcSet' => $coverImageSrcSet, | |
'tagList' => implode("\n", $tags), | |
'ogImage' => $coverImage, | |
'ogTitle' => $post['title'], | |
'description' => $post['excerpt'], | |
]; | |
return $data; | |
} | |
static public function buildTemplate($template, $data) | |
{ | |
$html = Page::parseTemplate($template); | |
$html = Page::replaceKeys($html, $data); | |
$html = Page::cleanUpTags($html); | |
return $html; | |
} | |
static public function buildPostCard($post) | |
{ | |
$post = self::buildPostData($post); | |
return self::buildTemplate(COMP_DIR . '/postItem.html', $post); | |
} | |
static public function buildPostCards($posts, $numCards = null) | |
{ | |
if (!$numCards) { | |
$numCards = count($posts); | |
} | |
$cards = []; | |
for ($i = 0; $i < $numCards; $i++) { | |
$cards[] = self::buildPostCard($posts[$i]); | |
if (($i + 1) % 3 === 0) { | |
$ad = self::buildTemplate(COMP_DIR . '/googleAdFeed.html', []); | |
$cards[] = $ad; | |
} | |
} | |
return $cards; | |
} | |
static public function mkSrcSet($src) | |
{ | |
$src = str_replace('cdn.designly.biz', 'cdn.designly.biz/imgr', $src); | |
$srcset = []; | |
$sizes = [1024, 640, 320]; | |
foreach ($sizes as $size) { | |
$srcset[] = "$src?w=$size&q=75 " . $size . "w"; | |
} | |
return implode(", ", $srcset); | |
} | |
static public function getUtf8($fn) | |
{ | |
$content = file_get_contents($fn); | |
$content = mb_convert_encoding( | |
$content, | |
'UTF-8', | |
'auto' | |
); | |
return $content; | |
} | |
static public function render404() | |
{ | |
http_response_code(404); | |
$page = new Page(COMP_DIR . '/404.html', [ | |
'title' => '404 Not Found', | |
]); | |
$page->render(); | |
exit; | |
} | |
static public function balanceText($text, $lineLength) | |
{ | |
// split the text into words | |
$words = explode(" ", $text); | |
// initialize variables | |
$currentLine = ""; | |
$output = ""; | |
// loop through each word | |
foreach ($words as $word) { | |
// add the current word to the current line | |
$tempLine = $currentLine . " " . $word; | |
// if the current line is too long, add it to the output with a <br> tag | |
if (strlen($tempLine) > $lineLength) { | |
$output .= $currentLine . "<br>"; | |
$currentLine = $word; | |
} else { | |
$currentLine = $tempLine; | |
} | |
} | |
// add the final line to the output | |
$output .= $currentLine; | |
return $output; | |
} | |
static public function cosineSimilarity($embedding1, $embedding2) | |
{ | |
// Calculate the dot product of the two embeddings | |
$dot_product = 0; | |
for ($i = 0; $i < count($embedding1); $i++) { | |
$dot_product += $embedding1[$i] * $embedding2[$i]; | |
} | |
// Calculate the magnitude of the first embedding | |
$magnitude1 = 0; | |
foreach ($embedding1 as $value) { | |
$magnitude1 += $value * $value; | |
} | |
$magnitude1 = sqrt($magnitude1); | |
// Calculate the magnitude of the second embedding | |
$magnitude2 = 0; | |
foreach ($embedding2 as $value) { | |
$magnitude2 += $value * $value; | |
} | |
$magnitude2 = sqrt($magnitude2); | |
// Calculate the cosine similarity | |
$cosine_similarity = $dot_product / ($magnitude1 * $magnitude2); | |
return $cosine_similarity; | |
} | |
static public function vectorSearch($term) | |
{ | |
$term = trim($term); | |
$term = strip_tags($term); | |
$term = preg_replace("/[^A-Za-z0-9 ]/", '', $term); | |
$client = OpenAI::client($_ENV['OPENAI_KEY']); | |
$response = $client->embeddings()->create([ | |
'model' => 'text-embedding-ada-002', | |
'input' => $term, | |
]); | |
$emb = $response->embeddings[0]; | |
$embs = json_decode(file_get_contents(DIR . '/data/embeddings.json'), true); | |
$results = []; | |
foreach ($embs as $e) { | |
$results[] = [ | |
'similarity' => Helper::cosineSimilarity($e['embedding'], $emb->embedding), | |
'slug' => $e['slug'], | |
'chunk' => $e['chunk'] | |
]; | |
} | |
usort($results, function ($a, $b) { | |
return $b['similarity'] <=> $a['similarity']; | |
}); | |
return $results; | |
} | |
static public function rateLimit($limit = 10, $timeFrame = 60) | |
{ | |
$ipAddress = $_SERVER['REMOTE_ADDR']; // Get the client's IP address | |
$filePath = '/tmp/rate_limit_' . $ipAddress; // File path where request data is stored | |
// Read the file if it exists, if not initialize an empty array | |
$data = file_exists($filePath) ? json_decode(file_get_contents($filePath), true) : []; | |
if (!isset($data['resetTime']) || time() > $data['resetTime']) { | |
// Reset the requests and set new resetTime | |
$data['requests'] = 1; | |
$data['resetTime'] = time() + $timeFrame; | |
} else { | |
// If we're within the time frame, increment 'requests' | |
$data['requests']++; | |
} | |
// If the request limit has been exceeded, send a 429 'Too Many Requests' response | |
if ($data['requests'] > $limit) { | |
http_response_code(429); | |
echo "Rate limit exceeded. Try again later."; | |
exit; | |
} | |
// Save the updated data back to the file | |
file_put_contents($filePath, json_encode($data)); | |
} | |
static public function getPostsByTagSlug($tagSlug) | |
{ | |
$posts = json_decode(file_get_contents(DIR . '/data/postMeta.json'), true); | |
$filteredPosts = []; | |
foreach ($posts as $post) { | |
$tagsCollection = $post['tagsCollection']['items']; | |
foreach ($tagsCollection as $tag) { | |
if ($tag['slug'] === $tagSlug) { | |
$filteredPosts[] = $post; | |
break; | |
} | |
} | |
} | |
return $filteredPosts; | |
} | |
static public function getAllUniqueTags($posts) | |
{ | |
$uniqueTags = []; | |
foreach ($posts as $post) { | |
$tagsCollection = $post['tagsCollection']['items']; | |
foreach ($tagsCollection as $tag) { | |
$tagSlug = $tag['slug']; | |
if (!isset($uniqueTags[$tagSlug])) { | |
$tagName = $tag['tag']; | |
$uniqueTags[$tagSlug] = $tagName; | |
} | |
} | |
} | |
asort($uniqueTags); | |
return $uniqueTags; | |
} | |
static public function buildTagList($tags) | |
{ | |
$tagsList = []; | |
foreach ($tags as $slug => $name) { | |
$tagsList[] = "<a href=\"/tag/$slug\">#$name</a>"; | |
} | |
return implode("\n", $tagsList); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Page | |
{ | |
private $title; | |
private $content; | |
private $data; | |
private $template; | |
static public function replaceKeys($string, $array) | |
{ | |
return preg_replace_callback('/{{(.*?)}}/', function ($matches) use ($array) { | |
return isset($array[$matches[1]]) ? $array[$matches[1]] : $matches[0]; | |
}, $string); | |
} | |
static public function cleanUpTags($string) | |
{ | |
return preg_replace('/{{.*?}}/', '', $string); | |
} | |
static public function removeEmptyLines($str) | |
{ | |
// Split the string into an array of lines | |
$lines = explode("\n", $str); | |
// Loop through each line and remove if it's empty or contains only whitespace | |
foreach ($lines as $key => $line) { | |
if (trim($line) === '') { | |
unset($lines[$key]); | |
} | |
} | |
// Re-join the remaining lines and return the result | |
return implode("\n", $lines); | |
} | |
static public function includeTemplateFiles($str) | |
{ | |
global $thisUrl; | |
return preg_replace_callback('/{{inc:(.*?)}}/', function ($matches) { | |
$file = COMP_DIR . trim($matches[1]) . '.html'; | |
if (file_exists($file)) { | |
ob_start(); | |
require $file; | |
$content = ob_get_clean(); | |
return $content; | |
} else { | |
throw new RuntimeException('Template file not found: ' . $file); | |
} | |
}, $str); | |
} | |
static public function minifyHtml($html) | |
{ | |
$search = array( | |
'/\>[^\S ]+/s', // strip whitespaces after tags, except space | |
'/[^\S ]+\</s', // strip whitespaces before tags, except space | |
'/(\s)+/s' // shorten multiple whitespace sequences | |
); | |
$replace = array( | |
'>', | |
'<', | |
'\\1' | |
); | |
return preg_replace($search, $replace, $html); | |
} | |
static public function prettifyHtml($html) | |
{ | |
$html = self::minifyHtml($html); | |
$doc = new DOMDocument(); | |
$doc->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); | |
// Set some options to make the output prettier | |
$doc->preserveWhiteSpace = false; | |
$doc->formatOutput = true; | |
// Convert the DOMDocument back to a string of HTML | |
return $doc->saveHTML(); | |
} | |
static public function hashDir($dir) | |
{ | |
$timestamps = array(); | |
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); | |
foreach ($iterator as $file) { | |
if ($file->isFile()) { | |
$timestamps[] = $file->getMTime(); | |
} | |
} | |
sort($timestamps); | |
$hash = hash('sha256', implode('', $timestamps)); | |
return $hash; | |
} | |
static public function parseTemplate($file) | |
{ | |
global $thisUrl; | |
ob_start(); | |
require($file); | |
$content = ob_get_clean(); | |
return $content; | |
} | |
static public function insertCopyButtons($html) | |
{ | |
$dom = new DOMDocument(); | |
$dom->formatOutput = false; | |
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); | |
// Format code blocks | |
$pres = $dom->getElementsByTagName('pre'); | |
foreach ($pres as $pre) { | |
$div = $dom->createElement('div'); | |
$div->setAttribute('class', 'code-wrapper shadow-lg'); | |
$pre->parentNode->insertBefore($div, $pre); | |
$div->appendChild($pre); | |
$span1 = $dom->createElement('span'); | |
$span1->setAttribute('class', 'mac1'); | |
$div->insertBefore($span1); | |
$span2 = $dom->createElement('span'); | |
$span2->setAttribute('class', 'mac2'); | |
$div->insertBefore($span2); | |
$span3 = $dom->createElement('span'); | |
$span3->setAttribute('class', 'mac3'); | |
$div->insertBefore($span3); | |
$button = $dom->createElement('button'); | |
$button->setAttribute('class', 'code-button'); | |
$button->setAttribute('aria-label', 'Copy to clipboard'); | |
$i = $dom->createElement('i'); | |
$i->setAttribute('class', 'fa-solid fa-copy code-copy-icon'); | |
$button->appendChild($i); | |
$div->appendChild($button); | |
} | |
// Format images | |
$imgs = $dom->getElementsByTagName('img'); | |
foreach ($imgs as $img) { | |
$img->setAttribute('class', 'blog-post-image'); | |
$img->setAttribute('width', '1920'); | |
$img->setAttribute('height', '1080'); | |
$src = $img->getAttribute('src'); | |
$src = str_replace('d340jo5zum8tsx.cloudfront.net', 'cdn.designly.biz', $src); | |
$img->setAttribute('data-original', $src); | |
$srcset = Helper::mkSrcSet($src); | |
$src = str_replace('cdn.designly.biz', 'cdn.designly.biz/imgr', $src); | |
$src .= '?w=1200&q=75'; | |
$img->setAttribute('src', $src); | |
$img->setAttribute('srcset', $srcset); | |
} | |
// Format links | |
$links = $dom->getElementsByTagName('a'); | |
foreach ($links as $link) { | |
$link->setAttribute('rel', 'noreferrer noopener'); | |
$link->setAttribute('target', '_blank'); | |
} | |
$modifiedHtml = $dom->saveHTML(); | |
$modifiedHtml = preg_replace('/^.*?<body.*?>|<\/body>.*$/si', '', $modifiedHtml); | |
$modifiedHtml = preg_replace('/^.*?<html.*?>|<\/html>.*$/si', '', $modifiedHtml); | |
return $modifiedHtml; | |
} | |
public function __construct($template, $data) | |
{ | |
if (!defined('MAIN_TEMPLATE_FILE')) { | |
throw new RuntimeException('MAIN_TEMPLATE_FILE not defined'); | |
} | |
if (!file_exists(MAIN_TEMPLATE_FILE)) { | |
throw new RuntimeException('MAIN_TEMPLATE_FILE not found'); | |
} | |
$this->template = $template; | |
$this->data = $data; | |
return $this->build(); | |
} | |
public function build() | |
{ | |
global $thisUrl; | |
if (!file_exists($this->template)) { | |
throw new RuntimeException('Template not found'); | |
} | |
// Get main template | |
ob_start(); | |
require MAIN_TEMPLATE_FILE; | |
$this->content = ob_get_clean(); | |
// Get page template | |
ob_start(); | |
require $this->template; | |
$pageContent = ob_get_clean(); | |
// Insert page content | |
$this->content = str_replace('{{PAGE::CONTENT}}', $pageContent, $this->content); | |
// Include template files | |
$this->content = self::includeTemplateFiles($this->content); | |
// Insert code copy buttons | |
if (isset($this->data['content'])) { | |
$this->data['content'] = self::insertCopyButtons($this->data['content']); | |
} | |
// Insert data keys | |
$this->content = self::replaceKeys($this->content, $this->data); | |
// Clean up tags | |
$this->content = self::cleanUpTags($this->content); | |
// Remove empty lines | |
$this->content = self::removeEmptyLines($this->content); | |
return true; | |
} | |
public function render() | |
{ | |
header('Content-Type: text/html; charset=utf-8'); | |
echo $this->content; | |
} | |
public function write($filename) | |
{ | |
return file_put_contents($filename, $this->content); | |
} | |
public function setTitle($title) | |
{ | |
$this->title = $title; | |
} | |
public function getTitle() | |
{ | |
return $this->title; | |
} | |
public function setContent($content) | |
{ | |
$this->content = $content; | |
} | |
public function getContent() | |
{ | |
return $this->content; | |
} | |
public function setData($data) | |
{ | |
$this->data = $data; | |
} | |
public function getData() | |
{ | |
return $this->data; | |
} | |
public function setTemplate($template) | |
{ | |
$this->template = $template; | |
} | |
public function getTemplate() | |
{ | |
return $this->template; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment