#!/usr/bin/env python3 """ build_listings.py -- interconnect Blog + Events/Projects + News + homepage. Single source of truth = the actual detail pages under /blog/, /projects/ and /news/. This script reads them and regenerates, between AUTO markers: /news/index.html the News hub grid (all items, newest first) /index.html the homepage "Latest news" block (top 3) So adding a blog post or an event/project and running this script makes it appear in News AND on the homepage automatically, with the correct date from the central ARTICLE_DATES registry (tools/seo_inject.py). Run after adding/removing content: python tools/build_listings.py Idempotent. Blocks are wrapped in and . """ import io, os, re, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import seo_inject ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LANGS = ("en", "mk", "si") DATES = seo_inject.ARTICLE_DATES N_LATEST = 3 # folder -> (data-type, badge modifier, icon, {lang: label}) TYPES = { "news": ("news", "news", "fa-bullhorn", {"en": "News", "mk": "Вест", "si": "Novica"}), "blog": ("blog", "blog", "fa-pen-nib", {"en": "Blog", "mk": "Блог", "si": "Blog"}), "projects": ("project", "project", "fa-calendar-day", {"en": "Event", "mk": "Настан", "si": "Dogodek"}), } MONTHS = { "en": ["January","February","March","April","May","June","July","August","September","October","November","December"], "mk": ["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"], "si": ["januar","februar","marec","april","maj","junij","julij","avgust","september","oktober","november","december"], } H1_RE = re.compile(r"

(.*?)

", re.DOTALL) DESC_RE = re.compile(r'(.*?)', re.DOTALL) IMG_RE = re.compile(r']*)>') ATTR_RE = lambda a, s: (re.search(a + r'="([^"]*)"', s) or [None, None])[1] def strip_tags(s): return re.sub(r"<[^>]+>", "", s).strip() def fmt_date(lang, iso): y, m, d = iso.split("-") mon = MONTHS[lang][int(m) - 1] return f"{int(d)}. {mon} {y}" if lang == "si" else f"{int(d)} {mon} {y}" def rootrel_img(src): m = re.search(r'(images/.*)$', src) return "/" + m.group(1) if m else src def collect_items(lang): items = [] for folder in ("news", "blog", "projects"): base = os.path.join(ROOT, lang, folder) if not os.path.isdir(base): continue for slug in sorted(os.listdir(base)): page = os.path.join(base, slug, "index.html") if not os.path.exists(page): continue key = f"{folder}/{slug}" date = DATES.get(key, {}) iso = date.get("published") or date.get("modified") if not iso: continue # only list dated content html = io.open(page, encoding="utf-8").read() h1 = H1_RE.search(html) title = strip_tags(h1.group(1)) if h1 else slug desc = (DESC_RE.search(html) or [None, ""])[1] img = None fig = FIG_RE.search(html) if fig: im = IMG_RE.search(fig.group(1)) if im: a = im.group(1) src = ATTR_RE("src", a) if src: img = {"src": rootrel_img(src), "w": ATTR_RE("width", a), "h": ATTR_RE("height", a), "alt": ATTR_RE("alt", a) or title} items.append({"folder": folder, "slug": slug, "iso": iso, "title": title, "excerpt": desc, "img": img, "url": f"/{lang}/{folder}/{slug}/"}) items.sort(key=lambda x: x["iso"], reverse=True) return items def badge(folder, lang): dtype, mod, icon, labels = TYPES[folder] return dtype, f' {labels[lang]}' def esc_attr(s): return s.replace('"', """) def media(it, folder, lang): _, _, icon, _ = TYPES[folder] if it["img"]: i = it["img"] wh = (f' width="{i["w"]}"' if i["w"] else "") + (f' height="{i["h"]}"' if i["h"] else "") return f'' return f'
' def news_hub_card(it, lang): dtype, badge_html = badge(it["folder"], lang) return ( f' ' ) def landing_card(it, lang): dtype, badge_html = badge(it["folder"], lang) return ( f' ' ) def replace_block(html, start, end, inner, insert_pat=None): """Replace content between markers; if markers absent, use insert_pat regex (group1)(...content...)(group3) to seed them.""" block = f'{start}\n{inner}\n {end}' if start in html and end in html: return re.sub(re.escape(start) + r".*?" + re.escape(end), block, html, count=1, flags=re.DOTALL) if insert_pat: m = insert_pat.search(html) if m: return html[:m.start()] + m.group(1) + "\n" + block + "\n " + m.group(3) + html[m.end():] return html NEWS_GRID_PAT = re.compile(r'(
)(.*)(
\s*