17. XML Practical Project: Book Catalogue
What you will build
A small book catalogue that uses everything in this course:
- An XML data file with books, attributes, and a namespace-free structure.
- An XML Schema that describes the rules.
- An XSLT stylesheet that turns the data into an HTML page.
- 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
idand acategoryof 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
DOMParserbuilds a document from the string, and the code checks forparsererror.- The XML is converted to an array of plain objects, with the year and price changed from text to numbers.
- The category list is built from the data, using
Setto remove duplicates. render()filters the array and rebuilds the list withcreateElementandtextContent, 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
subtitleelement, and update the schema withminOccurs="0". - Make the XSLT show books over 30 in bold using
xsl:iforxsl:choose. - Add a sort control to the viewer for title, year, or price.
- Load
books.xmlwithfetch()on a local server instead of using a string. - Write a PHP page that reads
books.xmlwithDOMXPathand 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.