···11+<!--
22+ Wander 0.2.0.dev8
33+ Source: https://codeberg.org/susam/wander
44+ Licence: MIT
55+-->
66+<!DOCTYPE html>
77+<html lang="en">
88+ <head>
99+ <title>Wander Console - Browse the Small Web</title>
1010+ <meta charset="UTF-8">
1111+ <meta name="viewport" content="width=device-width, initial-scale=1">
1212+ <style>
1313+ /* Colours */
1414+ body {
1515+ background: #696;
1616+ }
1717+ button, input, noscript, dialog {
1818+ background: #bdb;
1919+ border-color: #363;
2020+ color: #030
2121+ }
2222+ dialog {
2323+ background: #cec;
2424+ }
2525+ button:hover {
2626+ background: #9c9;
2727+ }
2828+ button:active {
2929+ background: #8c8;
3030+ }
3131+ dialog::backdrop {
3232+ background: #0009;
3333+ }
3434+ /* Dimensions and decorations */
3535+ body {
3636+ line-height: 1.5;
3737+ margin: 0;
3838+ padding: 1em;
3939+ }
4040+ button, input, noscript, dialog {
4141+ font-family: courier, monospace;
4242+ font-size: medium;
4343+ font-weight: bold;
4444+ }
4545+ button, input {
4646+ box-sizing: border-box;
4747+ margin-bottom: 1em;
4848+ min-height: 2.5em;
4949+ width: 6em;
5050+ }
5151+ button {
5252+ cursor: pointer;
5353+ }
5454+ input {
5555+ padding-left: 0.5em;
5656+ width: 18.5em;
5757+ }
5858+ header input:not(:last-of-type),
5959+ header button:not(:last-of-type) {
6060+ margin-right: 0.25em;
6161+ }
6262+ dialog {
6363+ border: thick solid;
6464+ max-width: 42em;
6565+ max-height: 80vh;
6666+ overflow: auto;
6767+ }
6868+ dialog header, dialog footer {
6969+ text-align: center;
7070+ }
7171+ dialog header h1, dialog header form {
7272+ display: inline-block;
7373+ vertical-align: top;
7474+ }
7575+ dialog header h1 {
7676+ font-size: 1.5em;
7777+ margin: 0;
7878+ padding-left: 2rem;
7979+ text-align: center;
8080+ width: calc(100% - 4rem);
8181+ }
8282+ dialog header button {
8383+ box-sizing: border-box;
8484+ height: auto;
8585+ margin: 0;
8686+ min-height: 0;
8787+ padding: 0.25em;
8888+ vertical-align: top;
8989+ width: 2em;
9090+ }
9191+ dialog h2 {
9292+ font-size: 1.2em;
9393+ margin-bottom: 0;
9494+ }
9595+ dialog h2 + p {
9696+ margin-top: 0.5em;
9797+ }
9898+ dialog footer button {
9999+ margin-bottom: 0;
100100+ }
101101+ iframe {
102102+ background: #fff;
103103+ border: thick solid #363;
104104+ box-sizing: border-box;
105105+ display: none;
106106+ height: calc(100vh - 12.5em);
107107+ width: 100%;
108108+ }
109109+ #url-wide-input, #reload-button, #go-button, #open-button {
110110+ display: none;
111111+ }
112112+ #url-narrow-input {
113113+ margin-right: 0;
114114+ }
115115+ noscript {
116116+ border: medium double #600;
117117+ color: #600;
118118+ display: block;
119119+ font-size: x-large;
120120+ margin-bottom: 1em;
121121+ padding: 1em;
122122+ text-align: center;
123123+ }
124124+ @media (min-width: 70em) {
125125+ iframe {
126126+ height: calc(100vh - 5.5em);
127127+ }
128128+ #url-narrow-input {
129129+ display: none;
130130+ }
131131+ #reload-button, #go-button, #open-button, #url-wide-input {
132132+ display: inline-block;
133133+ }
134134+ #url-wide-input {
135135+ width: 25em;
136136+ }
137137+ }
138138+ </style>
139139+ <script src="wander.js"></script>
140140+ <script>
141141+ const LOGGING = true
142142+ const VERSION = '0.2.0.dev8'
143143+ const KEEP_CONSOLES = 1000
144144+145145+ const consoles = []
146146+ const pages = []
147147+ const ignores = []
148148+ const history = []
149149+150150+ let consolesFound = 0
151151+152152+ function init () {
153153+ log(`Wander ${VERSION}`)
154154+ initCustomStyle()
155155+ initCustomScript()
156156+ initListeners()
157157+ initConsoleList()
158158+ initVersion()
159159+ collectIgnores(wander.ignore)
160160+ collectConsoles(window.location.href, wander.consoles)
161161+ collectPages(window.location.href, wander.pages)
162162+ loadNewPage()
163163+ log('Initialised')
164164+ }
165165+166166+ function initCustomStyle () {
167167+ const styles = wander.styles || []
168168+ for (const href of styles) {
169169+ const link = document.createElement('link')
170170+ link.rel = 'stylesheet'
171171+ link.href = href
172172+ document.head.append(link)
173173+ }
174174+ }
175175+176176+ function initCustomScript () {
177177+ const scripts = wander.scripts || []
178178+ for (const src of scripts) {
179179+ const script = document.createElement('script')
180180+ script.src = src
181181+ document.head.append(script)
182182+ }
183183+ }
184184+185185+ function initListeners () {
186186+ document.getElementById('about-button').addEventListener('click', showAboutDialog)
187187+ document.getElementById('console-button').addEventListener('click', showConsoleDialog)
188188+ document.getElementById('open-button').addEventListener('click', openURL)
189189+ document.getElementById('go-button').addEventListener('click', gotoURL)
190190+ document.getElementById('reload-button').addEventListener('click', reloadPage)
191191+ document.getElementById('url-narrow-input').addEventListener('keydown', handleURLInput)
192192+ document.getElementById('url-wide-input').addEventListener('keydown', handleURLInput)
193193+ document.getElementById('wander-button').addEventListener('click', loadNewPage)
194194+ document.getElementById('wander-iframe').addEventListener('load', wanderDone)
195195+ window.addEventListener('message', collectConsoleData)
196196+ }
197197+198198+ function initConsoleList () {
199199+ const ul = document.getElementById('console-dialog-list')
200200+ for (const url of wander.consoles) {
201201+ const li = document.createElement('li')
202202+ const a = document.createElement('a')
203203+ li.append(a)
204204+ ul.append(li)
205205+ a.href = url
206206+ a.textContent = url
207207+ }
208208+ }
209209+210210+ function initVersion () {
211211+ for (const element of document.querySelectorAll('.version')) {
212212+ element.textContent = VERSION
213213+ }
214214+ }
215215+216216+ function collectIgnores (patterns) {
217217+ for (const pattern of patterns) {
218218+ ignores.push(new RegExp(pattern))
219219+ }
220220+ }
221221+222222+ function collectConsoles (consoleURL, urls) {
223223+ for (const url of urls) {
224224+ const normURL = normaliseWanderURL(normaliseURL(url))
225225+ if (duplicateURL(consoles, normURL)) {
226226+ log(`Skipped duplicate console URL: ${consoleURL} => ${normURL}`)
227227+ continue
228228+ }
229229+ if (ignoredURL(ignores, normURL)) {
230230+ log(`Skipped ignored console URL: ${consoleURL} => ${normURL}`)
231231+ continue
232232+ }
233233+ consoles.push({ referrer: consoleURL, url: normURL })
234234+ consolesFound++
235235+ log(`Added console URL: ${consoleURL} => ${normURL} ` +
236236+ `(total ${consolesFound}, cached ${consoles.length})`)
237237+ }
238238+ if (consoles.length > KEEP_CONSOLES) {
239239+ consoles.splice(0, consoles.length - KEEP_CONSOLES)
240240+ }
241241+ log('Trimmed console cache ' +
242242+ `(total ${consolesFound}, cached ${consoles.length})`)
243243+ }
244244+245245+ function collectPages (consoleURL, urls) {
246246+ const goodPages = []
247247+ for (const url of urls) {
248248+ const normURL = normaliseURL(url)
249249+ if (ignoredURL(ignores, normURL)) {
250250+ log(`Skipped ignored page URL: ${consoleURL} => ${normURL}`)
251251+ continue
252252+ }
253253+ goodPages.push({ referrer: consoleURL, url: normURL })
254254+ log(`Added page URL: ${consoleURL} => ${normURL} (cached ${goodPages.length})`)
255255+ }
256256+ if (goodPages.length > 0) {
257257+ pages.length = 0
258258+ goodPages.forEach(function (item) { pages.push(item) })
259259+ }
260260+ }
261261+262262+ function duplicateURL (cache, url) {
263263+ return cache.some(function (item) {
264264+ return item.url === url
265265+ })
266266+ }
267267+268268+ function ignoredURL (ignores, url) {
269269+ return ignores.some(function (re) {
270270+ return re.test(url)
271271+ })
272272+ }
273273+274274+ function loadNewPage () {
275275+ const iframe = document.getElementById('wander-iframe')
276276+ const page = pick(filter(pages))
277277+ document.getElementById('url-narrow-input').value = page.url
278278+ document.getElementById('url-wide-input').value = page.url
279279+ iframe.src = page.url
280280+ history.push(page)
281281+ log(`Loading page: ${page.referrer} => ${page.url}`)
282282+ }
283283+284284+ function wanderDone () {
285285+ const iframe = document.getElementById('wander-iframe')
286286+ log('Loaded new page')
287287+ if (iframe.style.display !== 'block') {
288288+ iframe.style.display = 'block'
289289+ log('Displayed iframe')
290290+ }
291291+ iframe.focus()
292292+ pickNewConsole()
293293+ }
294294+295295+ function showConsoleDialog () {
296296+ document.getElementById('host-console-link').textContent = window.location.href
297297+ document.getElementById('host-console-link').href = window.location.href
298298+ const ul = document.getElementById('history-list')
299299+ ul.textContent = ''
300300+ for (const item of history.slice().reverse()) {
301301+ const li = document.createElement('li')
302302+ const p = document.createElement('p')
303303+ const a1 = document.createElement('a')
304304+ const a2 = document.createElement('a')
305305+ const a3 = document.createElement('a')
306306+ const br = document.createElement('br')
307307+ ul.append(li)
308308+ li.append(p)
309309+ p.append(a1, br, a2, ' (', a3, ')')
310310+ a1.href = a1.textContent = item.url
311311+ a2.href = item.referrer
312312+ a2.textContent = item.referrer
313313+ a3.href = item.referrer + 'wander.js'
314314+ a3.textContent = 'js'
315315+ }
316316+ document.getElementById('console-dialog').showModal()
317317+ }
318318+319319+ function pickNewConsole () {
320320+ const console = pick(filter(consoles))
321321+ const jsURL = console.url + 'wander.js'
322322+ const consoleURLAsCode = JSON.stringify(console.url)
323323+ const sandbox = document.getElementById('sandbox-iframe')
324324+ log('Loading next console:', console.url)
325325+ sandbox.srcdoc = `
326326+ <script src="${jsURL}"><\/script>
327327+ <script>parent.postMessage({wander: wander, url: ${consoleURLAsCode}}, '*')<\/script>
328328+ `
329329+ }
330330+331331+ function collectConsoleData (e) {
332332+ log('Loaded next console:', e.data.url)
333333+ collectConsoles(e.data.url, e.data.wander.consoles)
334334+ collectPages(e.data.url, e.data.wander.pages)
335335+ }
336336+337337+ function normaliseWanderURL (url) {
338338+ if (url.endsWith('index.html')) {
339339+ url = url.slice(0, -'index.html'.length)
340340+ }
341341+ if (!url.endsWith('/')) {
342342+ url += '/'
343343+ }
344344+ return url
345345+ }
346346+347347+ function reloadPage () {
348348+ const iframe = document.getElementById('wander-iframe')
349349+ const src = iframe.src
350350+ iframe.src = src
351351+ }
352352+353353+ function handleURLInput (e) {
354354+ if (e.key === 'Enter') {
355355+ gotoURL()
356356+ }
357357+ }
358358+359359+ function gotoURL () {
360360+ const value = normaliseURL(visibleInput().value)
361361+ log('Navigating to user entered URL:', value)
362362+ document.getElementById('url-narrow-input').value = value
363363+ document.getElementById('url-wide-input').value = value
364364+ document.getElementById('wander-iframe').src = value
365365+ }
366366+367367+ function matchingProtocol () {
368368+ let protocol = window.location.protocol
369369+ if (protocol !== 'http:' && protocol !== 'https:') {
370370+ protocol = 'https:'
371371+ }
372372+ return protocol
373373+ }
374374+375375+ function openURL () {
376376+ window.open(visibleInput().value)
377377+ }
378378+379379+ function showAboutDialog () {
380380+ document.getElementById('about-dialog').showModal()
381381+ }
382382+383383+ function pick (items) {
384384+ return items[Math.floor(Math.random() * items.length)]
385385+ }
386386+387387+ function filter (items) {
388388+ return items.filter(function (item) {
389389+ const diffProtocol = !item.url.startsWith(matchingProtocol())
390390+ const ignored = ignores.some(function (regex) {
391391+ return regex.test(item.url)
392392+ })
393393+ if (diffProtocol) {
394394+ log('Ignored due to protocol mismatch:', item.url)
395395+ return false
396396+ }
397397+ if (ignored) {
398398+ log('Ignored due to explicit ignore:', item.url)
399399+ return false
400400+ }
401401+ return true
402402+ })
403403+ }
404404+405405+ function normaliseURL (url) {
406406+ let result = url.trim()
407407+ const re = /^[A-Za-z][A-Za-z\d+\-.]*:\/\// // RFC 3986, section 3.1.
408408+ if (!re.test(result)) {
409409+ result = matchingProtocol() + '//' + result
410410+ }
411411+ return new URL(result).href
412412+ }
413413+414414+ function visibleInput () {
415415+ const urlNarrowInput = document.getElementById('url-narrow-input')
416416+ const urlWideInput = document.getElementById('url-wide-input')
417417+ return urlNarrowInput.offsetParent ? urlNarrowInput : urlWideInput
418418+ }
419419+420420+ function log () {
421421+ if (LOGGING) {
422422+ const args = Array.prototype.slice.call(arguments)
423423+ args.unshift('[Wander]')
424424+ console.log.apply(console, args)
425425+ }
426426+ }
427427+428428+ window.addEventListener('load', init)
429429+ </script>
430430+ </head>
431431+ <body>
432432+ <noscript>JavaScript is required to use this application.</noscript>
433433+ <header>
434434+ <button id="wander-button">Wander</button><!--
435435+ --><button id="console-button">Console</button><!--
436436+ --><button id="reload-button">Reload</button><!--
437437+ --><input id="url-wide-input" autocomplete="off"><!--
438438+ --><button id="go-button">Go</button><!--
439439+ --><button id="open-button">Open</button><!--
440440+ --><button id="about-button">About</button><!--
441441+ --><input id="url-narrow-input">
442442+ </header>
443443+ <iframe id="wander-iframe"></iframe>
444444+ <iframe id="sandbox-iframe" sandbox="allow-scripts"></iframe>
445445+ <dialog id="console-dialog">
446446+ <header>
447447+ <h1>Console Explorer</h1><form method="dialog"><button>x</button></form>
448448+ </header>
449449+ <h2>Host Console</h2>
450450+ <p>
451451+ You are currently on <a id="host-console-link"></a>.
452452+ This console is helping you navigate the Wander network.
453453+ </p>
454454+ <h2>Console Neighbourhood</h2>
455455+ <p>
456456+ Your host console lists the following consoles as its
457457+ neighbours:
458458+ </p>
459459+ <ul id="console-dialog-list"></ul>
460460+ <p>
461461+ You can wander to a neighbouring console by clicking one of
462462+ the links above. Note that you do not really need to change
463463+ console to browse the Wander network. The host console can
464464+ fetch recommendations from other consoles, then from the
465465+ consoles they link to and so on recursively.
466466+ </p>
467467+ <h2>Wandering History</h2>
468468+ <p>
469469+ Your wandering history in reverse chronological order:
470470+ </p>
471471+ <ul id="history-list"></ul>
472472+ <p>
473473+ Each item above includes three links: the page recommended to
474474+ you, the console that recommended it and the wander.js file of
475475+ that console.
476476+ </p>
477477+ <footer><form method="dialog"><button>Close</button></form></footer>
478478+ </dialog>
479479+ <dialog id="about-dialog">
480480+ <header>
481481+ <h1>Wander Console</h1><form method="dialog"><button>x</button></form>
482482+ <div>Version <span class="version"> </span></div>
483483+ </header>
484484+ <p>
485485+ Hello! You are currently on a Wander console! A Wander
486486+ console lets you browse random websites and pages from the
487487+ Wander community. The Wander community consists of
488488+ individuals who develop and maintain their own personal
489489+ websites.
490490+ </p>
491491+ <p>
492492+ To set up your own Wander console, download
493493+ <a href="https://codeberg.org/susam/wander/archive/wander.zip">this ZIP file</a>,
494494+ extract index.html and wander.js, and place them
495495+ in the /wander/ directory of your website. Then edit
496496+ wander.js by following the directions at
497497+ <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
498498+ </p>
499499+ <p>
500500+ That's it! Once your /wander/ directory is ready on your web
501501+ server, you can share a link to your Wander console in
502502+ <a href="https://codeberg.org/susam/wander/issues/1">this
503503+ community thread</a>. Hopefully, someone will add your
504504+ console to theirs and you will become part of the Wander
505505+ network.
506506+ </p>
507507+ <p>
508508+ For more information about Wander, please
509509+ see <a href="https://codeberg.org/susam/wander#readme">codeberg.org/susam/wander</a>.
510510+ </p>
511511+ <footer><form method="dialog"><button>Close</button></form></footer>
512512+ </dialog>
513513+ </body>
514514+</html>
+28
wander/wander.js
···11+const wander = {
22+ // Other Wander consoles that visitors can reach from my console.
33+ consoles: [
44+ 'https://susam.net/wander/',
55+ ],
66+77+ // My favourite websites and pages I recommend to the Wander community.
88+ pages: [
99+ 'https://boingboing.net/',
1010+ 'https://www.swiss-miss.com/',
1111+ 'https://waxy.org/',
1212+ 'https://le-chouchou.ghost.io/',
1313+ ],
1414+1515+ // Websites and consoles to ignore. When this console serves as
1616+ // your host console, it will never contact consoles or recommend
1717+ // web pages with addresses that match the following regular
1818+ // expression patterns.
1919+ ignore: [
2020+ // Off-topic since these are commercial services, not personal websites.
2121+ '.*://medium\\.com/.*',
2222+ '.*://.*\\.substack\\.com/.*',
2323+2424+ // These do not load in the console due to frame-embedding restrictions.
2525+ '.*://cari\\.institute/.*',
2626+ '.*://wdl\\.mcdaniel\\.edu/.*',
2727+ ]
2828+}