feat: generate metadata

This commit is contained in:
DIYgod 2023-05-08 22:01:38 +01:00
parent 7007375ba7
commit 5a1264cb95
No known key found for this signature in database
16 changed files with 326 additions and 67 deletions

View File

@ -1,10 +1,17 @@
import { Metadata } from "next"
import { Hydrate, dehydrate } from "@tanstack/react-query"
import { HomeFeed } from "~/components/home/HomeFeed"
import { HomeSidebar } from "~/components/home/HomeSidebar"
import { APP_NAME } from "~/lib/env"
import getQueryClient from "~/lib/query-client"
import { prefetchGetFeed } from "~/queries/home.server"
export const metadata: Metadata = {
title: `Activities - ${APP_NAME}`,
}
async function Activities() {
const queryClient = getQueryClient()
await prefetchGetFeed(

View File

@ -1,6 +1,21 @@
import { Metadata } from "next"
import { SearchInput } from "~/components/common/SearchInput"
import { HomeFeed } from "~/components/home/HomeFeed"
import { HomeSidebar } from "~/components/home/HomeSidebar"
import { APP_NAME } from "~/lib/env"
export function generateMetadata({
searchParams,
}: {
searchParams: {
[key: string]: string | string[] | undefined
}
}): Metadata {
return {
title: `Search: ${searchParams.q} - ${APP_NAME}`,
}
}
async function Search() {
return (

View File

@ -1,8 +1,23 @@
import { Metadata } from "next"
import { HomeFeed } from "~/components/home/HomeFeed"
import { HomeSidebar } from "~/components/home/HomeSidebar"
import { APP_NAME } from "~/lib/env"
import topics from "../../../../../data/topics.json"
export function generateMetadata({
params,
}: {
params: {
topic: string
}
}): Metadata {
return {
title: `Topic: ${params.topic} - ${APP_NAME}`,
}
}
function Topic({
params,
}: {

View File

@ -5,22 +5,68 @@ import { Toaster } from "react-hot-toast"
import "~/css/main.css"
import { useAcceptLang } from "~/hooks/useAcceptLang"
import { APP_DESCRIPTION, APP_NAME, SITE_URL } from "~/lib/env"
import { APP_DESCRIPTION, APP_NAME, APP_SLOGAN, SITE_URL } from "~/lib/env"
import Providers from "./providers"
// default site meta
export const metadata: Metadata = {
title: APP_NAME,
title: `${APP_NAME} - ${APP_SLOGAN}`,
description: APP_DESCRIPTION,
icons: `${SITE_URL}/assets/logo.svg`, // default site icon
applicationName: APP_NAME,
generator: APP_NAME,
keywords: [
"blog",
"xlog",
"blockchain",
"ethereum",
"web3",
"dapp",
"crypto",
],
themeColor: "#ffffff",
alternates: {
types: {
"application/rss+xml": [
{ url: "/feed/latest?format=xml", title: "xLog Latest" },
{ url: "/feed/hottest?interval=0&format=xml", title: "xLog Hottest" },
{
url: "/feed/hottest?interval=1&format=xml",
title: "xLog Hottest of the Day",
},
{
url: "/feed/hottest?interval=7&format=xml",
title: "xLog Hottest of the Week",
},
{
url: "/feed/hottest?interval=30&format=xml",
title: "xLog Hottest of the Month",
},
],
"application/feed+json": [
{ url: "/feed/latest", title: "xLog Latest" },
{ url: "/feed/hottest?interval=0", title: "xLog Hottest" },
{ url: "/feed/hottest?interval=1", title: "xLog Hottest of the Day" },
{ url: "/feed/hottest?interval=7", title: "xLog Hottest of the Week" },
{
url: "/feed/hottest?interval=30",
title: "xLog Hottest of the Month",
},
],
},
},
icons: `${SITE_URL}/assets/logo.svg`,
openGraph: {
siteName: APP_NAME,
description: "", // modify by pages
siteName: `${APP_NAME} - ${APP_SLOGAN}`,
description: APP_DESCRIPTION,
images: [`${SITE_URL}/assets/logo.svg`],
},
twitter: {
title: undefined,
description: undefined,
card: "summary_large_image",
title: `${APP_NAME} - ${APP_SLOGAN}`,
description: APP_DESCRIPTION,
images: [`${SITE_URL}/assets/logo.svg`],
site: "@_xLog",
creator: "@_xLog",
},
}

View File

@ -1,13 +1,68 @@
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { Hydrate, dehydrate } from "@tanstack/react-query"
import { SitePage } from "~/components/site/SitePage"
import { SITE_URL } from "~/lib/env"
import { useTranslation } from "~/lib/i18n"
import getQueryClient from "~/lib/query-client"
import { fetchGetPage } from "~/queries/page.server"
import { fetchGetSite } from "~/queries/site.server"
export async function generateMetadata({
params,
}: {
params: {
site: string
slug: string
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
const page = await fetchGetPage(
{
characterId: site?.characterId,
slug: params.slug,
useStat: true,
},
queryClient,
)
const title = `${page?.metadata?.content?.title} - ${
site?.metadata?.content?.name || site?.handle
}`
const description = page?.metadata?.content?.summary
const siteImages =
site?.metadata?.content?.avatars || `${SITE_URL}/assets/logo.svg`
const images = page?.metadata?.content?.cover || siteImages
const twitterCreator =
"@" +
site?.metadata?.content?.connected_accounts
?.find((account) => account?.endsWith?.("@twitter"))
?.match(/csb:\/\/account:([^@]+)@twitter/)?.[1]
return {
title,
description,
openGraph: {
siteName: title,
description,
images,
},
twitter: {
card: "summary_large_image",
title,
description,
images,
site: "@_xLog",
creator: twitterCreator,
},
}
}
export default async function SitePagePage({
params,
}: {

View File

@ -1,3 +1,5 @@
import { Metadata } from "next"
import { Hydrate, dehydrate } from "@tanstack/react-query"
import { SiteArchives } from "~/components/site/SiteArchives"
@ -6,6 +8,24 @@ import { PageVisibilityEnum } from "~/lib/types"
import { prefetchGetPagesBySite } from "~/queries/page.server"
import { fetchGetSite } from "~/queries/site.server"
export async function generateMetadata({
params,
}: {
params: {
site: string
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
const title = `Archives - ${site?.metadata?.content?.name || site?.handle}`
return {
title,
}
}
export default async function SiteArchivesPage({
params,
}: {

View File

@ -1,3 +1,4 @@
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { Hydrate, dehydrate } from "@tanstack/react-query"
@ -8,6 +9,7 @@ import { BackToTopFAB } from "~/components/site/BackToTopFAB"
import { SiteFooter } from "~/components/site/SiteFooter"
import { SiteHeader } from "~/components/site/SiteHeader"
import { FABContainer } from "~/components/ui/FAB"
import { SITE_URL } from "~/lib/env"
import getQueryClient from "~/lib/query-client"
import { cn } from "~/lib/utils"
import {
@ -16,6 +18,60 @@ import {
prefetchGetSiteToSubscriptions,
} from "~/queries/site.server"
export async function generateMetadata({
params,
}: {
params: {
site: string
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
const title = site?.metadata?.content?.name || site?.handle
const description = site?.metadata?.content?.bio
const images =
site?.metadata?.content?.avatars || `${SITE_URL}/assets/logo.svg`
const twitterCreator =
"@" +
site?.metadata?.content?.connected_accounts
?.find((account) => account?.endsWith?.("@twitter"))
?.match(/csb:\/\/account:([^@]+)@twitter/)?.[1]
return {
title,
description,
themeColor: "#ffffff", // TODO
alternates: {
types: {
"application/rss+xml": [
{ url: "/feed?format=xml", title },
{ url: "/feed/comments?format=xml", title: `Comments on ${title}` },
],
"application/feed+json": [
{ url: "/feed", title },
{ url: "/feed/comments", title: `Comments on ${title}` },
],
},
},
icons: images,
openGraph: {
siteName: title,
description,
images,
},
twitter: {
card: "summary_large_image",
title,
description,
images,
site: "@_xLog",
creator: twitterCreator,
},
}
}
export default async function SiteLayout({
children,
params,
@ -71,22 +127,8 @@ export default async function SiteLayout({
// `xlog-page-${type}`,
}
>
{/* <SEOHead
title={title || tag || page.data?.metadata?.content?.title || ""}
siteName={site.data?.metadata?.content?.name || ""}
description={
page.data?.metadata?.content?.summary ??
site.data?.metadata?.content?.bio?.replace(/<[^>]*>/g, "")
}
image={
page.data?.metadata?.content?.cover ||
getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])
}
icon={getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])}
site={domainOrSubdomain}
/> */}
<Style content={site?.metadata?.content?.css} />
{site && <SiteHeader site={site} />}
{site && <SiteHeader handle={params.site} />}
<div
className={cn(
"max-w-screen-md mx-auto px-5 pt-12 relative",

View File

@ -1,3 +1,4 @@
import { Metadata } from "next"
import Script from "next/script"
import type { Asset } from "unidata.js"
@ -7,6 +8,24 @@ import { useTranslation } from "~/lib/i18n"
import getQueryClient from "~/lib/query-client"
import { fetchGetSite, getNFTs } from "~/queries/site.server"
export async function generateMetadata({
params,
}: {
params: {
site: string
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
const title = `NFT - ${site?.metadata?.content?.name || site?.handle}`
return {
title,
}
}
export default async function SiteNFTPage({
params,
}: {

View File

@ -1,5 +1,33 @@
import { Metadata } from "next"
import { SearchInput } from "~/components/common/SearchInput"
import { SiteSearch } from "~/components/site/SiteSearch"
import getQueryClient from "~/lib/query-client"
import { fetchGetSite } from "~/queries/site.server"
export async function generateMetadata({
params,
searchParams,
}: {
params: {
site: string
}
searchParams: {
[key: string]: string | string[] | undefined
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
const title = `Search: ${searchParams.q} - ${
site?.metadata?.content?.name || site?.handle
}`
return {
title,
}
}
export default async function SiteSearchPage() {
return (

View File

@ -1,3 +1,5 @@
import { Metadata } from "next"
import { Hydrate, dehydrate } from "@tanstack/react-query"
import { SiteArchives } from "~/components/site/SiteArchives"
@ -6,6 +8,28 @@ import { PageVisibilityEnum } from "~/lib/types"
import { prefetchGetPagesBySite } from "~/queries/page.server"
import { fetchGetSite } from "~/queries/site.server"
export async function generateMetadata({
params,
}: {
params: {
site: string
tag: string
}
}): Promise<Metadata> {
const queryClient = getQueryClient()
const site = await fetchGetSite(params.site, queryClient)
params.tag = decodeURIComponent(params.tag)
const title = `Tag: ${params.tag} - ${
site?.metadata?.content?.name || site?.handle
}`
return {
title,
}
}
export default async function SiteTagPage({
params,
}: {

View File

@ -19,9 +19,8 @@ import { Tooltip } from "~/components/ui/Tooltip"
import { useIsDark } from "~/hooks/useDarkMode"
import { CSB_IO, CSB_SCAN, CSB_XCHAR } from "~/lib/env"
import { useTranslation } from "~/lib/i18n/client"
import { ExpandedCharacter } from "~/lib/types"
import { getUserContentsUrl } from "~/lib/user-contents"
import { cn } from "~/lib/utils"
import { useGetSite } from "~/queries/site"
import { ConnectButton } from "../common/ConnectButton"
import { Avatar } from "../ui/Avatar"
@ -58,28 +57,28 @@ const HeaderLink: React.FC<{ link: HeaderLinkType }> = ({ link }) => {
}
export const SiteHeader: React.FC<{
site?: ExpandedCharacter
}> = ({ site }) => {
handle: string
}> = ({ handle }) => {
const site = useGetSite(handle)
const { t } = useTranslation("site")
const leftLinks: HeaderLinkType[] = site?.metadata?.content?.navigation?.find(
(nav) => nav.url === "/",
)
? site.metadata?.content?.navigation
: [
{ label: "Home", url: "/" },
...(site?.metadata?.content?.navigation || []),
]
const leftLinks: HeaderLinkType[] =
site.data?.metadata?.content?.navigation?.find((nav) => nav.url === "/")
? site.data.metadata?.content?.navigation
: [
{ label: "Home", url: "/" },
...(site.data?.metadata?.content?.navigation || []),
]
const moreMenuItems = [
{
text: "View on xChar",
icon: <XCharLogo className="w-full h-full" />,
url: `${CSB_XCHAR}/${site?.handle}`,
url: `${CSB_XCHAR}/${site.data?.handle}`,
},
{
text: "View on xFeed",
icon: <XFeedLogo className="w-full h-full" />,
url: `${CSB_IO}/@${site?.handle}`,
url: `${CSB_IO}/@${site.data?.handle}`,
},
{
text: "View on Hoot It",
@ -94,12 +93,12 @@ export const SiteHeader: React.FC<{
/>
</div>
),
url: `https://hoot.it/search/${site?.handle}.csb/activities`,
url: `https://hoot.it/search/${site.data?.handle}.csb/activities`,
},
{
text: "View on Crossbell Scan",
icon: <BlockchainIcon className="fill-[#c09526] w-full h-full" />,
url: `${CSB_SCAN}/address/${site?.owner}`,
url: `${CSB_SCAN}/address/${site.data?.owner}`,
},
{
text: "Subscribe to JSON Feed",
@ -200,13 +199,13 @@ export const SiteHeader: React.FC<{
>
{(() => {
switch (
site?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
site.data?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
) {
case "image":
return (
<Image
className="max-w-screen-md mx-auto object-cover"
src={site?.metadata?.content?.banners?.[0]?.address}
src={site.data?.metadata?.content?.banners?.[0]?.address}
alt="banner"
fill
imageRef={bannerRef as MutableRefObject<HTMLImageElement>}
@ -216,7 +215,7 @@ export const SiteHeader: React.FC<{
return (
<video
className="max-w-screen-md mx-auto object-cover h-full w-full"
src={site?.metadata?.content?.banners?.[0]?.address}
src={site.data?.metadata?.content?.banners?.[0]?.address}
autoPlay
muted
playsInline
@ -235,26 +234,24 @@ export const SiteHeader: React.FC<{
<div
className={cn(
"xlog-site-info flex space-x-6 items-center w-full",
site?.metadata?.content?.banners?.[0]?.address
site.data?.metadata?.content?.banners?.[0]?.address
? "bg-white bg-opacity-50 backdrop-blur-sm rounded-xl p-4 sm:p-8 z-[1] border"
: "",
)}
>
{site?.metadata?.content?.avatars?.[0] && (
{site.data?.metadata?.content?.avatars?.[0] && (
<Avatar
className="xlog-site-icon max-w-[80px] max-h-[80px] sm:max-w-none sm:max-h-none"
images={[
getUserContentsUrl(site?.metadata?.content?.avatars?.[0]),
]}
images={site.data?.metadata?.content?.avatars}
size={120}
name={site?.metadata?.content?.name}
name={site.data?.metadata?.content?.name}
imageRef={avatarRef as MutableRefObject<HTMLImageElement>}
/>
)}
<div className="flex-1 min-w-0 relative">
<div className="flex items-center justify-between">
<div className="xlog-site-name text-2xl sm:text-3xl font-bold text-zinc-900 leading-snug break-words min-w-0">
{site?.metadata?.content?.name}
{site.data?.metadata?.content?.name}
</div>
<div className="ml-0 sm:ml-8 space-x-3 sm:space-x-4 flex items-center sm:static absolute -bottom-0 right-0">
<div className="xlog-site-more-menu relative inline-block align-middle">
@ -310,21 +307,21 @@ export const SiteHeader: React.FC<{
</div>
</div>
<div className="xlog-site-follow-button">
<FollowingButton site={site} />
<FollowingButton site={site.data || undefined} />
</div>
</div>
</div>
{site?.metadata?.content?.bio && (
{site.data?.metadata?.content?.bio && (
<div className="xlog-site-description text-gray-500 leading-snug my-2 sm:my-3 text-sm sm:text-base line-clamp-4 whitespace-pre-wrap">
{site?.metadata?.content?.bio}
{site.data?.metadata?.content?.bio}
</div>
)}
<div className="flex space-x-0 sm:space-x-5 space-y-2 sm:space-y-0 flex-col sm:flex-row text-sm sm:text-base">
<span className="xlog-site-follow-count block sm:inline-block whitespace-nowrap">
<FollowingCount characterId={site?.characterId} />
<FollowingCount characterId={site.data?.characterId} />
</span>
<span className="xlog-site-patron">
<PatronButton site={site} />
<PatronButton site={site.data || undefined} />
</span>
</div>
</div>

View File

@ -25,7 +25,7 @@ export const Avatar: React.FC<
const image = useMemo(() => {
for (const image of images) {
if (image) return toGateway(image)
if (image) return toGateway((image as any)?.url || image)
}
}, [images])

View File

@ -16,7 +16,6 @@ export const useNProgress = () => {
disposers.push(
events.onStart(() => {
console.log("start")
instance.current.start()
}),
)

View File

@ -1,6 +1,8 @@
import { IS_PROD } from "./constants"
export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "xLog"
export const APP_SLOGAN =
process.env.NEXT_PUBLIC_APP_SLOGAN || "Write. Own. Earn."
export const OUR_DOMAIN =
process.env.NEXT_PUBLIC_OUR_DOMAIN || process.env.NEXT_PUBLIC_VERCEL_URL
export const SCORE_API_DOMAIN = process.env.NEXT_PUBLIC_SCORE_API_DOMAIN

View File

@ -1,9 +0,0 @@
export function getUserContentsUrl(filename: string): string
export function getUserContentsUrl(filename: undefined | null): undefined
export function getUserContentsUrl<T extends string | undefined | null>(
filename: T,
): T
export function getUserContentsUrl(filename: string | undefined | null) {
if (!filename) return undefined
return filename
}

View File

@ -4,7 +4,6 @@ import { visit } from "unist-util-visit"
import { IS_PROD } from "~/lib/constants"
import { toGateway } from "~/lib/ipfs-parser"
import { getUserContentsUrl } from "~/lib/user-contents"
import { MarkdownEnv } from "."
@ -41,7 +40,7 @@ export const rehypeImage: Plugin<Array<{ env: MarkdownEnv }>, Root> = ({
return
}
node.properties.src = getUserContentsUrl(url)
node.properties.src = url
})
}
}