Commit 4eabd19d authored by Emma's avatar Emma 🦉

Merge branch 'feat-keyset-pagination' into 'master'

completely redo the way submissions are fetched

Closes #8 and #11

See merge request edgyemma/Postmill!42
parents 049b378e d4325c10
Pipeline #20294577 passed with stage
in 4 minutes and 41 seconds
multi:
controller: App\Controller\ForumController::multi
defaults: { sortBy: hot, page: 1 }
path: /f/{names}/{sortBy}/{page}
defaults: { sortBy: hot }
path: /f/{names}/{sortBy}
requirements:
names: '(?:\w{3,25}\+){1,70}\w{3,25}'
sortBy: hot|new
sortBy: "%submission_sort_modes%"
forum:
controller: App\Controller\ForumController::front
defaults: { sortBy: hot, page: 1 }
path: /f/{forum_name}/{sortBy}/{page}
defaults: { sortBy: hot }
path: /f/{forum_name}/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
forum_feed:
controller: App\Controller\ForumController::feed
defaults: { sortBy: hot, page: 1, _format: xml }
path: /f/{forum_name}/{sortBy}/{page}.atom
defaults: { sortBy: hot, _format: xml }
path: /f/{forum_name}/{sortBy}.atom
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
forum_feed_legacy_redirect:
controller: FrameworkBundle:Redirect:redirect
defaults: { route: forum_feed, ignoreAttributes: [page] }
path: /f/{forum_name}/{sortBy}/{page}.atom
requirements: { page: \d+ }
edit_forum:
controller: App\Controller\ForumController::editForum
......
......@@ -6,10 +6,10 @@ manage_forum_categories:
forum_category:
controller: App\Controller\ForumCategoryController::category
defaults: { sortBy: hot, page: 1 }
path: /c/{name}/{sortBy}/{page}
defaults: { sortBy: hot }
path: /c/{name}/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
create_forum_category:
controller: App\Controller\ForumCategoryController::create
......
front:
controller: App\Controller\FrontController::front
defaults: { sortBy: hot, page: 1 }
path: /{sortBy}/{page}
defaults: { sortBy: hot }
path: /{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
featured:
controller: App\Controller\FrontController::featured
defaults: { sortBy: hot, page: 1}
path: /featured/{sortBy}/{page}
defaults: { sortBy: hot }
path: /featured/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
subscribed:
controller: App\Controller\FrontController::subscribed
defaults: { sortBy: hot, page: 1 }
path: /subscribed/{sortBy}/{page}
defaults: { sortBy: hot }
path: /subscribed/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
all:
controller: App\Controller\FrontController::all
defaults: { sortBy: hot, page: 1 }
path: /all/{sortBy}/{page}
defaults: { sortBy: hot }
path: /all/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
moderated:
controller: App\Controller\FrontController::moderated
defaults: { sortBy: hot, page: 1 }
path: /moderated/{sortBy}/{page}
defaults: { sortBy: hot }
path: /moderated/{sortBy}
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
featured_feed:
controller: App\Controller\FrontController::featuredFeed
defaults: { sortBy: hot, page: 1, _format: xml }
path: /featured/{sortBy}/{page}.atom
defaults: { sortBy: hot, _format: xml }
path: /featured/{sortBy}.atom
methods: [GET]
requirements: { sortBy: hot|new, page: \d+ }
requirements: { sortBy: "%submission_sort_modes%" }
featured_feed_legacy_redirect:
controller: FrameworkBundle:Redirect:redirect
defaults: { route: featured_feed, ignoreAttributes: true }
methods: [GET]
path: /featured/{sortBy}/{_page}.atom
requirements: { sortBy: "%submission_sort_modes%", page: \d+ }
......@@ -6,6 +6,7 @@ parameters:
uuid_regex: '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}'
wiki_page_regex: '[A-Za-z][A-Za-z0-9_-]*(/[A-Za-z][A-Za-z0-9_-]*)*'
env(APP_ENABLE_WEBHOOKS): false
submission_sort_modes: hot|new|top|controversial|most_commented
services:
_defaults:
......
......@@ -2,10 +2,16 @@
namespace App\Controller;
use App\Repository\Submission\SubmissionPager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
abstract class AbstractController extends BaseAbstractController {
protected function submissionPage(string $sortBy, Request $request): array {
return SubmissionPager::getParamsFromRequest($sortBy, $request);
}
protected function validateCsrf(string $id, string $token) {
if (!$this->isCsrfTokenValid($id, $token)) {
throw new BadRequestHttpException('Invalid CSRF token');
......
......@@ -16,13 +16,16 @@ use Symfony\Component\HttpFoundation\Response;
class ForumCategoryController extends AbstractController {
public function category(
ForumCategory $category,
string $sortBy, int $page,
string $sortBy,
ForumRepository $fr,
SubmissionRepository $sr
SubmissionRepository $sr,
Request $request
): Response {
$forums = $fr->findForumsInCategory($category);
$submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page);
$submissions = $sr->findSubmissions($sortBy, [
'forums' => array_keys($forums),
], $this->submissionPage($sortBy, $request));
return $this->render('forum_category/category.html.twig', [
'category' => $category,
......
......@@ -34,27 +34,34 @@ use Symfony\Component\HttpFoundation\Response;
* @Entity("user", expr="repository.findOneOrRedirectToCanonical(username, 'username')")
*/
final class ForumController extends AbstractController {
/**
* @var SubmissionRepository
*/
private $submissions;
/**
* @var bool
*/
private $enableWebhooks;
public function __construct(bool $enableWebhooks) {
public function __construct(SubmissionRepository $submissions, bool $enableWebhooks) {
$this->submissions = $submissions;
$this->enableWebhooks = $enableWebhooks;
}
/**
* Show the front page of a given forum.
*
* @param SubmissionRepository $sr
* @param Forum $forum
* @param string $sortBy
* @param int $page
* @param Forum $forum
* @param string $sortBy
*
* @return Response
*/
public function front(SubmissionRepository $sr, Forum $forum, string $sortBy, int $page) {
$submissions = $sr->findForumSubmissions($forum, $sortBy, $page);
public function front(Forum $forum, string $sortBy, Request $request): Response {
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => [$forum->getId()],
'stickies' => true,
], $this->submissionPage($sortBy, $request));
return $this->render('forum/forum.html.twig', [
'forum' => $forum,
......@@ -63,8 +70,7 @@ final class ForumController extends AbstractController {
]);
}
public function multi(ForumRepository $fr, SubmissionRepository $sr,
string $names, string $sortBy, int $page) {
public function multi(ForumRepository $fr, string $names, string $sortBy, Request $request) {
$names = preg_split('/[^\w]+/', $names, -1, PREG_SPLIT_NO_EMPTY);
$names = array_map(Forum::class.'::normalizeName', $names);
$names = $fr->findForumNames($names);
......@@ -73,7 +79,9 @@ final class ForumController extends AbstractController {
throw $this->createNotFoundException('no such forums');
}
$submissions = $sr->findFrontPageSubmissions($names, $sortBy, $page);
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => array_keys($names),
], $this->submissionPage($sortBy, $request));
return $this->render('forum/multi.html.twig', [
'forums' => $names,
......@@ -148,17 +156,18 @@ final class ForumController extends AbstractController {
}
/**
* @param Forum $forum
* @param SubmissionRepository $sr
* @param string $sortBy
* @param int $page
* @param Forum $forum
* @param string $sortBy
* @param Request $request
*
* @return Response
*/
public function feed(Forum $forum, SubmissionRepository $sr, string $sortBy, int $page) {
public function feed(Forum $forum, string $sortBy, Request $request) {
return $this->render('forum/feed.xml.twig', [
'forum' => $forum,
'submissions' => $sr->findForumSubmissions($forum, $sortBy, $page),
'submissions' => $this->submissions->findSubmissions($sortBy, [
'forums' => [$forum->getId()],
], $this->submissionPage($sortBy, $request)),
]);
}
......
......@@ -5,6 +5,8 @@ namespace App\Controller;
use App\Entity\User;
use App\Repository\ForumRepository;
use App\Repository\SubmissionRepository;
use App\Repository\Submission\SubmissionPager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
......@@ -25,7 +27,25 @@ use Symfony\Component\HttpFoundation\Response;
* instead.
*/
final class FrontController extends AbstractController {
public function front(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) {
/**
* @var ForumRepository
*/
private $forums;
/**
* @var SubmissionRepository
*/
private $submissions;
public function __construct(
ForumRepository $forums,
SubmissionRepository $submissions
) {
$this->forums = $forums;
$this->submissions = $submissions;
}
public function front(string $sortBy, Request $request): Response {
$user = $this->getUser();
if (!$user instanceof User) {
......@@ -38,21 +58,24 @@ final class FrontController extends AbstractController {
switch ($listing) {
case User::FRONT_SUBSCRIBED:
return $this->subscribed($fr, $sr, $sortBy, $page);
return $this->subscribed($sortBy, $request);
case User::FRONT_FEATURED:
return $this->featured($fr, $sr, $sortBy, $page);
return $this->featured($sortBy, $request);
case User::FRONT_ALL:
return $this->all($sr, $sortBy, $page);
return $this->all($sortBy, $request);
case User::FRONT_MODERATED:
return $this->moderated($fr, $sr, $sortBy, $page);
return $this->moderated($sortBy, $request);
default:
throw new \InvalidArgumentException('bad front page selection');
}
}
public function featured(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) {
$forums = $fr->findFeaturedForumNames();
$submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page);
public function featured(string $sortBy, Request $request): Response {
$forums = $this->forums->findFeaturedForumNames();
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => array_keys($this->forums->findFeaturedForumNames()),
], $this->submissionPage($sortBy, $request));
return $this->render('front/featured.html.twig', [
'forums' => $forums,
......@@ -62,17 +85,19 @@ final class FrontController extends AbstractController {
]);
}
public function subscribed(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) {
public function subscribed(string $sortBy, Request $request): Response {
$this->denyAccessUnlessGranted('ROLE_USER');
$forums = $fr->findSubscribedForumNames($this->getUser());
$hasSubscriptions = count($forums) > 0;
$forums = $this->forums->findSubscribedForumNames($this->getUser());
$hasSubscriptions = \count($forums) > 0;
if (!$hasSubscriptions) {
$forums = $fr->findFeaturedForumNames();
$forums = $this->forums->findFeaturedForumNames();
}
$submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page);
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => array_keys($forums),
], $this->submissionPage($sortBy, $request));
return $this->render('front/subscribed.html.twig', [
'forums' => $forums,
......@@ -83,15 +108,9 @@ final class FrontController extends AbstractController {
]);
}
/**
* @param SubmissionRepository $sr
* @param string $sortBy
* @param int $page
*
* @return Response
*/
public function all(SubmissionRepository $sr, string $sortBy, int $page) {
$submissions = $sr->findAllSubmissions($sortBy, $page);
public function all(string $sortBy, Request $request): Response {
$submissions = $this->submissions->findSubmissions($sortBy, [],
$this->submissionPage($sortBy, $request));
return $this->render('front/all.html.twig', [
'listing' => 'all',
......@@ -100,11 +119,14 @@ final class FrontController extends AbstractController {
]);
}
public function moderated(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page) {
public function moderated(string $sortBy, Request $request): Response {
$this->denyAccessUnlessGranted('ROLE_USER');
$forums = $fr->findModeratedForumNames($this->getUser());
$submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page);
$forums = $this->forums->findModeratedForumNames($this->getUser());
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => array_keys($forums),
], $this->submissionPage($sortBy, $request));
return $this->render('front/moderated.html.twig', [
'forums' => $forums,
......@@ -114,9 +136,12 @@ final class FrontController extends AbstractController {
]);
}
public function featuredFeed(ForumRepository $fr, SubmissionRepository $sr, string $sortBy, int $page = 1) {
$forums = $fr->findFeaturedForumNames();
$submissions = $sr->findFrontPageSubmissions($forums, $sortBy, $page);
public function featuredFeed(string $sortBy, Request $request): Response {
$forums = $this->forums->findFeaturedForumNames();
$submissions = $this->submissions->findSubmissions($sortBy, [
'forums' => array_keys($forums),
], $this->submissionPage($sortBy, $request));
return $this->render('front/featured.xml.twig', [
'forums' => $forums,
......
......@@ -215,6 +215,10 @@ class Submission extends Votable {
return $this->comments;
}
public function getCommentCount(): int {
return \count($this->comments);
}
/**
* Get top-level comments, ordered by descending net score.
*
......
<?php
namespace App\Repository\Submission;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class NoSubmissionsException extends \OutOfBoundsException implements HttpExceptionInterface {
public function __construct() {
parent::__construct('There are no submissions to display');
}
public function getStatusCode(): int {
return 404;
}
public function getHeaders(): array {
return [];
}
}
<?php
namespace App\Repository\Submission;
use App\Entity\Submission;
use App\Repository\SubmissionRepository;
use Symfony\Component\HttpFoundation\Request;
class SubmissionPager implements \IteratorAggregate {
/**
* @var string[]
*/
private $nextPageParams = [];
/**
* @var Submission[]
*/
private $submissions = [];
public static function getParamsFromRequest(string $sortBy, Request $request): array {
if (!isset(SubmissionRepository::SORT_COLUMN_MAP[$sortBy])) {
throw new \InvalidArgumentException("Invalid sort mode '$sortBy'");
}
$params = [];
foreach (SubmissionRepository::SORT_COLUMN_MAP[$sortBy] as $column) {
$value = $request->query->get('next_'.$column);
$type = SubmissionRepository::SORT_COLUMN_TYPES[$column];
if ($value === null || !self::valueIsOfType($type, $value)) {
// missing columns - no pagination
return [];
}
$params[$column] = $value;
}
// complete pager params
return $params;
}
/**
* @param Submission[]|iterable $submissions List of submissions, including
* one more than $maxPerPage to
* tell if there's a next page
* @param int $maxPerPage
* @param string $sortBy property to use for pagination
*/
public function __construct(iterable $submissions, int $maxPerPage, string $sortBy) {
if (!isset(SubmissionRepository::SORT_COLUMN_MAP[$sortBy])) {
throw new \InvalidArgumentException("Invalid sort mode '$sortBy'");
}
$count = 0;
foreach ($submissions as $submission) {
if (++$count > $maxPerPage) {
foreach (SubmissionRepository::SORT_COLUMN_MAP[$sortBy] as $column) {
$accessor = $this->columnNameToAccessor($column);
$value = $submission->{$accessor}();
$this->nextPageParams['next_'.$column] = $value;
}
break;
}
$this->submissions[] = $submission;
}
}
public function getIterator() {
return new \ArrayIterator($this->submissions);
}
public function hasNextPage(): bool {
return (bool) $this->nextPageParams;
}
/**
* @throws \BadMethodCallException if there is no next page
*/
public function getNextPageParams(): array {
if (!$this->hasNextPage()) {
throw new \BadMethodCallException('There is no next page');
}
return $this->nextPageParams;
}
private function columnNameToAccessor(string $columnName): string {
return 'get'.str_replace('_', '', ucwords($columnName, '_'));
}
private static function valueIsOfType(string $type, $value): bool {
switch ($type) {
case 'integer':
return ctype_digit($value) && \is_int(+$value) &&
$value >= 0x80000000 && $value <= 0x7fffffff;
case 'bigint':
// if this causes problems on 32-bit systems, the site operators
// deserved it.
return ctype_digit($value) && \is_int(+$value);
default:
throw new \InvalidArgumentException("Unexpected type '$type'");
}
}
}
This diff is collapsed.
{% with {attr: app.request.attributes, get: app.request.query.all} %}
{% if pager.hasPreviousPage %}
<link rel="prev" href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? {})|merge(get)|merge({page: pager.previousPage})) }}">
{% if pager.hasPreviousPage ?? false %}
<link rel="prev" href="{{ path(
attr.get('_route'),
(attr.get('_route_params') ?? {})|merge(get)|merge({page: pager.previousPage})
) }}">
{% endif %}
{% if pager.hasNextPage %}
<link rel="next" href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? {})|merge(get)|merge({page: pager.nextPage})) }}">
<link rel="next" href="{{ path(
attr.get('_route'),
(attr.get('_route_params') ?? {})|merge(get)|merge(pager.nextPageParams ?? {page: pager.nextPage})
) }}">
{% endif %}
{% endwith %}
{% with {
attr: app.request.attributes,
get: app.request.query.all,
hasPrev: pager.hasPreviousPage,
hasPrev: pager.hasPreviousPage ?? false,
hasNext: pager.hasNextPage,
} %}
{% if hasPrev or hasNext %}
......@@ -16,7 +16,7 @@
{% endif %}
{% if hasNext %}
<li class="next">
<a href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? {})|merge(get)|merge({page: pager.nextPage})) }}">
<a href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? {})|merge(get)|merge(pager.nextPageParams ?? {page: pager.nextPage})) }}">
{{ 'nav.next'|trans }}
</a>
</li>
......
{% macro pagination(pager) %}
{% with { route: app.request.attributes.get('_route'), params: app.request.attributes.get('_route_params') ?? {} } %}
{% if pager.hasNextPage %}
<link rel="next" href="{{ url(route, params|merge({page: pager.nextPage})) }}"/>
{% endif %}
{% if pager.hasPreviousPage %}
<link rel="previous" href="{{ url(route, params|merge({page: pager.nextPage})) }}"/>
<link rel="next" href="{{ url(route, params|merge(pager.nextPageParams)) }}"/>
{% endif %}
{% endwith %}
{% endmacro %}
......@@ -33,7 +33,7 @@
{%- macro submission_filter(choice, sort_by) -%}
{% with { active: choice == 'featured' } %}
<li class="tabs__tab {{ active ? 'tabs__tab--active active' }}">
<a href="{{ path('featured', {sortBy: sort_by, page: 1}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
<a href="{{ path('featured', {sortBy: sort_by}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
{{- 'front.featured'|trans -}}
</a>
</li>
......@@ -42,7 +42,7 @@
{% if is_granted('ROLE_USER') %}
{% with { active: choice == 'subscribed' } %}
<li class="tabs__tab {{ active ? 'tabs__tab--active active' }}">
<a href="{{ path('subscribed', {sortBy: sort_by, page: 1}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
<a href="{{ path('subscribed', {sortBy: sort_by}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
{{- 'front.subscribed'|trans -}}
</a>
</li>
......@@ -51,7 +51,7 @@
{% with { active: choice == 'all' } %}
<li class="tabs__tab {{ active ? 'tabs__tab--active active' }}">
<a href="{{ path('all', {sortBy: sort_by, page: 1}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
<a href="{{ path('all', {sortBy: sort_by}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
{{- 'front.all'|trans -}}
</a>
</li>
......@@ -60,7 +60,7 @@
{% if choice == 'moderated' or app.user and app.user.moderatorTokens|length > 0 %}
{% with { active: choice == 'moderated' } %}
<li class="tabs__tab {{ active ? 'tabs__tab--active active' }}">
<a href="{{ path('moderated', {sortBy: sort_by, page: 1}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
<a href="{{ path('moderated', {sortBy: sort_by}) }}" class="tabs__link {{ active ? 'tabs__link--active' }}">
{{- 'nav.moderated'|trans -}}
</a>
</li>
......
......@@ -10,9 +10,9 @@
{%- macro submission_sort(current) -%}
{%- set attr = app.request.attributes -%}
{%- spaceless -%}
{%- for type in ['hot', 'new'] -%}
{%- for type in ['hot', 'new', 'top', 'controversial', 'most_commented'] -%}
<li class="tabs__tab {{ type == current ? 'tabs__tab--active active' }}">
<a href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? [])|merge({page: 1, sortBy: type})) }}" class="tabs__link {{ type == current ? 'tabs__link--active' }}">
<a href="{{ path(attr.get('_route'), (attr.get('_route_params') ?? [])|merge({sortBy: type})) }}" class="tabs__link {{ type == current ? 'tabs__link--active' }}">
{{- ('submissions.sort_by_'~type)|trans -}}
</a>
</li>
......
......@@ -14,7 +14,7 @@ class ApplicationAvailabilityTest extends WebTestCase {
* @param string $url
*/
public function testCanAccessPublicPages($url) {
$client = $this->createClient();
$client = self::createClient();
$client->request('GET', $url);
$this->assertTrue($client->getResponse()->isSuccessful());
......@@ -26,7 +26,7 @@ class ApplicationAvailabilityTest extends WebTestCase {
* @param string $url
*/
public function testCanAccessPagesThatNeedAuthentication($url) {
$client = $this->createClient([], [
$client = self::createClient([], [
'PHP_AUTH_USER' => 'emma',
'PHP_AUTH_PW' => 'goodshit',
]);
......@@ -41,7 +41,7 @@ class ApplicationAvailabilityTest extends WebTestCase {
* @param string $url
*/
public function testCannotAccessPagesThatNeedAuthenticationWhenNotAuthenticated($url) {
$client = $this->createClient();
$client = self::createClient();
$client->request('GET', $url);
$this->assertTrue($client->getResponse()->isRedirect());
......@@ -55,10 +55,12 @@ class ApplicationAvailabilityTest extends WebTestCase {
* @param string $url
*/
public function testRedirectedUrlsGoToExpectedLocation($expectedLocation, $url) {
$client = $this->createClient();
$client = self::createClient();
$client->followRedirects();
$client->request('GET', $url);
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(
"http://localhost{$expectedLocation}",
$client->getCrawler()->getUri()
......@@ -73,29 +75,37 @@ class ApplicationAvailabilityTest extends WebTestCase {
yield ['/'];
yield ['/hot'];
yield ['/new'];
yield ['/hot/1'];
yield ['/new/1'];
yield ['/top'];
yield ['/controversial'];
yield ['/most_commented'];
yield ['/all/hot'];
yield ['/all/new'];
yield ['/all/hot/1'];
yield ['/all/new/1'];
yield ['/all/top'];
yield [