Wednesday, April 22, 2026

Enhancing Your Koha OPAC: A Modern Book Display side by side location

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 .xsl file 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
<?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.

No comments:

Post a Comment

Enhancing Your Koha OPAC: A Modern Book Display side by side location

Enhancing Your Koha OPAC: A Modern Book Display with Location Tracking The default search results in Koha are functional, but they don't...