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
1 change: 1 addition & 0 deletions src/main/kotlin/fr/shikkanime/dtos/animes/AnimeDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ data class AnimeDto(
var thumbnail: String? = null,
var banner: String? = null,
var carousel: String? = null,
var jsonLd: String? = null,
) : Serializable
14 changes: 10 additions & 4 deletions src/main/kotlin/fr/shikkanime/factories/impl/AnimeFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fr.shikkanime.factories.IGenericFactory
import fr.shikkanime.services.SimulcastService.Companion.sortBySeasonAndYear
import fr.shikkanime.services.caches.AnimeCacheService
import fr.shikkanime.services.caches.AnimePlatformCacheService
import fr.shikkanime.services.seo.JsonLdBuilder
import fr.shikkanime.utils.StringUtils
import fr.shikkanime.utils.toTreeSet
import fr.shikkanime.utils.withUTCString
Expand All @@ -16,14 +17,19 @@ class AnimeFactory : IGenericFactory<Anime, AnimeDto> {
@Inject private lateinit var animeCacheService: AnimeCacheService
@Inject private lateinit var animePlatformCacheService: AnimePlatformCacheService
@Inject private lateinit var simulcastFactory: SimulcastFactory
@Inject
private lateinit var jsonLdBuilder: JsonLdBuilder

override fun toDto(entity: Anime) = toDto(entity, false)

fun toDto(entity: Anime, showAllPlatforms: Boolean): AnimeDto {
val audioLocales = animeCacheService.getAudioLocales(entity.uuid!!)
val langTypes = animeCacheService.getLangTypes(entity).toSet()
val seasons = animeCacheService.findAllSeasons(entity)
val seasons = animeCacheService.findAllSeasons(entity).toSet()

val platforms = animePlatformCacheService.findAllByAnime(entity)
.filter { showAllPlatforms || it.platform.isStreaming }
.toSet()

return AnimeDto(
uuid = entity.uuid,
Expand All @@ -38,8 +44,8 @@ class AnimeFactory : IGenericFactory<Anime, AnimeDto> {
simulcasts = if (Hibernate.isInitialized(entity.simulcasts)) entity.simulcasts.sortBySeasonAndYear().map(simulcastFactory::toDto).toSet() else null,
audioLocales = audioLocales.toTreeSet(),
langTypes = langTypes,
seasons = seasons.toSet(),
platformIds = platforms.filter { showAllPlatforms || it.platform.isStreaming }.toTreeSet()
)
seasons = seasons,
platformIds = platforms.toTreeSet(),
).also { dto -> dto.jsonLd = jsonLdBuilder.build(dto, seasons, platforms) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a bug here. The JsonLdBuilder.build method is called with the original, unfiltered platforms set. However, the AnimeDto's platformIds property is assigned a filtered set of platforms on the previous line. This causes a discrepancy where the generated JSON-LD for search engines includes all platforms (even non-streaming ones when they should be hidden), while the UI shows a filtered list. The JSON-LD should be consistent with the displayed data.

You can fix this by passing the filtered platformIds from the DTO to the builder, which is available within the also block.

Suggested change
).also { dto -> dto.jsonLd = jsonLdBuilder.build(dto, seasons, platforms) }
).also { dto -> dto.jsonLd = jsonLdBuilder.build(dto, seasons, dto.platformIds ?: emptySet()) }

}
}
91 changes: 91 additions & 0 deletions src/main/kotlin/fr/shikkanime/services/seo/JsonLdBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package fr.shikkanime.services.seo

import com.google.gson.annotations.SerializedName
import fr.shikkanime.dtos.AnimePlatformDto
import fr.shikkanime.dtos.PlatformDto
import fr.shikkanime.dtos.SeasonDto
import fr.shikkanime.dtos.animes.AnimeDto
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.ObjectParser

class JsonLdBuilder {
private data class TvSeriesJsonLd(
@SerializedName("@context") val context: String = "https://schema.org",
@SerializedName("@type") val type: String = "TVSeries",
val name: String,
val alternateName: String,
val url: String,
val thumbnailUrl: String,
val image: String,
val description: String?,
val startDate: String,
val dateModified: String,
val inLanguage: Collection<String>?,
val provider: Collection<ProviderJsonLd>?,
val numberOfEpisodes: Long,
val numberOfSeasons: Int,
val containsSeason: Collection<SeasonJsonLd>?,
val keywords: String?,
)

private data class ProviderJsonLd(
@SerializedName("@type") val type: String = "Organization",
val name: String,
val url: String,
val logo: String,
)

private data class SeasonJsonLd(
@SerializedName("@type") val type: String = "TVSeason",
val name: String,
val seasonNumber: Int,
val startDate: String,
val dateModified: String,
val numberOfEpisodes: Long,
)

fun build(anime: AnimeDto, seasons: Set<SeasonDto>, platforms: Set<AnimePlatformDto>): String {
val sortedSeasons = seasons.sortedBy { it.number }
val containsSeason = sortedSeasons.map { season ->
SeasonJsonLd(
name = "Saison ${season.number}",
seasonNumber = season.number,
startDate = season.releaseDateTime,
dateModified = season.lastReleaseDateTime,
numberOfEpisodes = season.episodes
)
}

val totalEpisodes = sortedSeasons.sumOf { it.episodes }

val uniqueProviders = platforms
.map(AnimePlatformDto::platform)
.distinctBy(PlatformDto::id)
.map { platform ->
ProviderJsonLd(
name = platform.name,
url = platform.url,
logo = "${Constant.baseUrl}/assets/img/platforms/${platform.image}",
)
}

val jsonLd = TvSeriesJsonLd(
name = anime.name,
alternateName = anime.shortName,
url = "${Constant.baseUrl}/animes/${anime.slug}",
thumbnailUrl = "${Constant.apiUrl}/v1/attachments?uuid=${anime.uuid}&type=THUMBNAIL",
image = "${Constant.apiUrl}/v1/attachments?uuid=${anime.uuid}&type=BANNER",
description = anime.description,
startDate = anime.releaseDateTime,
dateModified = anime.lastUpdateDateTime,
inLanguage = anime.audioLocales,
provider = uniqueProviders.takeIf { it.isNotEmpty() },
numberOfEpisodes = totalEpisodes,
numberOfSeasons = sortedSeasons.size,
containsSeason = containsSeason.takeIf { it.isNotEmpty() },
keywords = anime.simulcasts?.joinToString(", ") { it.label }
)

return ObjectParser.toJson(jsonLd)
}
}
1 change: 1 addition & 0 deletions src/main/kotlin/fr/shikkanime/utils/ObjectParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private class ZonedDateTimeAdapterSerializer : JsonSerializer<ZonedDateTime> {

object ObjectParser {
private val gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(ZonedDateTime::class.java, ZonedDateTimeAdapterDeserializer())
.registerTypeAdapter(ZonedDateTime::class.java, ZonedDateTimeAdapterSerializer())
.create()
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/templates/site/anime.ftl
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<#import "_navigation.ftl" as navigation />
<#import "components/episode-mapping.ftl" as episodeMappingComponent />
<#import "components/langType.ftl" as langTypeComponent />
<#import "./seo/json-ld.ftl" as jsonLd />

<#assign canonicalUrl = baseUrl + "/animes/" + anime.slug>

Expand Down Expand Up @@ -165,5 +164,7 @@
</div>
</#if>

<@jsonLd.anime anime=anime />
<#if anime.jsonLd??>
<script type="application/ld+json">${anime.jsonLd}</script>
</#if>
</@navigation.display>
5 changes: 3 additions & 2 deletions src/main/resources/templates/site/calendar.ftl
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<#import "_navigation.ftl" as navigation />
<#import "components/episode-duration.ftl" as durationComponent />
<#import "components/langType.ftl" as langTypeComponent />
<#import "./seo/json-ld.ftl" as jsonLd />

<@navigation.display canonicalUrl="${baseUrl}/calendar">
<div x-data="{
Expand Down Expand Up @@ -186,7 +185,9 @@
</div>
</a>

<#if !isReleased><@jsonLd.anime anime=release.anime /></#if>
<#if !isReleased && release.anime.jsonLd??>
<script type="application/ld+json">${release.anime.jsonLd}</script>
</#if>
</article>
</#list>
</td>
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/templates/site/components/anime.ftl
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<#import "langType.ftl" as langTypeComponent />
<#import "../seo/json-ld.ftl" as jsonLd />

<#macro display anime>
<#assign animeSanitized = anime.shortName?html />
Expand Down Expand Up @@ -54,7 +53,9 @@
</div>
</a>

<@jsonLd.anime anime=anime />
<#if anime.jsonLd??>
<script type="application/ld+json">${anime.jsonLd}</script>
</#if>
</article>
</div>
</#macro>
4 changes: 4 additions & 0 deletions src/main/resources/templates/site/search.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@
<div class="text-truncate-6" x-text="anime.description"></div>
</div>
</a>

<template x-if="anime.jsonLd">
<script type="application/ld+json" x-text="anime.jsonLd"></script>
</template>
</article>
</div>
</template>
Expand Down
72 changes: 0 additions & 72 deletions src/main/resources/templates/site/seo/json-ld.ftl

This file was deleted.