Skip to content

Commit

Permalink
feat: seo improvements (closes #1385)
Browse files Browse the repository at this point in the history
  • Loading branch information
MiniDigger committed Jul 20, 2024
1 parent 14d070f commit d826728
Show file tree
Hide file tree
Showing 21 changed files with 570 additions and 304 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public String getSitemap() {
public String getGlobalSitemap() {
return SitemapGenerator.of(this.config.getBaseUrl())
.addPage(WebPage.builder().name("").changeFreq(ChangeFreq.HOURLY).build())
.addPage(WebPage.builder().name("paper").changeFreq(ChangeFreq.HOURLY).build())
.addPage(WebPage.builder().name("waterfall").changeFreq(ChangeFreq.HOURLY).build())
.addPage(WebPage.builder().name("velocity").changeFreq(ChangeFreq.HOURLY).build())
.addPage(WebPage.builder().name("authors").changeFreq(ChangeFreq.WEEKLY).build())
.addPage(WebPage.builder().name("staff").changeFreq(ChangeFreq.WEEKLY).build())
.addPage(WebPage.builder().name("guidelines").changeFreq(ChangeFreq.WEEKLY).build())
Expand Down
358 changes: 358 additions & 0 deletions frontend/src/components/Homepage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
<script lang="ts" setup>
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { Tag } from "~/types/backend";
import type { Platform, PaginatedResultProject, PlatformVersion } from "~/types/backend";
const props = defineProps<{
platform?: Platform;
platformName?: string;
index?: boolean;
}>();
const i18n = useI18n();
const route = useRoute("index");
const router = useRouter();
const sorters = [
{ id: "-stars", label: i18n.t("project.sorting.mostStars") },
{ id: "-recent_downloads", label: i18n.t("project.sorting.recentDownloads") },
{ id: "-downloads", label: i18n.t("project.sorting.mostDownloads") },
{ id: "-updated", label: i18n.t("project.sorting.recentlyUpdated") },
{ id: "-newest", label: i18n.t("project.sorting.newest") },
];
const toArray = (input: unknown) => (Array.isArray(input) ? input : input ? [input] : []);
const filters = ref({
versions: toArray(route.query.version),
categories: toArray(route.query.category),
platform: (route.query.platform || undefined) as string | undefined,
tags: toArray(route.query.tag),
});
if (props.platform) {
filters.value.platform = props.platform;
}
const activeSorter = ref<string>((route.query.sort as string) || "-stars");
const page = ref(route.query.page ? Number(route.query.page) : 0);
const query = ref<string>((route.query.query as string) || "");
const projects = ref<PaginatedResultProject | null>();
const requestParams = computed(() => {
const limit = 10;
const params: Record<string, any> = {
limit,
offset: page.value * limit,
version: filters.value.versions,
category: filters.value.categories,
platform: filters.value.platform !== null ? [filters.value.platform] : [],
tag: filters.value.tags,
};
if (query.value) {
params.query = query.value;
}
if (activeSorter.value) {
params.sort = activeSorter.value;
}
return params;
});
const p = await useProjects(requestParams.value);
if (p && p.value) {
projects.value = p.value;
await checkOffsetLargerCount();
}
const projectList = ref();
function resetPage() {
projectList.value.updatePage(0);
}
watch(filters, resetPage, { deep: true });
watch(query, resetPage);
watch(activeSorter, resetPage);
watchDebounced(
requestParams,
async () => {
// dont want limit in url, its hardcoded in frontend
// offset we dont want, we set page instead
const { limit, offset, ...paramsWithoutLimit } = requestParams.value;
// set the request params
await router.replace({ query: { page: page.value, ...paramsWithoutLimit } });
// do the update
return updateProjects();
},
{ deep: true, debounce: 250 }
);
async function updateProjects() {
projects.value = await useApi<PaginatedResultProject>("projects", "get", requestParams.value);
await checkOffsetLargerCount();
}
// if somebody set page too high, lets reset it back
async function checkOffsetLargerCount() {
if (projects.value && projects.value.pagination?.offset !== 0 && projects.value.pagination?.offset > projects.value.pagination?.count) {
page.value = 0;
await updateProjects();
}
}
function versions(platform: Platform): PlatformVersion[] {
const platformData = useBackendData.platforms?.get(platform);
if (!platformData) {
return [];
}
return platformData.platformVersions;
}
function updatePlatform(platform: any) {
filters.value.platform = platform;
const allowedVersion: PlatformVersion[] = versions(platform);
filters.value.versions = filters.value.versions.filter((existingVersion) => {
return allowedVersion.find((allowedNewVersion) => allowedNewVersion === existingVersion);
});
}
const config = useConfig();
const pageChangeScrollAnchor = ref<Element>();
const ssr = import.meta.server;
useHead(
useSeo(
`Hangar - The best place to download ${!props.index ? props.platformName : "Minecraft"} plugins`,
`Hangar allows you to find and download the best ${!props.index ? props.platformName : "Minecraft"} plugins for your Minecraft server`,
route,
null,
[
{
type: "application/ld+json",
children: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
url: config.publicHost,
potentialAction: {
"@type": "SearchAction",
target: config.publicHost + "/?q={search_term_string}",
"query-input": "required name=search_term_string",
},
}),
key: "website",
},
],
true
)
);
</script>

<template>
<div>
<Container class="flex flex-col items-center gap-4">
<template v-if="index">
<h1 ref="pageChangeScrollAnchor" class="text-3xl font-bold uppercase text-center mt-4 flex flex-col">
<template v-if="ssr">
Find your favorite <strong class="highlight bg-gradient-to-r from-primary-500 to-primary-400 text-transparent">Paper</strong> plugins
</template>
<template v-else>
Find your favorite
<div class="h-[36px] overflow-hidden relative">
<span class="flex flex-col absolute w-full anim">
<strong class="highlight bg-gradient-to-r from-primary-500 to-primary-400 text-transparent">Paper</strong>
<strong class="highlight bg-gradient-to-r from-primary-500 to-primary-400 text-transparent">Velocity</strong>
<strong class="highlight bg-gradient-to-r from-primary-500 to-primary-400 text-transparent">Waterfall</strong>
</span>
</div>
plugins
</template>
</h1>
<div class="text-1xl text-center mb-2">
Hangar allows you to find and download the best Paper plugins, Velocity plugins or Waterfall plugins for your Minecraft server
</div>
</template>
<template v-else>
<h1 ref="pageChangeScrollAnchor" class="text-3xl font-bold uppercase text-center mt-4">Find your favorite {{ platformName }} plugins</h1>
<div class="text-1xl text-center mb-2">Hangar allows you to find and download the best {{ platformName }} plugins for your Minecraft server</div>
</template>
<div v-if="!index" class="text-center">
Looking for more plugins?
<div class="flex gap-3 mt-2 mb-2">
<Button v-if="platform != 'PAPER'" to="/paper">Download Paper plugins</Button>
<Button v-if="platform != 'VELOCITY'" to="/velocity">Download Velocity Plugins</Button>
<Button v-if="platform != 'WATERFALL'" to="/waterfall">Download Waterfall plugins</Button>
</div>
</div>
<!-- Search Bar -->
<div class="relative rounded-md flex shadow-md w-full max-w-screen-md">
<!-- Text Input -->
<input
v-model="query"
name="query"
class="rounded-l-md md:rounded-md p-4 basis-full min-w-0 dark:bg-gray-700"
type="text"
:placeholder="i18n.t('hangar.projectSearch.query', [projects?.pagination.count])"
/>
<div class="md:hidden flex">
<Menu>
<MenuButton
id="sort-button"
class="bg-gradient-to-r from-primary-500 to-primary-400 rounded-r-md text-left font-semibold flex items-center gap-2 text-white p-2"
>
<span class="whitespace-nowrap">{{ i18n.t("hangar.projectSearch.sortBy") }}</span>
<icon-mdi-sort-variant class="text-xl pointer-events-none" />
</MenuButton>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems
class="absolute right-0 top-15 flex flex-col z-10 background-default filter shadow-default drop-shadow-md rounded border-top-primary border-t-3"
>
<MenuItem v-for="sorter in sorters" :key="sorter.id" v-slot="{ active }">
<button
:class="{
'bg-gray-100 dark:bg-gray-700': active,
'bg-gradient-to-r from-primary-500 to-primary-400 text-white': activeSorter === sorter.id,
}"
class="px-4 py-2 text-left"
@click="activeSorter = sorter.id"
>
{{ sorter.label }}
</button>
</MenuItem>
</MenuItems>
</transition>
</Menu>
</div>
</div>
<div class="justify-center inline-flex gap-1 lt-md:hidden">
<div v-for="sorter in sorters" :key="sorter.id">
<button
:class="{ 'bg-gradient-to-r from-primary-500 to-primary-400 text-white': activeSorter === sorter.id }"
class="rounded-lg py-2 px-4 hover:(bg-gray-300 dark:bg-gray-700)"
@click="activeSorter = sorter.id"
>
{{ sorter.label }}
</button>
</div>
</div>
</Container>
<Container lg="flex items-start gap-6">
<!-- Projects -->
<div v-if="projects" class="w-full min-w-0 mb-5 flex flex-col gap-2 lg:mb-0">
<h2 class="font-bold text-2xl mb-2">Projects</h2>
<ProjectList ref="projectList" :projects="projects" :reset-anchor="pageChangeScrollAnchor" @update:page="(newPage) => (page = newPage)" />
</div>
<!-- Sidebar -->
<Card accent class="min-w-300px flex flex-col gap-4">
<h2 class="font-bold text-xl">Filters</h2>
<div v-if="!platform" class="platforms">
<h3 class="font-bold mb-1">
{{ i18n.t("hangar.projectSearch.platforms") }}
<span
v-if="filters.platform"
class="font-normal text-sm hover:(underline) text-gray-600 dark:text-gray-400"
cursor="pointer"
@click="filters.platform = undefined"
>
{{ i18n.t("hangar.projectSearch.clear") }}
</span>
</h3>
<div class="flex flex-col gap-1">
<ul>
<li v-for="platform in useVisiblePlatforms" :key="platform.enumName" class="inline-flex w-full">
<InputRadio :label="platform.name" :model-value="filters.platform" :value="platform.enumName" @update:model-value="updatePlatform">
<PlatformLogo :platform="platform.enumName" :size="24" class="mr-1" />
</InputRadio>
</li>
</ul>
</div>
</div>
<div v-if="filters.platform" class="versions">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.versions." + filters.platform) }}</h3>
<div class="max-h-40 overflow-auto">
<VersionSelector v-model="filters.versions" :versions="versions(filters.platform)" :open="false" col />
</div>
</div>
<div class="tags">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.tags") }}</h3>
<div class="flex flex-col gap-1">
<InputCheckbox v-for="tag in Object.values(Tag)" :key="tag" v-model="filters.tags" :value="tag">
<template #label>
<IconMdiPuzzleOutline v-if="tag === Tag.ADDON" />
<IconMdiBookshelf v-else-if="tag === Tag.LIBRARY" />
<IconMdiLeaf v-else-if="tag === Tag.SUPPORTS_FOLIA" />
<span class="ml-1">{{ i18n.t("project.settings.tags." + tag + ".title") }}</span>
</template>
</InputCheckbox>
</div>
</div>
<div class="categories">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.categories") }}</h3>
<div class="flex flex-col gap-1">
<InputCheckbox
v-for="category in useVisibleCategories"
:key="category.apiName"
v-model="filters.categories"
:value="category.apiName"
:label="i18n.t(category.title)"
>
<CategoryLogo :category="category.apiName" :size="22" class="mr-1" />
</InputCheckbox>
</div>
</div>
</Card>
</Container>
<h2 class="text-2xl font-bold mt-8">Frequently asked Questions about Hangar (FAQ)</h2>
<Card class="mt-4" itemscope itemprop="mainEntity" itemtype="https://schema.org/Question">
<h3 class="text-lg font-bold mb-1" itemprop="name">What is Hangar?</h3>
<div itemscope itemprop="acceptedAnswer" itemtype="https://schema.org/Answer">
<div itemprop="text">
Hangar is the best place to download {{ platformName }} plugins. Created by the <Link href="https://papermc.io/team">PaperMC Team</Link>, we took
great care that you can find the newest and best {{ platformName }} plugins.
</div>
</div>
</Card>
<Card class="mt-4" itemscope itemprop="mainEntity" itemtype="https://schema.org/Question">
<h3 class="text-lg font-bold mb-1" itemprop="name">How do I download {{ platformName }} plugins from Hangar?</h3>
<div itemscope itemprop="acceptedAnswer" itemtype="https://schema.org/Answer">
<div itemprop="text">
To download {{ platformName }} plugins, simply use the search on this page to find the plugin you are looking for and download the
{{ platformName }} plugin from the resource page.
</div>
</div>
</Card>
</div>
</template>
<style lang="scss" scoped>
.anim {
animation: anim 5s infinite;
}
.highlight {
background-clip: text;
}
@keyframes anim {
0%,
30% {
top: 0;
}
40%,
60% {
top: -36px;
}
70%,
90% {
top: -72px;
}
100% {
top: 0;
}
}
</style>
7 changes: 6 additions & 1 deletion frontend/src/components/layout/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { t } = useI18n();
<template>
<footer class="relative mt-10 py-1 background-default text-light-10">
<div class="mt-3 mb-3 max-w-screen-xl mx-auto px-4">
<div class="flex flex-wrap flex-row-reverse flex-grow-1 gap-4 justify-around items-center text-sm">
<div class="flex flex-wrap flex-col gap-4 justify-around items-center text-sm">
<div class="flex flex-row flex-wrap justify-center">
<a
href="https://github.com/HangarMC"
Expand Down Expand Up @@ -38,6 +38,11 @@ const { t } = useI18n();
{{ t("hangar.footer.legalNotice") }}
</NuxtLink>
</div>
<div class="flex flex-row flex-wrap justify-center gap-2">
<Link to="/paper">Download Paper Plugins</Link>
<Link to="/velocity">Download Velocity Plugins</Link>
<Link to="/waterfall">Download Waterfall Plugins</Link>
</div>
<div class="flex flex-grow-1 lt-md:justify-center">
<p class="font-bold">© {{ new Date().getFullYear() }} <a href="https://papermc.io/">PaperMC</a></p>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/modals/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defineExpose({
<button data-close @click="close">
<IconMdiClose class="cursor-pointer mr-1" />
</button>
<h2 class="font-bold">{{ props.title }}</h2>
<div class="font-bold">{{ props.title }}</div>
</div>
<slot :on="{ click: close }"></slot>
</dialog>
Expand Down
Loading

0 comments on commit d826728

Please sign in to comment.