75 Commits
main ... themes

Author SHA1 Message Date
SG
808aab3609 updates 2025-06-30 20:57:01 +03:00
sg
162b45809b hopefully fixed bad images in content_item
template
2025-06-30 20:21:03 +03:00
SG
664a6adb77 updates 2025-06-22 14:08:45 +03:00
sg
b633c21ef2 updates 2025-06-22 14:06:52 +03:00
SG
802f8022d0 audio player added 2025-06-22 12:12:36 +03:00
SG
d58d9464a9 updates 2025-06-20 15:18:28 +03:00
SG
dca4aa7a7e updates 2025-06-19 11:17:01 +03:00
SG
8813d91493 updates 2025-06-19 08:56:37 +03:00
SG
9fa58d4700 updates 2025-06-18 15:55:52 +03:00
SG
15879e66b7 updates 2025-06-18 15:26:11 +03:00
SG
3abe3f2fac updates 2025-06-18 14:59:23 +03:00
SG
d3b369be46 updates 2025-06-18 14:00:00 +03:00
SG
66d3132e53 updates 2025-06-18 13:27:14 +03:00
SG
4136a5684a updates 2025-06-18 13:26:29 +03:00
SG
39b5b64e77 updates 2025-06-18 12:14:54 +03:00
SG
0a2d621a1d updates 2025-06-18 10:50:03 +03:00
SG
6ad76fc082 updates 2025-06-18 10:28:56 +03:00
SG
fd6e8d15c0 updates 2025-06-17 18:20:41 +03:00
SG
00f1c1a9ae updates 2025-06-17 18:13:47 +03:00
SG
0ea2fbeda6 updates 2025-06-17 17:07:00 +03:00
SG
326e5c5edd updates 2025-06-17 16:22:47 +03:00
SG
4ea54eaa91 Merge branch 'themes' of https://git.exocortex.ru/Exocortex/hydrogen into themes 2025-06-17 13:46:14 +03:00
SG
438a8d6a14 updates 2025-06-17 13:46:08 +03:00
sg
83e6b7f0a5 updates 2025-06-17 00:21:01 +03:00
SG
0583eb07ee updates 2025-06-16 22:11:24 +03:00
SG
7d81226d83 updates 2025-06-16 18:06:11 +03:00
SG
cfe8e9a799 updates 2025-06-16 17:55:58 +03:00
SG
87484fe341 updates 2025-06-15 01:37:30 +03:00
SG
3dfc802509 updates 2025-06-15 01:09:10 +03:00
SG
3ddf1a97d0 updates 2025-06-15 01:09:05 +03:00
SG
2072bf966f updates 2025-06-14 18:49:56 +03:00
SG
fe74be81c6 updates 2025-06-14 18:46:31 +03:00
SG
793388d4ba moved static files into themes 2025-06-14 18:14:24 +03:00
SG
ed9d1dade2 added script path detection
prepare for PyInstaller
2025-06-14 17:37:33 +03:00
SG
0b31079654 updated templates.py 2025-06-14 14:39:34 +03:00
SG
1830b9484e not failing on missing 'categories' in .md file's
frontmatter header
2025-06-13 23:13:29 +03:00
SG
c9c1c883a2 update index layout to "justify-content-between"
the preview cards
2025-06-13 18:42:49 +03:00
SG
ece9730f37 added sorting of preview cards 2025-06-13 18:36:13 +03:00
SG
b5b3de23bb updates: change 'px' to 'em' for width 2025-06-13 16:07:09 +03:00
SG
818f3ee37f updates 2025-06-13 02:10:29 +03:00
SG
aa2a7c279d updates 2025-06-12 20:52:28 +03:00
SG
cc503593d8 updates 2025-06-12 20:21:11 +03:00
SG
0bc1987856 Added color theme toggle and saving as preference
into localStorage
2025-06-12 20:05:57 +03:00
SG
af4fd0a618 updates 2025-06-12 19:58:08 +03:00
SG
51b7cd586b updates 2025-06-12 19:51:26 +03:00
SG
08caaba9ee updates 2025-06-12 17:49:45 +03:00
SG
5a44c570d4 updates 2025-06-12 17:49:31 +03:00
SG
62562c9afd updates 2025-06-12 17:48:31 +03:00
SG
6cb80e6720 updates 2025-06-12 17:43:36 +03:00
SG
ab1109a900 switching to Bootstrap 5 2025-06-12 17:20:31 +03:00
SG
47badd9d5a updates 2025-06-12 14:12:46 +03:00
SG
095424f252 updates 2025-06-12 12:38:57 +03:00
SG
4927f9907d updates 2025-06-12 02:47:25 +03:00
SG
a0e858f416 updates - content_item.css_file not working in
'content_item.html' template
2025-06-12 02:41:31 +03:00
SG
141060368c updates 2025-06-12 01:32:06 +03:00
SG
acfa45295b updates - ui 2025-06-11 23:09:14 +03:00
SG
3efc5e2390 updates 2025-06-11 22:53:59 +03:00
SG
21b4066288 updates - hidden items 2025-06-11 22:47:41 +03:00
SG
3f832a1af5 updates - categories 2025-06-11 22:35:20 +03:00
SG
2132cfd48d updates 2025-06-11 19:13:43 +03:00
SG
7bbbd2e8ad updates 2025-06-11 14:30:44 +03:00
SG
eeb1462210 updates 2025-06-11 13:48:08 +03:00
SG
f9c14b3e2a updates 2025-06-11 12:38:14 +03:00
SG
706c354e00 updates 2025-06-11 02:06:24 +03:00
SG
2e93be7184 updates 2025-06-10 23:11:10 +03:00
SG
7c7dedcfa5 updates 2025-06-10 20:50:21 +03:00
sg
26c4b9c1e1 updates 2025-06-10 11:37:57 +03:00
SG
45d12ab97d updates 2025-06-09 23:34:52 +03:00
SG
2d7f1c1f7d updates 2025-06-09 23:34:36 +03:00
SG
3348df26e5 updates 2025-06-08 20:18:18 +03:00
SG
09621b73c3 Merge branch 'self-contained' of https://git.exocortex.ru/Exocortex/hydrogen into self-contained 2025-06-07 13:18:03 +03:00
SG
1854366a9d some reworking 2025-06-07 13:17:51 +03:00
sg
2a473429f6 Fix typo 2025-06-07 02:43:31 +03:00
sg
7baae89942 Added package 2025-06-07 02:41:01 +03:00
SG
fc1ee27146 Switching to self-contained binary -
initial commit.
2025-06-06 15:36:38 +03:00
24 changed files with 575 additions and 255 deletions

3
.gitignore vendored
View File

@@ -169,4 +169,5 @@ cython_debug/
#.idea/
public
content/*
content/*
config.yml

16
argparser.py Normal file
View File

@@ -0,0 +1,16 @@
import argparse
from config import config
argparser = argparse.ArgumentParser(
prog = config.app_name,
description = config.app_description,
epilog = f"See {config.app_src_url} for more info.")
subparsers = argparser.add_subparsers(dest='command', required=True)
subparsers.add_parser('init', help = 'Initialize new site.')
subparsers.add_parser('build', help = "Build the site from existing content.")
new_content = subparsers.add_parser('create', aliases = ['new'], help = "Create a new content file.")
new_content.add_argument('filename', type=str, help = "Name of the content file to create.")
edit_content = subparsers.add_parser('edit', help = "Edit a content file.")
edit_content.add_argument('filename', type=str, help = "Name of the content file to edit.")

203
classes.py Normal file
View File

@@ -0,0 +1,203 @@
from datetime import datetime
import os
import sys
import shutil
from collections import defaultdict
import markdown
import frontmatter
from pathlib import Path
from logger import logger
from config import config
class ContentItem:
def render_content(self, categories, target_file = False):
if target_file:
self.target_file = Path(target_file)
logger.debug(f"Rendering {self.source_file} to {self.target_file}")
if hasattr(config, "footer"):
footer_data = config.footer
else:
footer_data = ''
try:
if self.image_file and Path(self.image_file).exists():
self.image_file = Path(self.image_file)
image_targetfile = Path(f"{config.output_dir}/{config.static_dir}/images/{self.source_file.stem}.jpg")
logger.debug(f"Copying {self.image_file} to {image_targetfile}")
shutil.copyfile(self.image_file, image_targetfile)
self.image_file = f"{self.image_file.stem}.jpg"
else:
self.image_file = False
except Exception as e:
logger.error(f"Couldn't render image file {self.image_file}: {e}")
try:
if self.audio_file and Path(self.audio_file).exists():
self.audio_file = Path(self.audio_file)
audio_targetfile = Path(f"{config.output_dir}/{config.static_dir}/audio/{self.audio_file.name}")
logger.debug(f"Copying {self.audio_file} to {audio_targetfile}")
shutil.copyfile(self.audio_file, audio_targetfile)
self.audio_file = f"{self.audio_file.stem}.mp3"
except Exception as e:
logger.error(f"Couldn't render audio file {self.audio_file}: {e}")
try:
if self.css_file and Path(self.css_file).exists():
self.css_file = Path(self.css_file)
css_targetfile = Path(f"{config.output_dir}/{config.static_dir}/css/{self.css_file.name}")
logger.debug(f"Copying {self.css_file} to {css_targetfile}")
shutil.copyfile(self.css_file, css_targetfile)
except Exception as e:
logger.error(f"Couldn't render CSS file {self.css_file}: {e}")
try:
if self.js_file and Path(self.js_file).exists():
self.js_file = Path(self.js_file)
js_targetfile = Path(f"{config.output_dir}/{config.static_dir}/js/{self.js_file.name}")
logger.debug(f"Copying {self.js_file} to {js_targetfile}")
shutil.copyfile(self.js_file, js_targetfile)
except Exception as e:
logger.error(f"Couldn't render JS file {self.js_file}: {e}")
try:
with self.target_file.open("w", encoding="utf-8") as f:
f.write(config.content_item_template.render(content_item = self, page_title = f"{config.site_name}: {self.title}", parent_path = '../', categories = categories, footer_data = footer_data))
except Exception as e:
logger.error(f"Couldn't render content file: {e}")
def parse_content(self):
logger.debug(f"Parsing file {self.source_file}")
try:
self.source_file = Path(self.source_file)
self.parent_dir = self.source_file.parent # Most likely './content'
self.slug = self.source_file.stem
self.target_file = Path(f"{config.output_dir}/{self.source_file.parent}/{self.source_file.stem}.html")
self.data = frontmatter.load(self.source_file)
self.preview = self.data.content.replace('\n', '<br>')[:300]
self.title = self.data.get("title", self.slug)
self.omit_second_title = self.data.get("omit_second_title", False)
self.date = self.data.get("date", "2000-01-01T00:00:00+03:00")
self.categories = [c for c in self.data.get("categories", []) if c != 'default']
self.hidden = self.data.get("hidden", str(False))
self.data.content = self.data.content.replace('\n', ' \n') # For markdown newline rendering
self.html = markdown.markdown(self.data.content)
cover_image_path = f"{self.source_file.parent}/{self.source_file.stem}.jpg"
self.image_file = cover_image_path if Path(cover_image_path).exists() else ""
audio_filepath = f"{self.source_file.parent}/{self.source_file.stem}.mp3"
self.audio_file = audio_filepath if Path(audio_filepath).exists() else ""
css_filepath = f"{self.source_file.parent}/{self.source_file.stem}.css"
self.css_file = css_filepath if Path(css_filepath).exists() else ""
js_filepath = f"{self.source_file.parent}/{self.source_file.stem}.js"
self.js_file = js_filepath if Path(js_filepath).exists() else ""
except Exception as e:
logger.error(f"Parser error, {e}")
def create_content(self):
with open(self.source_file, mode="w", encoding="utf-8") as f:
f.writelines([
"---\n",
f"title: {self.source_file.stem}\n",
f"date: '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}'\n",
"description: ''\n",
"author: ''\n",
"categories: ['default']\n",
"hidden: False\n",
"omit_second_title: False\n",
"---\n",
"\n\n\n"
])
def __init__(self, file):
self.source_file = file
class Site:
def __init__(self):
self.output_dir = Path(config.output_dir)
self.content_dir = Path(config.content_dir)
self.static_dir = Path(config.static_dir)
self.templates_dir = Path(config.templates_dir)
self.images_dir = Path(f"{config.static_dir}/images")
self.css_dir = Path(f"{config.static_dir}/css")
self.js_dir = Path(f"{config.static_dir}/js")
self.output_dir = Path(config.output_dir)
self.content_items = []
self.categories = defaultdict(list)
def init_site(self):
# Exit if current directory not empty
if os.path.isdir('.') and os.listdir('.'):
logger.error("Current directory is not empty.")
sys.exit(1)
logger.info("Initializing new site")
config.create_default_config()
# Create directories
for subdir in [self.content_dir]:
os.makedirs(subdir, exist_ok=True)
# Copy default theme
shutil.copytree(f"{config.base_dir}/themes/default", "themes/default", dirs_exist_ok=True)
def get_content_items(self):
logger.debug("Getting content items")
self.get_content_items = []
logger.debug(f"Scanning {Path(config.content_dir)}")
for md_file in Path(config.content_dir).glob("*.md"):
content_item = ContentItem(md_file)
content_item.parse_content()
self.content_items.append(content_item)
self.content_items.sort(key=lambda i: (-datetime.fromisoformat(i.date).timestamp(), i.title))
def map_categories(self):
for content_item in self.content_items:
for category in content_item.categories:
if not category == "default":
self.categories[category].append(content_item.slug)
def build(self):
# Recreate the output dir if exists
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
self.output_dir.mkdir()
# Create public subdirs
subdirs = ["categories", "content", "static"]
for subdir in subdirs:
subdir = self.output_dir / subdir
subdir.mkdir(parents=True, exist_ok=True)
# Copy theme's static dir to 'public/static
shutil.copytree(f"{config.base_dir}/themes/{config.theme}/static", f"{self.output_dir}/static", dirs_exist_ok=True)
# Get content items
self.get_content_items()
# Build categories map
self.map_categories()
# Render the content files
for content_item in self.content_items:
content_item.render_content(categories = self.categories)
# Render the about file
about_content = ContentItem(Path(f'themes/{config.theme}/content/about.md'))
about_content.parse_content()
about_content.render_content(categories = self.categories, target_file='public/static/about.html')
# Render the index file
if hasattr(config, "footer"):
footer_data = config.footer
else:
footer_data = ''
visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True]
with (self.output_dir / "index.html").open("w", encoding="utf-8") as f:
f.write(config.index_template.render(page_title = config.site_name, content_items=visible_content_items, categories = self.categories, footer_data = footer_data))
# Render the categories indices
if hasattr(config, "footer"):
footer_data = config.footer
else:
footer_data = ''
visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True]
for category in self.categories:
category_index = Path(f"{self.output_dir}/categories/{category}.html")
category_items = [i for i in visible_content_items if category in i.data.get("categories", "default")]
with (category_index).open(mode="w", encoding="utf-8") as f:
f.write(config.index_template.render(page_title=f"{config.site_name}: [ {category} ]", content_items=category_items, categories = self.categories, parent_path = '../', footer_data = footer_data))
logger.info(f"Created {len(self.content_items)} content items.")

View File

@@ -1,24 +1,69 @@
import logging, os, sys
import os, sys, yaml
from jinja2 import Environment, FileSystemLoader
from pathlib import Path
# Main config section
class Config:
APP_NAME = "hydrogen"
MAIN_PAGE_TITLE = "Selected poems"
OUTPUT_DIR = os.environ.get('OUTPUT_DIR') or 'public'
HEADER_IMAGE = 'static/header.jpg'
LOG_TO = sys.stdout
LOG_LEVEL = logging.DEBUG
def __init__(self):
self.config_path = "config.yml"
self.app_name = "microgen"
self.app_description = "Simplistic static site generator"
self.app_src_url = f"https://git.exocortex.ru/Exocortex/{self.app_name}"
self.content_dir = 'content'
self.templates_dir = 'templates'
self.static_dir = 'static'
self.output_dir = 'public'
self.header_image = 'static/header.jpg'
self.site_name = f"{self.app_name} library"
self.theme = "default"
self.debug = False
if os.path.isfile (self.config_path):
with open(self.config_path, "r") as f:
config = yaml.safe_load(f)
if config:
self.update_from_dict(config)
# Logging config section
logger = logging.getLogger(Config.APP_NAME)
logger.setLevel(Config.LOG_LEVEL)
stdout_handler = logging.StreamHandler(Config.LOG_TO)
stdout_handler.setLevel(Config.LOG_LEVEL)
formatter = logging.Formatter(
'[%(asctime)s] %(name)s: %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
stdout_handler.setFormatter(formatter)
if not logger.hasHandlers():
logger.addHandler(stdout_handler)
logger.propagate = False
# Get the script base_dir and check for templates dir
if Path(f'themes/{self.theme}/templates').exists():
self.base_dir = "."
elif getattr(sys, 'frozen', False):
# Running fron Pyinstaller-packed binary
self.base_dir = Path(sys._MEIPASS)
else:
# Running as a plain Python app
self.base_dir = Path(__file__).resolve().parent
# Prepare template rendering engine
if Path(f"themes/{self.theme}/templates").exists():
# Use the templates from the initialized site instance and the installed theme
jinja_env_dir = f"themes/{self.theme}/templates"
#logger.debug(f"Using locally initialized templates from {jinja_env_dir}")
else:
# Use default templates from the default theme shipped with the app
# i.e. PyInstaller-packed single executable
#logger.debug("Using shipped default temlpates.")
jinja_env_dir = f"{self.base_dir}/themes/default/templates"
self.env = Environment(loader=FileSystemLoader(f"{jinja_env_dir}"))
self.env.globals['header_image'] = f"{self.base_dir}/themes/{self.theme}/static/images/header.jpg"
self.index_template = self.env.get_template("index.html")
self.content_item_template = self.env.get_template("content_item.html")
def update_from_dict(self, config_dict):
for key, value in config_dict.items():
setattr(self, key, value)
def __repr__(self):
return f"{self.__dict__}"
def create_default_config(self):
defaults = {
"site_name": self.site_name,
"theme": "default",
"debug": False,
"footer": "'' # Author / copyright / date / links, can be plaintext or valid HTML"
}
with open(self.config_path, mode="w", encoding="utf-8") as f:
yaml.safe_dump(defaults, f)
config = Config()

View File

29
helpers.py Normal file
View File

@@ -0,0 +1,29 @@
import os, subprocess
from pathlib import Path
from classes import *
from config import config
from logger import logger
def create_content(filename):
filename = Path(f"{config.content_dir}/{filename}.md")
if not filename.exists():
logger.debug(f"Creating new content {filename}")
new_content_item = ContentItem(filename)
new_content_item.create_content()
def edit_content(filename):
logger.debug(f"Editing content {filename}")
editor = os.environ.get('EDITOR', 'vim')
# Create the content file if not exists
if not Path(f"{config.content_dir}/{filename}.md").exists():
create_content(filename)
filename = Path(f"{config.content_dir}/{filename}.md")
subprocess.call([editor, filename])
def init_site():
site = Site()
site.init_site()
def build_site():
site = Site()
site.build()

View File

@@ -1,96 +1,18 @@
#!/usr/bin/env python3
from pathlib import Path
import frontmatter
import markdown
from jinja2 import Environment, FileSystemLoader
import shutil
from config import Config, logger
from argparser import argparser
from helpers import *
content_dir = Path('content')
template_dir = Path('templates')
output_dir = Path(Config.OUTPUT_DIR)
img_dir = Path('public/images')
assets_dir = Path('public/assets')
assets_css_dir = Path('public/assets/css')
assets_js_dir = Path('public/assets/js')
static_dir = Path('static')
header_image = Config.HEADER_IMAGE or '/static/header.jpg'
def main():
args = argparser.parse_args()
match args.command:
case "build":
build_site()
case "init":
init_site()
return
case "new" | "create" | "edit":
edit_content(args.filename)
class ContentItemPrototype:
def render_content(self):
if self.image_src_file.exists():
shutil.copyfile(self.image_src_file, output_dir / self.image)
if self.custom_css_src_file.exists():
shutil.copyfile(self.custom_css_src_file, output_dir / self.custom_css)
if self.custom_js_src_file.exists():
shutil.copyfile(self.custom_js_src_file, output_dir / self.custom_js)
with self.content_output_path.open("w", encoding="utf-8") as f:
f.write(content_item_template.render(content_item = self, page_title = self.title))
def __init__(self, md_file):
self.slug = md_file.stem
self.data = frontmatter.load(md_file)
self.html = markdown.markdown(self.data.content)
self.preview = self.html[:300] + "<a href=" + f"{self.slug}.html" + ">... read more</a>"
self.title = self.data.get("title", self.slug)
self.omit_second_title = self.data.get("omit_second_title", False)
self.date = self.data.get("data", "2000-01-01T00:00:00+03:00")
self.content_output_path = output_dir / f"{self.slug}.html"
self.image_src_file = Path(f"{content_dir}/{md_file.stem}.jpg")
self.image = f"images/{md_file.stem}.jpg" if self.image_src_file.exists() else None
self.custom_css_src_file = Path(f"{content_dir}/{md_file.stem}.css")
self.custom_css = f"assets/css/{md_file.stem}.css" if self.custom_css_src_file.exists() else None
self.custom_js_src_file = Path(f"{content_dir}/{md_file.stem}.js")
self.custom_js = f"assets/js/{md_file.stem}.js" if self.custom_js_src_file.exists() else None
# Special handling for the 'about' item
if self.slug == 'about':
self.image = 'static/about.jpg'
logger.info(f"Building the '{Config.MAIN_PAGE_TITLE}' site.")
# Recreate the output dir if needed
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.mkdir()
# Create public subdirs
img_dir.mkdir()
assets_dir.mkdir()
assets_css_dir.mkdir()
assets_js_dir.mkdir()
# Copy static files if exist
if static_dir.exists():
shutil.copytree(static_dir, output_dir / "static")
# Prepare template rendering engine
env = Environment(loader=FileSystemLoader(str(template_dir)))
env.globals['header_image'] = header_image
index_template = env.get_template("index.html")
content_item_template = env.get_template("content_item.html")
# Parse the content files
content_items = []
for md_file in content_dir.glob("*.md"):
current_content_item = ContentItemPrototype(md_file)
current_content_item.render_content()
content_items.append({
"slug": current_content_item.slug,
"title": current_content_item.title,
"date": current_content_item.date,
"preview": markdown.markdown(current_content_item.preview),
"image": current_content_item.image,
})
# Render the index file
with (output_dir / "index.html").open("w", encoding="utf-8") as f:
f.write(index_template.render(page_title = Config.MAIN_PAGE_TITLE, content_items=content_items))
# Render the about file
about_content = ContentItemPrototype(Path('static/about.md'))
about_content.render_content()
# Move 'robots.txt' into output_dir
shutil.copyfile(static_dir / 'robots.txt', output_dir / 'robots.txt')
logger.info(f"Created {len(content_items)} content items.")
if __name__ == '__main__':
main()

19
jinja_env.py Normal file
View File

@@ -0,0 +1,19 @@
import sys
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from config import Config
# get the scrip directory
if getattr(sys, 'frozen', False):
# Running fron Pyinstaller-packed binary
base_dir = Path(sys._MEIPASS)
else:
# Running as a plain Python app
base_dir = Path(__file__).resolve().parent
# Prepare template rendering engine
jinja_env_dir = f"{base_dir}/themes/{Config.THEME}/templates"
env = Environment(loader=FileSystemLoader(f"{jinja_env_dir}"))
env.globals['header_image'] = f"{base_dir}/static/header.jpg"
index_template = env.get_template("index.html")
content_item_template = env.get_template("content_item.html")

22
logger.py Normal file
View File

@@ -0,0 +1,22 @@
import logging, sys
from config import config
# Logging config section
LOG_TO = sys.stdout
LOG_LEVEL = logging.INFO
if config.debug == True:
LOG_LEVEL = logging.DEBUG
logger = logging.getLogger(config.app_name)
logger.setLevel(LOG_LEVEL)
stdout_handler = logging.StreamHandler(LOG_TO)
stdout_handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter(
'[%(asctime)s] %(name)s: %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
stdout_handler.setFormatter(formatter)
if not logger.hasHandlers():
logger.addHandler(stdout_handler)
logger.propagate = False

View File

@@ -1,3 +1,5 @@
argparse
bs4
python-frontmatter
jinja2
markdown

View File

@@ -1,6 +0,0 @@
---
title: "About"
omit_second_title: True
---

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -1,24 +0,0 @@
{% extends "default.html" %}
{% block head_includes %}
{% if content_item.custom_css %}
<link href="{{ content_item.custom_css }}">
{% endif %}
{% endblock %}
{% block content %}
<div class="container mt-4 mb-5">
{% if content_item.image %}<img src="{{ content_item.image }}" class="img-fluid mb-5" style="border-radius: 5px;">{% endif %}
{% if not content_item.omit_second_title %}
<h1>{{ content_item.title }}</h1>
{% endif %}
<article>{{ content_item.html | safe }}</article>
<a href="index.html" class="btn btn-secondary mt-4">← Back</a>
</div>
{% endblock %}
{% block footer_includes %}
{% if content_item.custom_js %}
<script src="{{ content_item.custom_js }}"></script>
{% endif %}
{% endblock %}

View File

@@ -1,90 +0,0 @@
<!doctype html>
<html>
<head>
{% set base = "" %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<title>{{ page_title }}</title>
<style>
.top_header {
position: relative;
height: 250px;
background: rgb(70, 70, 124);
background-image: url("{{ header_image }}");
background-size: cover;
background-position: center;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.top_header_text {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
font-weight: bold;
font-size: 2.5rem;
background: rgba(0, 0, 0, 0.05);
border-radius: 5px;
padding: 10px 20px;
border-radius: 10px;
}
.top_menu a {
font-weight: bold;
color: #333;
padding: 6px 12px;
margin-left: 10px;
text-decoration: none;
transition: background 0.2s ease;
}
.top_menu a:hover {
background: rgba(255, 255, 255, 0.3);
}
@media (min-width: 768px) {
.top_header_text {
font-size: 4rem;
}
}
.card-title {
font-weight: bold;
}
article {
font-size: 1.1rem;
line-height: 1.6;
}
{% block head_includes %}
{% endblock %}
</style>
</head>
<body>
<div class="top_header">
<div >
<h1 class="top_header_text"><a style="border-radius: 5px; padding: 10px; opacity: 0.7; background: black">{{ page_title }}</a></h1>
</div>
</div>
<div class="top_menu d-flex justify-content-end" style="margin-bottom: 20px;">
<a href="index.html">Home</a>
<a href="about.html">About</a>
</div>
<div id="content">{% block content %}{% endblock %}</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.min.js" integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" crossorigin="anonymous"></script>
{% block footer_includes %}
{% endblock %}
</body>

View File

@@ -1,19 +0,0 @@
{% extends "default.html" %}
{% block content %}
<div class="row row-cols-1 row-cols-md-3 g-4 px-1 py-2">
{% for content_item in content_items %}
<div class="col mb-3">
<div class="card h-100" style="border-radius: 5px;">
<img src="{{ content_item.image }}" alt="{{ content_item.title }}" class="content-cover-img card-img-top img-fluid" style="object-fit: cover; height: 200px; border-radius: 5px;">
<div class="card-body d-flex flex-column">
<h5 class="card-title"><a class="text-decoration-none text-body" href="{{ content_item.slug}}.html ">{{ content_item.title }}</a></h5>
<p class="card-text mb-3">{{ content_item.preview | safe}}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1,9 @@
---
title: "About"
omit_second_title: True
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

View File

@@ -0,0 +1,37 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.top_header {
position: relative;
height: 150px;
/*background: rgb(70, 70, 124);*/
overflow: hidden;
object-fit: cover;
}
.top_header_text {
position: absolute;
bottom: 0;
left: 0;
color: rgba(255,255,255,0.75);
font-weight: bold;
font-size: 1em;
line-height: 2em;
/*background: rgba(0, 0, 0, 0.75);*/
background: #121212;
opacity: 0.95;
width: 100%;
text-decoration: none;
}
article {
font-size: 1.1rem;
line-height: 1.6;
}
#footer-data, #footer-data-secondary {
line-height: 2em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,20 @@
console.log('Default theme loaded.')
const saved_color_theme = localStorage.getItem('saved_color_theme');
if (saved_color_theme) {
document.documentElement.setAttribute('data-bs-theme', saved_color_theme);
}
document.getElementById("categories_toggle").onclick = function() {
categories_menu_item = document.getElementById("categories_menu");
categories_menu_item.style.display = categories_menu_item.style.display === "none" ? "block" : "none"
}
document.getElementById("dark_theme_toggle").onclick = function() {
html = document.documentElement;
current_color_theme = html.getAttribute('data-bs-theme');
new_color_theme = current_color_theme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-bs-theme', new_color_theme);
localStorage.setItem('saved_color_theme', new_color_theme);
}

View File

@@ -0,0 +1,44 @@
{% extends "default.html" %}
{% block head_includes %}
{% if content_item.css_file %}
<link rel="stylesheet" href="{{ parent_path }}static/css/{{ content_item.slug }}.css">
{% endif %}
{% endblock %}
{% block content %}
<div class="container mt-4 mb-5">
{% if content_item.image_file %}
<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" alt="{{ content_item.image_file }}" class="img-fluid mb-5 rounded w-100 h-auto dotr">
{% endif %}
{% if not content_item.omit_second_title %}
<h1>{{ content_item.title }}</h1>
{% endif %}
<article>{{ content_item.html | safe }}</article>
{% if content_item.audio_file %}
<div id="audio" class="mb-4 mt-4">
<audio controls>
<source src="{{ parent_path }}static/audio/{{ content_item.audio_file }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
<div id="categories">
{% for category in content_item.categories %}
<a href="{{ parent_path }}categories/{{ category }}.html" class="mx-1 text-decoration-none small text-muted">{{ category }} </a>
{% endfor %}
</div>
<a href="../index.html" class="btn btn-secondary mt-4">← Back</a>
</div>
{% endblock %}
{% block footer_includes %}
{% if content_item.js_file %}
<script src="{{ parent_path}}static/js/{{ content_item.slug }}.js"></script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,64 @@
<!doctype html>
<html data-bs-theme="light">
<head>
{% set base = "" %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<link href="{{ parent_path }}static/css/theme.css" rel="stylesheet">
<title>{{ page_title }}</title>
{% block head_includes %}
{% endblock %}
</head>
<body>
<div id="top_wrapper">
<div id= "top_header" class="row top_header">
<img id="top_header_image" src="{{ parent_path }}static/images/header.jpg" class="img-fluid">
<div id="top_title" class="row">
<a class="top_header_text ms-1 text-decoration-none">{{ page_title }}</a>
</div>
</div>
<div id="menu_container" class="row justify-content-end pe-2 py-2" >
<div id="menu_wrapper" class="col-auto fw-bold">
<a class="link-body-emphasis mx-2 text-decoration-none" href="{{ parent_path }}index.html">Home</a>
<a class="link-body-emphasis mx-2 text-decoration-none" id="categories_toggle" href="#none">Categories</a>
<a class="link-body-emphasis mx-2 text-decoration-none" href="{{ parent_path }}static/about.html">About</a>
<a class="link-body-emphasis mx-2 text-decoration-none" id="dark_theme_toggle" href="#none">🌓︎</a>
</div>
</div>
<div id="categories_container" class="container">
<div id="categories_menu" class="row justify-content-center mx-auto mb-2" style="display: none">
{% for category in categories %}
<a href="{{ parent_path }}categories/{{ category }}.html" class="mx-1 text-decoration-none">{{ category }}</a>
{% endfor %}
</div>
</div>
</div>
<div id="content_wrapper" class="container-fluid mb-2">
{% block content %}
{% endblock %}
</div>
<div id="footer_wrapper" class="container-fluid">
<div id="footer-data" class="row d-flex text-muted justify-content-end">
{% block footer_includes %}
{% endblock %}
{% if footer_data %}
<div class="d-flex align-items-center justify-content-center">
<p class="my-0">{{ footer_data }}</p>
</div>
{% endif %}
</div>
<div id="footer-data-secondary" class="row d-flex text-muted justify-content-end">
<div class="d-flex align-items-center justify-content-center">
<p class="small my-0" style="font-size: 0.75rem;">Page created with <a class="text-decoration-none" href="#microgen">microgen</a></p>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.min.js" integrity="sha384-RuyvpeZCxMJCqVUGFI0Do1mQrods/hhxYlcVfGPOfQtPJh0JCw12tUAZ/Mv10S7D" crossorigin="anonymous"></script>
<script defer src="{{ parent_path }}static/js/theme.js"></script>
</body>

View File

@@ -0,0 +1,28 @@
{% extends "default.html" %}
{% block content %}
<div class="row justify-content-center g-4 mb-4 py-4">
{% for content_item in content_items %}
<div class="col-auto align-items-stretch d-flex mx-2" style="width: 100%; width: 24em;">
<div class="card h-100 px-0 rounded mx-1 my-3" style="width: 100%; width: 22em;">
<div class="card-header">
{% if content_item.image_file %}
<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" alt="{{ content_item.title }}" class="card-img-top img-fluid mx-auto d-block object-fit-fill rounded" style="height: 200px; width: auto;">
{% else %}
<img src="{{ parent_path }}static/images/1x1.png" alt="{{ content_item.title }}" class="card-img-top img-fluid mx-auto d-block object-fit-scale rounded" style="height: 200px; width: auto;">
{% endif %}
</div>
<div class="card-body">
<h5 class="fw-bold"><a class="text-decoration-none text-body" href="{{ parent_path }}content/{{ content_item.slug}}.html">{{ content_item.title }}</a></h5>
<p class="card-text mb-2">{{ content_item.preview | safe}}<a class="text-decoration-none" href="{{ parent_path }}content/{{ content_item.slug}}.html">... read more</a> </p>
</div>
<div class="card-footer" style="height: 4em;">
{% for item_category in content_item.categories %}
<small><a href="{{ parent_path }}categories/{{ item_category }}.html" class="text-muted text-decoration-none mx-1">{{ item_category }}</a></small>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}