42 Commits

Author SHA1 Message Date
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
15 changed files with 568 additions and 171 deletions

18
argparser.py Normal file
View File

@@ -0,0 +1,18 @@
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)
#argparser.add_argument('--init', action='store_true', help = 'Initialize new site')
#argparser.add_argument('--content', '--edit', type = str, help = 'Create new content')
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.")

169
classes.py Normal file
View File

@@ -0,0 +1,169 @@
from datetime import datetime
import os
import shutil
from collections import defaultdict
import markdown
import frontmatter
from bs4 import BeautifulSoup
from pathlib import Path
from logger import logger
from config import Config
from jinja_env import env, content_item_template, index_template
class ContentItem:
def render_content(self, categories):
logger.debug(f"Rendering {self.source_filename} to {self.target_filename}")
try:
if self.image_file and self.image_file.exists():
image_targetfile = Path(f"{Config.OUTPUT_DIR}/{Config.STATIC_DIR}/images/{self.image_file.name}")
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"
if self.css_file and self.css_file.exists():
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)
if self.js_file and self.js_file.exists():
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)
with self.target_filename.open("w", encoding="utf-8") as f:
f.write(content_item_template.render(content_item = self, page_title = self.title, parent_path = '../', categories = categories))
except Exception as e:
logger.error(e)
def parse_content(self):
logger.debug(f"Parsing file {self.source_filename}")
try:
self.source_filename = Path(self.source_filename)
self.subdir = self.source_filename.parent
self.slug = self.source_filename.stem
self.target_filename = Path(f"{Config.OUTPUT_DIR}/{self.source_filename.parent}/{self.source_filename.stem}.html")
self.data = frontmatter.load(self.source_filename)
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')
self.html = markdown.markdown(self.data.content)
cover_image_path = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.jpg")
self.image_file = cover_image_path if cover_image_path.exists() else ""
css_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.css")
self.css_file = css_filepath if css_filepath.exists() else ""
js_filepath = Path(f"{self.source_filename.parent}/{self.source_filename.stem}.js")
self.js_file = js_filepath if js_filepath.exists() else ""
except Exception as e:
logger.error(e)
def create_content(self):
with open(self.source_filename, mode="w", encoding="utf-8") as f:
f.writelines([
"---\n",
f"title: {self.source_filename.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, filename):
self.source_filename = filename
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):
logger.info("Initializing new site")
# Create directories
for subdir in [self.content_dir, self.static_dir, self.templates_dir,
self.images_dir, self.css_dir, self.js_dir]:
os.makedirs(subdir, exist_ok=True)
# Create templates from literals
import templates
template_names = [t for t in dir(templates) if not t.startswith('_')]
for template_name in template_names:
template_content = getattr(templates, template_name)
with open(self.templates_dir / f"{template_name}.html", "w", encoding="utf8") as f:
f.write(template_content)
# Create static/about.md
about_item = ContentItem(filename = Path('static/about.md'))
about_item.create_content()
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 needed
if self.output_dir.exists():
shutil.rmtree(self.output_dir)
self.output_dir.mkdir()
# Create public subdirs
subdirs = [self.images_dir, self.css_dir, self.js_dir, self.content_dir, "categories"]
for subdir in subdirs:
subdir = self.output_dir / subdir
subdir.mkdir(parents=True, exist_ok=True)
# Copy static files if exist
if self.static_dir.exists():
shutil.copytree(self.static_dir, self.output_dir / self.static_dir, 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('static/about.md'))
about_content.parse_content()
about_content.render_content(categories = self.categories)
# Render the index file
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(index_template.render(page_title = Config.MAIN_PAGE_TITLE, content_items=visible_content_items, categories = self.categories))
# Render the categories indices
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")]
with (category_index).open(mode="w", encoding="utf-8") as f:
f.write(index_template.render(page_title=category, content_items=category_items, categories = self.categories, parent_path = '../'))
logger.info(f"Created {len(self.content_items)} content items.")

View File

@@ -1,24 +1,14 @@
import logging, os, sys
import os
# Main config section
class Config:
MAIN_PAGE_TITLE = "microgen library"
APP_NAME = "hydrogen"
MAIN_PAGE_TITLE = "Selected poems"
OUTPUT_DIR = os.environ.get('OUTPUT_DIR') or 'public'
APP_DESCRIPTION = "Simplistic static site generator"
APP_SRC_URL = f"https://git.exocortex.ru/Exocortex/{APP_NAME}"
OUTPUT_DIR = 'public'
TEMPLATES_DIR = 'templates'
CONTENT_DIR = 'content'
STATIC_DIR = 'static'
HEADER_IMAGE = 'static/header.jpg'
LOG_TO = sys.stdout
LOG_LEVEL = logging.DEBUG
# 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
THEME = "default"

View File

30
helpers.py Normal file
View File

@@ -0,0 +1,30 @@
import os, sys, subprocess
import shutil
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()

17
jinja_env.py Normal file
View File

@@ -0,0 +1,17 @@
import sys
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from config import Config
# Prepare template rendering engine
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
env = Environment(loader=FileSystemLoader(f"{base_dir}/{Config.TEMPLATES_DIR}"))
env.globals['header_image'] = Config.HEADER_IMAGE
index_template = env.get_template("index.html")
content_item_template = env.get_template("content_item.html")

20
logger.py Normal file
View File

@@ -0,0 +1,20 @@
import logging, sys
from config import Config
# Logging config section
LOG_TO = sys.stdout
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,4 @@
argparse
python-frontmatter
jinja2
markdown

BIN
static/images/1x1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

182
templates.py Normal file
View File

@@ -0,0 +1,182 @@
default = """
<!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">
<title>{{ page_title }}</title>
<style>
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: 250px;
background: rgb(70, 70, 124);
background-image: url("{{ parent_path }}static/header.jpg");
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;
}
/*
@media (min-width: 768px) {
.top_header_text {
font-size: 4rem;
}
}
*/
article {
font-size: 1.1rem;
line-height: 1.6;
}
</style>
{% block head_includes %}
{% endblock %}
</head>
<body>
<div class="top_header">
<div class="col-auto">
<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="row justify-content-end pe-2 py-2" >
<div 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 id="content" class="container-fluid mb-2">
{% block content %}
{% endblock %}
</div>
<div id="footer" class="container-fluid">
<div id="footer-data" class="row my-1">
{% block footer_includes %}
{% endblock %}
</div>
<div class="row small d-flex text-muted justify-content-end">
<div class="col-auto" style="font-size: 0.75rem;">
<p>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>
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);
}
</script>
</body>
"""
index = """
{% extends "default.html" %}
{% block head_includes %}
{% endblock %}
{% block content %}
<div class="row justify-content-between 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 %}
"""
content_item="""
{% 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 }}" class="img-fluid mb-5 rounded w-100 h-auto">{% endif %}
{% if not content_item.omit_second_title %}
<h1>{{ content_item.title }}</h1>
{% endif %}
<article>{{ content_item.html | safe }}</article>
<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

@@ -1,24 +1,31 @@
{% extends "default.html" %}
{% block head_includes %}
{% if content_item.custom_css %}
<link href="{{ content_item.custom_css }}">
{% 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 %}<img src="{{ content_item.image }}" class="img-fluid mb-5" style="border-radius: 5px;">{% endif %}
{% if content_item.image_file %}<img src="{{ parent_path }}static/images/{{ content_item.image_file }}" class="img-fluid mb-5 rounded w-100 h-auto">{% 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 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.custom_js %}
<script src="{{ content_item.custom_js }}"></script>
{% if content_item.js_file %}
<script src="{{ parent_path}}static/js/{{ content_item.slug }}.js"></script>
{% endif %}
{% endblock %}
{% endblock %}

View File

@@ -1,18 +1,23 @@
<!doctype html>
<html>
<html data-bs-theme="light">
<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">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
<title>{{ page_title }}</title>
<style>
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: 250px;
background: rgb(70, 70, 124);
background-image: url("{{ header_image }}");
background-image: url("{{ parent_path }}static/header.jpg");
background-size: cover;
background-position: center;
display: flex;
@@ -33,58 +38,77 @@
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;
}
</style>
{% block head_includes %}
{% endblock %}
</style>
</head>
</head>
<body>
<div class="top_header">
<div >
<div class="col-auto">
<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 class="row justify-content-end pe-2 py-2" >
<div 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 id="content">{% block content %}{% endblock %}</div>
<div id="content" class="container-fluid mb-2">
{% block content %}
{% endblock %}
</div>
<div id="footer" class="container-fluid">
<div id="footer-data" class="row my-1">
{% block footer_includes %}
{% endblock %}
</div>
<div class="row small d-flex text-muted justify-content-end">
<div class="col-auto" style="font-size: 0.75rem;">
<p>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>
const saved_color_theme = localStorage.getItem('saved_color_theme');
if (saved_color_theme) {
document.documentElement.setAttribute('data-bs-theme', saved_color_theme);
}
<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>
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);
}
</script>
</body>

View File

@@ -1,19 +1,33 @@
{% extends "default.html" %}
{% block head_includes %}
{% endblock %}
{% 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 class="row justify-content-between 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>
{% endfor %}
</div>
{% endblock %}

3
test.py Normal file
View File

@@ -0,0 +1,3 @@
from pathlib import Path
app_dir = Path(__file__).resolve().parent
print(app_dir, type(app_dir))