Next.JS and Vrite #54
Replies: 2 comments 6 replies
-
Other guides and integrations are planned. Currently, I'm focused on reaching stability and feature completeness.
With custom transformers, this can be made even better (e.g. convert to JSX element instead of HTML string), but this definitely requires its own docs/blog posts. |
Beta Was this translation helpful? Give feedback.
-
How to use Vrite with Next with SSRCode structure in app router:
To render the blog content from Vrite as github flavored markdown and parse it to the client component we use next-mdx-remote APILet's start with the api where we use Vrite SDK: // api/route.js import { createClient } from '@vrite/sdk/api';
const vrite = createClient({
// your vrite token from env
token: process.env.VRITE,
});
// this way we don't use cache and fetch our blog on every call
export const revalidate = 0;
export async function GET(req) {
const { searchParams } = new URL(req.url);
// here we can get any sereachParam, in this example I want to get the language
const lang = searchParams.get('lang');
const contentGroups = await vrite.contentGroups.list();
// console.log('groups:', contentGroups);
// checking language
if (lang === 'en') {
const contentId = await contentGroups.filter((item) => item.name === 'YOUR-VRITE-TITLE[en]')[0]
.id;
const contentPieces = await vrite.contentPieces.list({
contentGroupId: contentId,
});
return Response.json(contentPieces);
} else {
const contentId = await contentGroups.filter((item) => item.name === 'YOUR-VRITE-TITLE')[0].id;
const contentPieces = await vrite.contentPieces.list({
contentGroupId: contentId,
});
return Response.json(contentPieces);
}
} // api/[slug]/route.js import { createClient } from '@vrite/sdk/api';
import { gfmOutputTransformer } from '@vrite/sdk/transformers';
const vrite = createClient({
token: process.env.VRITE,
});
export const revalidate = 0;
export async function GET(req, { params }) {
const { searchParams } = new URL(req.url);
const lang = searchParams.get('lang');
const content = searchParams.get('content');
const slug = params.slug;
const contentGroups = await vrite.contentGroups.list();
// checking language
if (lang === 'en') {
const contentId = await contentGroups.filter((item) => item.name === 'YOUR-VRITE-TITLE[en]')[0]
.id;
const contentPieces = await vrite.contentPieces.list({
contentGroupId: contentId,
});
// get id from slug
const id = contentPieces.filter((post) => post.slug === slug)[0].id;
const contentPiece = await vrite.contentPieces.get({
id: id,
content: true,
});
if (content === 'true') {
const gfmOutput = gfmOutputTransformer(contentPiece.content);
return Response.json(gfmOutput);
} else {
return Response.json(contentPiece);
}
} else {
const contentId = await contentGroups.filter((item) => item.name === 'YOUR-VRITE-TITLE')[0].id;
const contentPieces = await vrite.contentPieces.list({
contentGroupId: contentId,
});
// get id from slug
const id = contentPieces.filter((post) => post.slug === slug)[0].id;
const contentPiece = await vrite.contentPieces.get({
id: id,
content: true,
});
if (content === 'true') {
const gfmOutput = gfmOutputTransformer(contentPiece.content);
return Response.json(gfmOutput);
} else {
return Response.json(contentPiece);
}
}
} Server ComponentNow we will use our api to get all information from Vrite, in this example I included two Dashboards with different languages // blog/page.js import { Link } from '../../../navigation';
// I use styled-components here but you can also use *tailwindcss*
import { PostWrapper } from './blog.styles';
import Container from './blog.styles';
import { headers } from 'next/headers';
import { Suspense } from 'react';
// this is the client side component and we wrap it with *Suspense*
import Post from './post.component';
export const metadata = {
title: 'Blog Title',
metadataBase: new URL('YOUR-URL'),
description: 'your description',
referrer: 'origin-when-cross-origin',
openGraph: {
title: 'Blog',
description: 'your description',
},
};
export default async function Blog({ params: { locale } }) {
const host = headers().get('host');
const protocol = process?.env.NODE_ENV === 'production' ? 'https' : 'http';
// retrieve data from our api
const res = await fetch(`${protocol}://${host}/api/blog?lang=${locale}`, {
method: 'GET',
});
const blogPosts = await res.json();
return (
<div className="main">
<Container>
{blogPosts.map((post, index) => (
<Suspense
key={index}
fallback={<YOUR-SKELETON-HERE/>}
>
<PostWrapper key={index}>
<Link href={`/blog/${post.slug}`}>
<Post
title={post.title}
desc={post.description}
date={post.date}
tags={post.tags}
cover={post.coverUrl}
/>
</Link>
</PostWrapper>
</Suspense>
))}
</Container>
</div>
);
} // blog/[slug]/page.js import { Article, CoverWrapper } from '../blog.styles';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { headers } from 'next/headers';
import { Suspense } from 'react';
import Image from 'next/image';
export async function generateMetadata({ params: { locale, slug } }) {
const host = headers().get('host');
const protocol = process?.env.NODE_ENV === 'production' ? 'http' : 'http';
const res = await fetch(`${protocol}://${host}/api/blog/${slug}?lang=${locale}`, {
method: 'GET',
});
const post = await res.json();
return {
title: 'Blog',
keywords: post.tags.map((tag) => tag.label),
openGraph: {
title: post.title,
description: post.description.replace('<p>', '').replace('</p>', ''),
type: 'article',
publishedTime: post.date,
authors: ['AUTHOR'],
},
};
}
export default async function BlogId({ params: { locale, slug } }) {
const host = headers().get('host');
const protocol = process?.env.NODE_ENV === 'production' ? 'https' : 'http';
// post markdown
const res = await fetch(`${protocol}://${host}/api/blog/${slug}?lang=${locale}&content=true`, {
method: 'GET',
});
const markdown = await res.json();
// details
const resp = await fetch(`${protocol}://${host}/api/blog/${slug}?lang=${locale}`, {
method: 'GET',
});
const post = await resp.json();
const components = {
h3: (props) => <h3 className="mt-6">{props.children}</h3>,
h4: (props) => <h4 className="mt-6">{props.children}</h4>,
h2: (props) => <h2 className="mt-10 !text-2xl">{props.children}</h2>,
p: (props) => <p className="mt-4">{props.children}</p>,
table: (props) => (
<div className="p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto rounded-large shadow-small w-full">
<table className="min-w-full h-auto table-auto w-full">{props.children}</table>
</div>
),
code: (props) => (
<div className="overflow-x-auto">
<code className="px-2 py-1 mt-4 h-fit font-mono font-normal inline-block whitespace-wrap bg-secondary/20 text-secondary text-small rounded-small">
{props.children}
</code>
</div>
),
};
return (
<div className="main">
<Article>
<CoverWrapper className="rounded-lg border-4 border-primary dark:border-secondary">
<Suspense
fallback={
<div
role="status"
className="h-full p-3 relative w-full bg-content1 rounded-lg animate-pulse dark:bg-content1"
>
<div class="flex items-center justify-center w-full h-full bg-primary-200 rounded dark:bg-primary-100">
<svg
class="w-10 h-10 text-gray-200 dark:text-gray-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 20 18"
>
<path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z" />
</svg>
</div>
<span className="sr-only">Loading...</span>
</div>
}
>
<Image
src={post.coverUrl}
priority
alt="cover"
fill
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
quality={100}
/>
</Suspense>
</CoverWrapper>
<Suspense
fallback={
<div role="status" class="max-w animate-pulse">
<div class="h-6 bg-default-100 rounded-full dark:bg-default-100 w-48 mb-6"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[65%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[95%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[85%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[95%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[35%] mb-5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[95%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[95%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[95%] mb-2.5"></div>
<div class="h-3 bg-default-100 rounded-full dark:bg-default-100 max-w-[45%] mb-2.5"></div>
<span class="sr-only">Loading...</span>
</div>
}
>
<MDXRemote components={{ ...components }} source={markdown} />
</Suspense>
</Article>
</div>
);
} |
Beta Was this translation helpful? Give feedback.
-
I've just read an article about the Astro and Vrite in the blog.
It would be great to read a new post about NextJS and Vrite integration next time :)
If not possible, It would be nice to know how to integrate or utilize Vrite for NextJs based projects.
Beta Was this translation helpful? Give feedback.
All reactions