Compare commits
41 Commits
main
...
bootstrap-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b31079654 | |||
| 1830b9484e | |||
| c9c1c883a2 | |||
| ece9730f37 | |||
| b5b3de23bb | |||
| 818f3ee37f | |||
| aa2a7c279d | |||
| cc503593d8 | |||
| 0bc1987856 | |||
| af4fd0a618 | |||
| 51b7cd586b | |||
| 08caaba9ee | |||
| 5a44c570d4 | |||
| 62562c9afd | |||
| 6cb80e6720 | |||
| ab1109a900 | |||
| 47badd9d5a | |||
| 095424f252 | |||
| 4927f9907d | |||
| a0e858f416 | |||
| 141060368c | |||
| acfa45295b | |||
| 3efc5e2390 | |||
| 21b4066288 | |||
| 3f832a1af5 | |||
| 2132cfd48d | |||
| 7bbbd2e8ad | |||
| eeb1462210 | |||
| f9c14b3e2a | |||
| 706c354e00 | |||
| 2e93be7184 | |||
| 7c7dedcfa5 | |||
| 26c4b9c1e1 | |||
| 45d12ab97d | |||
| 2d7f1c1f7d | |||
| 3348df26e5 | |||
| 09621b73c3 | |||
| 1854366a9d | |||
| 2a473429f6 | |||
| 7baae89942 | |||
| fc1ee27146 |
18
argparser.py
Normal file
18
argparser.py
Normal 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.")
|
||||||
167
classes.py
Normal file
167
classes.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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.")
|
||||||
|
|
||||||
32
config.py
32
config.py
@@ -1,24 +1,14 @@
|
|||||||
import logging, os, sys
|
import os
|
||||||
|
|
||||||
# Main config section
|
# Main config section
|
||||||
class Config:
|
class Config:
|
||||||
APP_NAME = "hydrogen"
|
MAIN_PAGE_TITLE = "microgen library"
|
||||||
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
|
|
||||||
|
|
||||||
# Logging config section
|
APP_NAME = "hydrogen"
|
||||||
logger = logging.getLogger(Config.APP_NAME)
|
APP_DESCRIPTION = "Simplistic static site generator"
|
||||||
logger.setLevel(Config.LOG_LEVEL)
|
APP_SRC_URL = f"https://git.exocortex.ru/Exocortex/{APP_NAME}"
|
||||||
stdout_handler = logging.StreamHandler(Config.LOG_TO)
|
OUTPUT_DIR = 'public'
|
||||||
stdout_handler.setLevel(Config.LOG_LEVEL)
|
TEMPLATES_DIR = 'templates'
|
||||||
formatter = logging.Formatter(
|
CONTENT_DIR = 'content'
|
||||||
'[%(asctime)s] %(name)s: %(levelname)s: %(message)s',
|
STATIC_DIR = 'static'
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
HEADER_IMAGE = 'static/header.jpg'
|
||||||
)
|
THEME = "default"
|
||||||
stdout_handler.setFormatter(formatter)
|
|
||||||
if not logger.hasHandlers():
|
|
||||||
logger.addHandler(stdout_handler)
|
|
||||||
logger.propagate = False
|
|
||||||
47
functions.py
Normal file
47
functions.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import os, 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_old():
|
||||||
|
logger.debug(f"Initializing new site")
|
||||||
|
|
||||||
|
# Recreate output directory if necessary
|
||||||
|
output_dir = Path(Config.OUTPUT_DIR)
|
||||||
|
if output_dir.exists():
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
output_dir.mkdir()
|
||||||
|
|
||||||
|
# Create output directory subtree
|
||||||
|
for dir in [ Path(Config.CONTENT_DIR), Path(Config.STATIC_DIR), Path(Config.TEMPLATE_DIR),
|
||||||
|
Path(Config.OUTPUT_IMG_DIR), Path(Config.OUTPUT_CSS_DIR), Path(Config.OUTPUT_JS_DIR)]:
|
||||||
|
dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Create "About.md" content item
|
||||||
|
create_content("about")
|
||||||
|
|
||||||
|
def init_site():
|
||||||
|
site = Site()
|
||||||
|
site.init_site()
|
||||||
|
|
||||||
|
def build_site():
|
||||||
|
site = Site()
|
||||||
|
site.build()
|
||||||
106
hydrogen.py
106
hydrogen.py
@@ -1,96 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from pathlib import Path
|
from argparser import argparser
|
||||||
import frontmatter
|
from classes import *
|
||||||
import markdown
|
from functions import *
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
import shutil
|
|
||||||
from config import Config, logger
|
|
||||||
|
|
||||||
content_dir = Path('content')
|
def main():
|
||||||
template_dir = Path('templates')
|
args = argparser.parse_args()
|
||||||
output_dir = Path(Config.OUTPUT_DIR)
|
match args.command:
|
||||||
img_dir = Path('public/images')
|
case "build":
|
||||||
assets_dir = Path('public/assets')
|
build_site()
|
||||||
assets_css_dir = Path('public/assets/css')
|
case "init":
|
||||||
assets_js_dir = Path('public/assets/js')
|
init_site()
|
||||||
static_dir = Path('static')
|
case "new" | "create" | "edit":
|
||||||
header_image = Config.HEADER_IMAGE or '/static/header.jpg'
|
edit_content(args.filename)
|
||||||
|
|
||||||
class ContentItemPrototype:
|
if __name__ == '__main__':
|
||||||
def render_content(self):
|
main()
|
||||||
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.")
|
|
||||||
8
jinja_env.py
Normal file
8
jinja_env.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
# Prepare template rendering engine
|
||||||
|
env = Environment(loader=FileSystemLoader(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
20
logger.py
Normal 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
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
argparse
|
||||||
python-frontmatter
|
python-frontmatter
|
||||||
jinja2
|
jinja2
|
||||||
markdown
|
markdown
|
||||||
|
|||||||
BIN
static/images/1x1.png
Normal file
BIN
static/images/1x1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
182
templates.py
Normal file
182
templates.py
Normal 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 %}
|
||||||
|
"""
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
|
|
||||||
{% extends "default.html" %}
|
{% extends "default.html" %}
|
||||||
|
|
||||||
{% block head_includes %}
|
{% block head_includes %}
|
||||||
{% if content_item.custom_css %}
|
{% if content_item.css_file %}
|
||||||
<link href="{{ content_item.custom_css }}">
|
<link rel="stylesheet" href="{{ parent_path }}static/css/{{ content_item.slug }}.css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mt-4 mb-5">
|
<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 %}
|
{% if not content_item.omit_second_title %}
|
||||||
<h1>{{ content_item.title }}</h1>
|
<h1>{{ content_item.title }}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<article>{{ content_item.html | safe }}</article>
|
<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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer_includes %}
|
{% block footer_includes %}
|
||||||
{% if content_item.custom_js %}
|
{% if content_item.js_file %}
|
||||||
<script src="{{ content_item.custom_js }}"></script>
|
<script src="{{ parent_path}}static/js/{{ content_item.slug }}.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html data-bs-theme="light">
|
||||||
<head>
|
<head>
|
||||||
{% set base = "" %}
|
{% set base = "" %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta charset="UTF-8">
|
<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>
|
<title>{{ page_title }}</title>
|
||||||
<style>
|
<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 {
|
.top_header {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
background: rgb(70, 70, 124);
|
background: rgb(70, 70, 124);
|
||||||
background-image: url("{{ header_image }}");
|
background-image: url("{{ parent_path }}static/header.jpg");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -33,58 +38,77 @@
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border-radius: 10px;
|
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) {
|
@media (min-width: 768px) {
|
||||||
.top_header_text {
|
.top_header_text {
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
.card-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
article {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
{% block head_includes %}
|
{% block head_includes %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top_header">
|
<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>
|
<h1 class="top_header_text"><a style="border-radius: 5px; padding: 10px; opacity: 0.7; background: black">{{ page_title }}</a></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="top_menu d-flex justify-content-end" style="margin-bottom: 20px;">
|
<div class="row justify-content-end pe-2 py-2" >
|
||||||
|
<div class="col-auto fw-bold">
|
||||||
<a href="index.html">Home</a>
|
<a class="link-body-emphasis mx-2 text-decoration-none" href="{{ parent_path }}index.html">Home</a>
|
||||||
<a href="about.html">About</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">{% block content %}{% endblock %}</div>
|
<div id="content" class="container-fluid mb-2">
|
||||||
|
{% block content %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
{% endblock %}
|
||||||
<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>
|
</div>
|
||||||
<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>
|
<div id="footer" class="container-fluid">
|
||||||
|
<div id="footer-data" class="row my-1">
|
||||||
{% block footer_includes %}
|
{% block footer_includes %}
|
||||||
{% endblock %}
|
{% 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>
|
</body>
|
||||||
@@ -1,19 +1,33 @@
|
|||||||
|
|
||||||
{% extends "default.html" %}
|
{% extends "default.html" %}
|
||||||
|
|
||||||
|
{% block head_includes %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="row justify-content-between g-4 mb-4 py-4">
|
||||||
<div class="row row-cols-1 row-cols-md-3 g-4 px-1 py-2">
|
|
||||||
{% for content_item in content_items %}
|
{% for content_item in content_items %}
|
||||||
<div class="col mb-3">
|
<div class="col-auto align-items-stretch d-flex mx-2" style="width: 100%; width: 24em;">
|
||||||
<div class="card h-100" style="border-radius: 5px;">
|
<div class="card h-100 px-0 rounded mx-1 my-3" style="width: 100%; width: 22em;">
|
||||||
<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-header">
|
||||||
<div class="card-body d-flex flex-column">
|
{% if content_item.image_file %}
|
||||||
<h5 class="card-title"><a class="text-decoration-none text-body" href="{{ content_item.slug}}.html ">{{ content_item.title }}</a></h5>
|
<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;">
|
||||||
<p class="card-text mb-3">{{ content_item.preview | safe}}</p>
|
{% 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user