feed_url;
542 | $type = 'feed';
543 | }
544 |
545 |
546 | $sid = $this->feedID($url);
547 | $source = [
548 | 'sourceid' => $sid,
549 | 'sourceurl' => $url,
550 | 'added' => time(),
551 | 'type' => $type,
552 | ];
553 |
554 | $sql = "SELECT * FROM sources WHERE sourceid = ?";
555 | $result = $this->db->queryRecord($sql, [$sid]);
556 | if ($result) {
557 | throw new Exception("[$sid] Source already exists");
558 | }
559 |
560 | $this->db->saveRecord('sources', $source);
561 | return $source;
562 | }
563 |
564 | /**
565 | * Get all sources
566 | *
567 | * @return array
568 | */
569 | public function getSources()
570 | {
571 | $sql = "SELECT * FROM sources ORDER BY added DESC";
572 | return $this->db->queryAll($sql);
573 | }
574 |
575 | /**
576 | * Fetch a single list source and add new suggestions
577 | *
578 | * @param array $source A source record
579 | * @return int number of added suggestions
580 | * @throws Exception
581 | */
582 | public function fetchSourceList($source)
583 | {
584 | $lines = file_get_contents($source['sourceurl']);
585 | if (!$lines) throw new Exception('Could not fetch source list');
586 |
587 | $lines = explode("\n", $lines);
588 | $new = 0;
589 | foreach ($lines as $itemUrl) {
590 | $itemUrl = trim($itemUrl);
591 | if (empty($itemUrl)) continue;
592 |
593 | try {
594 | // check if we've seen this item already
595 | $this->rememberSourceSuggestion($itemUrl);
596 | // add the suggestion
597 | $this->suggestFeed($itemUrl);
598 | $new++;
599 | echo '✓';
600 | } catch (Exception $e) {
601 | // ignore
602 | echo '✗';
603 | }
604 | }
605 | return $new;
606 | }
607 |
608 | /**
609 | * Fetch a single OPML source and add new suggestions
610 | *
611 | * @param array $source A source record
612 | * @return int number of added suggestions
613 | * @throws Exception
614 | */
615 | public function fetchSourceOpml($source)
616 | {
617 | $lines = file_get_contents($source['sourceurl']);
618 | if (!$lines) throw new Exception('Could not fetch source list');
619 |
620 | $xml = simplexml_load_string($lines);
621 | if (!$xml) throw new Exception('Could not parse OPML');
622 |
623 | $new = 0;
624 | foreach ($xml->body->outline as $item) {
625 | $itemUrl = (string)$item['xmlUrl'];
626 | if (empty($itemUrl)) continue;
627 |
628 | try {
629 | // check if we've seen this item already
630 | $this->rememberSourceSuggestion($itemUrl);
631 | // add the suggestion
632 | $this->suggestFeed($itemUrl);
633 | $new++;
634 | echo '✓';
635 | } catch (Exception $e) {
636 | // ignore
637 | echo '✗';
638 | }
639 | }
640 | return $new;
641 | }
642 |
643 | /**
644 | * Fetch a single source and add new suggestions
645 | *
646 | * @param array $source A source record
647 | * @return int number of added suggestions
648 | * @throws Exception
649 | */
650 | public function fetchSource($source)
651 | {
652 | if ($source['type'] == 'feed') {
653 | return $this->fetchSourceFeed($source);
654 | } elseif ($source['type'] == 'list') {
655 | return $this->fetchSourceList($source);
656 | } elseif ($source['type'] == 'opml') {
657 | return $this->fetchSourceOpml($source);
658 | } else {
659 | throw new Exception('Unknown source type');
660 | }
661 | }
662 |
663 | /**
664 | * Fetch a single feed source and add new suggestions
665 | *
666 | * @param array $source A source record
667 | * @return int number of added suggestions
668 | * @throws Exception
669 | */
670 | public function fetchSourceFeed($source)
671 | {
672 | $simplePie = new SimplePie();
673 | $simplePie->enable_cache(false);
674 | $simplePie->set_feed_url($source['sourceurl']);
675 | $simplePie->force_feed(true); // no autodetect here
676 |
677 | if (!$simplePie->init()) {
678 | throw new Exception($simplePie->error());
679 | }
680 |
681 | $items = $simplePie->get_items();
682 | if (!$items) throw new Exception('no items found');
683 |
684 | $new = 0;
685 | foreach ($items as $item) {
686 | $itemUrl = $item->get_permalink();
687 | try {
688 | // check if we've seen this item already
689 | $this->rememberSourceSuggestion($itemUrl);
690 | // add the suggestion
691 | $this->suggestFeed($itemUrl);
692 | $new++;
693 | echo '✓';
694 | } catch (Exception $e) {
695 | // ignore
696 | echo '✗';
697 | }
698 | }
699 | return $new;
700 | }
701 |
702 | /**
703 | * Remember that this URL has been suggested in the past
704 | *
705 | * @param string $itemUrl
706 | * @throws Exception if the URL has been suggested before
707 | */
708 | protected function rememberSourceSuggestion($itemUrl)
709 | {
710 | $hash = $this->feedID($itemUrl);
711 |
712 | $sql = "SELECT seen FROM seen WHERE seen = ?";
713 | $seen = $this->db->queryValue($sql, [$hash]);
714 | if ($seen) throw new Exception('Already suggested');
715 |
716 | // remember the item to not suggest it again
717 | // we also remember it when it fails to add in the next step to not retry fails
718 | $sql = "INSERT INTO seen (seen) VALUES (?)";
719 | $this->db->queryValue($sql, [$hash]);
720 | }
721 | }
722 |
--------------------------------------------------------------------------------
/src/Mastodon.php:
--------------------------------------------------------------------------------
1 | httpget($homepage);
22 |
23 | // simplify homepage url
24 | $homepage = new Url($homepage);
25 | $homepage = $homepage->getHost() . rtrim($homepage->getPath(), '/');
26 |
27 | $dom = new Document();
28 | $dom->html($html);
29 | $links = $dom->find('a[rel=me]');
30 |
31 | foreach ($links as $link) {
32 | $href = $link->attr('href') . '.json';
33 | $json = $this->httpget($href);
34 | $data = json_decode($json, true);
35 |
36 | if ($data && isset($data['attachment'])) foreach ($data['attachment'] as $attachment) {
37 | if ($attachment['type'] === 'PropertyValue' && (stripos($attachment['value'], $homepage) !== false)) {
38 | $server = new Url($data['url']);
39 | return trim($server->getPath(),'/') . '@' . $server->getHost();
40 | }
41 | }
42 | }
43 |
44 | return '';
45 | }
46 |
47 | /**
48 | * Simple HTTP client
49 | *
50 | * @param string $url
51 | * @param array $headers
52 | * @return string
53 | */
54 | public function httpget($url, $headers = [])
55 | {
56 | $ch = curl_init();
57 | curl_setopt($ch, CURLOPT_URL, $url);
58 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
59 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
60 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
61 | curl_setopt($ch, CURLOPT_TIMEOUT, 25);
62 | $output = curl_exec($ch);
63 | curl_close($ch);
64 | return $output;
65 | }
66 |
67 | /**
68 | * Post to Mastodon
69 | *
70 | * @param string $status Status to post
71 | * @param string $instance Mastodon instance
72 | * @param string $token Mastodon Access Token
73 | * @return mixed
74 | */
75 | public function postStatus($status, $instance, $token)
76 | {
77 | $headers = [
78 | "Authorization: Bearer $token"
79 | ];
80 |
81 | $status_data = [
82 | "status" => $status,
83 | "language" => "en",
84 | "visibility" => "public"
85 | ];
86 |
87 | $ch = curl_init();
88 | curl_setopt($ch, CURLOPT_URL, "$instance/api/v1/statuses");
89 | curl_setopt($ch, CURLOPT_POST, 1);
90 | curl_setopt($ch, CURLOPT_POSTFIELDS, $status_data);
91 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
92 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
93 | $output = curl_exec($ch);
94 | curl_close ($ch);
95 |
96 | return json_decode($output, true);
97 | }
98 |
99 | /**
100 | * Post a single item to Mastodon
101 | *
102 | * @param string[] $post
103 | * @param string $instance
104 | * @param string $token
105 | * @return mixed
106 | */
107 | public function postItem($post, $instance, $token)
108 | {
109 |
110 | $text = $post['itemtitle'];
111 | $text .= ' (' . date('Y-m-d', $post['published']) . ')';
112 | if ($post['mastodon']) {
113 | $text .= ' by ' . $post['mastodon'];
114 | }
115 | $text .= "\n\n" . Controller::campaignURL($post['itemurl'], 'mastodon');
116 |
117 | $text .= "\n\n🎲 " . $post['feedid'] . '-' . $post['itemid'];
118 | $text .= "\n#blog #blogging #blogpost #random";
119 |
120 | return $this->postStatus($text, $instance, $token);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/RSS.php:
--------------------------------------------------------------------------------
1 | [1, 3, 5, 10, 20, 25],
25 | 7 => [5, 10, 15, 25],
26 | ];
27 |
28 |
29 | /**
30 | * Constructor
31 | */
32 | public function __construct()
33 | {
34 | $loader = new FilesystemLoader(__DIR__ . '/../templates');
35 | $this->twig = new Environment($loader);
36 | $this->feedManager = new FeedManager();
37 | }
38 |
39 | /**
40 | * Force a refresh of the feed
41 | */
42 | public function forceRefresh()
43 | {
44 | $this->force = true;
45 | }
46 |
47 | /**
48 | * Returns the wanted feed content
49 | *
50 | * @param int $freq wanted frequency in days
51 | * @param int $num wanted number of posts
52 | * @return false|string
53 | */
54 | public function getFeed($freq = 1, $num = 5)
55 | {
56 | // ensure only valid values are used
57 | if ($freq < 1) $freq = 1;
58 | if ($num < 1) $num = 1;
59 | if (!isset($this->feeds[$freq])) $freq = 1;
60 | while (!in_array($num, $this->feeds[$freq])) {
61 | $num--;
62 | }
63 |
64 | $cache = $this->getCacheName($freq, $num);
65 | if (!file_exists($cache)) {
66 | // this should not happen, but if it does, create the feed
67 | $this->createFeed($freq, $num);
68 | }
69 | return file_get_contents($cache);
70 | }
71 |
72 | /**
73 | * Where a feed file is cached
74 | *
75 | * @param int $freq wanted frequency in days
76 | * @param int $num wanted number of posts
77 | * @return string
78 | */
79 | protected function getCacheName($freq, $num)
80 | {
81 | return __DIR__ . '/../data/rss/' . $freq . '.' . $num . '.xml';
82 | }
83 |
84 | /**
85 | * Create a feed file
86 | *
87 | * @param int $freq wanted frequency in days
88 | * @param int $num wanted number of posts
89 | * @return int|string Name of the created file or time in seconds until the next update
90 | */
91 | public function createFeed($freq = 1, $num = 5)
92 | {
93 | $cache = $this->getCacheName($freq, $num);
94 |
95 | $now = time();
96 | $valid = @filemtime($cache) - ($now - $freq * 60 * 60 * 24);
97 |
98 |
99 | if ($valid < 0 || $this->force) {
100 | $creator = new \UniversalFeedCreator();
101 | $creator->title = 'indieblog.page daily random posts';
102 | $creator->description = 'Discover the IndieWeb, one blog post at a time.';
103 | $creator->link = 'https://indieblog.page';
104 |
105 | $result = $this->feedManager->getRandoms([], $num + 10); // get more than we need, to compensate for errors
106 | $added = 0;
107 | foreach ($result as $data) {
108 | try {
109 | $data = $this->fetchAdditionalData($data);
110 | } catch (\Exception $e) {
111 | continue;
112 | }
113 | $data['itemurl'] = Controller::campaignURL($data['itemurl'], 'rss');
114 | $data['feedurl'] = Controller::campaignURL($data['feedurl'], 'rss');
115 |
116 | $item = new \FeedItem();
117 | $item->title = '🎲 ' . $data['itemtitle'];
118 | $item->link = $data['itemurl'];
119 | $item->date = $now--; // separate each post by a second, first one being the newest
120 | $item->source = $data['feedurl'];
121 | $item->author = $data['feedtitle'];
122 | $item->description = $this->twig->render('partials/rssitem.twig', ['item' => $data]);
123 | $creator->addItem($item);
124 |
125 | if (++$added >= $num) break;
126 | }
127 | $creator->saveFeed('RSS2.0', $cache, false);
128 | return $cache;
129 | }
130 | return $valid;
131 | }
132 |
133 | /**
134 | * Create all feeds
135 | *
136 | * @param LoggerInterface $logger
137 | */
138 | public function createAllFeeds(LoggerInterface $logger){
139 | foreach ($this->feeds as $freq => $nums){
140 | foreach ($nums as $num){
141 | $logger->info('Creating feed for '.$freq.' days and '.$num.' posts');
142 | $ret = $this->createFeed($freq, $num);
143 | if(is_int($ret)){
144 | $logger->info('Feed still valid for ' . $ret . ' seconds');
145 | } else {
146 | $logger->success('Feed created: {feed}', ['feed' => $ret]);
147 | }
148 | }
149 | }
150 | }
151 |
152 | /**
153 | * Enhance the given item with data fetched from the web
154 | *
155 | * @param string[] $item
156 | * @return string[]
157 | * @throws \Exception
158 | */
159 | protected function fetchAdditionalData($item)
160 | {
161 | $goose = new GooseClient();
162 | $article = $goose->extractContent($item['itemurl']);
163 |
164 | $text = $article->getCleanedArticleText();
165 | $desc = $article->getMetaDescription();
166 | if (strlen($text) > strlen($desc)) {
167 | $summary = $text;
168 | } else {
169 | $summary = $desc;
170 | }
171 | if (mb_strlen($summary) > 500) {
172 | $summary = mb_substr($summary, 0, 500) . '…';
173 | }
174 | $item['summary'] = $summary;
175 |
176 | return $item;
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/templates/401.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | 401 - Unauthorized
5 |
6 | Sorry, you need to be logged in.
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/templates/404.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | 404 - Not Found
5 |
6 | Sorry, whatever you were looking for isn't here.
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/templates/admin.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | Admin
5 |
6 |
12 |
13 | {% include 'partials/feedback.twig' %}
14 |
15 |
28 |
29 |
35 |
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/templates/all.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block metahead %}
4 |
5 |
6 |
42 | {% endblock %}
43 |
44 | {% block content %}
45 | List of all Blogs
46 |
47 |
48 | Here's a list of all available blogs with their most recent post. You can sort by clicking on the column
49 | headers. You can also filter by typing in the input fields.
50 |
51 |
52 |
53 |
Sorry, this feature needs JavaScript
54 |
55 |
56 |
203 |
204 |
205 | All the above data is also available as JSON export .
206 | {% endblock %}
207 |
--------------------------------------------------------------------------------
/templates/faq.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | Frequently asked Questions
5 |
6 |
7 | These questions might have been asked frequently if I hadn't answered them here ;-)
8 |
9 |
10 |
11 | Why does this site exist?
12 |
13 | Because I wanted it.
14 |
15 |
16 | There is a small renaissance of having your own, personal website, independent of the
17 | large corporate entities. A place for your thoughts and ideas that you own and control. It's sometimes
18 | called the IndieWeb or
19 | SmolNet - back in my day it was just having a
20 | homepage.
21 |
22 |
23 |
24 | I love reading text written by real people. Texts that don't want to sell something.
25 | But how can you discover texts you can't search for because you don't know they exist?
26 |
27 |
28 |
29 | That's where this page comes in. Click a button, be surprised and maybe discover your new favorite thing.
30 |
31 |
32 |
33 |
34 | What are the sources?
35 |
36 | I initially seeded the database with personal websites from the following sources:
37 |
38 |
48 |
49 |
50 | To further grow it, you can suggest your own or a friend's personal site (as long as it has an RSS feed):
51 | Suggest a page .
52 |
53 |
54 |
55 |
56 | How many blogs and posts are in the database?
57 |
58 | Here are the current statistics:
59 |
60 |
61 | {{ stats.feeds }} websites with a feed
62 | {{ stats.mastodon }} websites with a verified Mastodon account
63 | {{ stats.items }} posts overall
64 | {{ stats.recentitems }} posts in the last six months
65 | {{ (stats.size / 1024 / 1024) |round(2) }} MiB database size
66 | {{ stats.suggestions }} pending suggestions
67 |
68 |
69 |
70 | Currently only recent posts (published within the last six months) are used when
71 | picking a random post. Below is a visualization of the number of recent posts per week.
72 |
73 |
74 |
75 | {% set maxposts = max(stats.weeklyposts) %}
76 | {% for week, posts in stats.weeklyposts %}
77 |
78 | W{{ week }}: {{ posts }} posts
79 |
80 | {% endfor %}
81 |
82 |
83 |
84 |
85 | Can I have the data?
86 |
87 | Sure, you can download the list of blog URLs as JSON here:
88 |
89 | Download JSON
90 |
91 |
92 |
93 | Broken Links, Spam, etc.
94 |
95 | People abandon or sell their domains. Things break. Sites get hacked.
96 |
97 |
98 | If you were sent to a broken site, please let me know at
99 | andi@splitbrain.org . Be sure to include the ID shown under each
100 | visited link on the front page - it helps me to identify the broken URLs.
101 |
102 |
103 |
104 | Please also let me know if you come across things that don't fit the spirit of personal webpages.
105 | Things like YouTube channels, corporate blogs, etc. should not be in the index but might have slipped
106 | through in the initial setup.
107 |
108 |
109 |
110 |
111 | Are there any alternatives?
112 |
113 | There are other attempts at making the indieweb discoverable.
114 |
115 |
116 |
117 | Blog Surf : a blog search engine. It lists a few random posts
118 | on the start page, but they are sorted by some kind of popularity score.
119 |
120 |
121 | Marginalia : another IndieWeb search engine.
122 | This one has an option to visually browse random sites.
123 |
124 |
125 | BlogDB : a small selection of blogs, but not necessarily personal ones.
126 | It has a random blog mechanism.
127 |
128 |
129 | ooh.directory : a Yahoo! style directory of blogs categorized
130 | by topic. It has a random blog mechanism.
131 |
132 |
133 |
134 |
135 |
136 | What tech does this run on?
137 |
138 | This is a very simple, custom PHP application standing on the shoulders of giants:
139 |
150 |
151 | The rest is just glue code. You can see it all on
152 | Github .
153 |
154 |
155 |
156 | {% endblock %}
157 |
--------------------------------------------------------------------------------
/templates/index.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | 👋 Hi there!
5 |
6 |
7 | This website lets you randomly explore the IndieWeb. Simply click the button below and you
8 | will be redirected to a random post from a personal blog.
9 |
10 |
11 |
12 |
13 | 🎲 Open Random Blog Post
14 |
15 |
16 |
17 |
18 | Disclaimer: the content linked to is aggregated automatically. I neither endorse nor necessarily agree with
19 | the linked sites. Use at your own risk.
20 |
21 |
22 |
23 | Hint: you can drag the button to your bookmarks and have it always available when you want to be
24 | inspired.
25 |
26 |
27 |
28 | {% include 'partials/seen.twig' %}
29 |
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/templates/inspect.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | Inspect
5 |
6 |
7 | {% if feed %}
8 |
9 | Title: {{ feed.feedtitle }}
10 | Feed URL: {{ feed.feedurl }}
11 | Homepage: {{ feed.homepage }}
12 | Errors: {{ feed.errors }}
13 | Last Error: {{ feed.lasterror }}
14 | Added: {{ feed.added|date }}
15 | Last Fetch: {{ feed.fetched|date }}
16 |
17 |
18 |
19 | {% for item in items %}
20 | {{ item.itemtitle }} {{ item.published|date }}
21 | {% endfor %}
22 |
23 |
24 |
25 |
26 | Remove
27 | Reset Errors
28 |
29 | {% else %}
30 | Feed not found
31 | {% endif %}
32 |
33 |
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/templates/partials/feedback.twig:
--------------------------------------------------------------------------------
1 | {% if error %}
2 |
3 | {{ error }}
4 |
5 | {% endif %}
6 |
7 | {% if feed %}
8 |
9 | Your feed has been added to the list of suggestions and will be reviewed soon.
10 |
11 | {{ feed.feedtitle }}
12 |
13 |
14 |
16 |
17 |
18 |
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/templates/partials/layout.twig:
--------------------------------------------------------------------------------
1 | {% set title = 'Discover the IndieWeb, one blog post at a time.' %}
2 | {% set description = 'A website to randomly explore the IndieWeb. Simply click a button and you will be redirected to a random post from a personal blog.' %}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ title }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {% block metahead %}
23 | {% endblock %}
24 |
25 |
26 |
27 | indieblog.page
28 | {{ title }}
29 |
30 | {% include 'partials/navigation.twig' %}
31 |
32 |
33 |
34 | {% block content %}
35 | {% endblock %}
36 |
37 |
38 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/templates/partials/navigation.twig:
--------------------------------------------------------------------------------
1 |
2 | Home
3 | FAQ
4 | RSS
5 | List
6 | Suggest a blog
7 |
8 |
--------------------------------------------------------------------------------
/templates/partials/rssitem.twig:
--------------------------------------------------------------------------------
1 |
2 | {{ item.summary|nl2br }}
3 |
4 |
5 |
6 | This random indieblog.page link was picked on {{ "now"|date("l, F jS Y") }}. It was originally published on {{ item.published|date("l, F jS Y") }} at {{ item.feedtitle }} .
7 |
8 |
9 | If you'd like to report any problems with this post or the blog, please include the following ID with your report: {{ item.feedid }}-{{ item.itemid }}
10 |
11 |
--------------------------------------------------------------------------------
/templates/partials/seen.twig:
--------------------------------------------------------------------------------
1 | {% if seen %}
2 |
3 | Your recent visits
4 |
5 | These are the posts you recently saw - just in case you want to revisit them or subscribe.
6 |
7 |
28 |
29 | {% endif %}
30 |
--------------------------------------------------------------------------------
/templates/rss.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | RSS Feeds
5 |
6 |
7 | Use these feeds to get your daily or weekly dose of random blog posts, straight to your RSS reader.
8 |
9 |
10 |
16 |
17 |
23 |
24 |
25 | If you want to stay up-to-date with changes made to indieblog.page itself, you can subscribe to
26 | the Github commits.
27 |
28 |
29 |
32 |
33 | Mastodon
34 |
35 |
36 | You can follow the indieblog.page Mastodon account for
37 | a random post twice a day. Occasional updates about the service itself will be posted as well.
38 |
39 |
40 | {% endblock %}
41 |
--------------------------------------------------------------------------------
/templates/suggest.twig:
--------------------------------------------------------------------------------
1 | {% extends 'partials/layout.twig' %}
2 |
3 | {% block content %}
4 | Suggest a blog
5 |
6 |
7 | Use this form to add a new source. Enter the URL to an RSS/ATOM feed or a homepage link.
8 |
9 |
10 |
11 |
22 |
23 |
24 |
25 | What sites can be submitted?
26 |
27 |
28 | I haven't decided on any firm rules. If it's a personal site with an RSS feed, it is probably
29 | welcome.
30 |
31 |
32 | No illegal stuff, no corporate blogs, no nazis.
33 |
34 |
35 |
36 | I tried to submit my site, but got an error!?
37 |
38 |
39 | Make sure your site has a valid RSS or ATOM feed. JSON Feed is not supported.
40 | If you're not sure, you can check with a service like
41 | the W3C Feed Validation Service . Pay especially close
42 | attention that your feed is served with the correct MIME type.
43 |
44 |
45 |
46 | When submitting your site, the feed parser will try to autodetect the feed URL. If it fails, you can
47 | always submit the feed URL manually.
48 |
49 |
50 |
51 |
52 | Using the Android Share Menu
53 |
54 |
55 | When you install indieblog.page as a Progressive Web App (PWA), you can find it in the share menu and
56 | easily suggest new blogs that way. If you missed the prompt earlier, you can still find the option to
57 | install in your browser's menu.
58 |
59 |
60 |
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------