aaaaaaa

What’s The Best Way to Format Chapter Books on the Web?

There are lots of places to read classic literature online such as Project Gutenberg, but I wanted to create an improved online reading experience.

I’ve been working on Story Res for a while now and the design is in a pretty good place. However, I’ve been debating between two different approaches for the “Long Format” chapter books.

The two competing options are

  1. Long Page with Chapter Navigation (current version)
  2. Expandable Chapters (Proposed version)

Current Approach
(Long Page with Chapter Navigation)

Right now, I have a right-hand chapter navigation that allow users to quickly navigate to a specific chapter.

screenshot of the Peter Pan page in its current form.

You can see a video of it in action below…

Features Include

  1. Sticky chapter header to display the book’s title and current chapter and you scroll down the page.
  2. Bookmark buttons help you keep track of your position
  3. Chapter Navigation on right-side so you can easily skip to any chapter.

Skip to: How I made These Bookmark Shapes with CSS

Proposed Approach – Expandable Chapters

While the current approach has some clever UI features, I do have a few concerns.

  1. Super long pages can be overwhelming and difficult to manage.
  2. The UI in unique and some people might not realize how to use it.
  3. Saving your place on such a long page is surprising difficult.

This is why I’m starting to think it might be better simply use the <details> tag to make collapsible chapters as shown below…

Peter Pan with Collaspable chapter.

This will make the initial page much shorter and more manageable. Collapsible sections are common UI feature, and will be familiar to most people. It also allows people to browse the chapters without having to scroll through such long pages.

Documenting My Existing Code

The real reason I’m writing this post is to document my existing code.

This new direction means cutting a lot of what I’ve already created. A lot of problem solving when into the current version and I think some of it may be valuable to other people (or myself).

I’ve archived some of the pages Wayback Machine but that won’t help me understand the back-end PHP code I used, so I’m documenting it here.

The PHP Code

The website uses Advanced Custom Fields with a chapter repeaters. This allows me to organize stories more efficiently and include on-page navigation.

Here is the entire code for Long-format stories…

<?php if(get_field('story_type') == "long_form"): ?>
<main class="long-story">
 

<!-- Right-hand Chapter Navigation Bar -->

<?php if( have_rows('chapter_repeater') ): ?>
	<nav class="chapter-nav section-nav">
	<ul>
		<?php while( have_rows('chapter_repeater') ): the_row(); 
		$chapterNumber = get_sub_field('chapter_number'); 
		$chapterClean = sanitize_text_field( $chapterNumber );
		?>
 			<li><a href="#<?php echo str_replace(' ', '', $chapterNumber ); ?>" title="Go to Chapter <?php echo $chapterClean; ?>" ><span><?php echo sanitize_text_field( $chapterNumber ) ?></span></a></li>
		<?php endwhile; ?>
		<li><a href="#storyEnd">End</a></li>
	</ul>
	</nav>
<?php endif; ?>

<!-- Story Header -->	

	<header class="story-header long-story-header">
		<?php if( has_post_thumbnail() ): ?>
			<div class="featured-image" style="background-image:url(<?php the_post_thumbnail_url('full'); ?>);  "></div> 
		<?php endif; ?>
		<h1><?php the_title(); ?></h1>
		<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
			<div class="pre-story-content"><?php the_content(); ?></div>
		<?php endwhile; endif; ?>
	</header>

	<article>

<!-- A floating Line Pointer I probably won't use -->

		<div role="button" title="Add Bookmark" id="pointer" class="line-pointer"></div>
		 
<!-- The Actual Chapters -->

		<?php if( have_rows('chapter_repeater') ): ?>

		<?php while( have_rows('chapter_repeater') ): the_row(); 
			$chapterNumber = get_sub_field('chapter_number');
			$totalChapters = count( get_field( 'chapter_repeater' ) );
			$chapterTitle = get_sub_field('chapter_title');
			$chapterText = get_sub_field('chapter_text');
		?>
		<section id="<?php echo str_replace(' ', '', $chapterNumber ); ?>" class="chapter">

<!-- Stick Chapter Header -->

			<div class="chapter-sticky-header" id="bookmark<?php echo $chapterNumber; ?>"> 
				<div class="bookmark-feedback"></div>
				<span class="currentBook"><?php the_title(); ?></span> 
				<?php if($chapterNumber):?>
					<span class="currentChapter" role="button" onclick="toggleChapterNav()">Chapter <?php echo $chapterNumber; ?> &rsaquo;</span> 
				<?php endif; ?>
			</div>
			
			<header class="chapter-header">

<!-- Bookmark buttons -->
				<div class="bookmark-btn" >
					<a class="unmarked" href="#<?php echo $chapterNumber; ?>"><span>Bookmark Here? &uarr;</span>  </a>
					<a class="marked" href="#bookmark<?php echo $chapterNumber; ?>"><span >Bookmarked &check;</span> </a>
				</div>
				<h2><?php echo $chapterNumber; ?></h2>
 				<h3> <?php echo $chapterTitle; ?></h3>
			</header>
<!-- Chapter Content -->
 			<?php echo $chapterText; ?>
		</section>

				<?php endwhile; ?>
			<?php endif; ?>
		<div id="storyEnd"></div>
	</article>
	
<!-- Reading Progress Bar (% of page scrolled) -->

	<footer id="storyFooter"> <span id="progress-percent">0%</span> read
			  | Chapter <span id="displayCurrentChapter">1</span>/<?php echo $totalChapters; ?>  
	</footer>

</main>

<!-- Various Javascripts -->

<script>
// Calculate Page Scroll
window.addEventListener('scroll', setScrollValue, false);
window.addEventListener('resize', setScrollValue, false);
function setScrollValue() {  
  var percent = window.pageYOffset / (document.body.offsetHeight - window.innerHeight);
  //document.getElementById('progress-percent').innerHTML(Math.round(percent * 100, 1)+"%");  
	document.getElementById('progress-percent').innerHTML = (Math.round(percent * 1000) / 10).toFixed(1) +"%";
  document.body.style.setProperty('--scroll', percent);   
}
</script>


<script>
// Chapter Navigation Collapse and Expand (when you click "chapter" in sticky header)
function toggleChapterNav() {
	console.log('chapter nav toggled');
	document.querySelector('.long-story').classList.toggle('chapter-nav-closed');
}
</script>


<script>
// Highlight Current Chapter
window.addEventListener('DOMContentLoaded', () => {
	
	let observer = new IntersectionObserver((entries) => {
		entries.forEach(entry => {
			var id = entry.target.getAttribute('id');
			console.log(id + ': '+ entry.isIntersecting);
			document.getElementById("displayCurrentChapter").innerHTML=id
			if (entry.isIntersecting) {
				document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.add('active');
			} else {
				document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.remove('active');
			}
		}, {rootMargin: "200px 0px -10px 0px"} );
	});

	// Track all sections that have an `id` applied
	document.querySelectorAll('.long-story section[id]').forEach((section) => {
		observer.observe(section);
	});
	
});
</script>


<?php endif; ?>

As you can see, there is a good amount of javascript as well as PHP and HTML in there.

The Full CSS Code

One that that isn’t included in the code above is the CSS code. There’s some especially fun code that uses clip-path to make a notched bookmark shapes. There’s a good chance I won’t be using that anymore which is why I want to document it here.

/*:::::::::::::::::::::::::::::::::: Longform Stories :::::::::::::::::::::::::::::::::::::::::::*/


.long-story {
line-height:1.55; 
--text-width:32rem;

--chapter-nav-width:38px;
--text-padding:38px;
}
.long-story.chapter-nav-closed {
	--chapter-nav-width:1px;
}

.long-story article {
box-shadow:0 2px 4px rgba(0,0,0,0.1);
padding:0;
padding-bottom:24px;
padding-left:var(--text-padding);
padding-right: calc( var(--text-padding) );
position:relative;
}


.long-story-header {
background: url(/wp-content/uploads/2021/07/paper4.png); 
background-color: var(--main-bg, white);
z-index:1;
position:relative;
}
.long-story .pre-story-content:empty { display:none; }

.long-story .pre-story-content {
    padding: 0.5em;
    background: rgba(10,150,220,0.12);
    display: flex;
    flex-direction: column;
	border-top:var(--border);
}
.long-story .pre-story-content p {
	text-indent:0;
	font-size:16px;
	font-size:clamp(14px , 1.2vw, 16px);
	margin:0.4em 0;
	line-height: 1.35;
	max-width: none;
}



.long_form p, .long_form pre , .long_form blockquote{ 
max-width:32rem; 
margin:.5rem auto;
}


.long_form p{
text-indent: 1.5em;
}

.long_form h2, .long_form h3 {
max-width:60rem; 
margin:.8rem auto;
text-align:center;
}

.long_form pre {
font:inherit;
	font-family: 'PT Mono', monospace;
	font-family: 'Nanum Gothic Coding', monospace;
	white-space: pre-wrap;
 }

.chapter {
    margin: 40px 0 130px;
}
.chapter-header { //padding-top:var(--offset); }

.chapter:first-of-type { //margin-top:0;  }
.chapter:first-of-type .chapter-header {padding-top:0; }


.chapter-sticky-header {
	position:sticky;
	top: var(--offset) ;
	background:#fffc;
	font-size:12px;
	padding:1px 2px; 
	margin:0 calc( var(--text-padding) * -1);
	margin:0;
	border-top: solid 1px #8888;
	display: flex;
	justify-content: space-between;
	align-items: center;
	display: grid;
	grid-template-columns: 1fr auto;
	gap:8px;
	background:url(https://www.storyres.com/wp-content/uploads/2021/07/paper4.png) var(--main-bg, white) ;
	border-bottom:solid 1px #8888;
	z-index:1;
}
.chapter-sticky-header:after { 
	content:""; 
	height: 12px; 
	width:100%; 
	background:#0004; 
	filter:blur(4px);
	position: absolute; 
	top:calc(100% - 6px); 
	border-radius:50%; 
	clip-path:polygon( 0% 50% , 100% 50% , 100% 200% , 0% 200% );
}

.chapter-sticky-header span { white-space:nowrap; overflow:hidden; text-overflow: ellipsis; display:inline-block; }
.chapter-sticky-header span[role=button] { border:solid 1px #aaa8; padding: 6px 4px; border-radius: 4px; cursor:pointer; }
.currentChapter { }
.currentBook {  padding-left:1px; font-style:italic; }

#storyEnd {
	//border:dotted thin red;
	position:absolute;
	bottom:0;
	height:60vh;
	width:8px;
}

#storyFooter {
	background:#fffe;
	position:sticky;
	bottom:0;
	font-size:13px;
	padding:1px 4px;
}
#storyFooter:before {
 content:"";
	--percent:calc( var(--scroll) * 100% );
	position: absolute;
	top:0;
	left:0; right:0;
	height:3px;
	background:linear-gradient( to right, #69cde8 var(--percent), #f5f5f5 var(--percent));
}

.details-chap summary{
	border:solid 1px #abc;
	padding: 24px;
	margin:12px 0;
	border-radius: 4px;
}


/*-- Chapter Navigation --*/
.chapter-nav {
display:grid;
flex-wrap:wrap;
flex-direction: column;
position:sticky; 
top:var(--offset);
height: calc(100vh - var(--offset) - 21px );
float:right;
overflow:auto;
background: var(--main-bg); 
scrollbar-width: thin;
width:var(--chapter-nav-width);
transition:.2s width cubic-bezier(.17,.84,.44,1);
z-index:2;
}
.chapter-nav::-webkit-scrollbar { width:8px; }
.chapter-nav::-webkit-scrollbar-track  { background:#0001; box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3);}
.chapter-nav::-webkit-scrollbar-thumb { background:#579; background:linear-gradient(90deg, #579 , #68a, #579); border-radius:4px; }

.chapter-nav ul {margin:0; padding:0; margin-bottom:64px; list-style:none; flex:1 1 auto; display:flex; flex-direction:column;  }
.chapter-nav li { 
	display:flex; flex:1 1 auto; 
	color: var(--text-color); 
	background: var(--main-bg); 
}

.chapter-nav a {
display: flex;
justify-content: center;
align-items: center;
flex:1 1 auto;
font-size:12px;
padding:8px 2px;
border:solid 1px #8883;
border-top:none;
text-decoration: none;
color:inherit;
text-align:center;
width:100%;
max-width:var(--chapter-nav-width);
	
}
.chapter-nav a > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.chapter-nav li.active { background:#159; color:white; outline:solid gold; }

.chapter-nav li.active + .active { background:#49d; }


/*----- Arrow pointing to current line -------*/
.line-pointer {
    height: 1.2em;
    width: min( calc( var(--text-width) + 1em) , calc(100vw - 50px) ) ;
    background: #1ad1;
    position: fixed;
    top: calc( var(--offset) + 14% );
    left: max( 4px, calc( 50vw - (var(--text-width) / 2 + 3em))  );
	opacity:0.1;
}
.line-pointer:after {
	content:"";
	border:solid 0.6em transparent;
	border-left:solid 1em #159;
	display:inline-block;
	height:0; width:0;
	cursor:pointer;
}

/*------ Bookmark Button ----------*/
.bookmark-btn { text-align:right; padding-top:4px; }
.bookmark-btn a { 
	display:inline-block;
	background:#159;
	color:white;
	text-decoration: none;
	font-size: 16px;
	margin-top:4px;
	padding: 2px 8px 2px 20px;
	clip-path: polygon(100% 100% , 100% 0% , 0% 0% , 14px 50% , 0% 100%);
}
.bookmark-btn a.marked { display:none; }

.chapter:target { //outline:dotted 2px #1745; outline-offset:14px; }
.chapter:target .bookmark-btn a.unmarked {display:none;}
.chapter:target .bookmark-btn a.marked {display:inline-block; background:#077d4c; padding-right:40px; }

.bookmark-feedback {
position:absolute;
top: -1px;
left: -32px;
background:#005d8f;
background:linear-gradient(#004978 , #2f7ca8, #004b7a);
width:24px;
height:36px;
clip-path: polygon( 0% 0% , 100% 0% , 100% 100%, 50% calc(100% - 12px), 0% 100%);
transform-origin:top ;
opacity:0.5;
transition:.4s height ease-out;
will-change: height, opacity;
}
.chapter:target .bookmark-feedback {
display:block;
filter:hue-rotate(-60deg);
height:110px;
opacity: 1;
}

/*------- Book Nav (Might Not Be Used) ---------*/
/*
.bookmark-nav {
	display:none;
    float: left;
    position: fixed;
    top: var(--offset);
	z-index:3;
}

.bookmark-nav img {
	width:24px;
	margin-left:0px;
	cursor:pointer;
}
.bookmark-nav nav { 
 	height:0px; 
	background:#135;
	overflow:hidden;
	transition: .35s cubic-bezier(.77,0,.18,1);
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	border-radius: 0 0 8px 0;
}
.bookmark-open .bookmark-nav nav{
	height:200px;
}
.bookmark-nav nav button {
	background:#123;
	color:white;
	padding:16px;
	margin:1px;
	font:inherit;
	font-size:12px;
	border:solid 1px #888;
	border-radius: 6px;
	cursor:pointer;
}

.bookmark-nav nav button[disabled] { opacity: 0.4; cursor:default; }

.bookmark-nav nav button#addBookmarkBtn {
	background:#074;
	margin-top:4px;
}
.bookmark-nav nav button#clearBtn { padding:4px; background:#911; }

.saved-ribbon {
    background: #159a;
    width: 100%;
    position: absolute;
    top: 0px;
    left: 0;
    text-align: center;
    color: #fe5;
}
.saved-ribbon.swoosh {
	animation: 0.6s Swoosh ease-in-out;
	transform-origin:top left;
}

@keyframes Swoosh{
	0% { transform:scaleX(0.01); }
	100% {transform:scaleX(1);}
}
*/

How to Make a Bookmark Shape with CSS clip-path

Horizontal Flexible Bookmark Shape

Using the clip-path: polygon(100% 100% , 100% 0% , 0% 0% , 14px 50% , 0% 100%); will make a bookmark like the following.

Try editing the text inside the book to see how it adapts…

.bookmark-btn { text-align:left; padding-top:0px; }
.bookmark-btn a { 
	display:inline-block;
	background:#159;
	color:white;
	text-decoration: none;
	font-size: 20px;
	margin-top:0px;
	padding: 2px 8px 2px 20px;
	clip-path: polygon(100% 100% , 100% 0% , 0% 0% , 14px 50% , 0% 100%);
}

Flexible Vertical Bookmark Shape

The vertical bookmark shape is similar but slightly more complicated because you need to use the calc() function inside the CSS code.

clip-path: polygon( 0% 0% , 100% 0% , 100% 100%, 50% calc(100% – 12px), 0% 100%);

Hover over the bookmark to see it expand

.bookmark-feedback {
	display:inline-block;
	background:linear-gradient(#004978 , #2f7ca8, #004b7a);
	width:24px;
	height:36px;
	clip-path: polygon( 0% 0% , 100% 0% , 100% 100%, 50% calc(100% - 12px), 0% 100%);
	transform-origin:top;
	transition:.4s height ease-out;
	vertical-align:top;
	will-change: height, opacity;
}
.bookmark-feedback:hover {
	filter:hue-rotate(-60deg);
	height:110px;
}

Collapsed Details Version

As I’ve continued down this path I am not certain that it is the right direction. As such I am documenting the code I currently have because I might just revert back to what I had earlier and see if I can make that work.

<?php if(get_field('story_type') == "long_form"): ?>
<main class="long-story">

<!-- Story Header -->	
	<header class="story-header long-story-header">
		<?php if( has_post_thumbnail() ): ?>
			<div class="featured-image" style="background-image:url(<?php the_post_thumbnail_url('full'); ?>);  "></div> 
		<?php endif; ?>
		<h1><?php the_title(); ?> 
			<?php if(get_field('author_name')): ?><small class="author">by <?php the_field('author_name'); ?></small><?php endif; ?>
		</h1>
		<div class="tags"><?php the_tags(); ?></div>

		<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
			<div class="pre-story-content"><?php the_content(); ?></div>
		<?php endwhile; endif; ?>

	</header>

		<?php $variable = $_GET['chap']; ?>
		<?php if (isset($_GET['chap'])) {  $chap = $_GET['chap'];  echo 'chapter '. $chap .' is open'; } else { } ?>

<!-- The Chapters -->
	<article>
		<?php if( have_rows('chapter_repeater') ): ?>

		<?php while( have_rows('chapter_repeater') ): the_row(); 
			$chapterNumber = get_sub_field('chapter_number');
			$totalChapters = count( get_field( 'chapter_repeater' ) );
			$chapterTitle = get_sub_field('chapter_title');
			$chapterText = get_sub_field('chapter_text');
		?>
		
<!-- Chapter Details that might update a paremeter -->
		<details class="details-chap" <?php if( $chap == $chapterNumber) { echo 'open=open'; } ?> id="bookmark<?php echo $chapterNumber; ?>">
		<summary onclick="changeUrl(<?php echo $chapterNumber; ?>)"> <b><?php  echo 'Chapter ';  echo $chapterNumber; ?></b> <?php if ($chapterTitle) { echo '-<span> '. $chapterTitle .'</span>'; } ?> </summary>
		<section id="<?php echo str_replace(' ', '', $chapterNumber ); ?>" class="chapter">
			
<!-- Sticky Chapter Header -->
			<div class="chapter-sticky-header" > 
				<span class="currentBook"><?php the_title(); ?></span> 
				<?php if($chapterNumber):?>
					<span class="currentChapter" >Chapter <?php echo $chapterNumber; ?> &rsaquo;</span> 
				<?php endif; ?>
			</div>
<!-- Normal Chapter Header -->
			<header class="chapter-header">
				<h2><?php echo $chapterNumber; ?></h2>
 				<h3> <?php echo $chapterTitle; ?></h3>
			</header>
<!-- Chapter Contents -->
 			<?php echo $chapterText; ?>
		</section>
		</details>

				<?php endwhile; ?>
			<?php endif; ?>
		<div id="storyEnd"></div>
	</article>
	
<script>
function changeUrl(chapterNum) {
// Construct URLSearchParams object instance from current URL querystring.
var queryParams = new URLSearchParams(window.location.search);
// Set new or modify existing parameter value. 
queryParams.set("chapter", chapterNum);
// Replace current querystring with the new one.
history.replaceState(null, null, "?"+queryParams.toString());
}	
</script>

<script>
// Chapter Navigation Collapse and Expand (when you click "chapter" in sticky header)
function toggleChapterNav() {
	console.log('chapter nav toggled');
	document.querySelector('.long-story').classList.toggle('chapter-nav-closed');
}
</script>


<?php endif; ?>
Comments (Write a Comment)

Did you enjoy this story ?  Do you have any ideas to improve Story Res? Share your thoughts below...

Write a Reply or Comment

Your email address will not be published. Required fields are marked *