17. XML Practical Project: Book Catalogue

What you will build

A small book catalogue that uses everything in this course:

  1. An XML data file with books, attributes, and a namespace-free structure.
  2. An XML Schema that describes the rules.
  3. An XSLT stylesheet that turns the data into an HTML page.
  4. A JavaScript viewer that reads the data, filters it, and shows it safely.

Create the files in a folder on your computer, for example catalogue/. To see the XSLT result in a browser, open the XML file through a local web server, such as your LocalWP site, because many browsers refuse to load the stylesheet from a plain folder.

The folder

Folder structure

catalogue/
  books.xml        (the data)
  books.xsd        (the rules)
  books.xsl        (turns data into HTML)
  viewer.html      (JavaScript viewer)

Step 1: The data

XML code: books.xml

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="books.xsl"?>
<catalogue>
  <book id="b1" category="web">
    <title>Learning XML</title>
    <author>Sam Rivera</author>
    <year>2024</year>
    <price currency="USD">12.00</price>
  </book>
  <book id="b2" category="web">
    <title>Learning PHP</title>
    <author>Ali Khan</author>
    <year>2025</year>
    <price currency="USD">35.50</price>
  </book>
  <book id="b3" category="design">
    <title>Learning CSS</title>
    <author>Mia Laurent</author>
    <year>2023</year>
    <price currency="USD">28.00</price>
  </book>
</catalogue>
  • The IDs and categories are attributes (metadata). The title, author, year, and price are elements (data).
  • The currency is an attribute of the price, as you learned in the Attributes lesson.
  • Check that the file is well-formed by opening it in a browser or editor.

Step 2: The rules (schema)

XSD code: books.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:simpleType name="categoryType">
    <xs:restriction base="xs:string">
      <xs:enumeration value="web"/>
      <xs:enumeration value="design"/>
      <xs:enumeration value="data"/>
    </xs:restriction>
  </xs:simpleType>

  <xs:simpleType name="yearType">
    <xs:restriction base="xs:integer">
      <xs:minInclusive value="1900"/>
      <xs:maxInclusive value="2100"/>
    </xs:restriction>
  </xs:simpleType>

  <xs:element name="catalogue">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="book" maxOccurs="unbounded">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="title" type="xs:string"/>
              <xs:element name="author" type="xs:string"/>
              <xs:element name="year" type="yearType"/>
              <xs:element name="price">
                <xs:complexType>
                  <xs:simpleContent>
                    <xs:extension base="xs:decimal">
                      <xs:attribute name="currency" type="xs:string" use="required"/>
                    </xs:extension>
                  </xs:simpleContent>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="id" type="xs:ID" use="required"/>
            <xs:attribute name="category" type="categoryType" use="required"/>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>

</xs:schema>

What the rules say

  • A catalogue holds one or more books.
  • Each book has a title, author, year (1900 to 2100), and a price with a required currency, in that order.
  • Each book needs a unique id and a category of web, design, or data.

To validate, run xmllint --noout --schema books.xsd books.xml if you have it, or use the PHP schemaValidate() snippet from the Schema lesson. Then break the file on purpose, for example by changing a category to cooking, and confirm the validator reports it.

Step 3: The page (XSLT)

XSLT code: books.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="html" encoding="UTF-8"/>

  <xsl:template match="/">
    <html lang="en">
      <head>
        <meta charset="UTF-8"/>
        <title>Book catalogue</title>
        <style>
          body { font-family: Arial, sans-serif; max-width: 640px; margin: 20px auto; color: #17345c; }
          table { border-collapse: collapse; width: 100%; }
          th, td { padding: 8px; border-bottom: 1px solid #cfe6ff; text-align: left; }
          th { background: #eaf5ff; }
        </style>
      </head>
      <body>
        <h1>Book catalogue</h1>
        <p>Total books: <xsl:value-of select="count(catalogue/book)"/></p>
        <table>
          <tr><th>Title</th><th>Author</th><th>Year</th><th>Price</th></tr>
          <xsl:for-each select="catalogue/book">
            <xsl:sort select="title"/>
            <tr>
              <td><xsl:value-of select="title"/></td>
              <td><xsl:value-of select="author"/></td>
              <td><xsl:value-of select="year"/></td>
              <td><xsl:value-of select="price"/> <xsl:value-of select="price/@currency"/></td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>

</xsl:stylesheet>

What the browser shows (an approximation)

Book catalogue

Total books: 3

Title Author Year Price
Learning CSS Mia Laurent 2023 28.00 USD
Learning PHP Ali Khan 2025 35.50 USD
Learning XML Sam Rivera 2024 12.00 USD

The books are sorted by title, and the count comes from an XPath function. Open books.xml through a web server to see it. Remember that browsers support XSLT version 1.0 only.

Step 4: The JavaScript viewer

This viewer parses the catalogue, lets the visitor filter by category, and shows the results with textContent. Here the XML is in a string, so the example runs in the editor. On a real page you would fetch books.xml instead.

Example: complete viewer

<style>
  body { font-family: system-ui, sans-serif; max-width: 520px; margin: 0 auto; padding: 12px; color: #17345c; }
  label { font-weight: 600; }
  select { padding: 6px; font: inherit; margin-left: 6px; }
  ul { list-style: none; padding: 0; }
  li { padding: 8px 0; border-bottom: 1px solid #cfe6ff; }
  .meta { color: #4f6b8f; font-size: 0.9rem; }
  :focus-visible { outline: 3px solid #0d6efd; outline-offset: 2px; }
</style>

<h2>Book catalogue</h2>

<p>
  <label for="cat">Category</label>
  <select id="cat">
    <option value="">All</option>
  </select>
</p>

<p id="status" role="status"></p>
<ul id="list"></ul>

<script>
  const xmlText = `<catalogue>
    <book id="b1" category="web"><title>Learning XML</title><author>Sam Rivera</author><year>2024</year><price currency="USD">12.00</price></book>
    <book id="b2" category="web"><title>Learning PHP</title><author>Ali Khan</author><year>2025</year><price currency="USD">35.50</price></book>
    <book id="b3" category="design"><title>Learning CSS</title><author>Mia Laurent</author><year>2023</year><price currency="USD">28.00</price></book>
  </catalogue>`;

  const doc = new DOMParser().parseFromString(xmlText, "application/xml");

  if (doc.getElementsByTagName("parsererror").length > 0) {
    document.getElementById("status").textContent = "The catalogue could not be read.";
  } else {
    const books = Array.from(doc.querySelectorAll("book")).map(function (b) {
      return {
        id: b.getAttribute("id"),
        category: b.getAttribute("category"),
        title: b.querySelector("title").textContent,
        author: b.querySelector("author").textContent,
        year: Number(b.querySelector("year").textContent),
        price: Number(b.querySelector("price").textContent),
        currency: b.querySelector("price").getAttribute("currency")
      };
    });

    const select = document.getElementById("cat");
    const list = document.getElementById("list");
    const status = document.getElementById("status");

    Array.from(new Set(books.map(function (b) { return b.category; }))).sort().forEach(function (c) {
      const option = document.createElement("option");
      option.value = c;
      option.textContent = c;
      select.appendChild(option);
    });

    function render() {
      const chosen = select.value;
      const shown = books.filter(function (b) { return chosen === "" || b.category === chosen; });

      list.textContent = "";
      shown.forEach(function (b) {
        const li = document.createElement("li");

        const strong = document.createElement("strong");
        strong.textContent = b.title;

        const meta = document.createElement("div");
        meta.className = "meta";
        meta.textContent = b.author + ", " + b.year + " | " + b.price.toFixed(2) + " " + b.currency;

        li.append(strong, meta);
        list.appendChild(li);
      });

      const total = shown.reduce(function (sum, b) { return sum + b.price; }, 0);
      status.textContent = shown.length + " book(s), total price " + total.toFixed(2);
    }

    select.addEventListener("change", render);
    render();
  }
</script>

What you see before choosing a category

3 book(s), total price 75.50

  • Learning XML
    Sam Rivera, 2024 | 12.00 USD
  • Learning PHP
    Ali Khan, 2025 | 35.50 USD
  • Learning CSS
    Mia Laurent, 2023 | 28.00 USD

Choosing design in the drop-down shows only Learning CSS, with a total of 28.00.

How the viewer works

  1. DOMParser builds a document from the string, and the code checks for parsererror.
  2. The XML is converted to an array of plain objects, with the year and price changed from text to numbers.
  3. The category list is built from the data, using Set to remove duplicates.
  4. render() filters the array and rebuilds the list with createElement and textContent, so nothing from the data can run as HTML.

Check the whole project

  • The XML is well-formed and valid against the schema.
  • The XSLT page shows three rows, sorted by title, with a correct total.
  • The viewer filters correctly, and typing tag-like text into a title shows it as plain text.
  • Everything works with the keyboard, and the filter has a visible label.

Challenges

  • Add a fourth book with a new category, and add that category to the schema.
  • Add an optional subtitle element, and update the schema with minOccurs="0".
  • Make the XSLT show books over 30 in bold using xsl:if or xsl:choose.
  • Add a sort control to the viewer for title, year, or price.
  • Load books.xml with fetch() on a local server instead of using a string.
  • Write a PHP page that reads books.xml with DOMXPath and prints the same table, with every value escaped.

Common mistakes

  • Opening the XML straight from a folder and seeing no XSLT result. Use a local web server.
  • A mismatch between the schema and the data, such as a different capital letter in a name.
  • Forgetting to convert numbers from text in JavaScript.
  • Putting XML text into the page with innerHTML.
  • Saving a file in an encoding that does not match the declaration.

Summary

  • You designed an XML data file, wrote a schema for it, and turned it into HTML with XSLT.
  • You also read it with JavaScript, filtered it, and showed it safely.
  • These are the same steps real systems use for feeds, sitemaps, and data exchange.
Scroll to Top