From b43313e7383bdd40d98e4132941ce54032a33b1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?=
Date: Thu, 20 Mar 2025 19:50:35 +0100
Subject: [PATCH] Redis and categories
---
Dockerfile | 2 +
assets/styles/app.css | 37 +++-
assets/styles/card.css | 8 +-
assets/styles/layout.css | 10 +-
assets/styles/theme.css | 2 +
config/bundles.php | 1 +
config/packages/cache.yaml | 12 +-
config/packages/fos_elastica.yaml | 2 +-
config/packages/framework.yaml | 2 +-
config/services.yaml | 36 ++++
src/Controller/ArticleController.php | 23 ++-
src/Controller/AuthorController.php | 17 +-
src/Controller/DefaultController.php | 164 +++++++++++++++++-
src/Entity/Article.php | 5 +-
src/EventListener/PopulateListener.php | 30 ++++
src/Provider/ArticleProvider.php | 27 +++
src/Repository/ArticleRepository.php | 1 +
src/Service/NostrClient.php | 5 +-
src/Twig/Components/SearchComponent.php | 13 +-
src/Util/IndexableArticleChecker.php | 14 ++
templates/components/Header.html.twig | 21 ++-
templates/components/Molecules/Card.html.twig | 13 +-
.../components/SearchComponent.html.twig | 28 +--
templates/home.html.twig | 2 +
templates/pages/article.html.twig | 9 +
templates/pages/author.html.twig | 32 ++++
templates/pages/category.html.twig | 13 ++
27 files changed, 480 insertions(+), 49 deletions(-)
create mode 100644 src/EventListener/PopulateListener.php
create mode 100644 src/Provider/ArticleProvider.php
create mode 100644 src/Util/IndexableArticleChecker.php
create mode 100644 templates/pages/category.html.twig
diff --git a/Dockerfile b/Dockerfile
index bd32add..e35e415 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -34,6 +34,8 @@ RUN set -eux; \
opcache \
zip \
gmp \
+ gd \
+ redis \
;
# https://getcomposer.org/doc/03-cli.md#composer-allow-superuser
diff --git a/assets/styles/app.css b/assets/styles/app.css
index 4431759..f87a760 100644
--- a/assets/styles/app.css
+++ b/assets/styles/app.css
@@ -25,6 +25,15 @@ h1 {
font-weight: 600;
}
+h1.brand {
+ font-family: var(--brand-font), serif;
+ font-size: 3.6rem;
+}
+
+h1.brand a {
+ color: white;
+}
+
h2 {
font-size: 3rem;
}
@@ -49,6 +58,18 @@ p {
margin: 0 0 15px;
}
+aside h1 {
+ font-size: 2rem;
+}
+
+aside h2 {
+ font-size: 1.7rem;
+}
+
+aside p.lede {
+ font-size: 1.2rem;
+}
+
.lede {
font-family: var(--main-body-font), serif;
font-size: 1.6rem;
@@ -102,7 +123,7 @@ svg.icon {
background-color: var(--color-bg);
color: var(--color-text);
padding: 0;
- margin: 20px 0;
+ margin: 20px 0 50px 0;
border-radius: 0; /* Sharp edges */
}
@@ -112,8 +133,6 @@ svg.icon {
.card-header {
font-size: 1.5rem;
- margin-bottom: 10px;
- padding-bottom: 10px;
}
.header__image {
@@ -165,16 +184,26 @@ svg.icon {
display: flex;
justify-content: center;
gap: 2em;
+ padding: 0;
}
.header__categories li {
list-style: none;
}
-.header__logo h1 {
+.header__categories li a:hover {
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.header__categories li.active a {
font-weight: bold;
}
+.header__logo h1 {
+ font-weight: normal;
+}
+
.header__logo img {
height: 40px; /* Adjust the height as needed */
}
diff --git a/assets/styles/card.css b/assets/styles/card.css
index 8b13789..6dc331a 100644
--- a/assets/styles/card.css
+++ b/assets/styles/card.css
@@ -1 +1,7 @@
-
+.card-header img {
+ max-width: 100%;
+ height: auto;
+ max-height: 200px;
+ width: 100%;
+ object-fit: cover;
+}
diff --git a/assets/styles/layout.css b/assets/styles/layout.css
index 24af931..e457c54 100644
--- a/assets/styles/layout.css
+++ b/assets/styles/layout.css
@@ -23,6 +23,7 @@
nav {
width: auto;
+ min-width: 150px;
flex-shrink: 0;
padding: 1em;
overflow-y: auto; /* Ensure the menu is scrollable if content is too long */
@@ -56,7 +57,10 @@ main {
/* Right sidebar */
aside {
- width: 180px; /* Adjust the width based on the left menu */
+ width: 190px;
+ min-width: 150px;
+ flex-shrink: 0;
+ flex-grow: 0;
padding: 1em;
}
@@ -92,3 +96,7 @@ footer {
position: relative;
width: 100%;
}
+
+.search input {
+ flex-grow: 1;
+}
diff --git a/assets/styles/theme.css b/assets/styles/theme.css
index bdcdac0..39eecae 100644
--- a/assets/styles/theme.css
+++ b/assets/styles/theme.css
@@ -1,6 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Josefin+Slab:ital,wght@0,100..700;1,100..700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
:root {
--color-bg: #222; /* Black background */
@@ -15,4 +16,5 @@
--font-family: 'Montserrat'; /* Set the Montserrat font as default */
--main-body-font: 'Newsreader'; /* Set the font for the main body */
--heading-font: 'Josefin Slab'; /* Set the font for headings */
+ --brand-font: 'Lobster';
}
diff --git a/config/bundles.php b/config/bundles.php
index 181adf4..dcbd623 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -15,4 +15,5 @@ return [
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
FOS\ElasticaBundle\FOSElasticaBundle::class => ['all' => true],
+ Endroid\QrCodeBundle\EndroidQrCodeBundle::class => ['all' => true],
];
diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml
index 6899b72..86c2e75 100644
--- a/config/packages/cache.yaml
+++ b/config/packages/cache.yaml
@@ -1,19 +1,17 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
- #prefix_seed: your_vendor_name/app_name
+ prefix_seed: newsroom/app
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
- #app: cache.adapter.redis
- #default_redis_provider: redis://localhost
-
- # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
- #app: cache.adapter.apcu
+ app: cache.adapter.redis
+ default_redis_provider: Redis
# Namespaced pools use the above "app" backend by default
- #pools:
+ pools:
#my.dedicated.cache: null
+ subscriptions.cache: null
diff --git a/config/packages/fos_elastica.yaml b/config/packages/fos_elastica.yaml
index 316fd1f..ded92e8 100644
--- a/config/packages/fos_elastica.yaml
+++ b/config/packages/fos_elastica.yaml
@@ -8,6 +8,7 @@ fos_elastica:
indexes:
# create the index by running php bin/console fos:elastica:populate
articles:
+ indexable_callback: [ 'App\Util\IndexableArticleChecker', 'isIndexable' ]
properties:
title: ~
summary: ~
@@ -20,4 +21,3 @@ fos_elastica:
provider: ~
listener: ~
finder: ~
-
diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml
index 9c865ce..b40ac9a 100644
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -5,7 +5,7 @@ framework:
# Note that the session will be started ONLY if you read or write from it.
session:
- handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
+ handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler
cookie_secure: auto
cookie_samesite: lax
cookie_lifetime: 0 # integer, lifetime in seconds, 0 means 'valid for the length of the browser session'
diff --git a/config/services.yaml b/config/services.yaml
index 7fde9a1..ec4a1c1 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -30,3 +30,39 @@ services:
#
FOS\ElasticaBundle\Finder\FinderInterface:
alias: fos_elastica.finder.articles
+
+ # Redis
+ Symfony\Component\Cache\Adapter\RedisAdapter:
+ arguments:
+ - '@Redis'
+
+
+ Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
+ arguments:
+ - '@Redis'
+ # you can optionally pass an array of options. The only options are 'prefix' and 'ttl',
+ # which define the prefix to use for the keys to avoid collision on the Redis server
+ # and the expiration time for any given entry (in seconds), defaults are 'sf_s' and null:
+ # - { 'prefix': 'my_prefix', 'ttl': 600 }
+
+ Redis:
+ # you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes
+ class: Redis
+ calls:
+ - connect:
+ - '%env(REDIS_HOST)%'
+ - auth:
+ - '%env(REDIS_PASSWORD)%'
+
+ App\Service\LnBitsService:
+ arguments:
+ $lnbitsUrl: '%env(LNBITS_URL)%'
+ $apiKey: '%env(LNBITS_API_KEY)%'
+
+ App\Provider\ArticleProvider:
+ tags:
+ - { name: fos_elastica.pager_provider, index: articles, type: article }
+
+ App\EventListener\PopulateListener:
+ tags:
+ - { name: kernel.event_listener, event: 'FOS\ElasticaBundle\Event\PostIndexPopulateEvent', method: 'postIndexPopulate' }
diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php
index f57dbcc..7d7f8ed 100644
--- a/src/Controller/ArticleController.php
+++ b/src/Controller/ArticleController.php
@@ -54,13 +54,32 @@ class ArticleController extends AbstractController
$articlesCache->save($cacheItem);
}
+// // suggestions
+// $suggestions = $repository->findBy(['pubkey' => $article->getPubkey()], ['createdAt' => 'DESC'], 3);
+// // skip current, if listed in suggestions
+// $suggestions = array_filter($suggestions, function ($s) use ($article) {
+// return $s->getId() !== $article->getId();
+// });
+// $suggestions = array_merge($suggestions, $repository->findBy([], ['createdAt' => 'DESC'], 6 - count($suggestions)));
+// // sort by date
+// usort($suggestions, function ($a, $b) {
+// return $b->getCreatedAt() <=> $a->getCreatedAt();
+// });
+
$meta = $nostrClient->getNpubMetadata($article->getPubkey());
- $author = (array) json_decode($meta->content);
+ if ($meta?->content) {
+ $author = (array) json_decode($meta->content);
+ } else {
+ $author = [
+ 'name' => ''
+ ];
+ }
return $this->render('Pages/article.html.twig', [
'article' => $article,
'author' => $author,
- 'content' => $cacheItem->get()
+ 'content' => $cacheItem->get(),
+ //'suggestions' => $suggestions
]);
}
diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php
index 484e7b0..0730ef1 100644
--- a/src/Controller/AuthorController.php
+++ b/src/Controller/AuthorController.php
@@ -7,7 +7,6 @@ namespace App\Controller;
use App\Entity\Article;
use App\Entity\Event;
use App\Entity\Nzine;
-use App\Entity\User;
use App\Enum\KindsEnum;
use App\Service\NostrClient;
use Doctrine\ORM\EntityManagerInterface;
@@ -25,12 +24,23 @@ class AuthorController extends AbstractController
#[Route('/p/{npub}', name: 'author-profile')]
public function index($npub, EntityManagerInterface $entityManager, NostrClient $client): Response
{
+
+
+
$meta = $client->getNpubMetadata($npub);
$author = (array) json_decode($meta->content ?? '{}');
- $client->getNpubLongForm($npub);
+ // $client->getNpubLongForm($npub);
+
+ $list = $entityManager->getRepository(Article::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::LONGFORM], ['createdAt' => 'DESC']);
- $articles = $entityManager->getRepository(Article::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::LONGFORM], ['createdAt' => 'DESC']);
+ // deduplicate by slugs
+ $articles = [];
+ foreach ($list as $item) {
+ if (!key_exists((string) $item->getSlug(), $articles)) {
+ $articles[(string) $item->getSlug()] = $item;
+ }
+ }
$indices = $entityManager->getRepository(Event::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]);
@@ -40,6 +50,7 @@ class AuthorController extends AbstractController
return $this->render('Pages/author.html.twig', [
'author' => $author,
+ 'npub' => $npub,
'articles' => $articles,
'nzine' => $nzine,
'nzines' => $nzines,
diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php
index eec6521..8e1f3b0 100644
--- a/src/Controller/DefaultController.php
+++ b/src/Controller/DefaultController.php
@@ -5,15 +5,22 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\Article;
+use App\Enum\IndexStatusEnum;
use App\Service\NostrClient;
use Doctrine\ORM\EntityManagerInterface;
+use FOS\ElasticaBundle\Finder\FinderInterface;
+use Psr\Cache\InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Contracts\Cache\CacheInterface;
+use Symfony\Contracts\Cache\ItemInterface;
class DefaultController extends AbstractController
{
- public function __construct(private readonly EntityManagerInterface $entityManager,
+ public function __construct(
+ private readonly FinderInterface $esFinder,
+ private readonly EntityManagerInterface $entityManager,
private readonly NostrClient $nostrClient)
{
}
@@ -24,11 +31,7 @@ class DefaultController extends AbstractController
#[Route('/', name: 'default')]
public function index(): Response
{
- $original = $this->entityManager->getRepository(Article::class)->findBy([], ['createdAt' => 'DESC'], 20);
-
- $list = array_filter($original, function ($obj) {
- return !empty($obj->getSlug());
- });
+ $list = $this->entityManager->getRepository(Article::class)->findBy(['indexStatus' => IndexStatusEnum::INDEXED], ['createdAt' => 'DESC'], 5);
// deduplicate by slugs
$deduplicated = [];
@@ -42,10 +45,157 @@ class DefaultController extends AbstractController
return $obj->getPubkey();
}, $list);
- $this->nostrClient->getMetadata(array_unique($npubs));
+ // $this->nostrClient->getMetadata(array_unique($npubs));
return $this->render('home.html.twig', [
'list' => array_values($deduplicated)
]);
}
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[Route('/business', name: 'business')]
+ public function business(CacheInterface $redisCache): Response
+ {
+ $articles = $redisCache->get('main_category_business', function (ItemInterface $item): array {
+ $item->expiresAfter(36000);
+ $search = [
+ 'finance business',
+ 'trading stock commodity',
+ 's&p500 gold oil',
+ 'currency bitcoin',
+ 'international military incident'
+ ];
+
+ return $this->getArticles($search);
+ });
+
+ return $this->render('pages/category.html.twig', [
+ 'list' => array_slice($articles, 0, 9)
+ ]);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[Route('/technology', name: 'technology')]
+ public function technology(CacheInterface $redisCache): Response
+ {
+ $articles = $redisCache->get('main_category_technology', function (ItemInterface $item): array {
+ $item->expiresAfter(36000);
+ $search = [
+ 'technology innovation',
+ 'ai llm chatgpt claude agent',
+ 'blockchain mining cryptography',
+ 'cypherpunk nostr',
+ 'server hosting'
+ ];
+
+ return $this->getArticles($search);
+ });
+
+ return $this->render('pages/category.html.twig', [
+ 'list' => array_slice($articles, 0, 9)
+ ]);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[Route('/world', name: 'world')]
+ public function world(CacheInterface $redisCache): Response
+ {
+ $articles = $redisCache->get('main_category_world', function (ItemInterface $item): array {
+ $item->expiresAfter(36000);
+ $search = [
+ 'politics policy president',
+ 'agreement law resolution',
+ 'tariffs taxes trade',
+ 'international military incident'
+ ];
+
+ return $this->getArticles($search);
+ });
+
+ return $this->render('pages/category.html.twig', [
+ 'list' => array_slice($articles, 0, 9)
+ ]);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[Route('/lifestyle', name: 'lifestyle')]
+ public function lifestyle(CacheInterface $redisCache): Response
+ {
+ $articles = $redisCache->get('main_category_lifestyle', function (ItemInterface $item): array {
+ $item->expiresAfter(36000);
+ $search = [
+ 'touch grass',
+ 'health healthy',
+ 'lifestyle wellness diet sunshine'
+ ];
+
+ return $this->getArticles($search);
+ });
+
+ return $this->render('pages/category.html.twig', [
+ 'list' => array_slice($articles, 0, 9)
+ ]);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[Route('/art', name: 'art')]
+ public function art(CacheInterface $redisCache): Response
+ {
+ $articles = $redisCache->get('main_category_art', function (ItemInterface $item): array {
+ $item->expiresAfter(36000);
+ $search = [
+ 'photo photography',
+ 'travel',
+ 'art painting'
+ ];
+
+ return $this->getArticles($search);
+ });
+
+ return $this->render('pages/category.html.twig', [
+ 'list' => array_slice($articles, 0, 9)
+ ]);
+ }
+
+ /**
+ * @param $search
+ * @return array
+ */
+ public function getArticles($search): array
+ {
+ $articles = [];
+
+ foreach ($search as $q) {
+ $articles = array_merge($articles, $this->esFinder->find($q, 10));
+ }
+
+ // sort articles by created at date
+ usort($articles, function ($a, $b) {
+ return $b->getCreatedAt() <=> $a->getCreatedAt();
+ });
+
+ // deduplicate by slugs
+ $deduplicated = [];
+ foreach ($articles as $item) {
+ if (!key_exists((string)$item->getSlug(), $deduplicated)) {
+ $deduplicated[(string)$item->getSlug()] = $item;
+ }
+ // keep 10
+ if (count($deduplicated) > 9) {
+ break;
+ }
+ }
+
+ return $deduplicated;
+ }
}
diff --git a/src/Entity/Article.php b/src/Entity/Article.php
index 21743b0..27e0716 100644
--- a/src/Entity/Article.php
+++ b/src/Entity/Article.php
@@ -8,6 +8,7 @@ use App\Enum\KindsEnum;
use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
+use FOS\ElasticaBundle\Provider\IndexableInterface;
/**
* Entity storing long-form articles
@@ -313,12 +314,12 @@ class Article
return $this->eventStatus === EventStatusEnum::PREVIEW;
}
- public function getRaw(): null
+ public function getRaw()
{
return $this->raw;
}
- public function setRaw(object $raw): void
+ public function setRaw($raw): void
{
$this->raw = $raw;
}
diff --git a/src/EventListener/PopulateListener.php b/src/EventListener/PopulateListener.php
new file mode 100644
index 0000000..0938110
--- /dev/null
+++ b/src/EventListener/PopulateListener.php
@@ -0,0 +1,30 @@
+entityManager->getRepository(Article::class)->findBy(['indexStatus' => IndexStatusEnum::TO_BE_INDEXED]);
+
+ foreach ($articles as $article) {
+ if ($article instanceof Article) {
+ $article->setIndexStatus(IndexStatusEnum::INDEXED);
+ $this->entityManager->persist($article);
+ }
+ }
+
+ $this->entityManager->flush();
+
+ }
+}
diff --git a/src/Provider/ArticleProvider.php b/src/Provider/ArticleProvider.php
new file mode 100644
index 0000000..75588c7
--- /dev/null
+++ b/src/Provider/ArticleProvider.php
@@ -0,0 +1,27 @@
+entityManager->getRepository(Article::class)->findBy(['indexStatus' => IndexStatusEnum::TO_BE_INDEXED],['createdAt' => 'ASC'],200);
+ return new PagerfantaPager(new Pagerfanta(new ArrayAdapter($articles)));
+ }
+}
diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php
index 161f307..9b619b4 100644
--- a/src/Repository/ArticleRepository.php
+++ b/src/Repository/ArticleRepository.php
@@ -3,6 +3,7 @@
namespace App\Repository;
use App\Entity\Article;
+use App\Enum\IndexStatusEnum;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\DBAL\Exception;
use Doctrine\Persistence\ManagerRegistry;
diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php
index ec52ea5..4a55b97 100644
--- a/src/Service/NostrClient.php
+++ b/src/Service/NostrClient.php
@@ -7,7 +7,6 @@ use App\Entity\User;
use App\Enum\KindsEnum;
use App\Factory\ArticleFactory;
use App\Repository\UserEntityRepository;
-use App\Security\UserDTO;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Cache\InvalidArgumentException;
@@ -90,6 +89,8 @@ class NostrClient
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
$relays = new RelaySet();
$relays->addRelay(new Relay('wss://purplepag.es')); // default metadata aggregator
+ $relays->addRelay(new Relay('wss://relay.damus.io')); // major public
+ $relays->addRelay(new Relay('wss://relay.snort.social')); // major public
$request = new Request($relays, $requestMessage);
@@ -125,7 +126,7 @@ class NostrClient
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
// if user is logged in, use their settings
- /* @var UserDTO $user */
+ /* @var $user */
$user = $this->tokenStorage->getToken()?->getUser();
$relays = $this->defaultRelaySet;
if ($user && $user->getRelays()) {
diff --git a/src/Twig/Components/SearchComponent.php b/src/Twig/Components/SearchComponent.php
index f4cf1cb..3900f1c 100644
--- a/src/Twig/Components/SearchComponent.php
+++ b/src/Twig/Components/SearchComponent.php
@@ -15,6 +15,8 @@ final class SearchComponent
#[LiveProp(writable: true)]
public string $query = '';
+ public bool $interactive = true;
+
private FinderInterface $finder;
public function __construct(FinderInterface $finder)
@@ -27,8 +29,15 @@ final class SearchComponent
if (empty($this->query)) {
return [];
}
- $res = $this->finder->find($this->query, 10); // Limit to 10 results
- return $res; // Limit to 10 results
+ $res = $this->finder->find($this->query, 12); // Limit to 10 results
+
+ // filter out items with bad slugs
+ $filtered = array_filter($res, function($r) {
+ return !str_contains($r->getSlug(), '/');
+ });
+
+
+ return $filtered;
}
}
diff --git a/src/Util/IndexableArticleChecker.php b/src/Util/IndexableArticleChecker.php
new file mode 100644
index 0000000..098fa52
--- /dev/null
+++ b/src/Util/IndexableArticleChecker.php
@@ -0,0 +1,14 @@
+getIndexStatus() !== IndexStatusEnum::DO_NOT_INDEX;
+ }
+}
diff --git a/templates/components/Header.html.twig b/templates/components/Header.html.twig
index a5b6da3..75d2d26 100644
--- a/templates/components/Header.html.twig
+++ b/templates/components/Header.html.twig
@@ -1,3 +1,22 @@
diff --git a/templates/components/Molecules/Card.html.twig b/templates/components/Molecules/Card.html.twig
index 1b1d4eb..22775f4 100644
--- a/templates/components/Molecules/Card.html.twig
+++ b/templates/components/Molecules/Card.html.twig
@@ -1,6 +1,11 @@
{% if article is defined %}
<{{ tag }} {{ attributes }}>
-
+
{{ article.createdAt|date('F j') }}
{{ article.title }}
@@ -8,10 +13,10 @@
{{ article.summary }}
-
{{ tag }}>
+
{% endif %}
{% if user is defined %}
<{{ tag }} {{ attributes }}>
diff --git a/templates/components/SearchComponent.html.twig b/templates/components/SearchComponent.html.twig
index 3eac378..d5da0c2 100644
--- a/templates/components/SearchComponent.html.twig
+++ b/templates/components/SearchComponent.html.twig
@@ -1,16 +1,22 @@
-
-
-
-
+ {% if interactive %}
+
+
+
+
+
+
+ Powered by Silk
+
-
-
- {{ 'text.searching'|trans }}
-
+
+
+ {{ 'text.searching'|trans }}
+
+ {% endif %}
{% if this.results is not empty %}
diff --git a/templates/home.html.twig b/templates/home.html.twig
index b66141c..4c95422 100644
--- a/templates/home.html.twig
+++ b/templates/home.html.twig
@@ -5,6 +5,8 @@
{% block body %}
{# content #}
+
+ {# replace list with featured #}
{% endblock %}
diff --git a/templates/pages/article.html.twig b/templates/pages/article.html.twig
index b06309f..6f7b9d8 100644
--- a/templates/pages/article.html.twig
+++ b/templates/pages/article.html.twig
@@ -45,4 +45,13 @@
+
+{# #}
+{# {{ article.content }}#}
+{# #}
+{% endblock %}
+
+{% block aside %}
+{# Suggestions #}
+{# #}
{% endblock %}
diff --git a/templates/pages/author.html.twig b/templates/pages/author.html.twig
index eef57da..766f5cd 100644
--- a/templates/pages/author.html.twig
+++ b/templates/pages/author.html.twig
@@ -13,6 +13,38 @@
{% endif %}
+ {% if app.user and app.user.userIdentifier is same as npub %}
+
+
Purchase Search Credits
+
+
+
Amount: {{ amount ?? 0 }} sats
+
Status: Pending
+
+
+ {# You can access the width and height via the matrix #}
+ {# Replace the string with the invoice #}
+ {% set qrCode = qr_code_result('My QR Code') %}
+
+
+
+
Check Payment Status
+
+
+
+ {% endif %}
+
{% if nzine %}
View as N-Zine
diff --git a/templates/pages/category.html.twig b/templates/pages/category.html.twig
new file mode 100644
index 0000000..5a933ea
--- /dev/null
+++ b/templates/pages/category.html.twig
@@ -0,0 +1,13 @@
+{% extends 'base.html.twig' %}
+
+{% block nav %}
+{% endblock %}
+
+{% block body %}
+
+{% endblock %}
+
+{% block aside %}
+ {# Magazines #}
+ {# #}
+{% endblock %}