tons of seo improvements (#798)

This commit is contained in:
MiniDigger | Martin 2022-07-31 15:18:53 +02:00
parent 5e57fa0a0e
commit 66393fda73
20 changed files with 55 additions and 69 deletions

View File

@ -48,6 +48,7 @@ server.get("*", async (request: Request, response: Response) => {
});
response.contentType("text/html");
response.setHeader("X-Powered-By", "HangarSSR");
response.writeHead(status || 200, statusText || headers, headers);
response.end(html);
});

View File

@ -344,7 +344,7 @@ function isRecent(date: string): boolean {
<!-- Login/register buttons -->
<div v-else class="flex gap-2">
<a class="flex items-center rounded-md p-2 hover:(text-primary-400 bg-primary-0)" :href="auth.loginUrl($route.fullPath)">
<a class="flex items-center rounded-md p-2 hover:(text-primary-400 bg-primary-0)" :href="auth.loginUrl($route.fullPath)" rel="nofollow">
<icon-mdi-key-outline class="mr-1 text-[1.2em]" />
{{ t("nav.login") }}
</a>

View File

@ -132,7 +132,7 @@ interface EditableMember {
<Card :class="props.class">
<template #header>
<div class="inline-flex w-full flex-cols space-between">
<span class="flex-grow">{{ i18n.t("project.members") }}</span>
<h3 class="flex-grow">{{ i18n.t("project.members") }}</h3>
</div>
</template>

View File

@ -36,17 +36,17 @@ function getBorderClasses(): string {
/>
</div>
<div class="overflow-clip overflow-hidden min-w-0">
<span class="inline-flex items-center gap-x-1">
<p>
<h2 class="inline-flex items-center gap-x-1">
<span>
<Link :to="'/' + project.namespace.owner + '/' + project.namespace.slug">{{ project.name }}</Link>
by
<Link :to="'/' + project.namespace.owner">{{ project.namespace.owner }}</Link>
</p>
</span>
<IconMdiCancel v-if="project.visibility === Visibility.SOFT_DELETE"></IconMdiCancel>
<IconMdiEyeOff v-else-if="project.visibility !== Visibility.PUBLIC"></IconMdiEyeOff>
</span>
</h2>
<p v-if="project.description" class="mb-1">{{ project.description }}</p>
<h3 v-if="project.description" class="mb-1">{{ project.description }}</h3>
<span class="<sm:hidden text-gray-500 dark:text-gray-400 flex flex-row items-center">
<CategoryLogo :category="project.category" :size="16" class="mr-1" />
{{ i18n.t("project.category." + project.category) }}

View File

@ -123,7 +123,7 @@ function requiresConfirmation(): boolean {
/>
<router-link class="!sm:ml-0" :to="'/' + project.namespace.owner">{{ project.namespace.owner }}</router-link>
<span class="text-gray-500 dark:text-gray-400"> / </span>
<span class="font-semibold">{{ project.name }}</span>
<h1 class="font-semibold">{{ project.name }}</h1>
</div>
<p>{{ project.description }}</p>
</div>

View File

@ -11,7 +11,6 @@ import { NamedPermission } from "~/types/enums";
import { HangarProject } from "hangar-internal";
import DonationModal from "~/components/donation/DonationModal.vue";
import VisibilityChangerModal from "~/components/modals/VisibilityChangerModal.vue";
import { MenuItem } from "@headlessui/vue";
const props = defineProps<{
project: HangarProject;
@ -22,7 +21,9 @@ const slug = computed(() => props.project.namespace.owner + "/" + props.project.
<template>
<Card>
<template #header>{{ i18n.t("project.info.title") }}</template>
<template #header>
<h3>{{ i18n.t("project.info.title") }}</h3>
</template>
<template #default>
<table class="w-full">
<tbody>

View File

@ -22,7 +22,7 @@ const route = useRoute();
<Card>
<template #header>
<div class="inline-flex w-full flex-cols space-between">
<span class="flex-grow">{{ i18n.t("page.plural") }}</span>
<h3 class="flex-grow">{{ i18n.t("page.plural") }}</h3>
<NewPageModal v-if="hasPerms(NamedPermission.EDIT_PAGE)" :pages="project.pages" :project-id="project.id" activator-class="mr-2" />
</div>
</template>

View File

@ -9,6 +9,7 @@ import { useSeo } from "~/composables/useSeo";
import { inject } from "vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
import { projectIconUrl } from "~/composables/useUrlHelper";
const props = defineProps<{
project: HangarProject;
@ -23,7 +24,8 @@ const updateProjectPages = inject<(pages: HangarProjectPage[]) => void>("updateP
const { editingPage, changeEditingPage, page, savePage, deletePage } = await useProjectPage(route, router, ctx, i18n, props.project);
if (page) {
useHead(useSeo(page.value?.name, props.project.description, route, null));
const title = page.value?.name === "Home" ? props.project.name : page.value?.name + " | " + props.project.name;
useHead(useSeo(title, props.project.description, route, projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)));
}
async function deletePageAndUpdateProject() {

View File

@ -9,7 +9,7 @@ export function useSeo(
image: string | null
): HeadObject {
description = description || "Plugin repository for Paper plugins and more!";
const canonical = baseUrl() + (route.fullPath.endsWith("/") ? route.fullPath : `${route.fullPath}/`);
const canonical = baseUrl() + (route.fullPath.endsWith("/") ? route.fullPath.substring(0, route.fullPath.length - 1) : route.fullPath);
image = image || "https://docs.papermc.io/img/paper.png";
image = image.startsWith("http") ? image : baseUrl() + image;
title = title ? title + " | Hangar" : "Hangar";
@ -17,49 +17,40 @@ export function useSeo(
title,
link: [{ rel: "canonical", href: canonical }],
meta: [
{ hid: "description", name: "description", content: description },
{ property: "description", name: "description", content: description },
{
property: "og:description",
name: "og:description",
vmid: "og:description",
hid: "og:description",
content: description,
},
{
property: "twitter:description",
name: "twitter:description",
vmid: "twitter:description",
hid: "twitter:description",
content: description,
},
{
property: "og:title",
name: "og:title",
hid: "og:title",
content: title,
},
{
property: "twitter:title",
name: "twitter:title",
hid: "twitter:title",
content: title,
},
{
property: "og:url",
name: "og:url",
hid: "og:url",
content: canonical,
},
{
property: "twitter:url",
name: "twitter:url",
hid: "twitter:url",
content: canonical,
},
{
property: "og:image",
name: "og:image",
hid: "og:image",
content: image,
},
],

View File

@ -52,6 +52,7 @@
"noProjects": "There are no projects. 😢",
"noProjectsFound": "Found 0 projects. 😢",
"title": "Find your favorite plugins",
"subTitle": "Hangar allows you to find the best Velocity, Waterfall or Paper plugins for your Minecraft Servers.",
"categories": "Categories",
"licenses": "Licenses",
"versions": "Minecraft versions",

View File

@ -9,10 +9,7 @@ import { handleRequestError } from "~/composables/useErrorHandling";
import { useErrorRedirect } from "~/lib/composables/useErrorRedirect";
import ProjectHeader from "~/components/projects/ProjectHeader.vue";
import ProjectNav from "~/components/projects/ProjectNav.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { HangarProject, HangarProjectPage } from "hangar-internal";
import { HangarProjectPage } from "hangar-internal";
defineProps({
user: {
@ -27,8 +24,6 @@ const route = useRoute();
const project = await useProject(route.params.user as string, route.params.project as string).catch((e) => handleRequestError(e, ctx, i18n));
if (!project || !project.value) {
await useRouter().replace(useErrorRedirect(route, 404, "Not found"));
} else {
useHead(useSeo(project.value.name, project.value.description, route, projectIconUrl(project.value.namespace.owner, project.value.namespace.slug)));
}
provide("updateProjectPages", function (pages: HangarProjectPage[]) {

View File

@ -12,9 +12,6 @@ import { useRoute, useRouter } from "vue-router";
import { useContext } from "vite-ssr/vue";
import Markdown from "~/components/Markdown.vue";
import ProjectPageList from "~/components/projects/ProjectPageList.vue";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import { projectIconUrl } from "~/composables/useUrlHelper";
import { ref } from "vue";
import { useInternalApi } from "~/composables/useApi";
import { handleRequestError } from "~/composables/useErrorHandling";
@ -56,7 +53,7 @@ function createPinnedVersionUrl(version: PinnedVersion): string {
return `/${props.project.namespace.owner}/${props.project.namespace.slug}/versions/${version.name}`;
}
useHead(useSeo(props.project.name, props.project.description, route, projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)));
// useSeo is in ProjectPageMarkdown
</script>
<template>
@ -92,7 +89,7 @@ useHead(useSeo(props.project.name, props.project.description, route, projectIcon
@save="saveSponsors"
/>
<template v-else>
<h1 class="mt-3 ml-5 text-xl">{{ i18n.t("project.sponsors") }}</h1>
<h2 class="mt-3 ml-5 text-xl">{{ i18n.t("project.sponsors") }}</h2>
<Markdown :raw="sponsors" class="pt-0" />
</template>
</Card>
@ -100,7 +97,9 @@ useHead(useSeo(props.project.name, props.project.description, route, projectIcon
<section class="basis-full md:basis-3/12 space-y-4 min-w-280px">
<ProjectInfo :project="project" />
<Card>
<template #header>{{ i18n.t("project.pinnedVersions") }}</template>
<template #header>
<h3>{{ i18n.t("project.pinnedVersions") }}</h3>
</template>
<ul class="divide-y divide-blue-500/50">
<li v-for="(version, index) in project.pinnedVersions" :key="`${index}-${version.name}`" class="p-1 py-2">
<div class="flex">

View File

@ -10,9 +10,6 @@ import MarkdownEditor from "~/components/MarkdownEditor.vue";
import { hasPerms } from "~/composables/usePerm";
import { NamedPermission } from "~/types/enums";
import Card from "~/lib/components/design/Card.vue";
import { useProjectPage } from "~/composables/useProjectPage";
import { useHead } from "@vueuse/head";
import { useSeo } from "~/composables/useSeo";
import ProjectPageMarkdown from "~/components/projects/ProjectPageMarkdown.vue";
import { useOpenProjectPages } from "~/composables/useOpenProjectPages";
@ -27,6 +24,7 @@ const route = useRoute();
const router = useRouter();
const open = await useOpenProjectPages(route, props.project);
// useSeo is in ProjectPageMarkdown
</script>
<template>

View File

@ -46,15 +46,6 @@ if (!route.params.platform) {
const entry = versionMap.keys().next();
await (entry.value ? useRouter().replace({ path: `${path}/${entry.value.toLowerCase()}` }) : useRouter().replace(useErrorRedirect(route, 404, "Not found")));
}
useHead(
useSeo(
(props.project.name + " " + route.params.version) as string,
props.project.description,
route,
projectIconUrl(props.project.namespace.owner, props.project.namespace.slug)
)
);
</script>
<template>

View File

@ -152,11 +152,11 @@ async function restoreVersion() {
<section class="basis-full md:basis-9/12 flex-grow overflow-auto">
<div class="flex flex-wrap gap-2 justify-between">
<div>
<h1 class="text-3xl sm:inline-flex items-center gap-x-1">
<h2 class="text-3xl sm:inline-flex items-center gap-x-1">
<TagComponent class="mr-1" :name="projectVersion.channel.name" :color="{ background: projectVersion.channel.color }" :short-form="true" />
{{ projectVersion.name }}
</h1>
<h2>
</h2>
<h3>
<span class="inline-flex <sm:flex-wrap ml-1">
{{ i18n.t("version.page.subheader", [projectVersion.author, lastUpdated(new Date(projectVersion.createdAt))]) }}
<span v-if="projectVersion.fileInfo?.sizeBytes" class="inline-flex items-center sm:ml-3">
@ -164,7 +164,7 @@ async function restoreVersion() {
{{ filesize(projectVersion.fileInfo.sizeBytes) }}
</span>
</span>
</h2>
</h3>
<em v-if="hasPerms(NamedPermission.REVIEWER) && projectVersion.approvedBy" class="text-lg ml-1">
{{ i18n.t("version.page.adminMsg", [projectVersion.approvedBy, i18n.d(projectVersion.createdAt, "date")]) }}
</em>
@ -201,7 +201,9 @@ async function restoreVersion() {
</section>
<section class="basis-full md:basis-3/12 flex-grow space-y-4">
<Card v-if="hasPerms(NamedPermission.DELETE_VERSION) || hasPerms(NamedPermission.VIEW_LOGS) || hasPerms(NamedPermission.REVIEWER)">
<template #header>{{ i18n.t("version.page.manage") }}</template>
<template #header>
<h3>{{ i18n.t("version.page.manage") }}</h3>
</template>
<span class="inline-flex items-center">
<IconMdiInformation class="mr-1" />
@ -274,7 +276,7 @@ async function restoreVersion() {
<Card>
<template #header>
<div class="inline-flex w-full">
<span class="flex-grow">{{ i18n.t("version.page.platform") }}</span>
<h3 class="flex-grow">{{ i18n.t("version.page.platform") }}</h3>
<PlatformVersionEditModal v-if="hasPerms(NamedPermission.EDIT_VERSION)" :project="project" :versions="versions" />
</div>
</template>
@ -289,7 +291,7 @@ async function restoreVersion() {
<Card v-if="projectVersion.pluginDependencies[platform?.name.toUpperCase()] || hasPerms(NamedPermission.EDIT_VERSION)">
<template #header>
<div class="inline-flex w-full">
<span class="flex-grow">{{ i18n.t("version.page.dependencies") }}</span>
<h3 class="flex-grow">{{ i18n.t("version.page.dependencies") }}</h3>
<DependencyEditModal :project="project" :versions="versions" />
</div>
</template>

View File

@ -129,7 +129,7 @@ function getVisibilityTitle(visibility: Visibility) {
<div class="flex flex-wrap">
<div class="basis-full md:basis-5/12 truncate">
<div class="flex flex-wrap items-center">
<span class="md:basis-full <md:mr-1 text-1.15rem leading-relaxed">{{ item.name }}</span>
<h2 class="md:basis-full <md:mr-1 text-1.15rem leading-relaxed">{{ item.name }}</h2>
<span class="md:hidden flex-grow"></span>
<Tag :name="item.channel.name" :color="{ background: item.channel.color }" />
<IconMdiCancel v-if="item.visibility === Visibility.SOFT_DELETE" class="ml-1"></IconMdiCancel>
@ -183,10 +183,10 @@ function getVisibilityTitle(visibility: Visibility) {
<template #header>
<div class="inline-flex w-full flex-cols space-between">
<InputCheckbox v-model="filter.allChecked.channels" @change="checkAllChannels" />
<span class="flex-grow">{{ i18n.t("version.channels") }}</span>
<h3 class="flex-grow">{{ i18n.t("version.channels") }}</h3>
<Link v-if="hasPerms(NamedPermission.EDIT_TAGS)" :to="`/${project.owner.name}/${project.name}/channels`">
<Button size="small" class="ml-2 text-sm"> <IconMdiPencil /> </Button
></Link>
<Button size="small" class="ml-2 text-sm"><IconMdiPencil /></Button>
</Link>
</div>
</template>
@ -203,7 +203,7 @@ function getVisibilityTitle(visibility: Visibility) {
<template #header>
<div class="inline-flex">
<InputCheckbox v-model="filter.allChecked.platforms" class="flex-right" @change="checkAllPlatforms" />
{{ i18n.t("version.platforms") }}
<h3>{{ i18n.t("version.platforms") }}</h3>
</div>
</template>

View File

@ -74,7 +74,7 @@ const buttons = computed<UserButton[]>(() => {
const isCurrentUser = computed<boolean>(() => authStore.user != null && authStore.user.name === props.user.name);
useHead(useSeo(props.user.name, props.user.tagline, route, avatarUrl(props.user.name)));
useHead(useSeo(props.user.name, props.user.name + " is an author on Hangar. " + props.user.tagline, route, avatarUrl(props.user.name)));
</script>
<template>

View File

@ -47,12 +47,16 @@ onMounted(() => {
bundle.onload = () => document.body.append(script);
});
useHead(useSeo(i18n.t("apiDocs.title"), null, route, null));
useHead(useSeo(i18n.t("apiDocs.title"), "API Docs for the Hangar REST API", route, null));
</script>
<template>
<div class="bg-gray-100 dark:(bg-gray-200) rounded-md my-auto mx-2 py-1" lg="w-2/3 min-w-2/3 max-w-2/3">
<div id="swagger-ui" />
<div id="swagger-ui">
<h1 class="text-3xl text-bold">Hangar API</h1>
<h2 class="text-2xl">API Docs for the Hangar REST API</h2>
<p>Loading...</p>
</div>
</div>
</template>

View File

@ -23,7 +23,7 @@ const headers = [
{ name: "projectCount", title: i18n.t("pages.headers.projects"), sortable: true },
] as Header[];
useHead(useSeo(i18n.t("pages.authorsTitle"), null, route, null));
useHead(useSeo(i18n.t("pages.authorsTitle"), "Hangar Project Authors", route, null));
</script>
<template>

View File

@ -124,7 +124,8 @@ useHead(meta);
<template>
<Container class="flex flex-col items-center gap-4">
<Alert v-if="loggedOut" type="success">{{ i18n.t("hangar.loggedOut") }}</Alert>
<h2 class="text-3xl font-bold uppercase text-center my-4">{{ i18n.t("hangar.projectSearch.title") }}</h2>
<h1 class="text-3xl font-bold uppercase text-center mt-4">{{ i18n.t("hangar.projectSearch.title") }}</h1>
<h2 class="text-1xl text-center my-2">{{ i18n.t("hangar.projectSearch.subTitle") }}</h2>
<!-- Search Bar -->
<div class="relative rounded-md flex shadow-md w-full max-w-screen-md">
<!-- Text Input -->
@ -167,12 +168,12 @@ useHead(meta);
<!-- Sidebar -->
<Card accent class="min-w-300px flex flex-col gap-4">
<div class="platforms">
<h3 class="font-bold mb-1">
<h4 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" @click="filters.platform = null">
{{ i18n.t("hangar.projectSearch.clear") }}
</span>
</h3>
</h4>
<div class="flex flex-col gap-1">
<ul>
<li v-for="platform in backendData.visiblePlatforms" :key="platform.enumName" class="inline-flex w-full">
@ -184,7 +185,7 @@ useHead(meta);
</div>
</div>
<div v-if="filters.platform" class="versions">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.versions") }}</h3>
<h4 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.versions") }}</h4>
<div class="flex flex-col gap-1 max-h-30 overflow-auto">
<InputCheckbox
v-for="version in versions(filters.platform)"
@ -196,7 +197,7 @@ useHead(meta);
</div>
</div>
<div class="categories">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.categories") }}</h3>
<h4 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.categories") }}</h4>
<div class="flex flex-col gap-1">
<InputCheckbox
v-for="category in backendData.visibleCategories"
@ -210,7 +211,7 @@ useHead(meta);
</div>
</div>
<div class="licenses">
<h3 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.licenses") }}</h3>
<h4 class="font-bold mb-1">{{ i18n.t("hangar.projectSearch.licenses") }}</h4>
<div class="flex flex-col gap-1">
<InputCheckbox v-for="license in backendData.licenses" :key="license" v-model="filters.licenses" :value="license" :label="license">
<LicenseLogo :license="license" :size="22" class="mr-1" />