From 76979d2fc41760ef12e7fff4c9b54c3d50691c3d Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sat, 9 Jul 2022 21:41:32 +0100 Subject: [PATCH 1/6] Add article view counts --- .../Commands/UpdateArticleViewCounts.php | 68 +++++++++++++++++++ app/Models/Article.php | 1 + config/services.php | 5 ++ ...3_update_articles_table_add_view_count.php | 15 ++++ phpunit.xml | 2 + .../Commands/UpdateArticleViewCountsTest.php | 60 ++++++++++++++++ 6 files changed, 151 insertions(+) create mode 100644 app/Console/Commands/UpdateArticleViewCounts.php create mode 100644 database/migrations/2022_07_09_191433_update_articles_table_add_view_count.php create mode 100644 tests/Integration/Commands/UpdateArticleViewCountsTest.php diff --git a/app/Console/Commands/UpdateArticleViewCounts.php b/app/Console/Commands/UpdateArticleViewCounts.php new file mode 100644 index 000000000..2876bf8ae --- /dev/null +++ b/app/Console/Commands/UpdateArticleViewCounts.php @@ -0,0 +1,68 @@ +siteId = config('services.fathom.site_id'); + $this->token = config('services.fathom.token'); + } + + public function handle() + { + if (! $this->siteId || ! $this->token) { + $this->error('Fathom site ID and token must be configured'); + + return; + } + + Article::published()->chunk(100, function ($articles) { + $articles->each(function ($article) { + $article->update([ + 'view_count' => $this->getViewCountFor($article), + ]); + }); + }); + } + + protected function getViewCountFor(Article $article): ?int + { + $response = Http::withToken($this->token) + ->get('https://api.usefathom.com/v1/aggregations', [ + 'date_from' => '2021-03-01 00:00:00', // Fathom data aggregations not accurate before this date. + 'field_grouping' => 'pathname', + 'entity' => 'pageview', + 'aggregates' => 'pageviews,visits,uniques', + 'entity_id' => $this->siteId, + 'filters' => json_encode([ + [ + 'property' => 'pathname', + 'operator' => 'is', + 'value' => "/articles/{$article->slug()}", + ], + ]), + ]); + + if ($response->failed()) { + return null; + } + + return $response->json('0.pageviews'); + } +} diff --git a/app/Models/Article.php b/app/Models/Article.php index b04e0e215..93fa42799 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -46,6 +46,7 @@ final class Article extends Model implements Feedable 'slug', 'hero_image', 'is_pinned', + 'view_count', 'tweet_id', 'submitted_at', 'approved_at', diff --git a/config/services.php b/config/services.php index ef09a4133..b343f6757 100644 --- a/config/services.php +++ b/config/services.php @@ -57,4 +57,9 @@ 'channel' => env('TELEGRAM_CHANNEL'), ], + 'fathom' => [ + 'site_id' => env('FATHOM_SITE_ID'), + 'token' => env('FATHOM_TOKEN'), + ], + ]; diff --git a/database/migrations/2022_07_09_191433_update_articles_table_add_view_count.php b/database/migrations/2022_07_09_191433_update_articles_table_add_view_count.php new file mode 100644 index 000000000..bf563776b --- /dev/null +++ b/database/migrations/2022_07_09_191433_update_articles_table_add_view_count.php @@ -0,0 +1,15 @@ +bigInteger('view_count')->nullable()->after('is_pinned'); + }); + } +}; diff --git a/phpunit.xml b/phpunit.xml index 185c04c2a..66d1728d8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,5 +28,7 @@ + + diff --git a/tests/Integration/Commands/UpdateArticleViewCountsTest.php b/tests/Integration/Commands/UpdateArticleViewCountsTest.php new file mode 100644 index 000000000..ad6c0e2c1 --- /dev/null +++ b/tests/Integration/Commands/UpdateArticleViewCountsTest.php @@ -0,0 +1,60 @@ + 1234, + ]]); + }); + + $article = Article::factory()->create([ + 'title' => 'My First Article', + 'slug' => 'my-first-article', + 'submitted_at' => now(), + 'approved_at' => now(), + ]); + + (new UpdateArticleViewCounts)->handle(); + + expect($article->fresh()->view_count)->toBe(1234); +}); + +test('article view counts are not updated if API call fails', function () { + Http::fake(function () { + return Http::response('Uh oh', 500); + }); + + $article = Article::factory()->create([ + 'title' => 'My First Article', + 'slug' => 'my-first-article', + 'submitted_at' => now(), + 'approved_at' => now(), + ]); + + (new UpdateArticleViewCounts)->handle(); + + expect($article->fresh()->view_count)->toBeNull(); +}); + +test('view counts are not updated for unpublished articles', function () { + Http::fake(); + + Article::factory()->create([ + 'title' => 'My First Article', + 'slug' => 'my-first-article', + 'submitted_at' => now(), + ]); + + (new UpdateArticleViewCounts)->handle(); + + Http::assertNothingSent(); +}); From d0daebabd6378169c5300a28188c2859b1e63a9e Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 10 Jul 2022 21:01:12 +0100 Subject: [PATCH 2/6] Render view count --- app/Models/Article.php | 5 +++++ resources/views/articles/show.blade.php | 10 ++++++++-- tests/Feature/ArticleTest.php | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/Models/Article.php b/app/Models/Article.php index 93fa42799..857cdace3 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -193,6 +193,11 @@ public function readTime() return $minutes == 0 ? 1 : $minutes; } + public function viewCount() + { + return number_format($this->view_count); + } + public function isUpdated(): bool { return $this->updated_at->gt($this->created_at); diff --git a/resources/views/articles/show.blade.php b/resources/views/articles/show.blade.php index c359e23c3..61a618087 100644 --- a/resources/views/articles/show.blade.php +++ b/resources/views/articles/show.blade.php @@ -65,14 +65,20 @@ -
- +
+ {{ $article->createdAt()->format('j M, Y') }} {{ $article->readTime() }} min read + + @unless($article->viewCount() < 10) + + {{ $article->viewCount() }} views + + @endunless
diff --git a/tests/Feature/ArticleTest.php b/tests/Feature/ArticleTest.php index c85b06bdd..d47bf7605 100644 --- a/tests/Feature/ArticleTest.php +++ b/tests/Feature/ArticleTest.php @@ -492,3 +492,17 @@ ->see('My First Article') ->dontSee('My Second Article'); }); + +test('only articles with ten or more views render a view count', function () { + $article = Article::factory()->create(['title' => 'My First Article', 'slug' => 'my-first-article', 'submitted_at' => now(), 'approved_at' => now(), 'view_count' => 9]); + + $this->get("/articles/{$article->slug()}") + ->see('My First Article') + ->dontSee('9 views'); + + $article->update(['view_count' => 10]); + + $this->get("/articles/{$article->slug()}") + ->see('My First Article') + ->see('10 views'); +}); From 671f72464cbd96fd7940266789c19b213ad0ff7e Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 10 Jul 2022 21:02:34 +0100 Subject: [PATCH 3/6] Schedule command --- app/Console/Kernel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f2474ae03..2a114f692 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -18,6 +18,7 @@ protected function schedule(Schedule $schedule) $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('post-article-to-twitter')->twiceDaily(14, 18); $schedule->command('sitemap:generate')->daily()->graceTimeInMinutes(25); + $schedule->command('update-article-view-counts')->twiceDaily(); } /** From 355e880509c98c40ff2887bdee6bca53b6c9639d Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 10 Jul 2022 21:05:25 +0100 Subject: [PATCH 4/6] Group commands --- app/Console/Commands/GenerateSitemap.php | 2 +- app/Console/Commands/PostArticleToTwitter.php | 2 +- app/Console/Commands/UpdateArticleViewCounts.php | 2 +- app/Console/Kernel.php | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/GenerateSitemap.php b/app/Console/Commands/GenerateSitemap.php index 9186f274b..4f5c08fb8 100644 --- a/app/Console/Commands/GenerateSitemap.php +++ b/app/Console/Commands/GenerateSitemap.php @@ -10,7 +10,7 @@ class GenerateSitemap extends Command { - protected $signature = 'sitemap:generate'; + protected $signature = 'lio:generate-sitemap'; protected $description = 'Crawl the site to generate a sitemap.xml file'; diff --git a/app/Console/Commands/PostArticleToTwitter.php b/app/Console/Commands/PostArticleToTwitter.php index eca5b4971..a5a94750a 100644 --- a/app/Console/Commands/PostArticleToTwitter.php +++ b/app/Console/Commands/PostArticleToTwitter.php @@ -9,7 +9,7 @@ final class PostArticleToTwitter extends Command { - protected $signature = 'post-article-to-twitter'; + protected $signature = 'lio:post-article-to-twitter'; protected $description = 'Posts the latest unshared article to Twitter'; diff --git a/app/Console/Commands/UpdateArticleViewCounts.php b/app/Console/Commands/UpdateArticleViewCounts.php index 2876bf8ae..819b41595 100644 --- a/app/Console/Commands/UpdateArticleViewCounts.php +++ b/app/Console/Commands/UpdateArticleViewCounts.php @@ -8,7 +8,7 @@ final class UpdateArticleViewCounts extends Command { - protected $signature = 'update-article-view-counts'; + protected $signature = 'lio:update-article-view-counts'; protected $description = 'Queries the Fathom Analytics API to update the view counts for all articles'; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 2a114f692..ffa6dabd9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,9 +16,9 @@ protected function schedule(Schedule $schedule) $schedule->command('schedule-monitor:sync')->dailyAt('04:56'); $schedule->command('model:prune', ['--model' => MonitoredScheduledTaskLogItem::class])->daily(); $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->command('post-article-to-twitter')->twiceDaily(14, 18); - $schedule->command('sitemap:generate')->daily()->graceTimeInMinutes(25); - $schedule->command('update-article-view-counts')->twiceDaily(); + $schedule->command('lio:post-article-to-twitter')->twiceDaily(14, 18); + $schedule->command('lio:generate-sitemap')->daily()->graceTimeInMinutes(25); + $schedule->command('lio:update-article-view-counts')->twiceDaily(); } /** From 61ac37abca8478411df7bc3489f121bd5d2a864b Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Sun, 10 Jul 2022 21:10:09 +0100 Subject: [PATCH 5/6] Formatting --- app/Console/Commands/UpdateArticleViewCounts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/UpdateArticleViewCounts.php b/app/Console/Commands/UpdateArticleViewCounts.php index 819b41595..c75aa59a2 100644 --- a/app/Console/Commands/UpdateArticleViewCounts.php +++ b/app/Console/Commands/UpdateArticleViewCounts.php @@ -45,7 +45,7 @@ protected function getViewCountFor(Article $article): ?int { $response = Http::withToken($this->token) ->get('https://api.usefathom.com/v1/aggregations', [ - 'date_from' => '2021-03-01 00:00:00', // Fathom data aggregations not accurate before this date. + 'date_from' => '2021-03-01 00:00:00', // Fathom data aggregations not accurate prior to this date. 'field_grouping' => 'pathname', 'entity' => 'pageview', 'aggregates' => 'pageviews,visits,uniques', From 58eb9a0599dcc18c1c38888ec3eb02b5ec200411 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Thu, 28 Jul 2022 13:25:40 +0100 Subject: [PATCH 6/6] Update fathom docs --- .env.example | 3 +++ README.md | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/.env.example b/.env.example index f17d64a5a..8279150e5 100644 --- a/.env.example +++ b/.env.example @@ -35,5 +35,8 @@ TWITTER_ACCESS_SECRET= TELEGRAM_BOT_TOKEN= TELEGRAM_CHANNEL= +FATHOM_SITE_ID= +FATHOM_TOKEN= + FLARE_KEY= VITE_FLARE_KEY="${FLARE_KEY}" diff --git a/README.md b/README.md index 82c39de39..07d598285 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,15 @@ TELEGRAM_BOT_TOKEN= TELEGRAM_CHANNEL= ``` +### Fathom Analytics (optional) + +To enable view counts on articles, you'll need to register a [Fathom Analytics](https://app.usefathom.com/register) account and [install](https://usefathom.com/docs/start/install) it on the site. You will then need to create an API token and find your site ID before updating the below environment variables in your `.env` file. + +``` +FATHOM_SITE_ID= +FATHOM_TOKEN= +``` + ## Commands Command | Description