diff --git a/js/package.json b/js/package.json index 9f31a85e1..ea93c57dc 100644 --- a/js/package.json +++ b/js/package.json @@ -34,6 +34,7 @@ "vue-apollo": "^3.0.0-rc.6", "vue-class-component": "^7.0.2", "vue-i18n": "^8.14.0", + "vue-meta": "^2.3.1", "vue-property-decorator": "^8.1.0", "vue-router": "^3.0.6", "vue2-leaflet": "^2.0.3" diff --git a/js/src/main.ts b/js/src/main.ts index 7f0178e51..d1697b849 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -10,12 +10,14 @@ import { apolloProvider } from './vue-apollo'; import { NotifierPlugin } from '@/plugins/notifier'; import filters from '@/filters'; import messages from '@/i18n/index'; +import VueMeta from 'vue-meta'; Vue.config.productionTip = false; Vue.use(Buefy); Vue.use(NotifierPlugin); Vue.use(filters); +Vue.use(VueMeta); const language = (window.navigator as any).userLanguage || window.navigator.language; diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index 6409624db..7a40c6a64 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -265,6 +265,14 @@ import { RouteName } from '@/router'; query: TAGS, }, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + title: (this.$props.isUpdate ? this.$t('Event edition') : this.$t('Event creation')) as string, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class EditEvent extends Vue { @Prop({ type: Boolean, default: false }) isUpdate!: boolean; diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index b2e5e8f51..2ee20b502 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -268,6 +268,15 @@ import { RouteName } from '@/router'; }, }, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + // @ts-ignore + title: this.eventTitle, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class Event extends EventMixin { @Prop({ type: String, required: true }) uuid!: string; @@ -282,6 +291,11 @@ export default class Event extends EventMixin { EventVisibility = EventVisibility; RouteName = RouteName; + get eventTitle() { + if (!this.event) return undefined; + return this.event.title; + } + mounted() { this.identity = this.currentActor; } diff --git a/js/src/views/Event/Explore.vue b/js/src/views/Event/Explore.vue index 489820449..73fc56707 100644 --- a/js/src/views/Event/Explore.vue +++ b/js/src/views/Event/Explore.vue @@ -46,6 +46,14 @@ import { RouteName } from '@/router'; query: FETCH_EVENTS, }, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + title: this.$t('Explore') as string, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class Explore extends Vue { events: IEvent[] = []; diff --git a/js/src/views/Event/MyEvents.vue b/js/src/views/Event/MyEvents.vue index 19e5f4e77..e5be7a402 100644 --- a/js/src/views/Event/MyEvents.vue +++ b/js/src/views/Event/MyEvents.vue @@ -111,6 +111,14 @@ import EventCard from '@/components/Event/EventCard.vue'; update: data => data.loggedUser.participations.map(participation => new Participant(participation)), }, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + title: this.$t('My events') as string, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class MyEvents extends Vue { futurePage: number = 1; diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue index c20c5d4b1..6f08954fe 100644 --- a/js/src/views/Home.vue +++ b/js/src/views/Home.vue @@ -150,6 +150,15 @@ import { IConfig } from '@/types/config.model'; EventListCard, EventCard, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + // @ts-ignore + title: this.instanceName, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class Home extends Vue { events: IEvent[] = []; @@ -169,6 +178,11 @@ export default class Home extends Vue { // : this.loggedPerson.name; // } + get instanceName() { + if (!this.config) return undefined; + return this.config.name; + } + isToday(date: Date) { return (new Date(date)).toDateString() === (new Date()).toDateString(); } diff --git a/js/src/views/User/Login.vue b/js/src/views/User/Login.vue index 08a631516..edb147f1c 100644 --- a/js/src/views/User/Login.vue +++ b/js/src/views/User/Login.vue @@ -81,6 +81,14 @@ import { IConfig } from '@/types/config.model'; query: CURRENT_USER_CLIENT, }, }, + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + title: this.$t('Login on Mobilizon!') as string, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, }) export default class Login extends Vue { @Prop({ type: String, required: false, default: '' }) email!: string; diff --git a/js/src/views/User/Register.vue b/js/src/views/User/Register.vue index cdfc2139e..edf09c22c 100644 --- a/js/src/views/User/Register.vue +++ b/js/src/views/User/Register.vue @@ -105,7 +105,16 @@ import { CREATE_USER } from '@/graphql/user'; import { Component, Prop, Vue } from 'vue-property-decorator'; import { RouteName } from '@/router'; -@Component +@Component({ + metaInfo() { + return { + // if no subcomponents specify a metaInfo.title, this title will be used + title: this.$t('Register an account on Mobilizon!') as string, + // all titles will be injected into this template + titleTemplate: '%s | Mobilizon', + }; + }, +}) export default class Register extends Vue { @Prop({ type: String, required: false, default: '' }) email!: string; @Prop({ type: String, required: false, default: '' }) password!: string; diff --git a/js/yarn.lock b/js/yarn.lock index 4f9e62a45..d664ccbf7 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -3817,6 +3817,11 @@ deepmerge@^1.5.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== +deepmerge@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.1.1.tgz#ee0866e4019fe62c1276b9062d4c4803d9aea14c" + integrity sha512-+qO5WbNBKBaZez95TffdUDnGIo4+r5kmsX8aOb7PDHvXsTbghAmleuxjs6ytNaf5Eg4FGBXDS5vqO61TRi6BMg== + default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -12502,6 +12507,13 @@ vue-loader@^15.7.0: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" +vue-meta@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/vue-meta/-/vue-meta-2.3.1.tgz#32a1c2634f49433f30e7e7a028aa5e5743f84f6a" + integrity sha512-hnZvDNvLh+PefJLfYkZhG6cSBNKikgQyiEK8lI/P2qscM1DC/qHHOfdACPQ/VDnlaWU9VlcobCTNyVtssTR4XQ== + dependencies: + deepmerge "^4.0.0" + vue-property-decorator@^8.1.0: version "8.2.2" resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-8.2.2.tgz#ac895e9508ee1bf86e3a28568d94d842c2c8e42f" diff --git a/lib/mobilizon_web/views/page_view.ex b/lib/mobilizon_web/views/page_view.ex index 3037a9853..e8fd7f5e1 100644 --- a/lib/mobilizon_web/views/page_view.ex +++ b/lib/mobilizon_web/views/page_view.ex @@ -7,6 +7,7 @@ defmodule MobilizonWeb.PageView do alias Mobilizon.Service.ActivityPub.{Converter, Utils} alias Mobilizon.Service.Metadata alias Mobilizon.Service.MetadataUtils + alias Mobilizon.Service.Metadata.Instance def render("actor.activity-json", %{conn: %{assigns: %{object: actor}}}) do public_key = Utils.pem_to_public_key_pem(actor.keys) @@ -80,6 +81,8 @@ defmodule MobilizonWeb.PageView do def render("index.html", _assigns) do with {:ok, index_content} <- File.read(index_file_path()) do + tags = Instance.build_tags() |> MetadataUtils.stringify_tags() + index_content = String.replace(index_content, "", tags) {:safe, index_content} end end diff --git a/lib/service/metadata/instance.ex b/lib/service/metadata/instance.ex new file mode 100644 index 000000000..e1e53145e --- /dev/null +++ b/lib/service/metadata/instance.ex @@ -0,0 +1,46 @@ +defmodule Mobilizon.Service.Metadata.Instance do + @moduledoc """ + Generates metadata for every other page that isn't event/actor/comment + """ + + alias Phoenix.HTML + alias Phoenix.HTML.Tag + alias Mobilizon.Config + alias MobilizonWeb.Endpoint + + def build_tags() do + description = process_description(Config.instance_description()) + title = "#{Config.instance_name()} - Mobilizon" + + instance_json_ld = """ + + """ + + [ + Tag.content_tag(:title, title), + Tag.tag(:meta, name: "description", content: description), + Tag.tag(:meta, property: "og:title", content: title), + Tag.tag(:meta, property: "og:url", content: Endpoint.url()), + Tag.tag(:meta, property: "og:description", content: description), + Tag.tag(:meta, property: "og:type", content: "website"), + HTML.raw(instance_json_ld) + ] + end + + defp process_description(description) do + description + |> HtmlSanitizeEx.strip_tags() + |> String.slice(0..200) + |> (&"#{&1}…").() + end +end