diff --git a/src/main/kotlin/fr/shikkanime/dtos/animes/AnimeDto.kt b/src/main/kotlin/fr/shikkanime/dtos/animes/AnimeDto.kt index 5d6307069..526406a5b 100644 --- a/src/main/kotlin/fr/shikkanime/dtos/animes/AnimeDto.kt +++ b/src/main/kotlin/fr/shikkanime/dtos/animes/AnimeDto.kt @@ -26,4 +26,5 @@ data class AnimeDto( var thumbnail: String? = null, var banner: String? = null, var carousel: String? = null, + var jsonLd: String? = null, ) : Serializable \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/factories/impl/AnimeFactory.kt b/src/main/kotlin/fr/shikkanime/factories/impl/AnimeFactory.kt index f81003100..7ca146b93 100644 --- a/src/main/kotlin/fr/shikkanime/factories/impl/AnimeFactory.kt +++ b/src/main/kotlin/fr/shikkanime/factories/impl/AnimeFactory.kt @@ -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 @@ -16,14 +17,19 @@ class AnimeFactory : IGenericFactory { @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, @@ -38,8 +44,8 @@ class AnimeFactory : IGenericFactory { 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) } } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/services/seo/JsonLdBuilder.kt b/src/main/kotlin/fr/shikkanime/services/seo/JsonLdBuilder.kt new file mode 100644 index 000000000..599bbd8b1 --- /dev/null +++ b/src/main/kotlin/fr/shikkanime/services/seo/JsonLdBuilder.kt @@ -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?, + val provider: Collection?, + val numberOfEpisodes: Long, + val numberOfSeasons: Int, + val containsSeason: Collection?, + 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, platforms: Set): 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) + } +} diff --git a/src/main/kotlin/fr/shikkanime/utils/ObjectParser.kt b/src/main/kotlin/fr/shikkanime/utils/ObjectParser.kt index 7c73b23ea..599ae762b 100644 --- a/src/main/kotlin/fr/shikkanime/utils/ObjectParser.kt +++ b/src/main/kotlin/fr/shikkanime/utils/ObjectParser.kt @@ -23,6 +23,7 @@ private class ZonedDateTimeAdapterSerializer : JsonSerializer { object ObjectParser { private val gson = GsonBuilder() + .disableHtmlEscaping() .registerTypeAdapter(ZonedDateTime::class.java, ZonedDateTimeAdapterDeserializer()) .registerTypeAdapter(ZonedDateTime::class.java, ZonedDateTimeAdapterSerializer()) .create() diff --git a/src/main/resources/templates/site/anime.ftl b/src/main/resources/templates/site/anime.ftl index 13c2cef67..e4723d520 100644 --- a/src/main/resources/templates/site/anime.ftl +++ b/src/main/resources/templates/site/anime.ftl @@ -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> @@ -165,5 +164,7 @@ - <@jsonLd.anime anime=anime /> + <#if anime.jsonLd??> + + \ No newline at end of file diff --git a/src/main/resources/templates/site/calendar.ftl b/src/main/resources/templates/site/calendar.ftl index 22b56c820..70faf0aa5 100644 --- a/src/main/resources/templates/site/calendar.ftl +++ b/src/main/resources/templates/site/calendar.ftl @@ -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">
${release.anime.jsonLd} + diff --git a/src/main/resources/templates/site/components/anime.ftl b/src/main/resources/templates/site/components/anime.ftl index 8fbe54473..948ce55a9 100644 --- a/src/main/resources/templates/site/components/anime.ftl +++ b/src/main/resources/templates/site/components/anime.ftl @@ -1,5 +1,4 @@ <#import "langType.ftl" as langTypeComponent /> -<#import "../seo/json-ld.ftl" as jsonLd /> <#macro display anime> <#assign animeSanitized = anime.shortName?html /> @@ -54,7 +53,9 @@
- <@jsonLd.anime anime=anime /> + <#if anime.jsonLd??> + + diff --git a/src/main/resources/templates/site/search.ftl b/src/main/resources/templates/site/search.ftl index 18864ae8d..caec4fef3 100644 --- a/src/main/resources/templates/site/search.ftl +++ b/src/main/resources/templates/site/search.ftl @@ -141,6 +141,10 @@
+ + diff --git a/src/main/resources/templates/site/seo/json-ld.ftl b/src/main/resources/templates/site/seo/json-ld.ftl deleted file mode 100644 index 4e487b7ae..000000000 --- a/src/main/resources/templates/site/seo/json-ld.ftl +++ /dev/null @@ -1,72 +0,0 @@ -<#macro anime anime> - <#assign platforms = []> - - <#if anime.platformIds??> - <#list anime.platformIds as platform> - <#if platforms?filter(p -> p.id == platform.platform.id)?size == 0> - <#assign platforms = platforms + [platform.platform]> - - - - - <#assign totalEpisodes = 0> - - <#list anime.seasons as season> - <#assign totalEpisodes = totalEpisodes + season.episodes> - - - -