BookStack icon indicating copy to clipboard operation
BookStack copied to clipboard

Add Schema Markup Functionality to Enhance SEO and Structured Data

Open bridgeyuwa opened this issue 1 year ago • 2 comments

Describe the feature you'd like

I would like to propose adding schema markup functionality to BookStack. This feature would allow users to embed schema.org structured data into their content, enhancing how search engines interpret and display the information. Users would have options to customize and manage schema markup directly through the BookStack admin panel.

Describe the benefits this would bring to existing BookStack users

Improved SEO: Schema markup can significantly improve search engine rankings by providing clearer content structure, leading to better visibility in search results. Enhanced User Experience: Rich snippets generated by schema markup make search results more engaging, often increasing click-through rates. Standardization: Implementing schema.org standards would ensure that BookStack content is compatible with various tools and services that rely on structured data.

Can the goal of this request already be achieved via other means?

Currently, the goal of implementing schema markup cannot be easily achieved through existing features in BookStack. While users could manually add schema markup within the HTML, this process is cumbersome and lacks the user-friendly customization and management that the proposed feature would provide.

Have you searched for an existing open/closed issue?

  • [X] I have searched for existing issues and none cover my fundamental request

How long have you been using BookStack?

Over 5 years

Additional context

For implementation, the feature could support common schema types like Books, Article, and FAQPage. Integrating this functionality with an intuitive interface in the admin panel would empower users to effectively manage their structured data.

Existing libraries or plugins that facilitate schema markup integration could be considered to streamline development. Additionally, BookStack’s documentation should be updated with guidelines and examples to help users effectively utilize this feature.

bridgeyuwa avatar Aug 22 '24 09:08 bridgeyuwa

Thanks for the request @bridgeyuwa,

Personally though I'm not too keen on supporting added complexity, and especially a UI & controls, for this, especially as most of the benefit is external to BookStack itself.

ssddanbrown avatar Oct 14 '24 14:10 ssddanbrown

@ssddanbrown.

I decided to start small with the implementation of Schema structured data by focusing on breadcrumbs first. To keep things simple and avoid any UI or config overhead, I implemented a minimal, view-level JSON-LD breadcrumb solution using the existing $crumbs variable. It automatically outputs valid schema.org/BreadcrumbList data for search engines, improving visibility for public BookStack instances. This approach keeps BookStack's internal structure untouched and passes Google's Rich Results validator without requiring extra dependencies.

<nav class="breadcrumbs text-center" aria-label="{{ trans('common.breadcrumb') }}">
    <?php $breadcrumbCount = 0; ?>

    {{-- Show top level books item --}}
    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Book)
        <a href="{{ url('/books') }}" class="text-book icon-list-item outline-hover">
            <span>@icon('books')</span>
            <span>{{ trans('entities.books') }}</span>
        </a>
        <?php $breadcrumbCount++; ?>
    @endif

    {{-- Show top level shelves item --}}
    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Bookshelf)
        <a href="{{ url('/shelves') }}" class="text-bookshelf icon-list-item outline-hover">
            <span>@icon('bookshelf')</span>
            <span>{{ trans('entities.shelves') }}</span>
        </a>
        <?php $breadcrumbCount++; ?>
    @endif

    @foreach ($crumbs as $key => $crumb)
        <?php $isEntity = $crumb instanceof \BookStack\Entities\Models\Entity; ?>

        @if (is_null($crumb))
            <?php continue; ?>
        @endif
        @if ($breadcrumbCount !== 0 && !$isEntity)
            <div class="separator">@icon('chevron-right')</div>
        @endif

        @if (is_string($crumb))
            <a href="{{ url($key) }}">
                {{ $crumb }}
            </a>
        @elseif (is_array($crumb))
            <a href="{{ url($key) }}" class="icon-list-item outline-hover">
                <span>@icon($crumb['icon'])</span>
                <span>{{ $crumb['text'] }}</span>
            </a>
        @elseif($isEntity && userCan('view', $crumb))
            @if ($breadcrumbCount > 0)
                @include('entities.breadcrumb-listing', ['entity' => $crumb])
            @endif
            <a href="{{ $crumb->getUrl() }}" class="text-{{ $crumb->getType() }} icon-list-item outline-hover">
                <span>@icon($crumb->getType())</span>
                <span>
                    {{ $crumb->getShortName() }}
                </span>
            </a>
        @endif
        <?php $breadcrumbCount++; ?>
    @endforeach
</nav>


{{-- JSON-LD breadcrumb structured data (schema.org implementation) --}}


@php
    // Build JSON-LD breadcrumb data
    $breadcrumbList = [];
    $position = 1;

    // Home breadcrumb
    $breadcrumbList[] = [
        '@type' => 'ListItem',
        'position' => $position,
        'name' => 'Home',
        'item' => url('/'),
    ];
    $position++;

    // Top level books item
    if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Book) {
        $breadcrumbList[] = [
            '@type' => 'ListItem',
            'position' => $position,
            'name' => trans('entities.books'),
            'item' => url('/books'),
        ];
        $position++;
    }

    // Top level shelves item
    if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Bookshelf) {
        $breadcrumbList[] = [
            '@type' => 'ListItem',
            'position' => $position,
            'name' => trans('entities.shelves'),
            'item' => url('/shelves'),
        ];
        $position++;
    }

    // Process crumbs
    foreach ($crumbs as $key => $crumb) {
        if (is_null($crumb)) {
            continue;
        }

        if (is_string($crumb)) {
            $breadcrumbList[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'name' => $crumb,
                'item' => url($key),
            ];
        } elseif (is_array($crumb)) {
            $breadcrumbList[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'name' => $crumb['text'],
                'item' => url($key),
            ];
        } elseif ($crumb instanceof \BookStack\Entities\Models\Entity && userCan('view', $crumb)) {
            $breadcrumbList[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'name' => $crumb->getShortName(),
                'item' => $crumb->getUrl(),
            ];
        }

        $position++;
    }
@endphp

@if (count($breadcrumbList) > 1)
    {{-- Only output if we have more than just home --}}
    <script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": @json($breadcrumbList)
}
</script>
@endif

This can be safely added to resources/views/entities/breadcrumbs.blade.php and automatically adapts to any entity type.

If this direction aligns with BookStack's simplicity goals, I'd be happy to prepare a small PR that includes this code.

bridgeyuwa avatar Oct 10 '25 16:10 bridgeyuwa