#!/usr/bin/env python3 “”” SaaS Africa โ€“ Mining tools blog generator (dark theme, green + gold) Reads from “inputdatasaas.csv” with these sections: Section 0 (meta info โ€“ marker “0” in column A): – key in column B, value in column C Example: 0 | title | Best mining saas tools | introduction | finding the right software… Section 1 (overview table โ€“ marker “1”): Header row: 1 | name | Ease of use | Mining industry fit | Core focus & key features | Scalability | Deployment model | Pricing | … Data rows: “” | CorePlan | … Section 2 (in-depth cards โ€“ marker “2”): Header row: 2 | name | Positioning | Strengths | Limitations | Technical capability | African market consideration | Detailed pricing | Best use cases | Website Data rows: “” | CorePlan | … Section 3 (recommendation โ€“ marker “3”): 3 | recommendation | Section 4 (FAQs โ€“ marker “4”): Header row: 4 | question | answer Data rows: “” | Which mining SaaS tool …? | Start with … Output: – WordPress-friendly HTML fragment (no , , JS, etc.) – Filename: “saaspost.html” “”” import csv import html import os import re from pathlib import Path CSV_NAME = “inputdatasaas.csv” OUTPUT_NAME = “saaspost.html” def norm_header(text: str) -> str: “””Normalize header to a safe dict key.””” t = (text or “”).strip().lower() t = re.sub(r”[^a-z0-9]+”, “_”, t) t = re.sub(r”_+”, “_”, t).strip(“_”) return t def slugify(text: str) -> str: “””Generate an id-friendly slug based on a tool name.””” t = (text or “”).lower() t = re.sub(r”[^a-z0-9]+”, “-“, t) t = re.sub(r”-+”, “-“, t).strip(“-“) return t or “item” def load_sections(csv_path: Path): “”” Parse the CSV into: meta (dict), overview (section 1), deep (section 2), recommendation (string), faq (section 4) “”” meta = {} sections = { “1”: {“headers”: None, “keys”: None, “rows”: []}, “2”: {“headers”: None, “keys”: None, “rows”: []}, “4”: {“headers”: None, “keys”: None, “rows”: []}, } recommendation_parts = [] current_section = None if not csv_path.exists(): raise FileNotFoundError(f”CSV file not found at: {csv_path}”) with csv_path.open(encoding=”utf-8″, newline=””) as f: reader = csv.reader(f) for row in reader: # Normalise cells row = [(c or “”).strip() for c in row] if not any(row): continue marker = row[0] rest = row[1:] # Section 0: meta key/value pairs if marker == “0”: current_section = “0” if len(rest) >= 2 and rest[0]: meta[rest[0].lower()] = rest[1] continue if marker == “” and current_section == “0”: if len(rest) >= 2 and rest[0]: meta[rest[0].lower()] = rest[1] continue # Sections 1, 2, 4 โ€“ header rows if marker in (“1”, “2”, “4”): current_section = marker if any(rest): # Strip trailing empty headers so we donโ€™t create empty keys while rest and not rest[-1]: rest.pop() sections[marker][“headers”] = rest sections[marker][“keys”] = [norm_header(h) for h in rest] continue # Section 3 โ€“ recommendation text if marker == “3”: current_section = “3” if len(rest) >= 2: recommendation_parts.append(rest[1]) continue # Data rows for sections 1, 2, 4 if marker == “” and current_section in (“1”, “2”, “4”): sec = sections[current_section] if sec[“headers”] is None: continue # Pad / trim to match number of headers if len(rest) < len(sec["headers"]): rest += [""] * (len(sec["headers"]) - len(rest)) elif len(rest) > len(sec[“headers”]): rest = rest[:len(sec[“headers”])] row_dict = dict(zip(sec[“keys”], rest)) if any(v for v in row_dict.values()): sec[“rows”].append(row_dict) continue # Extra recommendation lines, if any if marker == “” and current_section == “3”: if len(rest) >= 2 and rest[1]: recommendation_parts.append(rest[1]) continue recommendation = ” “.join(p for p in recommendation_parts if p) return meta, sections[“1”], sections[“2”], recommendation, sections[“4”] def build_html_fragment(meta, overview, deep, recommendation, faq): “””Create the final HTML fragment with styling.””” ov_keys = overview[“keys”] deep_keys = deep[“keys”] faq_keys = faq[“keys”] def esc(x: str) -> str: return html.escape(x or “”) def gi(row: dict, keys, idx: int) -> str: “””Get value by index from a row dict, already escaped.””” if idx >= len(keys): return “” return esc(row.get(keys[idx], “”)) def make_link(raw: str) -> str: “””Turn a raw website value into a clickable HTML link.””” raw = (raw or “”).strip() if not raw: return “” url = raw if raw.lower().startswith(“http”) else f”https://{raw}” esc_url = html.escape(url) return ( f’‘ f”{esc_url}” ) page_title = esc( meta.get(“title”, “Best SaaS tools for African companies”) ) intro = esc( meta.get( “introduction”, “Finding the right software can be hard, so we compared the leading tools for you.”, ) ) tool_names = “, “.join(esc(r.get(ov_keys[0], “”)) for r in overview[“rows”]) parts = [] # ========= CSS (dark theme, green + gold) ========= parts.append( “”” “”” ) # ========= HERO ========= parts.append( f”””
SaaS Africa ยท Mining Software

{page_title}

{intro}

Tools compared: {tool_names}
Note: Pricing and features can change. Always confirm the latest details on the official vendor sites.

Mining operations African context Data-driven selection
“”” ) # ========= SECTION 1 โ€“ OVERVIEW TABLE ========= parts.append( “””
Section 1

Overview Comparison Table

Tools are listed across the top. Key categories such as ease of use, mining fit and pricing are listed in the first column, so you can compare your options at a glance.

“”” ) for row in overview[“rows”]: parts.append(f” \n”) parts.append( “”” “”” ) for cat_index, cat_name in enumerate(overview[“headers”][1:], start=1): parts.append(” \n”) parts.append( f’ \n’ ) for row in overview[“rows”]: parts.append(f” \n”) parts.append(” \n”) parts.append( “””
Category{esc(row.get(ov_keys[0], ”))}
‘ f’{esc(cat_name)}{gi(row, ov_keys, cat_index)}
“”” ) # ========= SECTION 2 โ€“ IN-DEPTH CARDS + RECOMMENDATION ========= parts.append( “””
Section 2

In-depth Analysis of Each Tool

This section is built from your detailed mining SaaS notes: positioning, strengths, limitations, technical capabilities, African market considerations and pricing. Each card comes directly from the spreadsheet, so you can keep everything consistent by updating only one source.

“”” ) for idx, r in enumerate(deep[“rows”], start=1): name = gi(r, deep_keys, 0) best_use = gi(r, deep_keys, 7) website_raw = r.get(deep_keys[8], “”) parts.append( f”””
#{idx}

{name}

Best for: {best_use if best_use else “specific mining workflows”}

Positioning: {gi(r, deep_keys, 1)}

Strengths: {gi(r, deep_keys, 2)}

Limitations: {gi(r, deep_keys, 3)}

Technical capability: {gi(r, deep_keys, 4)}

African market consideration: {gi(r, deep_keys, 5)}

Detailed pricing: {gi(r, deep_keys, 6)} Pricing information is indicative only. Check the vendor site for current plans, currencies and implementation costs.

Best use cases: {best_use}

{make_link(website_raw)}

“”” ) parts.append(”
\n”) if recommendation.strip(): parts.append( f”””
Where should a mine start? {esc(recommendation)}
“”” ) parts.append( “””
“”” ) # ========= SECTION 3 โ€“ FAQ TABLE ========= parts.append( “””
Section 3

Frequently Asked Questions

These FAQs are taken from your spreadsheet and can be updated any time. They also work as a light conclusion for the post, addressing the most common concerns for mining stakeholders in Africa.

“”” ) for r in faq[“rows”]: parts.append( f””” “”” ) parts.append( “””
Question Answer
{gi(r, faq_keys, 0)} {gi(r, faq_keys, 1)}
“”” ) return “”.join(parts) def main(): script_dir = Path(os.path.dirname(os.path.abspath(__file__))) os.chdir(script_dir) csv_path = script_dir / CSV_NAME print(“Script directory:”, script_dir) print(“Expected CSV file:”, csv_path) try: meta, sec1, sec2, recommendation, sec4 = load_sections(csv_path) html_code = build_html_fragment(meta, sec1, sec2, recommendation, sec4) out_path = script_dir / OUTPUT_NAME out_path.write_text(html_code, encoding=”utf-8″) print(f”โœ“ HTML written to {out_path}”) except Exception as e: print(“ERROR:”, e) input(“\nPress ENTER to close…”) if __name__ == “__main__”: main()