on 3a5d267 help sign in
README.md+8−8
since splitsnapshots
unifiedsplit
@@ -1,6 +1,6 @@
1# Webring
1# Webring
2
2
3−This [webring](https://wiki.xxiivv.com/webring) is an attempt to inspire artists & developers to create and maintain their own website and share traffic among each other. The webring's aim is to share hand-crafted websites such as **diaries, wikis & portfolios**, single pagers with nothing but outgoing links to social platforms will be rejected. To add yourself to the ring, submit an [edit](https://github.com/XXIIVV/webring/edit/master/scripts/sites.js) to this repository.
3+This [webring](https://wiki.xxiivv.com/webring) is an attempt to inspire artists & developers to create and maintain their own website and share traffic among each other. The webring's aim is to share hand-crafted websites such as **diaries, wikis & portfolios**. To add yourself to the ring, submit an [edit](https://github.com/XXIIVV/webring/edit/master/scripts/sites.js) to this repository.
4
4
5## Join the webring
5## Join the webring
6
6
@@ -9,25 +9,25 @@ This [webring](https://wiki.xxiivv.com/webring) is an attempt to inspire artists
9```
9```
10
10
11- Add the webring icon to your website html.
11- Add the webring icon to your website html.
12−- Add your website url to the [sites.js](https://github.com/XXIIVV/webring/edit/master/scripts/sites.js) file.
12+- Add your website information to the [sites.js](https://github.com/XXIIVV/webring/edit/master/scripts/sites.js) file. The `url` key is required, but you can also include `title`, `type`, `author`, `contact`, and `rss`.
13−- Submit a Pull Request with the location of the webring icon on your site.
13+- Submit a Pull Request with **the location of the webring icon** on your site.
14
14
15−Alternatively, if you your website has a dark background, use `icon.white.svg`. If your website is complaning about *https*, go ahead and host the icon yourself.
15+Alternatively, if you your website has a dark background, use `icon.white.svg`. If your website is complaining about *https*, go ahead and host the icon yourself. **Single page websites, and websites acting only as portals to other social platforms, will be rejected**.
16
16
17### Circular Linking
17### Circular Linking
18
18
19Instead of linking to the directory, you can also link to the next link in the ring by adding parts of your site or domain in the hash of the request url:
19Instead of linking to the directory, you can also link to the next link in the ring by adding parts of your site or domain in the hash of the request url:
20
20
21```
21```
22−<a href='https://webring.xxiivv.com/#wiki.xxiivv' target='_blank'><img src='https://webring.xxiivv.com/icon.black.svg'/></a>
22+<a href='https://webring.xxiivv.com/#wiki.xxiivv' target='_blank' rel="noopener noreferrer"><img src='https://webring.xxiivv.com/icon.black.svg'/></a>
23```
23```
24
24
25## API
25## API
26
26
27−This repository does not contain mature API capabilities, but there is one way to request list of sites currently in the webring.
27+This repository does not contain mature API capabilities, but there are a couple ways to request a list of sites and other information currently in the webring.
28
28
29−- Request [sites.js on xxiivv](https://webring.xxiivv.com/scripts/sites.js) or [sites.js on github](https://raw.githubusercontent.com/XXIIVV/webring/master/scripts/sites.js)
29+- Request [sites.js on xxiivv](https://webring.xxiivv.com/scripts/sites.js) or [sites.js on github](https://raw.githubusercontent.com/XXIIVV/webring/master/scripts/sites.js) and parse it. An example can be seen [here](https://gist.github.com/ckipp01/2ab7ac42e2837b4359efeb76eb49bb54).
30−- Parse it as text from `[` to `]`, split by `,` and then trim all whitespace and `"` in resulting array.
30+- If you'd like a already parsed list, [webring-checker.now.sh/sites](https://webring-checker.now.sh/sites) will return an json array of the site objects in the webring. See [webring-checker.now.sh](https://webring-checker.now.sh) for more info on what else is available.
31
31
32## Need Help?
32## Need Help?
33
33
hallway.htmlnew file+19
@@ -0,0 +1,19 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+ <meta charset="utf-8">
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6+ <link rel="stylesheet" type="text/css" href="links/fonts.css"/>
7+ <link rel="stylesheet" type="text/css" href="links/main.css"/>
8+ <script src="scripts/hallway.js"></script>
9+ <script src="scripts/sites.js"></script>
10+ <title>Hallway</title>
11+</head>
12+<body>
13+ <script>
14+ const hallway = new Hallway(sites);
15+ hallway.install(document.body);
16+ hallway.start();
17+ </script>
18+</body>
19+</html>
index.html+1
@@ -7,6 +7,7 @@
7 <link rel="stylesheet" type="text/css" href="links/main.css"/>
7 <link rel="stylesheet" type="text/css" href="links/main.css"/>
8 <script src="scripts/portal.js"></script>
8 <script src="scripts/portal.js"></script>
9 <script src="scripts/sites.js"></script>
9 <script src="scripts/sites.js"></script>
10+ <script src="scripts/opml.js"></script>
10 <title>Webring</title>
11 <title>Webring</title>
11</head>
12</head>
12<body>
13<body>
links/main.css+52−13
@@ -1,19 +1,58 @@
1* { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:collapse;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
1* { margin:0;padding:0;border:0;outline:0;text-decoration:none;font-weight:inherit;font-style:inherit;color:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;list-style:none;border-collapse:collapse;border-spacing:0; -webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}
2
2
3body { background:#f4f4f4; padding:0px; margin:0px; }
3body { background:#f4f4f4; padding:0px; margin:0px; }
4−body div { display:block; background: #ddd;min-height: calc(100vh - 60px);width: calc(100vw - 60px);margin: 15px;color: #555;padding: 15px;font-family:'input_mono_regular', 'Monospaced','Courier New',courier;font-size:11px; position: relative; border-radius: 2px }
4+body div { display:block; background: #111;min-height: calc(100vh - 60px);width: calc(100vw - 60px);margin: 15px;color: #333;padding: 15px;font-family:'input_mono_regular', 'Monospaced','Courier New',courier;font-size:11px; position: relative; border-radius: 2px; color:white;}
5−body div #icon { display: block;width: 30px;height: 30px;background-image: url(../icon.white.svg);background-size: cover;background-repeat: no-repeat;position: absolute;bottom: 10px;right:10px;background-position: center;padding:0px }
5+body div ul { max-width: 800px; border-bottom: 2px solid white;padding-bottom: 15px;margin-bottom:15px; overflow: hidden;}
6−body div p { display:block; padding:0px 5px; line-height: 20px}
6+body div p { display:block; padding:0px 5px; line-height: 20px; max-width: 600px}
7−body div p a { color:black; text-decoration: none; text-decoration: underline;}
7+body div p a { text-decoration: underline;}
8body div p a:hover { text-decoration: underline; }
8body div p a:hover { text-decoration: underline; }
9−body div p b { font-weight: normal; color:#000; font-family: 'input_mono_medium' }
9+body div p b { font-weight: normal; font-family: 'input_mono_medium' }
10body div p.readme { max-width:500px; }
10body div p.readme { max-width:500px; }
11−body div ul { display:block; max-width: 800px; columns:3; border-bottom: 2px solid white;padding-bottom: 15px;margin-bottom:15px; overflow: hidden;}
11+
12+/* Hallway */
13+
14+body #hallway #entries { width: calc(100vw - 300px) !important; }
15+body #hallway #sidebar { position: absolute;top: 0px;right: 0px;padding: 0px 15px;width: 200px; }
16+
17+body #hallway #sidebar > * { border-bottom: 0px; line-height: 16px; padding-bottom: 0px }
18+body #hallway #sidebar > * li { text-transform: lowercase; }
19+body #hallway #sidebar > * li span.right { color:#555; float:right; }
20+body #hallway #sidebar > * li span.right:after { content: " msgs"; }
21+
22+body #hallway .entry { line-height: 16px; display: block; position: relative; padding-left:100px; }
23+body #hallway .entry .date { color: #555;display: block;float:right; margin-left:20px; padding-bottom: 16px;}
24+body #hallway .entry .author { font-family: 'input_mono_medium'; position: absolute;left: 0px; }
25+body #hallway .entry .body { display: block;border-bottom: 1px solid #333;padding-bottom: 10px;margin-bottom: 10px; }
26+body #hallway .entry .body a { text-decoration: underline; }
27+body #hallway .entry .body .channel { display: inline-block;border-radius: 1px;font-family: 'input_mono_medium';color: #777; }
28+body #hallway .entry .body .user { display: inline-block;border-radius: 1px;font-family: 'input_mono_medium';color: #777; }
29+body #hallway .entry.highlight .author { color:#555; }
30+body #hallway .entry:hover .author { color:#ccc; }
31+body #hallway .entry:hover .date { color:#ccc; }
32+body #hallway .entry:hover .body { color:#ccc; border-bottom-color: #555 }
33+body #hallway p { padding-left: 60px;background-image: url(../icon.white.svg);background-size: 40px;background-repeat: no-repeat;background-position: 0px 0px; }
34+body #hallway ul { max-width: 100%; }
35+body #hallway ul li:last-child { border-bottom:0px; }
36+
37+/* Portal */
38+
39+body #portal #icon { display: block;width: 30px;height: 30px;background-image: url(../icon.white.svg);background-size: cover;background-repeat: no-repeat;position: absolute;bottom: 15px;right:15px;background-position: center;padding:0px }
40+body #portal ul { columns:3; }
12−body div ul li { width: 22em; display:block; padding-left:5px; overflow: hidden; text-overflow: ellipsis; }
41+body #portal ul li { display:block; padding-left:5px; text-overflow: ellipsis; text-transform: lowercase;}
42+body #portal ul li.wiki::after { content:"<wiki>"; color:#555; }
43+body #portal ul li.blog::after { content:"<blog>"; color:#555; }
13−body div ul li a { line-height: 20px; display: inline-block; color:#000; padding:0px 5px; text-decoration: none;}
44+body #portal ul li a { line-height: 20px; padding:0px 5px; text-decoration: none;}
14−body div ul li a:visited { color:#999; text-decoration: none;}
45+body #portal ul li a:visited { color:#ccc; text-decoration: none;}
15−body div ul li a:hover { background:#ccc; color:#000; text-decoration: none; border-radius: 2px }
46+body #portal ul li a:hover { text-decoration: underline; }
16−
47+
48+@media (max-width: 730px) {
49+ body #portal ul li span.counter { display: none }
50+}
17−@media (max-width: 950px){
51+@media (max-width: 630px) {
18− body div ul { columns: 2 }
52+ body #portal ul { columns: 2 !important; }
53+ body #portal ul li span.counter { display: none }
19}
54}
55+@media (max-width: 530px) {
56+ body #portal ul { columns: 1 !important; }
57+ body #portal ul li span.counter { display: inline }
58+}
scripts/hallway.jsnew file+182
@@ -0,0 +1,182 @@
1+'use strict'
2+
3+function Hallway (sites) {
4+ const feeds = {}
5+ this.sites = sites
6+ this.el = document.createElement('div')
7+ this.el.id = 'hallway'
8+
9+ this.install = function (host) {
10+ host.appendChild(this.el)
11+ this.findFeeds()
12+ }
13+
14+ this.start = function () {
15+ this.el.innerHTML = 'hello.'
16+ this.fetchFeeds()
17+ }
18+
19+ this.refresh = function (feeds) {
20+ const entries = this.findEntries(feeds)
21+ const channels = this.findChannels(entries)
22+ const users = this.findUsers(entries)
23+
24+ this.el.innerHTML = `
25+ <ul id='entries'>
26+ ${entries.filter((val, id) => { return id < 50 }).reduce((acc, val, id) => { return acc + this.templateEntry(val) + '\n' }, '')}
27+ </ul>
28+ <div id='sidebar'>
29+ <ul id='channels'>
30+ ${Object.keys(channels).reduce((acc, val, id) => { return acc + `<li>${val} <span class='right'>${channels[val]}</span></li>\n` }, '')}
31+ </ul>
32+ <ul id='users'>
33+ ${Object.keys(users).reduce((acc, val, id) => { return acc + `<li>${val} <span class='right'>${users[val]}</span></li>\n` }, '')}
34+ </ul>
35+ </div>
36+ <p>The <b>Hallway</b> is a decentralized forum, to join the conversation, simply create yourself a <a href="https://twtxt.readthedocs.io/en/stable/user/twtxtfile.html">twtxt</a> feed and <a href="https://github.com/XXIIVV/Webring/">add it</a> to your entry in the <a href="index.html">webring</a>.</p>`
37+ }
38+
39+ // Entries
40+
41+ this.findChannels = function (entries) {
42+ const channels = {}
43+ for (const id in entries) {
44+ const entry = entries[id]
45+ if (!entry.channel) { continue }
46+ channels[entry.channel] = channels[entry.channel] ? channels[entry.channel] + 1 : 1
47+ }
48+ return channels
49+ }
50+
51+ this.findUsers = function (entries) {
52+ const users = {}
53+ for (const id in entries) {
54+ const entry = entries[id]
55+ users[entry.author] = users[entry.author] ? users[entry.author] + 1 : 1
56+ }
57+ return users
58+ }
59+
60+ this.findEntries = function (feeds) {
61+ const a = []
62+ for (const id in feeds) {
63+ for (const i in feeds[id].content) {
64+ a.push(feeds[id].content[i])
65+ }
66+ }
67+ return a.sort(compare)
68+ }
69+
70+ this.templateEntry = function (entry) {
71+ const words = entry.body.split(' ')
72+ const users = []
73+ const channels = []
74+ const tags = []
75+
76+ // Channels
77+ for (const id in words) {
78+ if (words[id].substr(0, 1) === '@') { users.push(words[id]) }
79+ if (words[id].substr(0, 1) === '/') { channels.push(words[id]) }
80+ if (words[id].substr(0, 1) === '#') { tags.push(words[id]) }
81+ }
82+
83+ for (const id in users) {
84+ entry.body = entry.body.replace(users[id], `<span class='user'>${users[id]}</span>`)
85+ }
86+ for (const id in channels) {
87+ entry.body = entry.body.replace(channels[id], `<span class='user'>${channels[id]}</span>`)
88+ }
89+ for (const id in tags) {
90+ entry.body = entry.body.replace(tags[id], `<span class='tag'>${tags[id]}</span>`)
91+ }
92+
93+ const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
94+ const highlight = filter === entry.author.toLowerCase()
95+ const origin = feeds[entry.author].path
96+
97+ return `<li class='entry ${highlight ? 'highlight' : ''}'><span class='date'>${timeAgo(Date.parse(entry.date))}</span> <a class='author' href='${origin}' target='_blank'>${entry.author}</a> <span class='body'>${entry.body}</span></li>`
98+ }
99+
100+ // Feeds
101+
102+ this.findFeeds = function () {
103+ console.log('Finding feeds..')
104+ for (const id in sites) {
105+ const site = sites[id]
106+ if (site.feed && site.author) {
107+ feeds[site.author] = { path: site.feed }
108+ }
109+ }
110+ console.log(`Found ${Object.keys(feeds).length} feeds.`)
111+ }
112+
113+ this.fetchFeeds = function () {
114+ console.log(`Fetching ${Object.keys(feeds).length} feeds..`)
115+ for (const id in feeds) {
116+ this.fetchFeed(id, feeds[id])
117+ }
118+ }
119+
120+ this.fetchFeed = function (id, feed) {
121+ console.log(`Fetching ${id}(${feed.path})..`)
122+ fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => {
123+ feeds[id].content = parseFeed(id, content)
124+ this.refresh(feeds)
125+ }).catch((err) => {
126+ console.warn(`${id}`, err)
127+ })
128+ }
129+
130+ // Utils
131+
132+ function compare (a, b) {
133+ return a.offset < b.offset ? -1 : a.offset > b.offset ? 1 : 0
134+ }
135+
136+ function parseFeed (author, feed) {
137+ const lines = feed.split('\n')
138+ const entries = []
139+ for (const id in lines) {
140+ const line = lines[id].trim()
141+ if (line === '') { continue }
142+ const parts = line.replace(' ', '\t').split('\t')
143+ const date = parts[0].trim()
144+ const body = escapeHtml(parts[1].trim()).trim()
145+ const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : null
146+ const offset = new Date() - new Date(date)
147+ entries.push({ date, body, author, offset, channel })
148+ }
149+ return entries
150+ }
151+
152+ function timeAgo (dateParam) {
153+ const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
154+ const DAY_IN_MS = 86400000 // 24 * 60 * 60 * 1000
155+ const today = new Date()
156+ const yesterday = new Date(today - DAY_IN_MS)
157+ const seconds = Math.round((today - date) / 1000)
158+ const minutes = Math.round(seconds / 60)
159+ const isToday = today.toDateString() === date.toDateString()
160+ const isYesterday = yesterday.toDateString() === date.toDateString()
161+
162+ if (seconds < 5) {
163+ return 'now'
164+ } else if (seconds < 60) {
165+ return `${seconds} seconds ago`
166+ } else if (seconds < 90) {
167+ return 'about a minute ago'
168+ } else if (minutes < 60) {
169+ return `${minutes} minutes ago`
170+ } else if (isToday) {
171+ return `${Math.floor(minutes / 60)} hours ago`
172+ } else if (isYesterday) {
173+ return 'yesterday'
174+ }
175+
176+ return `${Math.floor(minutes / 1440)} days ago`
177+ }
178+
179+ function escapeHtml(unsafe) {
180+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
181+ }
182+}
scripts/opml.jsnew file+11
@@ -0,0 +1,11 @@
1+let outlines = sites.filter((site) => !!site.rss).map((site) => {
2+ return `<outline type="rss" xmlUrl="${site.rss}" title="${site.title}"/>`
3+})
4+
5+let template = `<?xml version="1.0" encoding="UTF-8"?><opml version="1.1"><head></head><body>${outlines.join('')}</body></opml>`
6+
7+window.onload = function () {
8+ let a = document.getElementById('opml')
9+ a.setAttribute('href', 'data:text/plain;charset=utf-8,' + window.encodeURIComponent(template))
10+ a.setAttribute('download', 'webring.opml')
11+}
scripts/portal.js+12−6
@@ -2,6 +2,7 @@
2
2
3function Portal (sites) {
3function Portal (sites) {
4 this.el = document.createElement('div')
4 this.el = document.createElement('div')
5+ this.el.id = 'portal'
5 this.sites = sites
6 this.sites = sites
6
7
7 // Templates
8 // Templates
@@ -11,17 +12,21 @@ function Portal (sites) {
11 }
12 }
12
13
13 function _buttons () {
14 function _buttons () {
14− return `<p class='buttons'><a href='#random' onClick="portal.reload('random')">Random</a> | <a href='https://github.com/XXIIVV/webring'>Information</a> <a id='icon' href='#random' onClick="portal.reload('random')"></a></p>`
15+ return `<p class='buttons'><a href='#random' onClick="portal.reload('random')">Random</a> | <a href='https://github.com/XXIIVV/webring'>Information</a> <a id='icon' href='#random' onClick="portal.reload('random')"></a> | <a href="hallway.html">The Hallway</a> | <a id="opml">OPML</a></p>`
15 }
16 }
16
17
17 function _directory (sites) {
18 function _directory (sites) {
18 return `
19 return `
19− <ul>${sites.reduce((acc, val, id) => { return `${acc}<li>${id}) <a href='${val}'>${val.split('//')[1]}</a></li>` }, '')}</ul>\n${_readme()}${_buttons()}`
20+ <ul>${sites.reduce((acc, site, id) => { return `${acc}<li class='${site.type}'><span class='counter'>${id})</span><a href='${site.url}'>${_name(site)}</a></li>` }, '')}</ul>\n${_readme()}${_buttons()}`
20 }
21 }
21
22
22− function _redirect (target) {
23+ function _name (site) {
24+ return site.title ? site.title : `${site.url.split('//')[1]}`
25+ }
26+
27+ function _redirect (site) {
23− return `<p>Redirecting to <b>${target}</b></p><meta http-equiv="refresh" content="3; url=${target}">
28+ return `<p>Redirecting to <a href="${site.url}">${site.url}</a></p><meta http-equiv="refresh" content="3; url=${site.url}">
24− <p class='buttons'><a href='#' onClick="portal.reload('')">Directory</a> | <a href='#${target}' onClick="portal.reload('random')">Skip</a> | <a href='#random' onClick="portal.reload('random')">Random</a> | <a href='https://github.com/XXIIVV/webring'>Information</a> <a id='icon' href='#random' onClick="portal.reload('random')"></a></p>`
29+ <p class='buttons'><a href='#' onClick="portal.reload('')">Directory</a> | <a href='#${site.url}' onClick="portal.reload('random')">Skip</a> | <a href='#random' onClick="portal.reload('random')">Random</a> | <a href='https://github.com/XXIIVV/webring'>Information</a> <a id='icon' href='#random' onClick="portal.reload('random')"></a></p>`
25 }
30 }
26
31
27 //
32 //
@@ -50,7 +55,8 @@ function Portal (sites) {
50 }
55 }
51
56
52 for (const id in this.sites) {
57 for (const id in this.sites) {
58+ const site = this.sites[id]
53− if (this.sites[id].indexOf(hash) > -1) {
59+ if (site.url.indexOf(hash) > -1) {
54 return parseInt(id)
60 return parseInt(id)
55 }
61 }
56 }
62 }
scripts/sites.js+87−66
@@ -1,70 +1,91 @@
1−'use strict';
1+'use strict'
2
2
3// Don't forget the comma! No trailing slashes
3// Don't forget the comma! No trailing slashes
4−// protocole://url.domain.ext
4+// protocol://url.domain.ext
5
5
6const sites = [
6const sites = [
7− "https://wiki.xxiivv.com"
7+ { url: 'https://wiki.xxiivv.com', title: 'xxiivv', type: 'wiki', author: 'neauoire', contact: 'aliceffekt@gmail.com', rss: 'https://wiki.xxiivv.com/links/rss.xml', feed: 'https://wiki.xxiivv.com/twtxt.txt' },
8− , "http://estevancarlos.com"
8+ { url: 'http://estevancarlos.com' },
9− , "https://electro.pizza"
9+ { url: 'https://electro.pizza', title: 'electro pizza', type: 'blog', author: 'rho', rss: 'https://electro.pizza/feed.xml', feed: 'https://electro.pizza/twtxt.txt' },
10− , "https://monochromatic.co"
10+ { url: 'https://ianjbattaglia.co' },
11− , "https://joshavanier.github.io"
11+ { url: 'https://avanier.now.sh/wiki', title: 'Ath', type: 'wiki', author: 'Joshavanier', contact: 'joshavanier@protonmail.com' },
12− , "http://kaemura.com"
12+ { url: 'http://kaemura.com' },
13− , "https://liamcooke.com"
13+ { url: 'https://liamcooke.com' },
14− , "https://electricgecko.de"
14+ { url: 'https://electricgecko.de' },
15− , "https://wichniow.ski"
15+ { url: 'https://wichniow.ski' },
16− , "https://hraew.autophagy.io"
16+ { url: 'https://hraew.autophagy.io' },
17− , "https://evenunto.net"
17+ { url: 'http://evenunto.net' },
18− , "https://anxl.faith"
18+ { url: 'https://anxl.faith' },
19− , "https://xvw.github.io"
19+ { url: 'https://xvw.github.io', title: 'xvw.github.io', type: 'blog', author: 'xvw', contact: 'xaviervdw@gmail.com', rss: 'https://xvw.github.io/atom.xml' },
20− , "https://heracl.es"
20+ { url: 'https://heracl.es' },
21− , "https://felipecortez.net"
21+ { url: 'https://felipecortez.net' },
22− , "http://luminghao.com"
22+ { url: 'http://luminghao.com' },
23− , "https://theiceshelf.com"
24− , "https://turelio.github.io"
23+ { url: 'https://turelio.github.io' },
25− , "https://spaceshipsin.space"
24+ { url: 'https://spaceshipsin.space', title: 'spaceshipsin.space', type: 'hybrid', author: 'gaeel', contact: 'gaeel@spaceshipsin.space', feed: 'https://spaceshipsin.space/twtxts/hallway.txt' },
26− , "http://log.lectronice.com"
25+ { url: 'https://log.lectronice.com', title: 'lectrolog.', type: 'blog', author: 'lectronice', contact: 'i.love.spam@lectronice.com', feed: 'https://lectronice.com/hallway/twtxt.txt' },
27− , "https://craze.co.uk"
26+ { url: 'https://craze.co.uk' },
28− , "https://shaneckel.com"
27+ { url: 'https://shaneckel.com' },
29− , "https://cblgh.org"
28+ { url: 'https://cblgh.org', title: 'cblgh.org', author: 'cobleigh', contact: 'cabal://cblgh.org', feed: 'https://cblgh.org/twtxt.txt' },
30− , "https://ellugar.co"
29+ { url: 'https://ellugar.co', title: 'ellugar', type: 'wiki', author: 'afk', contact: 'afk@ellugar.co', rss: 'http://feeds.ellugar.co/ellugar-logs' },
31− , "http://hur.bet"
30+ { url: 'http://hur.bet' },
32− , "http://chigby.org"
31+ { url: 'http://chigby.org' },
33− , "https://longest.voyage"
32+ { url: 'https://longest.voyage' },
34− , "https://palomakop.tv"
33+ { url: 'https://palomakop.tv' },
35− , "https://travisshears.com"
34+ { url: 'https://travisshears.com' },
36− , "https://v-os.ca"
35+ { url: 'https://v-os.ca' },
37− , "http://secretinternet.club"
36+ { url: 'http://secretinternet.club' },
38− , "https://jmandel.xyz"
37+ { url: 'https://jmandel.xyz' },
39− , "https://systems.ws"
38+ { url: 'https://systems.ws' },
40− , "https://jamesin.space"
39+ { url: 'https://jamesin.space' },
41− , "https://nathanwentworth.co"
40+ { url: 'https://nathanwentworth.co' },
42− , "https://eswat.ca"
41+ { url: 'https://eswat.ca' },
43− , "https://uonai.space"
42+ { url: 'https://uonai.space' },
44− , "http://controls.ee"
43+ { url: 'http://controls.ee' },
45− , "https://wasin.io"
44+ { url: 'https://wasin.io' },
46− , "https://inns.studio"
45+ { url: 'https://inns.studio' },
47− , "http://kokorobot.ca"
46+ { url: 'http://kokorobot.ca' },
48− , "https://ameyama.com"
47+ { url: 'https://ameyama.com', title: '雨山', type: 'blog', author: 'jrc03c', contact: 'josh@ameyama.com', rss: 'https://ameyama.com/blog/rss.xml' },
49− , "https://wake.st"
48+ { url: 'https://wake.st', title: 'wake.st', author: 'wakest', contact: '@liaizon@wake.st', feed: 'https://wake.st/twtxt.txt' },
50− , "https://xarene.la"
49+ { url: 'https://xarene.la' },
51− , "https://alex.zyzhang.me"
50+ { url: 'https://alex.zyzhang.me' },
52− , "http://bildwissenschaft.vortok.info"
51+ { url: 'http://bildwissenschaft.vortok.info' },
53− , "https://jakofranko.github.com"
52+ { url: 'https://jakofranko.github.com' },
54− , "https://aeriform.io"
53+ { url: 'https://aeriform.io' },
55− , "http://blog.lucasdidthis.com"
54+ { url: 'http://blog.lucasdidthis.com' },
56− , "http://npisanti.com"
55+ { url: 'http://npisanti.com', title: 'npisanti.com', author: 'npisanti', contact: 'nicola@npisanti.com', rss: 'http://npisanti.com/journal/rss.xml' },
57− , "https://underscorediscovery.ca"
56+ { url: 'https://underscorediscovery.ca' },
58− , "https://drisc.io"
57+ { url: 'https://drisc.io', title: 'drisc.io', type: 'wiki', author: 'drisc', contact: 'cory@drisc.io', feed: 'https://drisc.io/hallway/twtxt.txt' },
59− , "https://neufv.website"
58+ { url: 'https://neufv.systems', author: 'neufv', feed: 'https://neufv.systems/twtxt.txt' },
60− , "https://simbolo.xyz"
61− , "https://ricky.codes"
59+ { url: 'https://ricky.codes' },
62− , "https://maxdeviant.com"
60+ { url: 'https://maxdeviant.com' },
63− , "https://tynandebold.com"
61+ { url: 'https://tynandebold.com' },
64− , "http://gytis.co"
62+ { url: 'http://gytis.co' },
65− , "https://nomand.co"
63+ { url: 'https://nomand.co' },
66− , "http://memoriata.com"
64+ { url: 'http://memoriata.com' },
67− , "https://mmm.s-ol.nu"
65+ { url: 'https://mmm.s-ol.nu' },
68− , "https://chad.is"
66+ { url: 'https://chad.is' },
69− , "https://smidgeo.com/bots"
67+ { url: 'https://smidgeo.com/bots' },
68+ { url: 'https://iko.soy' },
69+ { url: 'http://atelieroilandsugar.com' },
70+ { url: 'https://magoz.is' },
71+ { url: 'https://szymonkaliski.com' },
72+ { url: 'https://phse.net', title: 'phse.net', type: 'blog', author: 'phse', rss: 'https://phse.net/post/index.xml', feed: 'https://gist.githubusercontent.com/setphen/55a1865f90d8c3f918d4217dbd3bd490/raw/3399acc89fa20449cadd4597ca3fe043afcea719/feed.txt' },
73+ { url: 'https://rosano.ca' },
74+ { url: 'https://soyboysky.github.io' },
75+ { url: 'https://gndclouds.cc' },
76+ { url: 'https://xuv.be' },
77+ { url: 'https://dsdshcym.github.io' },
78+ { url: 'https://wiki.chronica.xyz', title: 'chronica', type: 'wiki', author: 'ckipp', contact: 'ckipp@pm.me' },
79+ { url: 'https://boffosocko.com' },
80+ { url: 'https://hex22.org' },
81+ { url: 'https://patrikarvidsson.com' },
82+ { url: 'https://sophieleetmaa.com' },
83+ { url: 'https://xinniw.github.io' },
84+ { url: 'https://mboxed.github.io/forida' },
85+ { url: 'https://letters.vexingworkshop.com' },
86+ { url: 'https://tom.org.nz/about' },
87+ { url: 'https://teknari.com' },
88+ { url: 'https://colectivo-de-livecoders.gitlab.io/', title: 'Colectivo de Livecoders', type: 'blog', author: 'clic', contact: 'https://t.me/clic_laplata' },
89+ { url: 'https://www.madewithtea.com', title: 'madewithtea.com', type: 'blog' },
90+ { url: 'https://amorris.ca', title: 'amorris', author: 'amorris', type: 'blog', feed: 'https://feed.amorris.ca/hallway.txt' }
70−];
91+]