Honey Butter

Let’s Build a Website Like It’s 2002 (But Smarter) – Part 2: Hooking JSON Into PHP Templates

Categories

Welcome to Part 2 of the tutorial.

Fair warning: this chapter gets detailed and delightfully nerdy.

That said, I’m a right-brained creative at heart – and if I can learn this, anyone can. The goal here isn’t to turn you into a hardcore developer; it’s to help you feel confident, empowered, and in control of your own brand and digital space. This tutorial assumes a working knowledge of basic HTML and semantic markup, so we won’t spend time rehashing those fundamentals.

At this point, we’ve got a content.json file holding everything we want to display on the site. Now we connect the dots: we’ll use PHP to load that JSON, figure out which page we’re on, pull the correct data, and render it into HTML.

This is the moment where a “static site” starts feeling oddly alive.

A Quick Word on PHP

PHP is a server-side scripting language, which means it runs before anything reaches the browser. By the time a user sees HTML, PHP has already done its work and quietly exited the stage.

In this project, PHP isn’t pretending to be a framework, a CMS, or an “app.” It’s doing exactly what it’s good at: loading data, assembling templates, and getting out of the way.

If your last experience with PHP was around 2008, you might associate it with messy includes and questionable tutorials. And sure — people love to dunk on it. The irony is that PHP still powers WordPress and a massive chunk of the modern web, including plenty of sites built by developers who claim to hate it.

Used like this – clean, minimal, and purpose-driven – PHP is fast, boring, dependable, and exactly what you want sitting between your data and your HTML.

Parsing JSON Into HTML

Reusable PHP Functions

The first thing we’ll create is a helper file to store functions we’ll reuse throughout the site – often on every page.

In programming, it’s best practice to avoid repeating yourself. That’s where functions shine: you write them once, then reuse them everywhere. These helpers will live in a functions.php file (you can name it whatever you like), giving us one predictable place for shared logic.

We’ll start with a function that loads our JSON data and makes it available to the rest of the site. This work happens entirely on the server before the user ever sees anything.

Function: load_site_data()

The Full Function
function load_site_data($jsonPath) {
  $json = file_get_contents($jsonPath);
  if ($json === false) {
    die("Error: Unable to read JSON file at: " . htmlspecialchars($jsonPath));
  }

  $data = json_decode($json, true);
  if ($data === null) {
    die("Error: JSON could not be decoded. Please check your JSON syntax.");
  }

  return $data;
}
What This Function Does

This function has one job: load our site’s JSON file and convert it into usable PHP data. Everything else depends on this working correctly, so we keep it small, explicit, and defensive.

  • It reads the JSON file from disk
  • Stops immediately if the file can’t be read
  • Decodes the JSON into a PHP associative array
  • Stops if the JSON is invalid
  • Returns clean, usable data

From this point forward, $data becomes our single source of truth for the entire site.

Function: e()

function e($str) {
  return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
}

Even though we control our JSON, escaping output is still best practice. This function ensures that any HTML or JavaScript – whether added accidentally or later – can’t execute in the browser.

Use e() for:

  • Names
  • Titles
  • Labels
  • Plain text

Function: safe_rich_html()

function safe_rich_html(string $html): string {
  return strip_tags($html, '<p><br><strong><em><b><i><ul><ol><li><a>');
}

Sometimes e() is too strict.

For longer editorial content – paragraphs, lists, links – we want to allow limited HTML while still staying safe.

safe_rich_html() allows a small, approved set of tags and strips everything else.

Rule of thumb:

  • Use e() for plain text
  • Use safe_rich_html() only when you intentionally allow basic formatting

This keeps templates clean, content flexible, and your site secure.

Function: active_class()

function active_class($page) {
  $current = basename($_SERVER['PHP_SELF']);
  return ($current === $page) ? ' class="is-active"' : '';
}

This helper lets us highlight the current page in navigation so users know where they are.

It compares the current PHP filename to a filename you pass in:

  • If they match → returns class="is-active"
  • If they don’t → returns an empty string

It’s a small helper, but it saves you from repeating logic in every template.

Pulling it all together

Now that we’ve got our helper functions sorted out, we need to create our page templates.

Before we start, here’s a visual overview of how our pages will work:

Php page process

We know our pages are:

  • Landing
  • About
  • Gallery
  • Rates
  • FAQs
  • Wishlist
  • Contact & Thank You (Part 3)

Create a .php file for each page and copy to the main directory of your site.

At the top of every page template, add:

<?php
require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

Then add this variable and the name of the current template

$pageName = 'gallery';

Next, we’ll pull in our header.php (we’ll create this shortly)

include __DIR__ . '/includes/header.php';
?>

What’s happening here

  • Load our helper functions
  • Load the site JSON
  • Tell the header which page this is
  • Include the shared header

At the bottom of each page:

<?php include __DIR__ . '/includes/footer.php'; ?>

This ensures every page shares the same structure and layout.

Here’s what each page should look like (note: $pageName will change each page):

<?php

require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

$pageName = 'gallery';
include __DIR__ . '/includes/header.php';

?>

<!-- THIS IS WHERE OUR PAGE-SPECIFIC CODE WILL GO -->

<?php include __DIR__ . '/includes/footer.php'; ?>

header.php: The Shared Entry Point

The header.php file handles everything we only want to write once:

  • Document setup
  • Metadata
  • CSS loading
  • Navigation
  • Page headers
  • Featured images
<?php
$pageName = $pageName ?? '';
$data = $data ?? [];

$pageSection = (!empty($data[$pageName]) && is_array($data[$pageName])) ? $data[$pageName] : [];
$pageHeader  = $pageSection['header'] ?? '';

$featuredImage = $pageSection['featured_img'] ?? null;

$featuredSrc = '';
$featuredAlt = '';

if (is_string($featuredImage)) {
  $featuredSrc = $featuredImage;
} elseif (is_array($featuredImage)) {
  $featuredSrc = $featuredImage['src'] ?? '';
  $featuredAlt = $featuredImage['alt'] ?? '';
}
?>

<?php include __DIR__ . '/navigation.php'; ?>

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title><?php echo e($data['site_meta']['name'] ?? ''); ?></title>

  <?php if (!empty($data['css_files']) && is_array($data['css_files'])): ?>
    <?php foreach ($data['css_files'] as $css): ?>
      <?php
        $href = $css['url'] ?? '';
        if ($href === '') continue;

        $v = $data['site_meta']['version'] ?? null;
        if ($v) {
          $sep = (strpos($href, '?') !== false) ? '&' : '?';
          $href .= $sep . 'v=' . rawurlencode((string)$v);
        }
      ?>
      <link rel="stylesheet" href="<?php echo htmlspecialchars($href, ENT_QUOTES, 'UTF-8'); ?>">
    <?php endforeach; ?>
  <?php endif; ?>
</head>

<body class="page-<?php echo htmlspecialchars($pageName, ENT_QUOTES, 'UTF-8'); ?>">

<header class="header">
  <div class="header__container">
    <h1><a href="index.php"><?php echo e($data['site_meta']['name'] ?? ''); ?></a></h1>
    <?php
      render_nav($data, 'main_navigation', [
        'nav_class' => 'navigation',
        'aria_label' => 'Main',
        'li_class' => 'main-navigation__item',
        'a_class'  => 'main-navigation__link',
        'active_li_class' => 'navigation__item--is-active',
        'active_a_class'  => 'navigation__link--is-active',
      ]);
    ?>
  </div>
</header>

<?php if ($featuredSrc !== ''): ?>
  <div class="featured-img">
    <img
      src="<?php echo htmlspecialchars($featuredSrc, ENT_QUOTES, 'UTF-8'); ?>"
      alt="<?php echo htmlspecialchars($featuredAlt, ENT_QUOTES, 'UTF-8'); ?>"
    />
  </div>
<?php endif; ?>

<main class="content">
  <div class="content__container">
    <header class="page-header">
      <h1><?php echo htmlspecialchars($pageHeader, ENT_QUOTES, 'UTF-8'); ?></h1>
    </header>

By centralizing this logic, individual page templates stay lean and focused on content.

1) The “setup” block at the very top
<?php
$pageName = $pageName ?? '';
$data = $data ?? [];

$pageSection = (!empty($data[$pageName]) && is_array($data[$pageName]))
  ? $data[$pageName]
  : [];

$pageHeader = $pageSection['header'] ?? '';

$featuredImage = $pageSection['featured_img'] ?? null;

$featuredSrc = '';
$featuredAlt = '';

if (is_string($featuredImage)) {
  $featuredSrc = $featuredImage;
} elseif (is_array($featuredImage)) {
  $featuredSrc = $featuredImage['src'] ?? '';
  $featuredAlt = $featuredImage['alt'] ?? '';
}
?>
$pageName = $pageName ?? '';
  • This is a fallback.
  • It means: “If $pageName already exists, keep it. If it doesn’t exist, make it an empty string.”
  • This prevents PHP warnings like Undefined variable: pageName.
$data = $data ?? [];
  • Same idea: if $data isn’t set yet, default it to an empty array.
  • Your pages likely set $data earlier (decoded JSON), but this keeps things safe if a page forgets.
$pageSection = (...) ? ... : ...;

This is a ternary (a compact if/else).

It’s doing:

  1. Check if $data[$pageName] exists and is not empty
  2. Make sure it’s actually an array
  3. If both are true → use it as the page’s content section
  4. Otherwise → use an empty array

So this line is basically:
“Give me the JSON section for the current page, or give me an empty safe default.”

$pageHeader = $pageSection['header'] ?? '';
  • Pulls the header value out of this page’s JSON section.
  • If it doesn’t exist, fallback to empty string.
  • This becomes the <h1> you output later.

Featured image logic

$featuredImage = $pageSection['featured_img'] ?? null;

featured_img can be stored in JSON in two possible formats:

  1. a string (just a URL/path)
  2. an object/array like { "src": "...", "alt": "..." }

So you set up variables:

  • $featuredSrc (the actual image path)
  • $featuredAlt (alt text for accessibility)

Then:

if (is_string($featuredImage))
  • If featured_img is just a string, treat it as the source:
    • featured_img: "img/hero.jpg"
    • becomes $featuredSrc = "img/hero.jpg"
elseif (is_array($featuredImage))
  • If it’s the object version:
    • featured_img: { "src": "img/hero.jpg", "alt": "..." }
  • grab src and alt safely with fallbacks.

This is a nice pattern because it lets your JSON be flexible without breaking the template.

2) Include the navigation helper file
<?php include __DIR__ . '/navigation.php'; ?>
  • Pssst: we’ll create navigation.php after this section
  • include pulls another PHP file into this one.
  • __DIR__ is the directory this file is in (absolute-ish path on the server).
  • So no matter where this script is called from, it reliably loads navigation.php.

Why it’s here:
Because later you call render_nav(...), and that function probably lives in navigation.php.

3) HTML document + <title>
<!doctype html>
<html lang="en">
<head>
  ...
  <title><?php echo e($data['site_meta']['name'] ?? ''); ?></title>
e(...)

You’re using a helper function e() (not defined in this snippet, so it must be defined elsewhere -commonly in functions.php).

Typically e() is a convenience wrapper for escaping text, something like:

function e($str) {
  return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8');
}

So:

echo e($data['site_meta']['name'] ?? '');

Means:

  • grab site_meta.name from JSON
  • fallback to empty string
  • escape it so it’s safe for HTML output
  • print it
4) The CSS files loop (your main “loop” in this snippet)
<?php if (!empty($data['css_files']) && is_array($data['css_files'])): ?>
  <?php foreach ($data['css_files'] as $css): ?>
    <?php
      $href = $css['url'] ?? '';
      if ($href === '') continue;

      $v = $data['site_meta']['version'] ?? null;
      if ($v) {
        $sep = (strpos($href, '?') !== false) ? '&' : '?';
        $href .= $sep . 'v=' . rawurlencode((string)$v);
      }
    ?>
    <link rel="stylesheet" href="<?php echo htmlspecialchars($href, ENT_QUOTES, 'UTF-8'); ?>">
  <?php endforeach; ?>
<?php endif; ?>

The outer if (…)

Before looping, you check two things:

  • css_files exists and isn’t empty
  • it’s an array

This prevents errors and lets pages work even if there are no CSS files listed.

foreach ($data['css_files'] as $css)

This loops through each CSS entry in your JSON, for example:

 "css_files": [
    { "url": "css/main.css" }
  ],

Each time through the loop, $css is one of those items.

$href = $css['url'] ?? '';
  • Pull the URL from the current CSS object.
  • Fallback to empty string if missing.
if ($href === '') continue;
  • If there’s no URL, skip this loop iteration entirely.
  • continue jumps to the next item in the foreach.

Output the <link> tag

<link rel="stylesheet" href="<?php echo htmlspecialchars($href, ENT_QUOTES, 'UTF-8'); ?>">

Even though it’s “just CSS,” you still escape the href – good habit.

5) Body class based on page name
<body class="page-<?php echo htmlspecialchars($pageName, ENT_QUOTES, 'UTF-8'); ?>">

This gives you page-specific styling hooks like:

  • page-about
  • page-gallery
  • page-contact

So in CSS you can do:

.page-about .header { ... }
6) Header + site title + navigation render
<h1><a href="index.php"><?php echo e($data['site_meta']['name'] ?? ''); ?></a></h1>

<?php
render_nav($data, 'main_navigation', [
  'nav_class' => 'navigation',
  'aria_label' => 'Main',
  'li_class' => 'main-navigation__item',
  'a_class'  => 'main-navigation__link',
  'active_li_class' => 'navigation__item--is-active',
  'active_a_class'  => 'navigation__link--is-active',
]);
?>
render_nav(...) (function call)

This function is responsible for:

  • finding main_navigation inside your $data (JSON)
  • looping through the nav items
  • outputting the <nav>, <ul>, <li>, <a> markup
  • applying classes you pass in

The array you pass is basically a styling + accessibility configuration, so the nav generator is reusable across projects.

Also:

  • aria_label => 'Main' becomes something like aria-label="Main" for screen readers.
7) Featured image conditional output
<?php if ($featuredSrc !== ''): ?>
  <div class="featured-img">
    <img src="<?php echo htmlspecialchars($featuredSrc, ENT_QUOTES, 'UTF-8'); ?>"
         alt="<?php echo htmlspecialchars($featuredAlt, ENT_QUOTES, 'UTF-8'); ?>" />
  </div>
<?php endif; ?>
  • If there’s no featured image source, it outputs nothing.
  • If there is one, it prints the wrapper + image.
  • alt falls back to empty string if you didn’t provide it in JSON (valid, but better to include real alt text when the image is meaningful).
8) Main content wrapper + page header
<main class="content">
  <div class="content__container">
    <header class="page-header">
      <h1><?php echo htmlspecialchars($pageHeader, ENT_QUOTES, 'UTF-8'); ?></h1>
    </header>
  • Notice we don’t close the main tag here – we’ll cover that in the footer later.
  • This prints the page’s <h1> using $pageHeader pulled earlier from JSON.
  • Escaped with htmlspecialchars to prevent accidental HTML injection.

Building a Flexible, JSON-Driven Navigation (With Code)

Remember I mentioned the navigation.php we included earlier? Let’s dive into it.

This function turns navigation data from our JSON file into clean, accessible HTML. Instead of hard-coding menus into every template, we define navigation once and let PHP render it wherever we need it.

Here’s the full function we’ll be walking through:

<?php

function render_nav(array $data, string $key, array $opts = []): void
{
    if (empty($data[$key]) || !is_array($data[$key])) {
        return;
    }

    $navClass   = $opts['nav_class']   ?? 'navigation';
    $ariaLabel  = $opts['aria_label']  ?? 'Navigation';
    $ulClass    = $opts['ul_class']    ?? '';
    $liClass    = $opts['li_class']    ?? 'main-nav__item';
    $aClass     = $opts['a_class']     ?? 'main-nav__link';

    $activeLiClass = $opts['active_li_class'] ?? ($liClass . ' is-active');
    $activeAClass  = $opts['active_a_class']  ?? ($aClass  . ' is-active');

    $highlightCurrent = $opts['highlight_current'] ?? true;

    $currentPath = basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '');

    ?>
    <nav class="<?php echo htmlspecialchars($navClass, ENT_QUOTES, 'UTF-8'); ?>"
         aria-label="<?php echo htmlspecialchars($ariaLabel, ENT_QUOTES, 'UTF-8'); ?>">
      <ul<?php echo $ulClass ? ' class="' . htmlspecialchars($ulClass, ENT_QUOTES, 'UTF-8') . '"' : ''; ?>>
        <?php foreach ($data[$key] as $item):
            $label = $item['label'] ?? '';
            $url   = $item['url'] ?? '#';

            if ($label === '') continue;

            $isCurrent = false;
            if ($highlightCurrent && $currentPath !== '') {
                $itemPath = basename(parse_url($url, PHP_URL_PATH) ?? '');
                $isCurrent = ($itemPath !== '' && $currentPath === $itemPath);
            }

            $finalLiClass = $isCurrent ? $activeLiClass : $liClass;
            $finalAClass  = $isCurrent ? $activeAClass  : $aClass;
        ?>
          <li class="<?php echo htmlspecialchars($finalLiClass, ENT_QUOTES, 'UTF-8'); ?>">
            <a
              class="<?php echo htmlspecialchars($finalAClass, ENT_QUOTES, 'UTF-8'); ?>"
              href="<?php echo htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); ?>"
              <?php echo $isCurrent ? 'aria-current="page"' : ''; ?>
            >
              <?php echo htmlspecialchars($label, ENT_QUOTES, 'UTF-8'); ?>
            </a>
          </li>
        <?php endforeach; ?>
      </ul>
    </nav>
    <?php
}
Defining the function
function render_nav(array $data, string $key, array $opts = []): void

This function accepts three arguments:

  • $data
    The decoded JSON array that holds all site content.
  • $key
    The specific navigation group we want to render (for example, main_navigation).
  • $opts
    An optional configuration array for classes, labels, and behavior.

The function returns void because its purpose is to output HTML, not return data.

A quick safety check (guard clause)
if (empty($data[$key]) || !is_array($data[$key])) {
    return;
}

Before we do anything, we check that:

  • The navigation key exists
  • The value is actually an array

If either condition fails, the function exits early. This prevents PHP notices, broken markup, and “why is my nav empty?” moments.

This pattern is simple, defensive, and worth getting used to.

Setting sensible defaults
$navClass   = $opts['nav_class']   ?? 'navigation';
$ariaLabel  = $opts['aria_label']  ?? 'Navigation';
$ulClass    = $opts['ul_class']    ?? '';
$liClass    = $opts['li_class']    ?? 'main-nav__item';
$aClass     = $opts['a_class']     ?? 'main-nav__link';

Here we’re defining defaults for all our CSS classes and accessibility labels.

If a value exists in $opts, we use it. If not, we fall back to a clean default. This makes the function reusable across different navigations without changing the underlying logic.

Handling the active page state
$activeLiClass = $opts['active_li_class'] ?? ($liClass . ' is-active');
$activeAClass  = $opts['active_a_class']  ?? ($aClass  . ' is-active');

These define which classes are applied when a navigation item represents the current page.

By default, we just append is-active to the base class, but this can be overridden if your CSS system prefers something else.

Toggling current-page highlighting
$highlightCurrent = $opts['highlight_current'] ?? true;

Not every navigation needs an active state. This flag lets you turn that behavior on or off per navigation instance.

Figuring out the current page
$currentPath = basename(
    parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? ''
);

This line extracts the filename of the current page (for example, about.php) from the URL.

We use this later to compare against each navigation item’s URL and determine which link represents the current page.

Switching to HTML output
?>

At this point, we exit PHP and start writing actual markup. This keeps the HTML readable and avoids messy echo statements.

Rendering the <nav> element
<nav class="<?php echo htmlspecialchars($navClass, ENT_QUOTES, 'UTF-8'); ?>"
     aria-label="<?php echo htmlspecialchars($ariaLabel, ENT_QUOTES, 'UTF-8'); ?>">

We output a semantic <nav> element with:

  • A configurable class
  • An aria-label for screen readers

All dynamic values are escaped with htmlspecialchars for safety.

Rendering the list container
<ul<?php echo $ulClass
    ? ' class="' . htmlspecialchars($ulClass, ENT_QUOTES, 'UTF-8') . '"'
    : ''; ?>>

This conditionally adds a class to the <ul> only if one is provided. No empty attributes, no unnecessary clutter.

Looping through navigation items
<?php foreach ($data[$key] as $item):
    $label = $item['label'] ?? '';
    $url   = $item['url'] ?? '#';

    if ($label === '') continue;

We loop through each navigation item in the JSON array.

  • We safely extract the label and URL
  • If a label is missing, we skip the item entirely

This ensures we never render empty or broken links.

Detecting the current page
$isCurrent = false;

if ($highlightCurrent && $currentPath !== '') {
    $itemPath = basename(parse_url($url, PHP_URL_PATH) ?? '');
    $isCurrent = ($itemPath !== '' && $currentPath === $itemPath);
}

If current-page highlighting is enabled, we compare the filename of the nav item’s URL to the filename of the current page.

When they match, the item is marked as active.

Choosing final classes
$finalLiClass = $isCurrent ? $activeLiClass : $liClass;
$finalAClass  = $isCurrent ? $activeAClass  : $aClass;

These ternary expressions determine which classes get applied based on whether the item is active.

Rendering each navigation item
<li class="<?php echo htmlspecialchars($finalLiClass, ENT_QUOTES, 'UTF-8'); ?>">
  <a
    class="<?php echo htmlspecialchars($finalAClass, ENT_QUOTES, 'UTF-8'); ?>"
    href="<?php echo htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); ?>"
    <?php echo $isCurrent ? 'aria-current="page"' : ''; ?>
  >
    <?php echo htmlspecialchars($label, ENT_QUOTES, 'UTF-8'); ?>
  </a>
</li>
<?php endforeach; ?>

Each item outputs:

  • A list item with the correct class
  • A link with:
    • A safe URL
    • A readable label
    • aria-current="page" when active

That last attribute is small but important—it makes navigation much clearer for screen-reader users.

Closing things out
</ul>
</nav>
<?php
}

We close the list, close the navigation, and finish the function.

Why this pattern works so well

This navigation system is:

  • Fully JSON-driven
  • Reusable across templates
  • Accessible by default
  • Safe from common PHP pitfalls
  • Easy to restyle without touching logic

Once you build navigation this way, hard-coding menus starts to feel… uncivilized.

Page-Specific Templates

Every page follows the same pattern:

  1. Load helpers and JSON
  2. Set $pageName and include the header
  3. Pull only the data that page needs
  4. Loop and render
  5. Include the footer

This consistency is what makes static sites scale cleanly.

About Page

The full about.php code:

<?php

require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

$pageName = 'about';
include __DIR__ . '/includes/header.php';
?>

<?php 
  if (!empty($data['about'])): $about = $data['about'];
?>

  <?php if (!empty($about['sections']) && is_array($about['sections'])): ?>

      <?php foreach ($about['sections'] as $index => $section): 
        $text = $section['text'] ?? '';
        $img  = $section['img']  ?? null;

        // Optional alternating layout class
        $layoutClass = ($index % 2 === 1) ? 'section--reverse' : '';
      ?>

        <article class="section section--about <?php echo $layoutClass; ?>">
          <div class="hb-grid hb-grid--2 hb-grid__align--center hb-grid__gap--4">

          <?php if (!empty($text)): ?>
            <div class="section__text">
              <?php echo safe_rich_html($text); ?>
            </div>
          <?php endif; ?>

          <?php if (!empty($img['src'])): ?>
            <figure class="section__image">
              <img
                src="<?php echo htmlspecialchars($img['src'], ENT_QUOTES); ?>"
                alt="<?php echo htmlspecialchars($img['alt'] ?? '', ENT_QUOTES); ?>"
                loading="lazy"
              />
            </figure>
          <?php endif; ?>
          </div>
        </article>

      <?php endforeach; ?>
  <?php endif; ?>
<?php endif; ?>

  <?php
if (!empty($data['stats']) && is_array($data['stats'])):
  $stats    = $data['stats'];
  $statsImg = $stats['stats_img'] ?? null;
  $rows     = $stats['stats_values'] ?? [];
  $statsHdr = $stats['header'] ?? 'Stats';
?>

<?php if (!empty($rows) && is_array($rows)): ?>
  <section class="section section--stats">
    <div class="section__inner hb-grid hb-grid--2 hb-grid__align--center hb-grid__gap--4">
    
<div class="section__text">
        <h2><?php echo htmlspecialchars($statsHdr, ENT_QUOTES); ?></h2>

          <ul class="stats">
            <?php foreach ($rows as $row): ?>
              <?php
                $label = $row['label'] ?? '';
                $value = $row['value'] ?? '';
                if ($label === '' && $value === '') continue;
              ?>
              <li class="stat">
                <span class="stat__label"><em><?php echo htmlspecialchars($label, ENT_QUOTES); ?></em></span>
                <span class="screen-reader-only">: </span>
                <span class="stat__value"><?php echo htmlspecialchars($value, ENT_QUOTES); ?></span>
            </li>
            <?php endforeach; ?>
            </ul>
            </div>

      <?php if (is_array($statsImg) && !empty($statsImg['src'])): ?>
        <div class="section__image">
          <figure>
          <img
            src="<?php echo htmlspecialchars($statsImg['src'], ENT_QUOTES); ?>"
            alt="<?php echo htmlspecialchars($statsImg['alt'] ?? '', ENT_QUOTES); ?>"
            loading="lazy"
          />
        </figure>
      </div>
      <?php endif; ?>
    </div>
  </section>
<?php endif; ?>
<?php endif; ?>

<?php include __DIR__ . '/includes/footer.php'; ?>
1) Set a local variable
if (!empty($data['about'])): $about = $data['about'];

What’s happening

  • You only render the About content if it actually exists in the JSON.
  • $about = $data['about']; is just a convenience variable so you don’t keep typing $data['about'].
2) Render a list of “sections” from JSON (with alternating layouts)
if (!empty($about['sections']) && is_array($about['sections'])):
  foreach ($about['sections'] as $index => $section):

What’s happening

  • Your JSON is expected to have something like:
"about": {
  "sections": [
    { "text": "<p>...</p>", "img": { "src": "img/a.jpg", "alt": "..." } },
    { "text": "<p>...</p>", "img": { "src": "img/b.jpg", "alt": "..." } }
  ]
}
  • foreach loops through each section and prints an <article> block per item.

Alternating layout trick

$layoutClass = ($index % 2 === 1) ? 'section--reverse' : '';
  • % is the “modulo” operator.
  • If $index is odd (1, 3, 5…), you add a CSS class to flip the layout.
  • That makes every other section “reverse” without needing extra data in JSON.
foreach ($items as $i => $item) {
  $class = ($i % 2) ? 'reverse' : '';
  echo "<div class='$class'>...</div>";
}
5) Pull values safely from the JSON item (with fallbacks)
$text = $section['text'] ?? '';
$img  = $section['img']  ?? null;

What’s happening

  • ?? means: “use this value if it exists; otherwise use a default.”
  • This prevents “undefined index” warnings.
6) Only output markup if there’s content (avoid empty HTML)

Text block

if (!empty($text)):
  echo safe_rich_html($text);
endif;

What’s happening

  • If there’s no text, the <div class="section__text">...</div> isn’t printed at all.
  • safe_rich_html() implies: “allow some HTML, but sanitize it.”

What safe_rich_html() might do

function safe_rich_html(string $html): string {
  // allow a safe subset of tags
  return strip_tags($html, '<p><a><strong><em><ul><ol><li><br><h2><h3>');
}
7) Image rendering: check src, escape attributes, lazy-load
if (!empty($img['src'])):
  <img
    src="<?php echo htmlspecialchars($img['src'], ENT_QUOTES); ?>"
    alt="<?php echo htmlspecialchars($img['alt'] ?? '', ENT_QUOTES); ?>"
    loading="lazy"
  />
endif;

What’s happening

  • You only print the <img> if you have a real source.
  • htmlspecialchars(..., ENT_QUOTES) prevents broken HTML / injection by escaping quotes and special characters.
  • loading="lazy" tells the browser to defer loading until the image scrolls near view.
8) Stats section: separate block, separate data shape

This part is independent of About sections:

if (!empty($data['stats']) && is_array($data['stats'])):
  $stats    = $data['stats'];
  $statsImg = $stats['stats_img'] ?? null;
  $rows     = $stats['stats_values'] ?? [];
  $statsHdr = $stats['header'] ?? 'Stats';

What’s happening

  • You grab the “stats” data from JSON (different top-level node).
  • You pull:
    • a header/title ($statsHdr)
    • an optional image ($statsImg)
    • the list of stat rows ($rows)

JSON shape it expects

"stats": {
  "header": "Stats",
  "stats_img": { "src": "img/stats.jpg", "alt": "" },
  "stats_values": [
    { "label": "Age", "value": "28" },
    { "label": "Eyes", "value": "Brown" }
  ]
}
9) Loop the stat rows, skip empty rows, output list items
foreach ($rows as $row):
  $label = $row['label'] ?? '';
  $value = $row['value'] ?? '';
  if ($label === '' && $value === '') continue;

What’s happening

  • Each row becomes one <li>.
  • continue; means “skip this item and move to the next loop iteration.”
  • This is a nice cleanup technique if your JSON has blanks.
10) Accessibility detail: screen-reader-only separator
<span class="screen-reader-only">: </span>

What’s happening

  • Visually, you might style label/value so punctuation isn’t needed.
  • But for screen readers, including “label: value” can be clearer.

Gallery Page

This block of code is responsible for turning image data from our JSON file into an actual image gallery on the page.

The full gallery.php code

<?php

require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

$pageName = 'gallery';
include __DIR__ . '/includes/header.php';

$gallery = $data['gallery'];
?>

<section class="gallery">
  <div class="gallery__images">
    <?php foreach ($gallery['images'] as $img): ?>
      <figure class="gallery__image">
        <a href="<?php echo e($img['src']); ?>">
        <img src="<?php echo e($img['src']); ?>" alt="<?php echo e($img['alt']); ?>">
        </a>
      </figure>
    <?php endforeach; ?>
  </div>
</section>

<?php include __DIR__ . '/includes/footer.php'; ?>

Rather than hard-coding images into the markup, we let our content live in JSON and use PHP to loop through it. That keeps the gallery flexible, easy to update, and consistent with the rest of the site.

Let’s break it down.

1) Pull the gallery data
$gallery = $data['gallery'];

At this point in the template, $data already contains our decoded JSON file.

Here, we’re simply grabbing the gallery section from that data and storing it in a local variable called $gallery. This makes the code that follows easier to read and avoids repeatedly writing $data['gallery'].

2) Loop through images
<?php foreach ($gallery['images'] as $img): ?>

This foreach loop is where the magic happens.

  • $gallery['images'] is an array of image objects from the JSON file
  • Each $img contains data for a single image (like src and alt)
  • The loop runs once for each image and outputs the markup below

This is how one chunk of template code can render ten images—or a hundred—without changing anything.

3) Link the image
<figure class="gallery__image">
<a href="<?php echo e($img['src']); ?>">

Each image is wrapped in a figure tag and a link that points to the full-size image file.

This makes it easy to:

  • Open the image in a new tab
  • Hook into a lightbox later
  • Reuse the same markup for different interactions

The e() function escapes the value for safety before outputting it.

4) Output the image
<img src="<?php echo e($img['src']); ?>" alt="<?php echo e($img['alt']); ?>">

Here we output the actual <img> tag.

  • src comes from the image data in JSON
  • alt provides accessible, descriptive text for screen readers
  • Both values are escaped to prevent malformed markup or security issues

This ensures the gallery is both accessible and robust.

5) Close the HTML tags and the loop
 </a>
</figure>
<?php endforeach; ?>

This ends the foreach loop.

At this point, PHP has rendered one complete <figure> block for every image defined in the JSON file.

6) Wrap up the markup
</div>
</section>

We close the image wrapper and the gallery section.

The result is a clean, semantic gallery structure that’s:

  • Driven entirely by JSON
  • Easy to update without touching templates
  • Accessible by default
  • Ready for grids, animations, or lightboxes

Rates Page

1) Pull the Rates section out of the JSON
$rates = $data['rates'];
$ratesImg1 = $rates['rates_img_1'];
$ratesImg2 = $rates['rates_img_2'];
?>
  • $data is your decoded JSON array (coming from the header include).
  • $rates = $data['rates']; grabs the “rates” section into a smaller, more convenient variable (this is a readability win: you can write $rates['offerings'] instead of $data['rates']['offerings'] everywhere).
  • $ratesImg1 and $ratesImg2 grab image sub-arrays (likely shaped like ['src' => '...', 'alt' => '...']).
  • Then you close PHP so the template can output markup.
2) Output rich disclaimer text safely
<?php echo safe_rich_html($rates['disclaimer']); ?>
  • This prints a disclaimer from JSON.
  • safe_rich_html() is presumably your helper that allows a controlled subset of HTML (like <p>, <strong>, <em>, links, etc.) while stripping anything unsafe.
  • You use this instead of e() because e() would escape HTML and show tags as text.

Big idea: choose the escaping function based on what the content is:

  • Plain text → e()
  • Rich text content → safe_rich_html()
3) Loop through offerings to build the rates table
<?php foreach ($rates['offerings'] as $o): ?>
  • $rates['offerings'] is an array of offerings from JSON.
  • Each iteration gives you one offering as $o (probably something like duration, description, rate).

Inside that loop you output three key bits of data:

a) Duration

<?php echo e($o['duration']); ?>
  • e() is your escaping helper (likely wraps htmlspecialchars).
  • This prevents markup injection and keeps output safe.

b) Description

<?php echo e($o['description']); ?>
  • Same idea: print a plain-text description safely.

c) Rate + currency formatting

<?php echo number_format((float)$o['rate'], 0); ?>
<?php echo e($rates['currency']); ?>
  • (float)$o['rate'] forces the value into a number. This is a small guard against weird strings in JSON.
  • number_format(..., 0) formats it with commas and no decimals (e.g., 25002,500).
  • Currency (like USD or $) is printed from $rates['currency'].

Big idea: your JSON stays “raw,” but the template decides how it should be formatted for humans.

4) Conditionally show image #1 only if it exists
<?php if (!empty($ratesImg1['src'])): ?>
  • This prevents broken images and empty markup.
  • !empty(...) checks that src exists and is not blank.

Then the image is output safely:

src="<?php echo htmlspecialchars($ratesImg1['src'], ENT_QUOTES); ?>"
alt="<?php echo htmlspecialchars($ratesImg1['alt'] ?? '', ENT_QUOTES); ?>"
  • You’re escaping attributes using htmlspecialchars.
  • alt uses ?? '' so if there’s no alt text, it won’t throw a notice.
  • loading="lazy" tells the browser not to load it until needed (performance win).
5) Render “Details” only if details exist
<?php if (!empty($rates['rates_details'])): ?>
  • Checks whether the JSON includes a rates_details list.
  • If it’s empty or missing, the entire section is skipped—no blank <ul>.

Then:

<?php foreach ($rates['rates_details'] as $detail): ?>
  <li><?php echo e($detail['item']); ?></li>
<?php endforeach; ?>
  • Loops the array of details.
  • Prints each detail’s item field safely.
6) Output image #2 (no conditional guard)
src="<?php echo htmlspecialchars($ratesImg2['src'], ENT_QUOTES); ?>"
alt="<?php echo htmlspecialchars($ratesImg2['alt'] ?? '', ENT_QUOTES); ?>"
  • Same safe attribute escaping as image #1.
  • Notice: there is no if (!empty($ratesImg2['src'])) check here.
    • That’s fine if your JSON guarantees this image always exists.
    • If it’s optional, you’d want the same guard you used for image #1 to avoid broken images.

FAQ Page

The full faqs.php code:

<?php

require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

$pageName = 'faqs';
include __DIR__ . '/includes/header.php';

$faqs = $data['faqs'];
$faqImg = $faqs['faqs_img'];
?>

<section class="section section--faqs">

    <div class="hb-grid hb-grid--2 hb-grid__align--center hb-grid__gap--4">

        <div class="section__text">
            <div class="qas hb-accordion">
                <?php foreach ($faqs['items'] as $item): ?>
                <div class="qa">
                    <h3 class="q hb-accordion__header"><span
                            class="hb-accordion__header-text"><?php echo e($item['question']); ?></span></h3>
                    <div class="a hb-accordion__panel"><?php echo safe_rich_html($item['answer']); ?></div>
                </div>
                <?php endforeach; ?>
            </div>
        </div>

        <div class="section__image">
            <figure>
                <img src="<?php echo e($faqImg['src']); ?>" alt="<?php echo e($faqImg['alt']); ?>">
            </figure>
        </div>
    </div>

</section>

<?php include __DIR__ . '/includes/footer.php'; ?>

Template for the FAQs page – its job is to:

  • Load site content from a JSON file
  • Pull the FAQ data out of the JSON
  • Loop through questions and answers
1. Pull the FAQ data from JSON
$faqs = $data['faqs'];
$faqImg = $faqs['faqs_img'];
  • $faqs holds the entire FAQ section from the JSON file.
  • $faqImg pulls out a related image object (usually src and alt).
  • This keeps the rest of the template readable and avoids deeply nested array access.
2. Loop through FAQ items
<?php foreach ($faqs['items'] as $item): ?>
  • items is an array of FAQ entries from JSON.
  • Each $item represents one question-and-answer pair.

Inside the loop:

<?php echo e($item['question']); ?>
  • The question is output as plain text.
  • e() safely escapes it for HTML.
<?php echo safe_rich_html($item['answer']); ?>
  • The answer is output as rich text.
  • safe_rich_html() allows trusted HTML like paragraphs, links, or emphasis while stripping unsafe markup
3. Render the associated FAQ image
<img src="<?php echo e($faqImg['src']); ?>" alt="<?php echo e($faqImg['alt']); ?>">
  • The image source and alt text come from JSON.
  • Both values are escaped for safety.
  • This keeps media content just as editable as text.

Wishlist Page

The full wishlist.php code:

<?php

require __DIR__ . '/includes/functions.php';
$data = load_site_data(__DIR__ . '/data/content.json');

$pageName = 'wishlist';
include __DIR__ . '/includes/header.php';

$wishlist = $data['wishlist'] ?? null;
$wishlistImg = $wishlist['wishlist_img'] ?? null;
$wishlistIntro = $wishlist['intro_text'] ?? null;
$items = $wishlist['items'] ?? null;

?>
<section class="section section--wishlist">

<div class="hb-grid hb-grid--12 section--wishlist-intro">
    <div class="hb-grid__col--3-11"><?php echo safe_rich_html($wishlistIntro); ?></div>
</div>

      <div class="wishlist__items hb-grid hb-grid--4">
        <?php foreach ($items as $item): ?>
          <div class="wishlist__item">
            <?php if (!empty($item['purchase_link'])): ?>
            <a href="<?php echo e($item['purchase_link']); ?>" target="_blank" rel="noopener">
            <?php endif; ?>
            <figure class="wishlist__item-thumbnail">
              <img src="<?php echo e($item['thumbnail'] ?? ''); ?>" alt="">
            </figure>
 <h3 class="wishlist__item-label">
              <?php echo e($item['title'] ?? ''); ?>
            </h3>

            <div class="wishlist__item-description">
              <?php echo e($item['description'] ?? ''); ?>
            </div>
           
            <?php if (!empty($item['purchase_link'])): ?>
            </a>
            <?php endif; ?>
          </div>
        <?php endforeach; ?>
      </div>
   
</section>

<?php include __DIR__ . '/includes/footer.php'; ?>

Template for the wishlist.php – its job is to:

  • Pull just the wishlist-related data out of that content
  • Loop through wishlist items and render them safely
  • Hand control back to shared layout files (header and footer)
1. Safely extract wishlist data
$wishlist = $data['wishlist'] ?? null;
$wishlistImg = $wishlist['wishlist_img'] ?? null;
$wishlistIntro = $wishlist['intro_text'] ?? null;
$items = $wishlist['items'] ?? null;

Here we pull out only the data this page cares about.

The ?? null operator ensures that:

  • If a key is missing from the JSON
  • The page won’t throw PHP notices or fatal errors

This makes the template resilient—it can fail gracefully if content is incomplete.

Each variable represents a different slice of the wishlist data:

  • $wishlist → the full wishlist section
  • $wishlistIntro → intro copy shown at the top
  • $items → the array of wishlist items
  • $wishlistImg → associated imagery (even if it’s not used later)
2. Output rich intro text
<?php echo safe_rich_html($wishlistIntro); ?>
  • The intro text is rendered using safe_rich_html()
  • This allows formatted content (paragraphs, links, emphasis)
  • While still stripping anything unsafe

This is ideal for editorial-style content that needs light formatting.

3. Loop through wishlist items
<?php foreach ($items as $item): ?>

This loop iterates over each wishlist item defined in JSON.

Each $item is expected to contain things like:

  • title
  • description
  • thumbnail
  • purchase_link

Because everything is data-driven, adding or removing wishlist items is as simple as editing the JSON.

4. Conditionally wrap items in a link
<?php if (!empty($item['purchase_link'])): ?>
<a href="<?php echo e($item['purchase_link']); ?>" target="_blank" rel="noopener">
<?php endif; ?>
  • If a wishlist item includes a purchase link:
    • The entire item becomes clickable
  • If it doesn’t:
    • The markup is rendered without a link wrapper

This pattern avoids broken or empty anchors and allows items to be informational or shoppable.

The rel="noopener" attribute is a small but important security best practice when opening links in a new tab.

5. Render item content safely

Throughout each wishlist item, values are printed like this:

<?php echo e($item['title'] ?? ''); ?>
<?php echo e($item['description'] ?? ''); ?>
  • e() escapes the content for safe output
  • ?? '' prevents errors if a field is missing
  • This ensures incomplete data never breaks the page

Even the thumbnail source is guarded:

<?php echo e($item['thumbnail'] ?? ''); ?>

If an image is missing, the markup still renders without crashing.

Final Recap

Nice work – this was a big chapter. You’ve officially crossed the line from “static” to structured. What probably felt overwhelming at the start has turned into a working, bare-bones website you can actually build on. It should look something like this – raw, unstyled html but functioning.

In this tutorial, we:

  • Created reusable PHP helpers
  • Loaded and validated JSON safely
  • Centralized layout logic
  • Rendered navigation from structured data
  • Built page templates the parse our JSON into readable HTML

In Part 3, we’ll wire up the contact form and start handling user input.