Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/Http/Controllers/ResourceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ResourceController extends Controller
public function show($id)
{
$data = Cache::rememberForever("resource_{$id}", function () use ($id) {
$resource = Resource::with('user')->findOrFail($id);
$resource = Resource::with(['user', 'node.subject'])->findOrFail($id);

$previousResourceId = Resource::where('node_id', $resource->node_id)
->where('id', '<', $resource->id)
Expand All @@ -26,6 +26,7 @@ public function show($id)

return [
'resource' => $resource->toArray(),
'subject' => $resource->node?->subject,
'previousResourceId' => $previousResourceId,
'nextResourceId' => $nextResourceId,
];
Expand Down
42 changes: 32 additions & 10 deletions resources/js/pages/Resource.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const props = defineProps({
type: Object,
required: true,
},
subject: {
type: Object,
default: null,
},
previousResourceId: {
type: Number,
default: null,
Expand Down Expand Up @@ -180,15 +184,25 @@ const toggleFullscreen = () => {

<template>
<Head>
<title>{{ resource.title }}</title>
<title>
{{ resource.title
}}{{
subject
? ` - ${subject.course.toUpperCase()} - ${subject.name}`
: ''
}}
</title>
<meta
name="description"
:content="`Study material: ${resource.title} (${resource.type}) on HSCStack - Open Learning Platform.`"
:content="`Study material: ${resource.title} (${resource.resource_type})${subject ? ` for ${subject.course.toUpperCase()} - ${subject.name}` : ''} on HSCStack.`"
/>
<meta
property="og:title"
:content="`${resource.title}${subject ? ` - ${subject.course.toUpperCase()} - ${subject.name}` : ''} - HSCStack`"
/>
<meta property="og:title" :content="`${resource.title} - HSCStack`" />
<meta
property="og:description"
:content="`Study material: ${resource.title} (${resource.type}) on HSCStack.`"
:content="`Study material: ${resource.title} (${resource.resource_type})${subject ? ` for ${subject.course.toUpperCase()} - ${subject.name}` : ''} on HSCStack.`"
/>
</Head>

Expand All @@ -211,12 +225,20 @@ const toggleFullscreen = () => {
/>
</button>

<h1
class="truncate text-sm font-bold text-slate-900 sm:text-base dark:text-gray-100"
:title="resource.title"
>
{{ resource.title }}
</h1>
<div class="min-w-0">
<span
v-if="subject"
class="block truncate text-xs font-semibold text-indigo-600 dark:text-indigo-400"
>
{{ subject.course.toUpperCase() }} - {{ subject.name }}
</span>
<h1
class="truncate text-sm font-bold text-slate-900 sm:text-base dark:text-gray-100"
:title="resource.title"
>
{{ resource.title }}
</h1>
</div>
</div>

<!-- Right: Action Buttons -->
Expand Down
30 changes: 25 additions & 5 deletions resources/views/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,31 @@
$res = $props['resource'];
$resTitle = data_get($res, 'title', 'Resource');
$resType = data_get($res, 'resource_type', 'material');
$metaTitle = "{$resTitle} - " . config('app.name', 'HSCStack');
$ogTitle = "{$resTitle} - HSCStack";
$metaDescription = "Study material: {$resTitle} ({$resType}) on HSCStack.";
if ($resType === 'image' && data_get($res, 'file_path')) {
$ogImage = str_starts_with(data_get($res, 'file_path'), 'http') ? data_get($res, 'file_path') : \Illuminate\Support\Facades\Storage::url(data_get($res, 'file_path'));
$subjectName = data_get($props, 'subject.name') ?: data_get($res, 'node.subject.name');
$subjectCourse = data_get($props, 'subject.course') ?: data_get($res, 'node.subject.course');
$subjectDisplay = $subjectName ? strtoupper($subjectCourse) . ' - ' . $subjectName : null;

$metaTitle = $subjectDisplay
? "{$resTitle} - {$subjectDisplay} - " . config('app.name', 'HSCStack')
: "{$resTitle} - " . config('app.name', 'HSCStack');
$ogTitle = $subjectDisplay
? "{$resTitle} - {$subjectDisplay} - " . config('app.name', 'HSCStack')
: "{$resTitle} - HSCStack";

$metaDescription = $subjectDisplay
? "Study material: {$resTitle} ({$resType}) for {$subjectDisplay} on HSCStack."
: "Study material: {$resTitle} ({$resType}) on HSCStack.";

$filePath = data_get($res, 'file_path') ?: data_get($res, 'file_url');
$externalUrl = data_get($res, 'external_url') ?: data_get($res, 'file_url');

if ($resType === 'video' && $externalUrl) {
if (preg_match('/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]+)/', $externalUrl, $matches)) {
$ogImage = "https://img.youtube.com/vi/{$matches[1]}/hqdefault.jpg";
}
$ogType = 'video.other';
} elseif ($resType === 'image' && $filePath) {
$ogImage = str_starts_with($filePath, 'http') ? $filePath : \Illuminate\Support\Facades\Storage::url($filePath);
}
} elseif ($pageComponent === 'Blog/Index') {
$metaTitle = 'Educational Blogs & Study Guides - ' . config('app.name', 'HSCStack');
Expand Down
2 changes: 1 addition & 1 deletion tests/Feature/AdminForumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@

// Show endpoint returns 404 for regular non-author user on rejected post
$hiddenResponse = $this->actingAs($user)->get(route('forum.show', $hiddenPost));
$hiddenResponse->assertSee('errors\/404');
$hiddenResponse->assertInertia(fn ($page) => $page->component('errors/404'));
$this->actingAs($user)->get(route('forum.show', $publishedPost))->assertStatus(200);
});

Expand Down
6 changes: 3 additions & 3 deletions tests/Feature/ForumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,11 +546,11 @@
$this->actingAs($admin)->get(route('forum.show', $flaggedPost))->assertStatus(200);

// Other user gets 404
$this->actingAs($otherUser)->get(route('forum.show', $pendingPost))->assertSee('errors\/404');
$this->actingAs($otherUser)->get(route('forum.show', $flaggedPost))->assertSee('errors\/404');
$this->actingAs($otherUser)->get(route('forum.show', $pendingPost))->assertInertia(fn ($page) => $page->component('errors/404'));
$this->actingAs($otherUser)->get(route('forum.show', $flaggedPost))->assertInertia(fn ($page) => $page->component('errors/404'));

// Guest gets 404
$this->get(route('forum.show', $pendingPost))->assertSee('errors\/404');
$this->get(route('forum.show', $pendingPost))->assertInertia(fn ($page) => $page->component('errors/404'));
});

test('suspended user cannot access ask question page', function () {
Expand Down
42 changes: 41 additions & 1 deletion tests/Feature/PublicPagesTest.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?php

use App\Models\Blog;
use App\Models\Node;
use App\Models\Notice;
use App\Models\Subject;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Inertia\Testing\AssertableInertia;

test('the homepage loads successfully', function () {
$response = $this->get('/');
Expand Down Expand Up @@ -61,5 +64,42 @@
$response = $this->get('/resources/999999');

$response->assertStatus(404);
$response->assertSee('errors\/404');
$response->assertInertia(fn (AssertableInertia $page) => $page->component('errors/404'));
});

test('public resource page loads with subject, node, and enriched SEO metadata', function () {
$subject = Subject::create([
'name' => 'Higher Mathematics',
'slug' => 'higher-math',
'course' => 'hsc',
'tailwind_format' => 'bg-indigo-500',
'icon' => 'calculator',
]);

$node = Node::create([
'subject_id' => $subject->id,
'name' => 'Matrix and Determinants',
'slug' => 'matrix-and-determinants',
]);

$resource = App\Models\Resource::create([
'node_id' => $node->id,
'resource_type' => 'video',
'title' => 'Matrix o nirnayok 01',
'external_url' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]);

$response = $this->get("/resources/{$resource->id}");

$response->assertStatus(200);
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('Resource')
->where('resource.id', $resource->id)
->where('subject.name', 'Higher Mathematics')
->where('subject.course', 'hsc')
);

$appName = config('app.name', 'HSCStack');
$response->assertSee("Matrix o nirnayok 01 - HSC - Higher Mathematics - {$appName}", false);
$response->assertSee('Study material: Matrix o nirnayok 01 (video) for HSC - Higher Mathematics on HSCStack.', false);
});