Redirect opening a Book to the first page instead of the page/section index if it only contains one page?
Describe the feature you'd like
I use BookStack as a wiki implementation, and often only have a single page contained within a book.
It would be nice to offer the ability for the user to toggle if clicking on a book should automatically redirect the visitor to the first page, instead of showing an additional page index that is basically just a second click to get to where they wanted to go since the index is otherwise empty.
Additionally, perhaps it would be a nice feature to offer a "page redirect" automatically, so when opening a book the author can select the first page that should be displayed rather than the page index (if there are multiple pages)
Describe the benefits this would bring to existing BookStack users
More clarity and efficient browsing, I often get requests/questions from users why they have to go through 2 "layers" when accessing articles instead of being able to view the main page when clicking the book right away.
Can the goal of this request already be achieved via other means?
Not that I am aware of.
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?
1 to 5 years
Additional context
Example page of my bookstack integration, where I would like this to apply: https://wiki.runerealm.org/books/cape-of-accomplishment-perks
i was thinking of the same thing. Due to the fact that the editor of the shelve and the editor of the book are not that feature rich, the first item which makes sense is a page. But when creating a book with only one page it would be great to skip the book between, to shortcut the clicking
Relevant hacky optional JS based solution shared in this thread: https://www.reddit.com/r/BookStack/comments/14u65y3/book_with_only_one_page_automatic_opening/
indeed the problem with that code will be, that you cannot edit the book between until you deactivate the code again. This makes it unmaintainable regarding to the role based access. You do not want to make editors to admins which can change the site code
i think the problem should be resolved more generic: it would be great if we can put pages to a shelf without putting that page into a book first. In reality we have some unsorted document paper stacks too. I have shelves at my home where i put paper and later i am going to sort them into a folder, when i have time
meanwhile: added some delay to make it possible to do at least some changes clicking fast enough
<!-- Bücher mit nur einer Seite: Seite direkt anzeigen -->
<script type="module">
const isBookPage = document.querySelector('.book-content') !== null;
if (isBookPage) {
const contents = document.querySelectorAll('.book-contents .entity-list-item');
if (contents.length === 1 && contents[0].classList.contains('page')) {
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(3000);
window.location = contents[0].href;
}
}
</script>
btw. it would be also cool to skip the book, if the shelve only contains one book, and if the book contains only one page, to open that page, so kind of double redirect. but skipping might also skip information like images or description ... hm
I have this same use case. I need to make many, many single-page books, and don't want them to have any index or navigation. I wonder if this could be implemented as a special kind of book?
Here's a script with a spinner and cancel button:
<!-- Auto-redirect single-page books with responsive layout and proper spinner -->
<script type="module">
(async () => {
const isBookPage = document.querySelector('.book-content') !== null;
if (!isBookPage) return;
const contents = document.querySelectorAll('.book-contents .entity-list-item');
if (!(contents.length === 1 && contents[0].classList.contains('page'))) return;
// Spinner animation
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.redirect-overlay {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border: 1px solid #ccc;
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
padding: 16px 20px;
z-index: 10000;
font-family: sans-serif;
border-radius: 8px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-direction: row;
}
@media (max-width: 600px) {
.redirect-overlay {
flex-direction: column;
}
}
.redirect-spinner {
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
border: 3px solid #ccc;
border-top: 3px solid #333;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.redirect-message {
font-size: 14px;
}
.redirect-cancel {
background: #eee;
border: 1px solid #aaa;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
`;
document.head.appendChild(style);
// Elements
const overlay = document.createElement('div');
overlay.className = 'redirect-overlay';
const spinner = document.createElement('div');
spinner.className = 'redirect-spinner';
const message = document.createElement('div');
message.className = 'redirect-message';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.className = 'redirect-cancel';
overlay.appendChild(spinner);
overlay.appendChild(message);
overlay.appendChild(cancelBtn);
document.body.appendChild(overlay);
// Cancel logic
let canceled = false;
cancelBtn.addEventListener('click', () => {
canceled = true;
overlay.remove();
});
// Countdown
let remaining = 3;
message.textContent = `Single-page book, redirecting in ${remaining}...`;
while (remaining > 0) {
await new Promise(res => setTimeout(res, 1000));
if (canceled) return;
remaining--;
message.textContent = `Single-page book, redirecting in ${remaining}...`;
}
if (!canceled) {
window.location.href = contents[0].href;
}
})();
</script>