✨ OpenGraph
This commit is contained in:
161
app/components/OgImage/NuxtSeo.vue
Normal file
161
app/components/OgImage/NuxtSeo.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { useOgImageRuntimeConfig } from "#og-image/app/utils"
|
||||
import { useSiteConfig } from "#site-config/app/composables"
|
||||
import { computed, defineComponent, h, resolveComponent } from "vue"
|
||||
const props = defineProps({
|
||||
colorMode: { type: String, required: false },
|
||||
title: { type: String, required: false, default: "title" },
|
||||
description: { type: String, required: false },
|
||||
icon: { type: [String, Boolean], required: false },
|
||||
siteName: { type: String, required: false },
|
||||
siteLogo: { type: String, required: false },
|
||||
theme: { type: String, required: false, default: "#3f51b5" },
|
||||
backgroundImage: { type: String, required: false }
|
||||
})
|
||||
const HexRegex = /^#(?:[0-9a-f]{3}){1,2}$/i
|
||||
const runtimeConfig = useOgImageRuntimeConfig()
|
||||
const colorMode = computed(() => {
|
||||
return props.colorMode || runtimeConfig.colorPreference || "light"
|
||||
})
|
||||
const themeHex = computed(() => {
|
||||
if (HexRegex.test(props.theme)) return props.theme
|
||||
if (HexRegex.test(`#${props.theme}`)) return `#${props.theme}`
|
||||
if (props.theme.startsWith("rgb")) {
|
||||
const rgb = props.theme
|
||||
.replace("rgb(", "")
|
||||
.replace("rgba(", "")
|
||||
.replace(")", "")
|
||||
.split(",")
|
||||
.map((v) => Number.parseInt(v.trim(), 10))
|
||||
const hex = rgb
|
||||
.map((v) => {
|
||||
const hex2 = v.toString(16)
|
||||
return hex2.length === 1 ? `0${hex2}` : hex2
|
||||
})
|
||||
.join("")
|
||||
return `#${hex}`
|
||||
}
|
||||
return "#FFFFFF"
|
||||
})
|
||||
const themeRgb = computed(() => {
|
||||
return themeHex.value
|
||||
.replace("#", "")
|
||||
.match(/.{1,2}/g)
|
||||
?.map((v) => Number.parseInt(v, 16))
|
||||
.join(", ")
|
||||
})
|
||||
const siteConfig = useSiteConfig()
|
||||
|
||||
const siteName = computed(() => {
|
||||
return props.siteName || siteConfig.name
|
||||
})
|
||||
const siteLogo = computed(() => {
|
||||
return props.siteLogo || siteConfig.logo
|
||||
})
|
||||
const IconComponent = runtimeConfig.hasNuxtIcon
|
||||
? resolveComponent("Icon")
|
||||
: defineComponent({
|
||||
render() {
|
||||
return h("div", "missing @nuxt/icon")
|
||||
}
|
||||
})
|
||||
if (
|
||||
typeof props.icon === "string" &&
|
||||
!runtimeConfig.hasNuxtIcon &&
|
||||
process.dev
|
||||
) {
|
||||
console.warn(
|
||||
"Please install `@nuxt/icon` to use icons with the fallback OG Image component."
|
||||
)
|
||||
console.log("\nnpx nuxi module add icon\n")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full flex justify-between relative p-[60px]"
|
||||
:class="[
|
||||
colorMode === 'light'
|
||||
? ['bg-white', 'text-gray-900']
|
||||
: ['bg-gray-900', 'text-white']
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="backgroundImage"
|
||||
class="absolute inset-0 w-full h-full bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${backgroundImage})` }"
|
||||
></div>
|
||||
<div
|
||||
v-if="backgroundImage"
|
||||
class="absolute inset-0 w-full h-full"
|
||||
:class="colorMode === 'light' ? 'bg-white/80' : 'bg-gray-900/80'"
|
||||
/>
|
||||
<div
|
||||
class="flex absolute top-0 right-[-100%]"
|
||||
:style="{
|
||||
width: '200%',
|
||||
height: '200%',
|
||||
backgroundImage: `radial-gradient(circle, rgba(${themeRgb}, 0.5) 0%, ${
|
||||
colorMode === 'dark'
|
||||
? 'rgba(5, 5, 5,0.3)'
|
||||
: 'rgba(255, 255, 255, 0.7)'
|
||||
} 50%, ${
|
||||
props.colorMode === 'dark'
|
||||
? 'rgba(5, 5, 5,0)'
|
||||
: 'rgba(255, 255, 255, 0)'
|
||||
} 70%)`
|
||||
}"
|
||||
/>
|
||||
<div class="h-full w-full justify-between relative">
|
||||
<div class="flex flex-row justify-between items-start">
|
||||
<div class="flex flex-col w-full max-w-[65%]">
|
||||
<h1
|
||||
class="m-0 font-bold mb-[30px] text-[75px]"
|
||||
style="display: block; text-overflow: ellipsis"
|
||||
:style="{ lineClamp: description ? 2 : 3 }"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p
|
||||
v-if="description"
|
||||
class="text-[35px] leading-12"
|
||||
:class="[
|
||||
colorMode === 'light' ? ['text-gray-700'] : ['text-gray-300']
|
||||
]"
|
||||
style="display: block; line-clamp: 3; text-overflow: ellipsis"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="Boolean(icon)" style="width: 30%" class="flex justify-end">
|
||||
<IconComponent
|
||||
:name="icon"
|
||||
size="250px"
|
||||
style="margin: 0 auto; opacity: 0.7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-center items-center text-left w-full">
|
||||
<img v-if="siteLogo" :src="siteLogo" height="30" width="30" />
|
||||
<template v-else>
|
||||
<svg
|
||||
height="50"
|
||||
width="50"
|
||||
class="mr-3"
|
||||
viewBox="0 0 200 200"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
:fill="theme.includes('#') ? theme : `#${theme}`"
|
||||
d="M62.3,-53.9C74.4,-34.5,73.5,-9,67.1,13.8C60.6,36.5,48.7,56.5,30.7,66.1C12.7,75.7,-11.4,74.8,-31.6,65.2C-51.8,55.7,-67.9,37.4,-73.8,15.7C-79.6,-6,-75.1,-31.2,-61.1,-51C-47.1,-70.9,-23.6,-85.4,0.8,-86C25.1,-86.7,50.2,-73.4,62.3,-53.9Z"
|
||||
transform="translate(100 100)"
|
||||
/>
|
||||
</svg>
|
||||
<p v-if="siteName" style="font-size: 25px" class="font-bold">
|
||||
{{ siteName }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
152
app/components/OgImage/WithAvatar.vue
Normal file
152
app/components/OgImage/WithAvatar.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { useOgImageRuntimeConfig } from "#og-image/app/utils"
|
||||
import { useSiteConfig } from "#site-config/app/composables"
|
||||
import { computed, defineComponent, h, resolveComponent } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
colorMode: { type: String, required: false },
|
||||
title: { type: String, required: false, default: "title" },
|
||||
description: { type: String, required: false },
|
||||
icon: { type: [String, Boolean], required: false },
|
||||
siteName: { type: String, required: false },
|
||||
siteLogo: { type: String, required: false },
|
||||
theme: { type: String, required: false, default: "#3f51b5" },
|
||||
backgroundImage: { type: String, required: false },
|
||||
avatarUrl: { type: String, required: false }
|
||||
})
|
||||
const HexRegex = /^#(?:[0-9a-f]{3}){1,2}$/i
|
||||
const runtimeConfig = useOgImageRuntimeConfig()
|
||||
const colorMode = computed(() => {
|
||||
return props.colorMode || runtimeConfig.colorPreference || "light"
|
||||
})
|
||||
|
||||
const themeHex = computed(() => {
|
||||
if (HexRegex.test(props.theme)) return props.theme
|
||||
if (HexRegex.test(`#${props.theme}`)) return `#${props.theme}`
|
||||
if (props.theme.startsWith("rgb")) {
|
||||
const rgb = props.theme
|
||||
.replace("rgb(", "")
|
||||
.replace("rgba(", "")
|
||||
.replace(")", "")
|
||||
.split(",")
|
||||
.map((v) => Number.parseInt(v.trim(), 10))
|
||||
const hex = rgb
|
||||
.map((v) => {
|
||||
const hex2 = v.toString(16)
|
||||
return hex2.length === 1 ? `0${hex2}` : hex2
|
||||
})
|
||||
.join("")
|
||||
return `#${hex}`
|
||||
}
|
||||
return "#FFFFFF"
|
||||
})
|
||||
const themeRgb = computed(() => {
|
||||
return themeHex.value
|
||||
.replace("#", "")
|
||||
.match(/.{1,2}/g)
|
||||
?.map((v) => Number.parseInt(v, 16))
|
||||
.join(", ")
|
||||
})
|
||||
const siteConfig = useSiteConfig()
|
||||
const siteName = computed(() => {
|
||||
return props.siteName || siteConfig.name
|
||||
})
|
||||
const siteLogo = computed(() => {
|
||||
return props.siteLogo || siteConfig.logo
|
||||
})
|
||||
const IconComponent = runtimeConfig.hasNuxtIcon
|
||||
? resolveComponent("Icon")
|
||||
: defineComponent({
|
||||
render() {
|
||||
return h("div", "missing @nuxt/icon")
|
||||
}
|
||||
})
|
||||
if (
|
||||
typeof props.icon === "string" &&
|
||||
!runtimeConfig.hasNuxtIcon &&
|
||||
process.dev
|
||||
) {
|
||||
console.warn(
|
||||
"Please install `@nuxt/icon` to use icons with the fallback OG Image component."
|
||||
)
|
||||
console.log("\nnpx nuxi module add icon\n")
|
||||
}
|
||||
|
||||
const apiBaseServer = useSolarNetworkUrl(true)
|
||||
|
||||
function toAbsoluteUrl(url: string | undefined) {
|
||||
if (!url) return undefined
|
||||
if (url.startsWith("http"))
|
||||
return `${siteConfig.url}/__og/convert-image?url=${encodeURIComponent(url)}`
|
||||
if (url.startsWith("/api"))
|
||||
return `${siteConfig.url}/__og/convert-image?url=${encodeURIComponent(
|
||||
`${apiBaseServer}${url.replace("/api", "")}`
|
||||
)}`
|
||||
return `${siteConfig.url}${url}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full h-full flex justify-between relative"
|
||||
:class="[
|
||||
...(colorMode === 'light'
|
||||
? ['bg-white', 'text-gray-900']
|
||||
: ['bg-gray-900', 'text-white'])
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="backgroundImage"
|
||||
class="absolute inset-0 w-full h-full"
|
||||
:class="colorMode === 'light' ? 'bg-white/80' : 'bg-gray-900/80'"
|
||||
/>
|
||||
<img
|
||||
v-if="backgroundImage"
|
||||
:src="toAbsoluteUrl(backgroundImage)"
|
||||
class="absolute top-0 left-0 w-full h-full object-cover opacity-70"
|
||||
style="min-width: 1200px; min-height: 600px;"
|
||||
/>
|
||||
<div class="h-full w-full justify-between relative p-[60px]">
|
||||
<div class="flex flex-row justify-between items-start">
|
||||
<div class="flex flex-col w-full max-w-[65%]">
|
||||
<h1
|
||||
class="m-0 font-bold mb-[30px] text-[75px]"
|
||||
style="display: block; text-overflow: ellipsis"
|
||||
:style="{ lineClamp: description ? 2 : 3 }"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p
|
||||
v-if="description"
|
||||
class="text-[35px] leading-12"
|
||||
:class="[
|
||||
colorMode === 'light' ? ['text-gray-700'] : ['text-gray-300']
|
||||
]"
|
||||
style="display: block; line-clamp: 3; text-overflow: ellipsis"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="Boolean(icon)" style="width: 30%" class="flex justify-end">
|
||||
<IconComponent
|
||||
:name="icon"
|
||||
size="250px"
|
||||
style="margin: 0 auto; opacity: 0.7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end items-center text-right gap-3 w-full">
|
||||
<p v-if="siteName" style="font-size: 25px" class="font-bold">
|
||||
{{ siteName }}
|
||||
</p>
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
:src="toAbsoluteUrl(avatarUrl)"
|
||||
height="60"
|
||||
width="60"
|
||||
class="rounded-full mr-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,8 @@
|
||||
<v-app :theme="colorMode.preference">
|
||||
<v-app-bar flat class="app-bar-blur">
|
||||
<v-container class="mx-auto d-flex align-center justify-center">
|
||||
<img :src="$vuetify.theme.current.dark ? IconDark : IconLight" width="32" height="32" class="me-4" alt="The Solar Network" />
|
||||
|
||||
<v-btn
|
||||
v-for="link in links"
|
||||
:key="link.title"
|
||||
@@ -30,6 +32,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import IconLight from '~/assets/images/cloudy-lamb.png'
|
||||
import IconDark from '~/assets/images/cloudy-lamb@dark.png'
|
||||
|
||||
import type { NavLink } from "~/types/navlink"
|
||||
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
|
||||
@@ -301,7 +301,36 @@ function getOffsetUTCString(targetTimeZone: string): string {
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
alias: ["/@[name]"]
|
||||
alias: ["/@:name()"]
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: computed(() => {
|
||||
if (notFound.value) {
|
||||
return "User not found"
|
||||
}
|
||||
if (user.value) {
|
||||
return user.value.nick || user.value.name
|
||||
}
|
||||
return "Loading user..."
|
||||
}),
|
||||
meta: computed(() => {
|
||||
if (user.value) {
|
||||
const description = `View the profile of ${user.value.nick || user.value.name} on Solar Network.`
|
||||
return [
|
||||
{ name: 'description', content: description },
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
})
|
||||
|
||||
defineOgImage({
|
||||
component: 'WithAvatar',
|
||||
title: computed(() => user.value ? user.value.nick || user.value.name : 'User Profile'),
|
||||
description: computed(() => user.value ? `View the profile of ${user.value.nick || user.value.name} on Solar Network.` : ''),
|
||||
avatarUrl: computed(() => userPicture.value),
|
||||
backgroundImage: computed(() => userBackground.value),
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@ import IconDark from '~/assets/images/cloudy-lamb@dark.png'
|
||||
const route = useRoute()
|
||||
const api = useSolarNetwork()
|
||||
|
||||
useHead({
|
||||
title: "Authorize Application"
|
||||
})
|
||||
|
||||
// State
|
||||
const isLoading = ref(true)
|
||||
const isAuthorizing = ref(false)
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: "Auth Completed"
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
layout: "minimal"
|
||||
})
|
||||
|
||||
@@ -36,6 +36,10 @@ import CaptchaWidget from "@/components/CaptchaWidget.vue"
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
useHead({
|
||||
title: "Captcha Verification"
|
||||
})
|
||||
|
||||
const onCaptchaVerified = (token: string) => {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(`captcha_tk=${token}`, "*")
|
||||
|
||||
@@ -153,6 +153,10 @@ import IconDark from "~/assets/images/cloudy-lamb@dark.png"
|
||||
const router = useRouter()
|
||||
const api = useSolarNetwork()
|
||||
|
||||
useHead({
|
||||
title: "Create Account"
|
||||
})
|
||||
|
||||
const stage = ref<"username-nick" | "email" | "password" | "captcha">(
|
||||
"username-nick"
|
||||
)
|
||||
|
||||
@@ -11,6 +11,10 @@ import IconLight from "~/assets/images/cloudy-lamb.png"
|
||||
import IconDark from "~/assets/images/cloudy-lamb@dark.png"
|
||||
|
||||
// State management
|
||||
useHead({
|
||||
title: "Sign In"
|
||||
})
|
||||
|
||||
const stage = ref<
|
||||
"find-account" | "select-factor" | "enter-code" | "token-exchange"
|
||||
>("find-account")
|
||||
|
||||
@@ -46,8 +46,22 @@ import type { SnVersion, SnActivity } from "~/types/api"
|
||||
import PostEditor from "~/components/PostEditor.vue"
|
||||
import PostItem from "~/components/PostItem.vue"
|
||||
|
||||
import IconLight from '~/assets/images/cloudy-lamb.png'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useHead({
|
||||
title: "Home",
|
||||
meta: [
|
||||
{ name: 'description', content: 'The open social network. Friendly to everyone.' },
|
||||
]
|
||||
})
|
||||
|
||||
defineOgImage({
|
||||
title: 'Home',
|
||||
description: 'The open social network. Friendly to everyone.',
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const version = ref<SnVersion | null>(null)
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue"
|
||||
import { ref, onMounted, computed } from "vue"
|
||||
import { Marked } from "marked"
|
||||
import type { SnPost } from "~/types/api"
|
||||
|
||||
@@ -68,6 +68,29 @@ import AttachmentItem from "~/components/AttachmentItem.vue"
|
||||
const route = useRoute()
|
||||
const id = route.params.id as string
|
||||
|
||||
useHead({
|
||||
title: computed(() => {
|
||||
if (loading.value) return 'Loading post...'
|
||||
if (error.value) return 'Error'
|
||||
if (!post.value) return 'Post not found'
|
||||
return post.value.title || 'Post'
|
||||
}),
|
||||
meta: computed(() => {
|
||||
if (post.value) {
|
||||
const description = post.value.description || post.value.content?.substring(0, 150) || ''
|
||||
return [
|
||||
{ name: 'description', content: description },
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
})
|
||||
|
||||
defineOgImage({
|
||||
title: computed(() => post.value?.title || 'Post'),
|
||||
description: computed(() => post.value?.description || post.value?.content?.substring(0, 150) || ''),
|
||||
})
|
||||
|
||||
const post = ref<SnPost | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref("")
|
||||
|
||||
Reference in New Issue
Block a user