refactor(frontend): move detail view state initialization to server plugin
Remove top-level await from useAsyncData in detail views Remove manual state initialization blocks in components Introduce 03-detail-seo.server.ts to handle SEO and state
This commit is contained in:
76
frontend/plugins/03-detail-seo.server.ts
Normal file
76
frontend/plugins/03-detail-seo.server.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { resolvedSeoHead, resolveSeo, type SeoConfig } from '../src/seo';
|
||||||
|
import { api } from '../src/services/api';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
|
const route = useRoute();
|
||||||
|
const routeId = typeof route.params.id === 'string' && route.params.id.trim() !== '' ? route.params.id : null;
|
||||||
|
if (!routeId || typeof route.name !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
const t = (nuxtApp.$pokopiaI18n as { global: { t: (key: string, values?: Record<string, string | number>) => string } }).global.t;
|
||||||
|
const seo = await detailSeo(String(route.name), routeId, t);
|
||||||
|
if (seo) {
|
||||||
|
useHead(resolvedSeoHead(resolveSeo(seo)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function detailSeo(
|
||||||
|
routeName: string,
|
||||||
|
routeId: string,
|
||||||
|
t: (key: string, values?: Record<string, string | number>) => string
|
||||||
|
): Promise<SeoConfig | null> {
|
||||||
|
try {
|
||||||
|
if (routeName === 'pokemon-detail') {
|
||||||
|
const pokemon = await api.pokemonDetail(routeId);
|
||||||
|
return {
|
||||||
|
title: `${pokemon.name} - ${t(pokemon.isEventItem ? 'pages.eventPokemon.title' : 'pages.pokemon.title')}`,
|
||||||
|
description: t('seo.pokemonDetailDescription', { name: pokemon.name }),
|
||||||
|
canonicalPath: `/pokemon/${pokemon.id}`,
|
||||||
|
image: pokemon.image?.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeName === 'habitat-detail') {
|
||||||
|
const habitat = await api.habitatDetail(routeId);
|
||||||
|
return {
|
||||||
|
title: `${habitat.name} - ${t(habitat.isEventItem ? 'pages.eventHabitats.title' : 'pages.habitats.title')}`,
|
||||||
|
description: t('seo.habitatDetailDescription', { name: habitat.name }),
|
||||||
|
canonicalPath: `/habitats/${habitat.id}`,
|
||||||
|
image: habitat.image?.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeName === 'item-detail' || routeName === 'ancient-artifact-detail') {
|
||||||
|
const item = await api.itemDetail(routeId);
|
||||||
|
const ancientArtifactRoute = routeName === 'ancient-artifact-detail';
|
||||||
|
if (ancientArtifactRoute && !item.ancientArtifactCategory) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleKey = ancientArtifactRoute ? 'pages.ancientArtifacts.title' : item.isEventItem ? 'pages.eventItems.title' : 'pages.items.title';
|
||||||
|
const descriptionKey = ancientArtifactRoute ? 'seo.ancientArtifactDetailDescription' : 'seo.itemDetailDescription';
|
||||||
|
return {
|
||||||
|
title: `${item.name} - ${t(titleKey)}`,
|
||||||
|
description: t(descriptionKey, { name: item.name }),
|
||||||
|
canonicalPath: ancientArtifactRoute ? `/ancient-artifacts/${item.id}` : `/items/${item.id}`,
|
||||||
|
image: item.image?.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routeName === 'recipe-detail') {
|
||||||
|
const recipe = await api.recipeDetail(routeId);
|
||||||
|
return {
|
||||||
|
title: `${recipe.name} - ${t('pages.recipes.title')}`,
|
||||||
|
description: t('seo.recipeDetailDescription', { name: recipe.name }),
|
||||||
|
canonicalPath: `/recipes/${recipe.id}`,
|
||||||
|
image: recipe.item.image?.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ const detailTabs = computed<TabOption[]>(() => [
|
|||||||
{ value: 'history', label: t('history.editHistory') }
|
{ value: 'history', label: t('history.editHistory') }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { data: initialHabitat } = await useAsyncData<HabitatDetail | null>(
|
const { data: initialHabitat } = useAsyncData<HabitatDetail | null>(
|
||||||
`habitat-detail:${activeHabitatRouteId() ?? 'none'}:${locale.value}`,
|
`habitat-detail:${activeHabitatRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
const routeId = activeHabitatRouteId();
|
const routeId = activeHabitatRouteId();
|
||||||
@@ -52,11 +52,6 @@ const { data: initialHabitat } = await useAsyncData<HabitatDetail | null>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initialHabitatLoaded = ref(false);
|
const initialHabitatLoaded = ref(false);
|
||||||
if (initialHabitat.value) {
|
|
||||||
habitat.value = initialHabitat.value;
|
|
||||||
initialHabitatLoaded.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const habitatSeo = computed(() =>
|
const habitatSeo = computed(() =>
|
||||||
habitat.value && route.meta.editorModal !== true
|
habitat.value && route.meta.editorModal !== true
|
||||||
? resolveSeo({
|
? resolveSeo({
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const possibleTagEvidenceSections = computed(() => [
|
|||||||
{ key: 'neutral', title: t('pages.pokemon.tradingNeutral'), rows: item.value?.possibleTags?.evidence.neutral ?? [] }
|
{ key: 'neutral', title: t('pages.pokemon.tradingNeutral'), rows: item.value?.possibleTags?.evidence.neutral ?? [] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { data: initialItem } = await useAsyncData<ItemDetail | null>(
|
const { data: initialItem } = useAsyncData<ItemDetail | null>(
|
||||||
`item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`,
|
`item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
const routeId = activeItemRouteId();
|
const routeId = activeItemRouteId();
|
||||||
@@ -92,11 +92,6 @@ const { data: initialItem } = await useAsyncData<ItemDetail | null>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initialItemLoaded = ref(false);
|
const initialItemLoaded = ref(false);
|
||||||
if (initialItem.value) {
|
|
||||||
item.value = initialItem.value;
|
|
||||||
initialItemLoaded.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemSeo = computed(() =>
|
const itemSeo = computed(() =>
|
||||||
item.value && route.meta.editorModal !== true
|
item.value && route.meta.editorModal !== true
|
||||||
? resolveSeo({
|
? resolveSeo({
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const weathers = ['晴天', '阴天', '雨天'];
|
|||||||
const relatedPokemonLimit = 6;
|
const relatedPokemonLimit = 6;
|
||||||
const pokemonDetailRouteNames = new Set(['pokemon-detail', 'pokemon-edit']);
|
const pokemonDetailRouteNames = new Set(['pokemon-detail', 'pokemon-edit']);
|
||||||
|
|
||||||
const { data: initialPokemon } = await useAsyncData<PokemonDetail | null>(
|
const { data: initialPokemon } = useAsyncData<PokemonDetail | null>(
|
||||||
`pokemon-detail:${activePokemonRouteId() ?? 'none'}:${locale.value}`,
|
`pokemon-detail:${activePokemonRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
const routeId = activePokemonRouteId();
|
const routeId = activePokemonRouteId();
|
||||||
@@ -59,12 +59,6 @@ const { data: initialPokemon } = await useAsyncData<PokemonDetail | null>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initialPokemonLoaded = ref(false);
|
const initialPokemonLoaded = ref(false);
|
||||||
if (initialPokemon.value) {
|
|
||||||
pokemon.value = initialPokemon.value;
|
|
||||||
relatedHabitatTab.value = habitatTabValue(initialPokemon.value.environment.id);
|
|
||||||
initialPokemonLoaded.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pokemonSeo = computed(() =>
|
const pokemonSeo = computed(() =>
|
||||||
pokemon.value && route.meta.editorModal !== true
|
pokemon.value && route.meta.editorModal !== true
|
||||||
? resolveSeo({
|
? resolveSeo({
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const recipeSubtitle = computed(() => {
|
|||||||
return categoryName ?? t('pages.recipes.detailSubtitle');
|
return categoryName ?? t('pages.recipes.detailSubtitle');
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: initialRecipe } = await useAsyncData<RecipeDetail | null>(
|
const { data: initialRecipe } = useAsyncData<RecipeDetail | null>(
|
||||||
`recipe-detail:${String(route.params.id)}:${locale.value}`,
|
`recipe-detail:${String(route.params.id)}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
@@ -55,11 +55,6 @@ const { data: initialRecipe } = await useAsyncData<RecipeDetail | null>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initialRecipeLoaded = ref(false);
|
const initialRecipeLoaded = ref(false);
|
||||||
if (initialRecipe.value) {
|
|
||||||
recipe.value = initialRecipe.value;
|
|
||||||
initialRecipeLoaded.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipeSeo = computed(() =>
|
const recipeSeo = computed(() =>
|
||||||
recipe.value && route.meta.editorModal !== true
|
recipe.value && route.meta.editorModal !== true
|
||||||
? resolveSeo({
|
? resolveSeo({
|
||||||
|
|||||||
Reference in New Issue
Block a user