Skip to content

Instantly share code, notes, and snippets.

@designly1
Last active May 21, 2023 17:27
Show Gist options
  • Save designly1/77bd50d03cea977311fb43fbb14ad376 to your computer and use it in GitHub Desktop.
Save designly1/77bd50d03cea977311fb43fbb14ad376 to your computer and use it in GitHub Desktop.
A static site generator (SSG) and templating engine built in PHP.
<?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());
}
<?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);
}
}
<?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