on master help sign in
Merge branch 'master' into master
14c7069Hamish MacDonald
.github/ISSUE_TEMPLATE/broken-link.mdnew file+10
@@ -0,0 +1,10 @@
1+---
2+name: broken.link
3+about: Report a broken link in the webring
4+title: ''
5+labels: ''
6+assignees: neauoire, kodedninja, joshavanier, ckipp01
7+
8+---
9+
10+**Name of the site:**
.github/ISSUE_TEMPLATE/inappropriate-content.mdnew file+14
@@ -0,0 +1,14 @@
1+---
2+name: inappropriate.content
3+about: Report inappropriate content in the webring
4+title: ''
5+labels: ''
6+assignees: neauoire, kodedninja, joshavanier, ckipp01
7+
8+---
9+
10+**Name of the site:**
11+
12+
13+
14+**Please explain what is inappropriate and link if necessary:**
.github/ISSUE_TEMPLATE/name-of-website.mddeleted−9
@@ -1,9 +0,0 @@
1−---
2−name: name.of.website
3−about: Describe this issue template's purpose here.
4−
5−---
6−
7−```
8−Position of webring icon on site: ex: footer, header
9−```
.github/PULL_REQUEST_TEMPLATE/add-site.mdnew file+14
@@ -0,0 +1,14 @@
1+---
2+name: add.site
3+about: Submitting a new site to the webring
4+title: ''
5+labels: ''
6+assignees: neauoire, kodedninja, joshavanier, ckipp01
7+
8+---
9+
10+**Position of webring icon on site:**
11+
12+ex: footer, header
13+
14+**Make sure the contact key is also filled out in case we need to contact you**
README.md+13−5
@@ -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**. 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**.
4
4
5## Join the webring
5## Join the webring
6
6
@@ -9,10 +9,18 @@ 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 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`, `rss`, and `feed`.
12+- Add your website information to the [sites.js](https://github.com/XXIIVV/webring/edit/master/scripts/sites.js) file. The `url` and `contact` keys are required, but you can also include `title`, `type`, `author`, `rss`, and `feed`.
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 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**.
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.
16+
17+### Webring criteria
18+
19+**Single page websites, websites acting only as portals to other social platforms and websites with content involving violence, racism, sexism or speciesism, will be rejected**.
20+
21+The aim of the webring is to dipslay **hand-crafted personal** websites. Your business site is probably not the best fit for the webring, and will be rejected.
22+
23+If it's seen that your website is in violation to any of these rules your site will be removed from the webring. If you fix the issues, feel free to submit another PR to join back in.
16
24
17### Circular Linking
25### Circular Linking
18
26
@@ -35,7 +43,7 @@ To join, create a `.txt` file on your site, and add its URL to [your webring ent
352015-12-12T12:00:00+01:00 Fiat lux!
432015-12-12T12:00:00+01:00 Fiat lux!
36```
44```
37
45
38−This file needs to be accessible via CORS, so if you're self-hosting, please make sure to allow the `webring.xxiivv.com` origin.
46+This file needs to be accessible via CORS, so if you're self-hosting, please make sure to allow the `webring.xxiivv.com` origin.
39Here's an example of how to do so with Nginx:
47Here's an example of how to do so with Nginx:
40
48
41```
49```
@@ -61,4 +69,4 @@ This repository does not contain mature API capabilities, but there are a couple
61
69
62## Need Help?
70## Need Help?
63
71
64−The ring is managed by [@neauoire](https://twitter.com/neauoire), but any member of the network is also welcome to join this repository as a collaborator to help manage new links and Pull Requests. Read more [here](https://wiki.xxiivv.com/webring).
72+The ring is managed by [@neauoire](https://twitter.com/neauoire), but any member of the network is also welcome to join this repository as a collaborator to help manage new links and Pull Requests. Read more about the webring [here](https://wiki.xxiivv.com/webring).
links/main.css+6−2
@@ -1,7 +1,7 @@
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: #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;}
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;}
5body div ul { max-width: 800px; border-bottom: 2px solid white;padding-bottom: 15px;margin-bottom:15px; overflow: hidden;}
5body div ul { max-width: 800px; border-bottom: 2px solid white;padding-bottom: 15px;margin-bottom:15px; overflow: hidden;}
6body div p { display:block; padding:0px 5px; line-height: 20px; max-width: 600px}
6body div p { display:block; padding:0px 5px; line-height: 20px; max-width: 600px}
7body div p a { text-decoration: underline;}
7body div p a { text-decoration: underline;}
@@ -12,7 +12,11 @@ body div p.readme { max-width:500px; }
12/* Hallway */
12/* Hallway */
13
13
14body #hallway #entries { width: calc(100vw - 300px) !important; }
14body #hallway #entries { width: calc(100vw - 300px) !important; }
15−body #hallway #sidebar { position: absolute;top: 0px;right: 0px;padding: 0px 15px;width: 200px; }
15+body #hallway #sidebar { position: absolute;top: 0px;right: 0px;padding: 15px;width: 200px; }
16+
17+body #hallway #pagination { margin: 10px; text-align: center; }
18+body #hallway #pagination span { padding: 3px; margin: 2px; cursor: pointer; }
19+body #hallway #pagination span.selected { background: white; color:black; }
16
20
17body #hallway #sidebar > * { border-bottom: 0px; line-height: 16px; padding-bottom: 0px }
21body #hallway #sidebar > * { border-bottom: 0px; line-height: 16px; padding-bottom: 0px }
18body #hallway #sidebar > * li { text-transform: lowercase; }
22body #hallway #sidebar > * li { text-transform: lowercase; }
scripts/hallway.js+41−41
@@ -1,7 +1,7 @@
1'use strict'
1'use strict'
2
2
3const re_channel = /(\s|^)\/([a-zA-Z0-9]+)/g
3const re_channel = /(\s|^)\/([a-zA-Z0-9]+)/g
4−const re_user = /(\s|^)\@([a-zA-Z0-9]+)/g
4+const re_user = /((\s|^)@<[a-zA-Z0-9./:\-_\+~#= ]*>)/g
5const re_tag = /([^&]|^)\#([a-zA-Z0-9]+)/g
5const re_tag = /([^&]|^)\#([a-zA-Z0-9]+)/g
6const re_url = /((https?):\/\/(([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)([-a-zA-Z0-9@:%_\+.~#?&//=]*)))/g
6const re_url = /((https?):\/\/(([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)([-a-zA-Z0-9@:%_\+.~#?&//=]*)))/g
7
7
@@ -10,7 +10,8 @@ function Hallway (sites) {
10 this.sites = sites
10 this.sites = sites
11 this.el = document.createElement('div')
11 this.el = document.createElement('div')
12 this.el.id = 'hallway'
12 this.el.id = 'hallway'
13− this.filter = window.location.hash.replace('#', '').trim()
13+ const filterAndPage = window.location.hash.split('^')
14+ this.finder = { filter: filterAndPage[0].trim().replace('#', ''), page: filterAndPage[1] || 1 }
14 this.cache = null
15 this.cache = null
15
16
16 this.install = function (host) {
17 this.install = function (host) {
@@ -25,24 +26,30 @@ function Hallway (sites) {
25
26
26 this.refresh = function (feeds = this.cache) {
27 this.refresh = function (feeds = this.cache) {
27 const entries = this.findEntries(feeds)
28 const entries = this.findEntries(feeds)
28− const channels = this.findChannels(entries)
29+ const channels = this.find(entries, 'channel')
29− const users = this.findUsers(entries)
30+ const users = this.find(entries, 'author')
30 const tags = this.findTags(entries)
31 const tags = this.findTags(entries)
32+ const relevantEntries = entries.filter(val => !this.finder.filter || (val.author === this.finder.filter || val.channel === this.finder.filter || val.tags.includes(this.finder.filter)))
31
33
32 this.el.innerHTML = `
34 this.el.innerHTML = `
33− <ul id='entries'>
35+ <div id='entries'>
36+ <ul>
34− ${entries.filter((val, id) => { return !this.filter || (val.author === this.filter || val.channel === this.filter || val.tags.includes(this.filter)) }).filter((val, id) => { return id < 50 }).reduce((acc, val, id) => { return acc + this.templateEntry(val) + '\n' }, '')}
37+ ${entries.filter((val) => !this.finder.filter || (val.author === this.finder.filter || val.channel === this.finder.filter || val.tags.includes(this.finder.filter))).filter((_, id) => id < Number(this.finder.page) * 20 && id >= (Number(this.finder.page) - 1) * 20).reduce((acc, val) => acc + this.templateEntry(val) + '\n', '')}
35− </ul>
38+ </ul>
39+ <div id='pagination'>
40+ ${[...Array(Math.ceil(relevantEntries.length / 20)).keys()].reduce((acc, num) => `${acc}<span class='${Number(hallway.finder.page) === num + 1 ? 'selected' : ''}' onclick='filter("${this.finder.filter}^${num + 1}")'>${num + 1}</span>`, '')}
41+ </div>
42+ </div>
36 <div id='sidebar'>
43 <div id='sidebar'>
37 <ul id='channels'>
44 <ul id='channels'>
38− <li onclick='filter("")' class='${hallway.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${entries.length}</span></a></li>
45+ <li onclick='filter("")' class='${hallway.finder.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${entries.length}</span></a></li>
39− ${Object.keys(channels).reduce((acc, val, id) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n` }, '')}
46+ ${Object.keys(channels).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n`, '')}
40 </ul>
47 </ul>
41 <ul id='users'>
48 <ul id='users'>
42− ${Object.keys(users).reduce((acc, val, id) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n` }, '')}
49+ ${Object.keys(users).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n`, '')}
43 </ul>
50 </ul>
44 <ul id='tags'>
51 <ul id='tags'>
45− ${Object.keys(tags).reduce((acc, val, id) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}' href='#${val}'>#${val} <span class='right'>${tags[val]}</span></li>\n` }, '')}
52+ ${Object.keys(tags).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>#${val} <span class='right'>${tags[val]}</span></li>\n`, '')}
46 </ul>
53 </ul>
47 </div>
54 </div>
48 <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>`
55 <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>`
@@ -54,26 +61,15 @@ function Hallway (sites) {
54
61
55 // Entries
62 // Entries
56
63
57− this.findChannels = function (entries) {
64+ this.find = function (entries, p) {
58− const channels = {}
65+ const h = {}
59 for (const id in entries) {
66 for (const id in entries) {
60− const entry = entries[id]
67+ if (entries[id] && entries[id][p]) { h[entries[id][p]] = h[entries[id][p]] ? h[entries[id][p]] + 1 : 1 }
61− if (!entry.channel) { continue }
62− channels[entry.channel] = channels[entry.channel] ? channels[entry.channel] + 1 : 1
63 }
68 }
64− return channels
69+ return h
65 }
70 }
66
71
67− this.findUsers = function (entries) {
68− const users = {}
69− for (const id in entries) {
70− const entry = entries[id]
71− users[entry.author] = users[entry.author] ? users[entry.author] + 1 : 1
72− }
73− return users
74− }
75−
76− this.findTags = function (entries){
72+ this.findTags = function (entries) {
77 const tags = {}
73 const tags = {}
78 for (const id in entries) {
74 for (const id in entries) {
79 const entry = entries[id]
75 const entry = entries[id]
@@ -91,29 +87,33 @@ function Hallway (sites) {
91 a.push(feeds[id].content[i])
87 a.push(feeds[id].content[i])
92 }
88 }
93 }
89 }
94− return a.sort( (a, b) => a.offset - b.offset )
90+ return a.sort((a, b) => a.offset - b.offset)
91+ }
92+
93+ this.findMention = function (found) {
94+ const mention = Object.keys(feeds).filter(user => found.indexOf(feeds[user].path) > -1)
95+ return mention.length === 1 ? ` <span class='user'>@${mention[0]}</span>` : ` <span class='user'>${found}</span>`
95 }
96 }
96
97
97 this.templateEntry = function (entry) {
98 this.templateEntry = function (entry) {
98 entry.html = entry.body
99 entry.html = entry.body
99 .replace(re_channel, `$1<span class='channel'>/$2</span>`)
100 .replace(re_channel, `$1<span class='channel'>/$2</span>`)
100− .replace(re_user, `$1<span class='user'>@$2</span>`)
101+ .replace(re_user, this.findMention)
101 .replace(re_tag, `$1<span class='tag'>#$2</span>`)
102 .replace(re_tag, `$1<span class='tag'>#$2</span>`)
102 .replace(re_url, `<a target="_blank" href='$1'>$1</a>`)
103 .replace(re_url, `<a target="_blank" href='$1'>$1</a>`)
103−
104+
104 const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
105 const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
105 const highlight = filter === entry.author.toLowerCase()
106 const highlight = filter === entry.author.toLowerCase()
106 const origin = feeds[entry.author].path
107 const origin = feeds[entry.author].path
107
108
108 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.html}</span></li>`
109 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.html}</span></li>`
109 }
110 }
110−
111+
111 // Feeds
112 // Feeds
112
113
113 this.findFeeds = function () {
114 this.findFeeds = function () {
114 console.log('Finding feeds..')
115 console.log('Finding feeds..')
115− for (const id in sites) {
116+ for (const site of sites) {
116− const site = sites[id]
117 if (site.feed && site.author) {
117 if (site.feed && site.author) {
118 feeds[site.author] = { path: site.feed }
118 feeds[site.author] = { path: site.feed }
119 }
119 }
@@ -141,7 +141,7 @@ function Hallway (sites) {
141 // Utils
141 // Utils
142
142
143 function parseFeed (author, feed) {
143 function parseFeed (author, feed) {
144− const lines = feed.split('\n')
144+ const lines = feed.split('\n').filter((line) => line.substr(0, 1) !== '#')
145 const entries = []
145 const entries = []
146 for (const id in lines) {
146 for (const id in lines) {
147 const line = lines[id].trim()
147 const line = lines[id].trim()
@@ -150,7 +150,7 @@ function Hallway (sites) {
150 const date = parts[0].trim()
150 const date = parts[0].trim()
151 const body = escapeHtml(parts[1].trim()).trim()
151 const body = escapeHtml(parts[1].trim()).trim()
152 const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
152 const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
153− const tags = (body.match(re_tag)||[]).map(a => a.substr(a.indexOf('#')+1).toLowerCase())
153+ const tags = (body.match(re_tag) || []).map(a => a.substr(a.indexOf('#') + 1).toLowerCase())
154 const offset = new Date() - new Date(date)
154 const offset = new Date() - new Date(date)
155 entries.push({ date, body, author, offset, channel, tags })
155 entries.push({ date, body, author, offset, channel, tags })
156 }
156 }
@@ -159,9 +159,8 @@ function Hallway (sites) {
159
159
160 function timeAgo (dateParam) {
160 function timeAgo (dateParam) {
161 const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
161 const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
162− const DAY_IN_MS = 86400000 // 24 * 60 * 60 * 1000
163 const today = new Date()
162 const today = new Date()
164− const yesterday = new Date(today - DAY_IN_MS)
163+ const yesterday = new Date(today - 86400000)
165 const seconds = Math.round((today - date) / 1000)
164 const seconds = Math.round((today - date) / 1000)
166 const minutes = Math.round(seconds / 60)
165 const minutes = Math.round(seconds / 60)
167 const isToday = today.toDateString() === date.toDateString()
166 const isToday = today.toDateString() === date.toDateString()
@@ -183,14 +182,15 @@ function Hallway (sites) {
183
182
184 return `${Math.floor(minutes / 1440)} days ago`
183 return `${Math.floor(minutes / 1440)} days ago`
185 }
184 }
185+}
186
186
187− function escapeHtml (unsafe) {
187+function escapeHtml (unsafe) {
188− return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
188+ return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
189− }
190}
189}
191
190
192function filter (name) {
191function filter (name) {
193 window.location.hash = name
192 window.location.hash = name
194− hallway.filter = window.location.hash.replace('#', '').trim()
193+ const filterAndPage = window.location.hash.split('^')
194+ hallway.finder = { filter: filterAndPage[0].trim().replace('#', ''), page: filterAndPage[1] || 1 }
195 hallway.refresh()
195 hallway.refresh()
196}
196}
scripts/sites.js+4−5
@@ -8,8 +8,8 @@ const sites = [
8 { url: 'http://estevancarlos.com' },
8 { url: 'http://estevancarlos.com' },
9 { url: 'https://electro.pizza', title: 'electro pizza', type: 'blog', author: 'rho', rss: 'https://electro.pizza/feed.xml', feed: 'https://electro.pizza/twtxt.txt' },
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 { url: 'https://ianjbattaglia.co' },
10 { url: 'https://ianjbattaglia.co' },
11− { url: 'https://avanier.now.sh/wiki', title: 'Ath', type: 'wiki', author: 'Joshavanier', contact: 'joshavanier@protonmail.com' },
11+ { url: 'https://avanier.now.sh', title: 'Arachne', type: 'wiki', author: 'joshavanier', contact: 'joshavanier@protonmail.com' },
12− { url: 'http://kaemura.com', title: 'kaemura.com', type: 'blog', author: 'kaemura', contact: 'chimera1190@gmail.com', feed: 'https://kaemura.com/twttxt.txt'},
12+ { url: 'http://kaemura.com', title: 'kaemura.com', type: 'blog', author: 'kaemura', contact: 'chimera1190@gmail.com', feed: 'https://kaemura.com/twttxt.txt' },
13 { url: 'https://liamcooke.com' },
13 { url: 'https://liamcooke.com' },
14 { url: 'https://electricgecko.de' },
14 { url: 'https://electricgecko.de' },
15 { url: 'https://wichniow.ski' },
15 { url: 'https://wichniow.ski' },
@@ -33,12 +33,10 @@ const sites = [
33 { url: 'https://palomakop.tv' },
33 { url: 'https://palomakop.tv' },
34 { url: 'https://travisshears.com' },
34 { url: 'https://travisshears.com' },
35 { url: 'https://v-os.ca' },
35 { url: 'https://v-os.ca' },
36− { url: 'http://secretinternet.club' },
37 { url: 'https://jmandel.xyz' },
36 { url: 'https://jmandel.xyz' },
38 { url: 'https://systems.ws' },
37 { url: 'https://systems.ws' },
39 { url: 'https://jamesin.space' },
38 { url: 'https://jamesin.space' },
40 { url: 'https://nathanwentworth.co' },
39 { url: 'https://nathanwentworth.co' },
41− { url: 'https://eswat.ca' },
42 { url: 'https://uonai.space' },
40 { url: 'https://uonai.space' },
43 { url: 'http://controls.ee' },
41 { url: 'http://controls.ee' },
44 { url: 'https://wasin.io' },
42 { url: 'https://wasin.io' },
@@ -93,5 +91,6 @@ const sites = [
93 { url: 'https://notes.stuartpb.com/', title: 'notes.stuartpb.com', type: 'wiki', author: 'stuartpb', contact: 's@stuartpb.com', feed: 'https://twtxt.stuartpb.com/xxiivv.txt' },
91 { url: 'https://notes.stuartpb.com/', title: 'notes.stuartpb.com', type: 'wiki', author: 'stuartpb', contact: 's@stuartpb.com', feed: 'https://twtxt.stuartpb.com/xxiivv.txt' },
94 { url: 'http://xxiii.co', title: 'xxiii', type: 'portfolio', author: 'serocell', contact: 'psignal@s900.net', rss: 'http://serocell.com/feeds/serocell.xml', feed: 'http://xxiii.co/twtxt.txt' },
92 { url: 'http://xxiii.co', title: 'xxiii', type: 'portfolio', author: 'serocell', contact: 'psignal@s900.net', rss: 'http://serocell.com/feeds/serocell.xml', feed: 'http://xxiii.co/twtxt.txt' },
95 { url: 'https://kor.nz', title: 'kor', type: 'wiki', author: 'kormyen', contact: 'h@kor.nz', feed: 'https://kor.nz/twtxt.txt' },
93 { url: 'https://kor.nz', title: 'kor', type: 'wiki', author: 'kormyen', contact: 'h@kor.nz', feed: 'https://kor.nz/twtxt.txt' },
96− { url: 'http://xxiii.co', title: 'xxiii', type: 'wiki', author: 'serocell', contact: 'psignal@s900.net', rss: 'http://serocell.com/feeds/serocell.xml' }
94+ { url: 'https://lublin.se', author: 'quite', contact: 'quite@hack.org', feed: 'https://lublin.se/twtxt.txt' },
95+ { url: 'https://zanneth.com', title: 'zanneth.com', author: 'zanneth', type: 'blog', contact: 'root@zanneth.com' }
97]
96]