@@ -19,6 +19,7 @@ const state = {
1919 currentPage : null ,
2020 pageTitles : new Map ( ) ,
2121 showAllTopics : false ,
22+ historyIndex : 0 ,
2223} ;
2324
2425const els = {
@@ -39,6 +40,7 @@ const jobsView = createJobsView({
3940 jobPath : ( runId ) => wikiApi ( `/jobs/${ encodeURIComponent ( runId ) } ` ) ,
4041 jobRoute : ( runId ) => wikiRoute ( `/jobs/${ runId } ` ) ,
4142 isCurrentJobRoute : ( runId ) => location . pathname === wikiRoute ( `/jobs/${ runId } ` ) ,
43+ pageActions : ( ) => renderPageActions ( wikiRoute ( "/" ) ) ,
4244 renderError,
4345 renderMarkdown,
4446 escapeHtml,
@@ -63,6 +65,7 @@ boot().catch((error) => renderError(error));
6365
6466async function boot ( ) {
6567 wireEvents ( ) ;
68+ initializeHistoryState ( ) ;
6669 const result = await api ( "/api/wikis" ) ;
6770 state . wikis = result . wikis ?? [ ] ;
6871 renderChrome ( ) ;
@@ -82,6 +85,13 @@ function wireEvents() {
8285 return ;
8386 }
8487
88+ const back = event . target . closest ( "[data-back]" ) ;
89+ if ( back !== null ) {
90+ event . preventDefault ( ) ;
91+ goBack ( ) ;
92+ return ;
93+ }
94+
8595 const target = event . target . closest ( "[data-route]" ) ;
8696 if ( target === null || target . disabled === true ) return ;
8797 event . preventDefault ( ) ;
@@ -97,13 +107,18 @@ function wireEvents() {
97107 : wikiRoute ( "/search" ) ) ;
98108 } ) ;
99109
100- window . addEventListener ( "popstate" , ( ) => {
110+ window . addEventListener ( "popstate" , ( event ) => {
111+ state . historyIndex = historyIndexFromState ( event . state ) ;
101112 route ( location . pathname , location . search , false ) . catch ( ( error ) => renderError ( error ) ) ;
102113 } ) ;
103114}
104115
105116async function route ( pathname , search = "" , push = true ) {
106- if ( push ) history . pushState ( null , "" , pathname + search ) ;
117+ if ( push ) {
118+ const nextIndex = state . historyIndex + 1 ;
119+ state . historyIndex = nextIndex ;
120+ history . pushState ( historyState ( nextIndex ) , "" , pathname + search ) ;
121+ }
107122 jobsView . clearPoll ( ) ;
108123
109124 if ( pathname === "/" || ! pathname . startsWith ( "/w/" ) ) {
@@ -338,6 +353,7 @@ async function renderOverview() {
338353 const overview = state . overview ;
339354 document . title = `${ state . currentWiki } - Almanac` ;
340355 els . reader . innerHTML = `
356+ ${ renderPageActions ( "/" ) }
341357 <section class="ca-hero">
342358 <div class="ca-kicker">Project overview</div>
343359 <h1 class="ca-title">${ escapeHtml ( projectName ( overview . repoRoot ) ) } </h1>
@@ -387,6 +403,7 @@ async function renderGettingStarted() {
387403
388404 document . title = `Getting started - ${ state . currentWiki } ` ;
389405 els . reader . innerHTML = `
406+ ${ renderPageActions ( wikiRoute ( "/" ) ) }
390407 <section class="ca-hero">
391408 <div class="ca-kicker">Getting started</div>
392409 <h1 class="ca-title">No getting started page</h1>
@@ -409,25 +426,24 @@ async function renderPage(slug) {
409426function renderPageArticle ( page ) {
410427 document . title = `${ page . title ?? page . slug } - Almanac` ;
411428 els . reader . innerHTML = `
429+ ${ renderPageActions ( wikiRoute ( "/" ) ) }
412430 <article class="ca-article">
413- <header class="ca-page-header">
414- <h1>${ escapeHtml ( pageTitle ( page ) ) } </h1>
415- <div class="ca-page-header-meta">Last revised ${ escapeHtml ( formatDate ( page . updated_at ) ) } </div>
416- <div class="ca-chip-row" style="justify-content: center;">
417- ${ page . topics . map ( ( topic ) => `<button class="ca-chip" data-route="${ escapeAttr ( wikiRoute ( `/topic/${ topic } ` ) ) } ">${ escapeHtml ( topic ) } </button>` ) . join ( "" ) }
418- </div>
419- <div class="ca-page-ornament"><span>✥</span></div>
420- </header>
421- <div class="ca-prose">${ renderMarkdown ( page . body ) } </div>
431+ <div class="ca-prose">${ renderMarkdown ( page . body , { decorateTitle : true , summary : page . summary } ) } </div>
422432 </article>
423433 ` ;
424434}
425435
436+ function renderArticleSummary ( summary ) {
437+ const text = summary ?. trim ( ) ;
438+ return text ? `<p class="ca-article-summary">${ escapeHtml ( text ) } </p>` : "" ;
439+ }
440+
426441async function renderTopic ( slug ) {
427442 const topic = await api ( wikiApi ( `/topic/${ encodeURIComponent ( slug ) } ` ) ) ;
428443 rememberPages ( topic . pages ) ;
429444 document . title = `${ topic . title ?? topic . slug } - Almanac` ;
430445 els . reader . innerHTML = `
446+ ${ renderPageActions ( wikiRoute ( "/" ) ) }
431447 <section class="ca-hero">
432448 <div class="ca-kicker">Topic</div>
433449 <h1 class="ca-title">${ escapeHtml ( topic . title ?? topic . slug ) } </h1>
@@ -462,6 +478,7 @@ async function renderSearch(query) {
462478 rememberPages ( result . pages ) ;
463479 document . title = `${ query ? `Search: ${ query } ` : "Recent pages" } - Almanac` ;
464480 els . reader . innerHTML = `
481+ ${ renderPageActions ( wikiRoute ( "/" ) ) }
465482 <section class="ca-hero">
466483 <div class="ca-kicker">${ query ? "Search" : "Recent" } </div>
467484 <h1 class="ca-title">${ query ? escapeHtml ( query ) : "Recent pages" } </h1>
@@ -475,6 +492,7 @@ async function renderFile(path) {
475492 const result = await api ( wikiApi ( `/file?path=${ encodeURIComponent ( path ) } ` ) ) ;
476493 rememberPages ( result . pages ) ;
477494 els . reader . innerHTML = `
495+ ${ renderPageActions ( wikiRoute ( "/" ) ) }
478496 <section class="ca-hero">
479497 <div class="ca-kicker">File reference</div>
480498 <h1 class="ca-title">${ escapeHtml ( path || "File references" ) } </h1>
@@ -500,7 +518,7 @@ function renderPageRail(page) {
500518 ` ;
501519 els . backlinks . innerHTML = page . wikilinks_in . length > 0
502520 ? page . wikilinks_in . map ( ( slug ) => linkButton ( pageLabel ( slug ) , wikiRoute ( `/page/${ slug } ` ) ) ) . join ( "" )
503- : `<div class="ca-meta-empty">No backlinks .</div>` ;
521+ : `<div class="ca-meta-empty">No pages link here .</div>` ;
504522 els . fileRefs . innerHTML = page . file_refs . length > 0
505523 ? page . file_refs . map ( ( ref ) => linkButton ( ref . path , wikiRoute ( `/file?path=${ encodeURIComponent ( ref . path ) } ` ) , "" , "ca-file-link" ) ) . join ( "" )
506524 : `<div class="ca-meta-empty">No file refs.</div>` ;
@@ -531,10 +549,20 @@ function linkButton(label, route, detail = "", extraClass = "") {
531549 ` ;
532550}
533551
534- function renderMarkdown ( source ) {
552+ function renderPageActions ( fallbackRoute ) {
553+ return `
554+ <div class="ca-page-actions">
555+ <button class="ca-back-button" type="button" data-back data-fallback-route="${ escapeAttr ( fallbackRoute ) } ">Back</button>
556+ </div>
557+ ` ;
558+ }
559+
560+ function renderMarkdown ( source , options = { } ) {
535561 const blocks = [ ] ;
536562 let inCode = false ;
537563 let code = [ ] ;
564+ let decoratedHeading = false ;
565+ const decorateTitle = options . decorateTitle === true ;
538566
539567 for ( const line of source . split ( / \r ? \n / ) ) {
540568 if ( line . startsWith ( "```" ) ) {
@@ -557,14 +585,24 @@ function renderMarkdown(source) {
557585 }
558586 if ( line . startsWith ( "### " ) ) blocks . push ( `<h3>${ inline ( line . slice ( 4 ) ) } </h3>` ) ;
559587 else if ( line . startsWith ( "## " ) ) blocks . push ( `<h2>${ inline ( line . slice ( 3 ) ) } </h2>` ) ;
560- else if ( line . startsWith ( "# " ) ) blocks . push ( `<h2>${ inline ( line . slice ( 2 ) ) } </h2>` ) ;
588+ else if ( line . startsWith ( "# " ) ) {
589+ blocks . push ( renderHeading ( line . slice ( 2 ) , decorateTitle && ! decoratedHeading , options . summary ) ) ;
590+ decoratedHeading = true ;
591+ }
561592 else if ( line . startsWith ( "- " ) ) blocks . push ( `<p>• ${ inline ( line . slice ( 2 ) ) } </p>` ) ;
562593 else blocks . push ( `<p>${ inline ( line ) } </p>` ) ;
563594 }
564595 if ( inCode ) blocks . push ( `<pre><code>${ escapeHtml ( code . join ( "\n" ) ) } </code></pre>` ) ;
565596 return blocks . filter ( Boolean ) . join ( "\n" ) ;
566597}
567598
599+ function renderHeading ( text , decorated , summary = null ) {
600+ const level = decorated ? "h1" : "h2" ;
601+ const heading = `<${ level } >${ inline ( text ) } </${ level } >` ;
602+ if ( ! decorated ) return heading ;
603+ return `${ heading } \n${ renderArticleSummary ( summary ) } \n<div class="ca-page-ornament" aria-hidden="true"><span>✥</span></div>` ;
604+ }
605+
568606function inline ( text ) {
569607 return escapeHtml ( text )
570608 . replace ( / ` ( [ ^ ` ] + ) ` / g, "<code>$1</code>" )
@@ -633,6 +671,29 @@ function setRailVisible(visible) {
633671 els . shell . classList . toggle ( "is-rail-hidden" , ! visible ) ;
634672}
635673
674+ function goBack ( ) {
675+ const active = document . querySelector ( "[data-back]" ) ;
676+ const fallbackRoute = active ?. dataset ?. fallbackRoute ?? "/" ;
677+ if ( state . historyIndex > 0 ) {
678+ history . back ( ) ;
679+ return ;
680+ }
681+ navigate ( fallbackRoute ) ;
682+ }
683+
684+ function initializeHistoryState ( ) {
685+ state . historyIndex = historyIndexFromState ( history . state ) ;
686+ history . replaceState ( historyState ( state . historyIndex ) , "" , location . href ) ;
687+ }
688+
689+ function historyState ( index ) {
690+ return { ...( history . state ?? { } ) , almanacHistoryIndex : index } ;
691+ }
692+
693+ function historyIndexFromState ( value ) {
694+ return typeof value ?. almanacHistoryIndex === "number" ? value . almanacHistoryIndex : 0 ;
695+ }
696+
636697function projectName ( repoRoot ) {
637698 return repoRoot . split ( "/" ) . filter ( Boolean ) . at ( - 1 ) ?? "Project" ;
638699}
0 commit comments