Portfolio Website: API & Script Reference

Portfolio Website: API & Script Reference

This is the technical reference documentation for the internal scripts and functions that power the portfolio project. Unlike the previous guides — which walk through tasks — this page documents every function, parameter, and data structure so you can confidently extend or debug the codebase.

Repository: github.com/bayzed123/sayadbayezid-portfolio-


📖 Portfolio Website Documentation Series

This page is Part 4 of a complete 4-part guide series. If you're following along step by step, here's the full path:

Step Guide What You'll Learn
1 Getting Started Guide Fork, clone, install, and run the project locally
2 Deployment & Blog System Guide Deploy to GitHub Pages/Netlify/Vercel + publish blog posts
3 Customization Guide Edit colors, sections, navigation, and add new pages
4 **API & Script Reference Deep technical reference for every script and function

Tip
New to this project? Start with Step 1 — Getting Started instead of this page. This reference is best used after you've completed initial setup, when you want to understand or modify the underlying code.

In this guide


setup.sh reference

Interactive shell script that personalizes the project with your information across all relevant files in a single run.

Usage

bash setup.sh

Prompts and variables

Prompt Variable Name Example Value
Full name $FULL_NAME Sayad Bayezid
Website URL $WEBSITE_URL www.example.com
GitHub username $GITHUB_USER bayzed123
LinkedIn URL $LINKEDIN_URL linkedin.com/in/yourprofile
Email address $EMAIL you@example.com
Blog URL $BLOG_URL yourblog.blogspot.com

Internal logic

#!/bin/bash

read -p "Enter your full name: " FULL_NAME
read -p "Enter your website URL: " WEBSITE_URL
read -p "Enter your GitHub username: " GITHUB_USER
read -p "Enter your LinkedIn profile URL: " LINKEDIN_URL
read -p "Enter your email address: " EMAIL
read -p "Enter your blog URL: " BLOG_URL

FILES=("index.html" "blog.html" "blog-loader.html" "README.md" "LICENSE" "scripts/convert_blogs.py")

for file in "${FILES[@]}"; do
  sed -i.bak "s/PLACEHOLDER_NAME/$FULL_NAME/g" "$file"
  sed -i.bak "s/PLACEHOLDER_WEBSITE/$WEBSITE_URL/g" "$file"
  sed -i.bak "s/PLACEHOLDER_GITHUB/$GITHUB_USER/g" "$file"
  sed -i.bak "s/PLACEHOLDER_LINKEDIN/$LINKEDIN_URL/g" "$file"
  sed -i.bak "s/PLACEHOLDER_EMAIL/$EMAIL/g" "$file"
  sed -i.bak "s/PLACEHOLDER_BLOG/$BLOG_URL/g" "$file"
  rm "$file.bak"
done

echo "✅ Setup Complete!"

Note
The script uses sed (stream editor) to perform find-and-replace across files. sed -i.bak creates a backup file first, then the script deletes it after a successful replacement — this is a safety pattern in case sed fails partway through on certain systems.

Return behavior

Exit Condition Behavior
All prompts answered Prints success summary, exits 0
Empty input on a prompt Placeholder remains unset in that field — script does not validate for empty strings
File not found sed silently skips missing files without halting the script

Warning
setup.sh does not validate input format. Entering an invalid URL (missing https://, typos) will insert exactly what you typed. Double-check each generated file after running it.


scripts/convert_blogs.py reference

The core Python module that converts Markdown/HTML files in blog_uploads/ into structured JSON consumed by the front end.

Module-level constants

UPLOADS_DIR = "blog_uploads"
OUTPUT_DIR = "blogs"

Function: slugify(title)

Converts a post title into a URL-safe slug.

def slugify(title):
    return title.lower().strip().replace(" ", "-")
Parameter Type Description
title str The raw post title extracted from the <h1>

Returns: str — lowercase, space-to-hyphen converted string

Example:

slugify("My First Blog Post")
# → "my-first-blog-post"

Note
This basic implementation does not strip punctuation. A title like "What's New?" produces "what's-new?" — special characters pass through unmodified. If you need stricter slugs, extend this function with a regex substitution (see Extending the codebase).

Function: extract_toc(soup)

Scans parsed HTML for <h2>/<h3> tags, assigns each an id, and builds a Table of Contents list.

def extract_toc(soup):
    toc = []
    for heading in soup.find_all(["h2", "h3"]):
        text = heading.get_text()
        slug = slugify(text)
        heading['id'] = slug
        toc.append({"level": heading.name, "text": text, "slug": slug})
    return toc
Parameter Type Description
soup BeautifulSoup Parsed HTML tree of the post body

Returns: list[dict] — each dict contains level ("h2" or "h3"), text, and slug

Side effect: Mutates soup in place by adding an id attribute to each matched heading — required so TOC links (#slug) can jump to the correct element.

Function: process_markdown_file(filepath)

The main per-file processor — reads a Markdown file, converts it to HTML, and extracts all metadata needed for the JSON output.

def process_markdown_file(filepath):
    with open(filepath, "r", encoding="utf-8") as f:
        raw = f.read()

    html = markdown.markdown(raw, extensions=["extra", "toc"])
    soup = BeautifulSoup(html, "html.parser")

    title_tag = soup.find("h1")
    title = title_tag.get_text() if title_tag else "Untitled"
    if title_tag:
        title_tag.extract()

    toc = extract_toc(soup)
    excerpt = soup.find("p").get_text()[:160] if soup.find("p") else ""

    return {
        "title": title,
        "slug": slugify(title),
        "excerpt": excerpt,
        "content": str(soup),
        "toc": toc,
    }
Parameter Type Description
filepath str Full path to the .md file being processed

Returns: dict with keys title, slug, excerpt, content, toc

Key Type Description
title str Extracted from the first <h1>; defaults to "Untitled" if missing
slug str URL-safe version of the title, used as filename and route
excerpt str First 160 characters of the first paragraph — used in blog listing previews
content str Full rendered HTML body (with <h1> removed, heading ids injected)
toc list[dict] Table of Contents structure from extract_toc()

Warning
If a Markdown file has no # Heading, the title defaults to "Untitled" and the slug becomes "untitled" — a second such file will silently overwrite the first's JSON output. Always include exactly one # Title per post.

Function: build_all_posts()

Orchestrates the full batch process — iterates every file in blog_uploads/, processes it, writes individual JSON files, and rebuilds the master index.

def build_all_posts():
    index = []
    for filename in os.listdir(UPLOADS_DIR):
        if filename.endswith(".md"):
            filepath = os.path.join(UPLOADS_DIR, filename)
            post = process_markdown_file(filepath)

            out_path = os.path.join(OUTPUT_DIR, f"{post['slug']}.json")
            with open(out_path, "w", encoding="utf-8") as f:
                json.dump(post, f, ensure_ascii=False, indent=2)

            index.append({
                "title": post["title"],
                "slug": post["slug"],
                "excerpt": post["excerpt"]
            })

    with open(os.path.join(OUTPUT_DIR, "index.json"), "w", encoding="utf-8") as f:
        json.dump(index, f, ensure_ascii=False, indent=2)

Parameters: None (reads from module-level UPLOADS_DIR constant)

Returns: None — writes files as a side effect

Files written:

  • blogs/{slug}.json — one per post
  • blogs/index.json — master list used by the blog listing page

Entry point:

if __name__ == "__main__":
    build_all_posts()

This means running python3 scripts/convert_blogs.py directly triggers the full batch conversion — the same call GitHub Actions makes automatically on every push to blog_uploads/.


Blog JSON data structures

Individual post schema — blogs/{slug}.json

{
  "title": "My First Blog Post",
  "slug": "my-first-blog-post",
  "excerpt": "This is a test of the automated blog system.",
  "content": "<p>Full rendered HTML body...</p><h2 id=\"section-1\">Section 1</h2>...",
  "toc": [
    { "level": "h2", "text": "Section 1", "slug": "section-1" },
    { "level": "h2", "text": "Section 2", "slug": "section-2" }
  ]
}
Field Type Required Notes
title string Yes Rendered as page <h1> by blog-loader.html
slug string Yes Used in the URL: blog-loader.html?post=my-first-blog-post
excerpt string Yes Displayed on blog.html listing page
content string Yes Raw HTML — inserted via innerHTML, not sanitized
toc array No Empty array [] if the post has no ##/### headings

Warning
content is inserted directly via innerHTML with no sanitization step. Since you control all input files yourself, this is safe for personal use — but never accept blog post uploads from untrusted third parties without adding an HTML sanitization library first, as this would otherwise be an XSS vulnerability.

Index schema — blogs/index.json

[
  { "title": "My First Blog Post", "slug": "my-first-blog-post", "excerpt": "..." },
  { "title": "Second Post", "slug": "second-post", "excerpt": "..." }
]

A flat array, ordered by filesystem iteration order (typically alphabetical by filename — not guaranteed to be chronological). See Extending the codebase for adding date-based sorting.


blog-loader.html JavaScript reference

Function: loadBlogPost(slug)

Fetches a single post's JSON and renders it into the page.

async function loadBlogPost(slug) {
  const res = await fetch(`/blogs/${slug}.json`);
  const post = await res.json();

  document.title = `${post.title} - Blog`;
  document.querySelector('.post-title').textContent = post.title;
  document.querySelector('.post-content').innerHTML = post.content;

  renderToc(post.toc);
}
Parameter Type Description
slug string Extracted from the URL query string (?post=slug-name)

DOM dependencies: Requires elements with classes .post-title and .post-content present in blog-loader.html.

Failure mode: If slug doesn't match any existing JSON file, fetch() resolves with a 404 response, and res.json() throws — currently unhandled, resulting in a blank page. See Extending the codebase for adding error handling.

Function: renderToc(toc)

Renders the Table of Contents sidebar from the toc array.

function renderToc(toc) {
  const container = document.querySelector('.toc-list');
  container.innerHTML = toc.map(item =>
    `<li class="toc-${item.level}"><a href="#${item.slug}">${item.text}</a></li>`
  ).join('');
}
Parameter Type Description
toc array The toc field from the post JSON

DOM dependency: Requires an element with class .toc-list.

CSS hook: Generated <li> elements receive classes toc-h2 or toc-h3, allowing indentation styling to differentiate heading levels in the sidebar.


assets/js/nav-active.js reference

Automatically highlights the current page's link in the navigation bar.

const currentPath = window.location.pathname.split('/').pop() || 'index.html';
document.querySelectorAll('.nav-links a').forEach(link => {
  if (link.getAttribute('href') === currentPath) {
    link.classList.add('active');
  }
});
Variable Type Description
currentPath string Current page's filename, extracted from the URL path

Note
This matches on exact href string equality. A link written as href="index.html" will not match a URL path of / (root) — if using a custom domain where the homepage loads without a filename, extend this logic to also check for an empty string case (see Extending the codebase).


assets/js/theme-toggle.js reference

Handles dark/light mode switching with persistence via localStorage.

const toggleBtn = document.querySelector('#theme-toggle');
const body = document.body;

if (localStorage.getItem('theme') === 'dark') {
  body.classList.add('dark-mode');
}

toggleBtn.addEventListener('click', () => {
  body.classList.toggle('dark-mode');
  localStorage.setItem('theme', body.classList.contains('dark-mode') ? 'dark' : 'light');
});
localStorage Key Possible Values Description
theme "dark" | "light" Persisted user preference, read on every page load

DOM dependency: Requires a button element with id="theme-toggle".

CSS dependency: Requires a .dark-mode class defined on :root or body with corresponding variable overrides (see Customization Guide → Enable dark mode).


i18n loader reference

Function: loadLanguage(lang)

Fetches a language JSON file and applies translated strings to matching DOM elements.

async function loadLanguage(lang) {
  const res = await fetch(`i18n/${lang}.json`);
  const strings = await res.json();

  document.querySelectorAll('[data-i18n]').forEach(el => {
    const key = el.getAttribute('data-i18n');
    if (strings[key]) el.textContent = strings[key];
  });
}
Parameter Type Description
lang string Language code matching a filename in i18n/ (e.g., "en", "bn")

DOM dependency: Elements must have a data-i18n="key.name" attribute matching a key in the JSON file.

Failure mode: If a key in data-i18n doesn't exist in the loaded JSON, the element's textContent is left unchanged (falls back to whatever is in the HTML source by default) — this is a safe default, not a crash.

i18n file schema

{
  "nav.about": "About",
  "nav.projects": "Projects",
  "hero.title": "Hi, I'm",
  "hero.subtitle": "Full-Stack Developer"
}

Flat key-value pairs — no nesting supported in the default implementation.


GitHub Actions workflow reference

.github/workflows/blog_automation.yml

name: Blog Automation

on:
  push:
    paths:
      - 'blog_uploads/**'

jobs:
  convert:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install beautifulsoup4 markdown

      - name: Run conversion script
        run: python scripts/convert_blogs.py

      - name: Commit generated files
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add blogs/
          git diff --staged --quiet || git commit -m "auto: rebuild blog JSON"
          git push
Trigger Field Value Effect
on.push.paths blog_uploads/** Workflow only runs when files inside this folder change — pushes elsewhere in the repo don't trigger it
runs-on ubuntu-latest GitHub-hosted runner, no self-hosted infrastructure needed
python-version '3.11' Pinned version — update here if you need a newer/older Python for compatibility

Permissions note: This workflow pushes commits back to the repository. Ensure Settings → Actions → General → Workflow permissions is set to "Read and write permissions" for this to succeed.


File and folder reference

Quick-lookup table of every functional file in the project:

Path Type Role
index.html HTML Main homepage
blog.html HTML Blog post listing page
blog-loader.html HTML Individual blog post renderer
contact.html HTML Contact page
privacy-policy.html HTML Privacy policy template
404.html HTML Custom error page
setup.sh Shell script One-time personalization script
scripts/convert_blogs.py Python Blog conversion engine
assets/css/style.css CSS Main stylesheet, theme variables
assets/js/nav-active.js JavaScript Active nav link highlighting
assets/js/theme-toggle.js JavaScript Dark/light mode toggle
blog_uploads/ Folder Source Markdown/HTML posts (input)
blogs/ Folder Generated JSON posts (output — do not edit)
i18n/ Folder Language translation JSON files
.github/workflows/blog_automation.yml YAML CI/CD automation config
sitemap.xml XML SEO sitemap
robots.txt Text Search engine crawl rules
CNAME Text Custom domain config for GitHub Pages
LICENSE Text Project license terms

Extending the codebase

Common extension points referenced throughout this document:

Stricter slugification

import re

def slugify(title):
    slug = title.lower().strip()
    slug = re.sub(r'[^\w\s-]', '', slug)   # strip punctuation
    slug = re.sub(r'[\s_]+', '-', slug)    # collapse whitespace to hyphens
    return slug

Chronological post ordering

Add a date field to each post (either from front-matter or filename convention), then sort before writing index.json:

index.sort(key=lambda p: p.get("date", ""), reverse=True)

Error handling in loadBlogPost

async function loadBlogPost(slug) {
  try {
    const res = await fetch(`/blogs/${slug}.json`);
    if (!res.ok) throw new Error('Post not found');
    const post = await res.json();
    // ...render post
  } catch (err) {
    document.querySelector('.post-content').innerHTML =
      '<p>Sorry, this post could not be found.</p>';
  }
}

Root-path handling in nav-active.js

const currentPath = window.location.pathname.split('/').pop() || 'index.html';
// Add this line to also match root-path homepage loads:
const isHome = currentPath === '' || currentPath === 'index.html';

Frequently asked questions

Can I add front-matter (like date, tags) to Markdown posts?

Not by default — the current process_markdown_file() function only extracts the <h1> title and first paragraph. To support front-matter, add the python-frontmatter package and parse YAML metadata at the top of each .md file before passing the remaining body to markdown.markdown().

Why does `content` use raw HTML instead of Markdown in the JSON?

Converting to HTML once at build time (rather than shipping raw Markdown to the browser) avoids needing a Markdown parser library in the front end — blog-loader.html simply inserts pre-rendered HTML via innerHTML, keeping the client-side code minimal.

Is there a way to preview JSON output without running the full GitHub Action?

Yes — run python3 scripts/convert_blogs.py locally as covered in the Getting Started guide. This runs the exact same function GitHub Actions calls, just on your machine instead of their runner.

Can I call `build_all_posts()` from another Python script?

Yes — since it's a standard function (not wrapped in a class), you can import it:

from scripts.convert_blogs import build_all_posts
build_all_posts()

This is useful if you want to integrate blog conversion into a larger custom build pipeline.


You've now reached the end of the Portfolio Website documentation series. Here's the complete path for reference:

  1. Getting Started Guide — Fork, clone, install, run locally
  2. Deployment & Blog System Guide — Go live + publish posts
  3. Customization Guide — Make it visually yours
  4. **API & Script Reference* — Technical deep dive

Tip
Bookmark this reference page — you'll likely return to it any time you extend the blog system or debug an unexpected script behavior, long after initial setup is complete.

Further reading