Enhancing Your Koha OPAC: A Modern Book Display with Location Tracking
The default search results in Koha are functional, but they don't always offer the "at-a-glance" clarity that modern library patrons expect. By using XSLT (Extensible Stylesheet Language Transformations), we can turn standard MARC data into a clean, professional, and responsive display.
This guide will show you how to apply a custom stylesheet to Koha 25.11 that highlights book locations, call numbers, and online access buttons.
Key Features of This Customization
Two-Column Layout: Separates bibliographic info from real-time availability.
Visual Cues: Includes item type icons and clear "Online Access" buttons.
Direct Call Numbers: Shows exactly where the book is on the shelf without clicking through to details.
Responsive Design: Uses CSS Flexbox to ensure it looks great on both desktops and smartphones.
Step-By-Step Implementation
Step 1: Access the Koha Administration
Log in to your Koha Staff Interface and navigate to: Koha Administration > Global System Preferences > OPAC > Appearance.
Step 2: Locate the XSLT System Preference
Look for the preference named OPACXSLTResultsDisplay. This preference controls how search results are rendered.
Note: By default, Koha uses a built-in file. To use your custom code, you will need to host this
.xslfile on your server or paste the logic into a custom stylesheet field if your version supports it.
Step 3: Prepare the XSLT Code
Copy the code provided below. It is specifically designed to pull MARC tags:
245 a/b: Title and Subtitle
100/700: Author information
260/264: Publication details
856: Electronic links
942c: Item Type
Step 4: The Code
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:marc="http://www.loc.gov/MARC21/slim"
xmlns:items="http://www.koha-community.org/items"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:str="http://exslt.org/strings"
exclude-result-prefixes="marc items str" extension-element-prefixes="exsl">
<xsl:import href="MARC21slimUtils.xsl"/>
<xsl:output method = "html" indent="yes" omit-xml-declaration = "yes" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="marc:record">
<xsl:variable name="biblionumber" select="marc:datafield[@tag=999]/marc:subfield[@code='c']"/>
<div class="record-flex-wrapper" style="display: flex; flex-wrap: wrap; gap: 15px; padding: 10px 0; border-bottom: 2px solid #f0f0f0; align-items: flex-start;">
<div class="book-info-col" style="flex: 1 1 300px; display: flex; flex-direction: column; gap: 2px;">
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 4px;">
<xsl:variable name="itype" select="normalize-space(marc:datafield[@tag='942']/marc:subfield[@code='c'])"/>
<span style="font-size: 0.8em; background: #eef1f5; color: #444; padding: 1px 6px; border-radius: 4px; font-weight: bold; border: 1px solid #ccd1d9; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; margin-top: -3px;">
📘 <xsl:value-of select="$itype"/>
</span>
</div>
<h3 style="margin: 0; font-size: 1.2em; line-height: 1.3;">
<a href="/cgi-bin/koha/opac-detail.pl?biblionumber={$biblionumber}" style="text-decoration: none; color: #0056b3; font-weight: bold;">
<xsl:value-of select="marc:datafield[@tag=245]/marc:subfield[@code='a']"/>
<xsl:if test="marc:datafield[@tag=245]/marc:subfield[@code='b']">
<xsl:text> </xsl:text><xsl:value-of select="marc:datafield[@tag=245]/marc:subfield[@code='b']"/>
</xsl:if>
</a>
</h3>
<xsl:if test="marc:datafield[@tag=100] or marc:datafield[@tag=700]">
<div style="color: #333; font-size: 0.95em; font-weight: 500; margin-top: 2px;">
<span style="color: #777;">by </span>
<xsl:value-of select="marc:datafield[@tag=100 or @tag=700]/marc:subfield[@code='a']"/>
</div>
</xsl:if>
<xsl:variable name="publication" select="marc:datafield[@tag=260 or @tag=264][1]"/>
<xsl:if test="$publication">
<div style="color: #666; font-size: 0.85em; line-height: 1.4; margin-top: 2px;">
<span>
<xsl:if test="$publication/marc:subfield[@code='a']">
<xsl:value-of select="$publication/marc:subfield[@code='a']"/>
<xsl:text> </xsl:text>
</xsl:if>
<xsl:if test="$publication/marc:subfield[@code='b']">
<xsl:value-of select="$publication/marc:subfield[@code='b']"/>
<xsl:text>, </xsl:text>
</xsl:if>
<xsl:if test="$publication/marc:subfield[@code='c']">
<xsl:value-of select="$publication/marc:subfield[@code='c']"/>
</xsl:if>
</span>
</div>
</xsl:if>
<xsl:if test="marc:datafield[@tag=856]">
<div style="margin-top: 8px;">
<xsl:for-each select="marc:datafield[@tag=856]">
<xsl:variable name="url" select="normalize-space(marc:subfield[@code='u'])"/>
<a href="{$url}" target="_blank" style="display: inline-block; font-size: 0.75em; background: #28a745; color: #fff; padding: 4px 10px; border-radius: 4px; text-decoration: none; font-weight: bold;">
🌐 Online Access
</a>
</xsl:for-each>
</div>
</xsl:if>
</div>
<div class="availability-col" style="flex: 1 1 240px; max-width: 100%; background: #fdfdfd; border: 1px solid #e1e4e8; border-radius: 6px; padding: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.03);">
<xsl:variable name="available_items" select="items:items/items:item[not(items:onloan) and not(items:withdrawn)]"/>
<xsl:choose>
<xsl:when test="count($available_items) > 0">
<strong style="color: #22863a; display: block; margin-bottom: 6px; font-size: 0.75em; border-bottom: 1px solid #eee; padding-bottom: 3px; letter-spacing: 0.5px;">
📍 AVAILABLE AT:
</strong>
<xsl:for-each select="$available_items">
<div style="display: flex; justify-content: space-between; font-size: 0.85em; margin-bottom: 4px; padding-bottom: 3px; border-bottom: 1px dashed #f0f0f0; gap: 10px;">
<div style="font-weight: bold; color: #24292e; flex: 1;">
<xsl:value-of select="items:homebranch"/>
</div>
<div style="font-family: 'Courier New', monospace; color: #d73a49; font-weight: bold; white-space: nowrap;">
<xsl:value-of select="items:itemcallnumber"/>
</div>
</div>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<div style="color: #6a737d; font-style: italic; font-size: 0.8em; text-align: center; padding: 5px 0;">
No physical copies.
</div>
</xsl:otherwise>
</xsl:choose>
</div>
</div>
</xsl:template>
</xsl:stylesheet>
Troubleshooting Tips
Missing Icons: If the 📘 or 🌐 emojis don't show up, ensure your Koha database and web server are set to UTF-8 encoding.
Clear Cache: After saving the changes in System Preferences, clear your browser cache or try an Incognito window to see the new layout.
Availability Logic: The "Available At" section is set to hide items that are On Loan or Withdrawn. You can adjust the
not(items:onloan)logic if you want to show checked-out items as well.