diff --git a/apps/web/app/(marketing)/blog/[slug]/page.tsx b/apps/web/app/(marketing)/blog/[slug]/page.tsx index ed2363d70..804f9b415 100644 --- a/apps/web/app/(marketing)/blog/[slug]/page.tsx +++ b/apps/web/app/(marketing)/blog/[slug]/page.tsx @@ -1,3 +1,5 @@ +import { cache } from 'react'; + import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -8,13 +10,18 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import { Post } from '../../blog/_components/post'; +const getPostBySlug = cache(async (slug: string) => { + const client = await createCmsClient(); + + return client.getContentItemBySlug({ slug, collection: 'posts' }); +}); + export async function generateMetadata({ params, }: { params: { slug: string }; }): Promise { - const cms = await createCmsClient(); - const post = await cms.getContentItemById(params.slug); + const post = await getPostBySlug(params.slug); if (!post) { notFound(); @@ -29,7 +36,7 @@ export async function generateMetadata({ title, description, type: 'article', - publishedTime: publishedAt.toDateString(), + publishedTime: publishedAt?.toDateString(), url: post.url, images: image ? [ @@ -49,8 +56,7 @@ export async function generateMetadata({ } async function BlogPost({ params }: { params: { slug: string } }) { - const cms = await createCmsClient(); - const post = await cms.getContentItemById(params.slug); + const post = await getPostBySlug(params.slug); if (!post) { notFound(); diff --git a/apps/web/app/(marketing)/blog/_components/post.tsx b/apps/web/app/(marketing)/blog/_components/post.tsx index 2d94b370d..38d38ce25 100644 --- a/apps/web/app/(marketing)/blog/_components/post.tsx +++ b/apps/web/app/(marketing)/blog/_components/post.tsx @@ -12,7 +12,7 @@ export const Post: React.FC<{
-
+
diff --git a/apps/web/app/(marketing)/blog/page.tsx b/apps/web/app/(marketing)/blog/page.tsx index 40aca4189..0b1becd36 100644 --- a/apps/web/app/(marketing)/blog/page.tsx +++ b/apps/web/app/(marketing)/blog/page.tsx @@ -15,12 +15,18 @@ export const generateMetadata = async () => { }; }; -async function BlogPage() { +async function BlogPage({ searchParams }: { searchParams: { page: string } }) { const { t } = await createI18nServerInstance(); const cms = await createCmsClient(); + const page = searchParams.page ? parseInt(searchParams.page) : 0; + const limit = 10; + const offset = page * limit; + const posts = await cms.getContentItems({ - categories: ['blog'], + collection: 'posts', + limit, + offset, }); return ( diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index a8e038005..53a463612 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -14,7 +14,7 @@ import { DocsCards } from '../_components/docs-cards'; const getPageBySlug = cache(async (slug: string) => { const client = await createCmsClient(); - return client.getContentItemById(slug); + return client.getContentItemBySlug({ slug, collection: 'documentation' }); }); interface PageParams { diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx index 0b14326cb..5fd0c9504 100644 --- a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx +++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx @@ -40,11 +40,14 @@ const Node: React.FC<{ level: number; activePath: string; }> = ({ node, level, activePath }) => { + const pathPrefix = `/docs`; + const url = `${pathPrefix}/${node.url}`; + return ( <> diff --git a/apps/web/app/(marketing)/docs/layout.tsx b/apps/web/app/(marketing)/docs/layout.tsx index badfe1822..fdc54c06e 100644 --- a/apps/web/app/(marketing)/docs/layout.tsx +++ b/apps/web/app/(marketing)/docs/layout.tsx @@ -6,9 +6,11 @@ async function DocsLayout({ children }: React.PropsWithChildren) { const cms = await createCmsClient(); const pages = await cms.getContentItems({ - categories: ['documentation'], + collection: 'documentation', }); + console.log(pages); + return (
diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx index be73ad678..68c2e5aee 100644 --- a/apps/web/app/(marketing)/docs/page.tsx +++ b/apps/web/app/(marketing)/docs/page.tsx @@ -19,7 +19,7 @@ async function DocsPage() { const { t } = await createI18nServerInstance(); const docs = await client.getContentItems({ - categories: ['documentation'], + collection: 'documentation', }); // Filter out any docs that have a parentId, as these are children of other docs diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx index c28189677..3967e05e4 100644 --- a/apps/web/app/(marketing)/page.tsx +++ b/apps/web/app/(marketing)/page.tsx @@ -76,7 +76,7 @@ function Home() { } width={3069} height={1916} - src={`/assets/images/dashboard-demo.webp`} + src={`/images/dashboard-demo.webp`} alt={`App Image`} />
@@ -135,7 +135,7 @@ function Home() { {'Sign {'Dashboard'}; + abstract getContentItemBySlug(params: { + slug: string; + collection: string; + }): Promise; /** * Retrieves categories based on the provided options. diff --git a/packages/cms/keystatic/src/client.ts b/packages/cms/keystatic/src/client.ts index f729ff82d..d5125ea3f 100644 --- a/packages/cms/keystatic/src/client.ts +++ b/packages/cms/keystatic/src/client.ts @@ -9,8 +9,15 @@ const reader = createReader('.', config); type EntryProps = Entry<(typeof config)['collections']['posts']>; export class KeystaticClient implements CmsClient { - async getContentItems(options?: Cms.GetContentItemsOptions) { - const docs = await reader.collections.posts.all(); + async getContentItems(options: Cms.GetContentItemsOptions) { + const collection = + options.collection as keyof (typeof config)['collections']; + + if (!reader.collections[collection]) { + throw new Error(`Collection ${collection} not found`); + } + + const docs = await reader.collections[collection].all(); const startOffset = options?.offset ?? 0; const endOffset = startOffset + (options?.limit ?? 10); @@ -44,21 +51,26 @@ export class KeystaticClient implements CmsClient { (item) => item.entry.parent === item.slug, ); - console.log(item); - return this.mapPost(item, children); }), ); } - async getContentItemById(id: string) { - const doc = await reader.collections.posts.read(id); + async getContentItemBySlug(params: { slug: string; collection: string }) { + const collection = + params.collection as keyof (typeof config)['collections']; + + if (!reader.collections[collection]) { + throw new Error(`Collection ${collection} not found`); + } + + const doc = await reader.collections[collection].read(params.slug); if (!doc) { return Promise.resolve(undefined); } - return this.mapPost({ entry: doc, slug: id }, []); + return this.mapPost({ entry: doc, slug: params.slug }, []); } async getCategories() { @@ -96,7 +108,6 @@ export class KeystaticClient implements CmsClient { slug: item.slug, description: item.entry.description, publishedAt, - author: item.entry.author, content, image: item.entry.image ?? undefined, categories: diff --git a/packages/cms/keystatic/src/keystatic.config.ts b/packages/cms/keystatic/src/keystatic.config.ts index 07f2a2bdc..45bfe8438 100644 --- a/packages/cms/keystatic/src/keystatic.config.ts +++ b/packages/cms/keystatic/src/keystatic.config.ts @@ -27,7 +27,6 @@ function createKeyStaticConfig(path: string) { tags: fields.array(fields.text({ label: 'Tag' })), description: fields.text({ label: 'Description' }), publishedAt: fields.date({ label: 'Published At' }), - author: fields.text({ label: 'Author' }), parent: fields.relationship({ label: 'Parent', collection: 'posts', @@ -52,6 +51,46 @@ function createKeyStaticConfig(path: string) { }), }, }), + documentation: collection({ + label: 'Documentation', + slugField: 'title', + path: `${path}/documentation/**`, + format: { contentField: 'content' }, + schema: { + title: fields.slug({ name: { label: 'Title' } }), + content: fields.document({ + label: 'Content', + formatting: true, + dividers: true, + links: true, + images: { + directory: 'public/site/images', + publicPath: '/site/images', + schema: { + title: fields.text({ + label: 'Caption', + description: + 'The text to display under the image in a caption.', + }), + }, + }, + }), + image: fields.image({ + label: 'Image', + directory: 'public/site/images', + publicPath: '/site/images', + }), + description: fields.text({ label: 'Description' }), + publishedAt: fields.date({ label: 'Published At' }), + order: fields.number({ label: 'Order' }), + parent: fields.relationship({ + label: 'Parent', + collection: 'documentation', + }), + categories: fields.array(fields.text({ label: 'Category' })), + tags: fields.array(fields.text({ label: 'Tag' })), + }, + }), }, }); } diff --git a/packages/cms/wordpress/src/wp-client.ts b/packages/cms/wordpress/src/wp-client.ts index fcf557f4b..f5a41de52 100644 --- a/packages/cms/wordpress/src/wp-client.ts +++ b/packages/cms/wordpress/src/wp-client.ts @@ -25,7 +25,7 @@ export class WordpressClient implements CmsClient { * * @param {Cms.GetContentItemsOptions} options - The options to customize the retrieval of content items. */ - async getContentItems(options?: Cms.GetContentItemsOptions) { + async getContentItems(options: Cms.GetContentItemsOptions) { const queryParams = new URLSearchParams({ _embed: 'true', }); @@ -70,20 +70,21 @@ export class WordpressClient implements CmsClient { } const endpoints = [ - `/wp-json/wp/v2/pages?${queryParams.toString()}`, `/wp-json/wp/v2/posts?${queryParams.toString()}`, + `/wp-json/wp/v2/pages?${queryParams.toString()}`, ]; - const urls = endpoints.map((endpoint) => `${this.apiUrl}${endpoint}`); + const endpoint = + options.collection === 'posts' ? endpoints[0] : endpoints[1]; - const responses = await Promise.all( - urls.map((url) => - fetch(url).then((value) => value.json() as Promise), - ), - ).then((values) => values.flat().filter(Boolean)); + const url = `${this.apiUrl}${endpoint}`; - return await Promise.all( - responses.map(async (item: WP_REST_API_Post) => { + const posts = await fetch(url).then( + (value) => value.json() as Promise, + ); + + return Promise.all( + posts.map(async (item: WP_REST_API_Post) => { let parentId: string | undefined; if (!item) { @@ -94,7 +95,6 @@ export class WordpressClient implements CmsClient { parentId = item.parent.toString(); } - const author = await this.getAuthor(item.author); const categories = await this.getCategoriesByIds(item.categories ?? []); const tags = await this.getTagsByIds(item.tags ?? []); const image = item.featured_media ? this.getFeaturedMedia(item) : ''; @@ -109,7 +109,6 @@ export class WordpressClient implements CmsClient { url: item.link, slug: item.slug, publishedAt: new Date(item.date), - author: author?.name, categories: categories, tags: tags, parentId, @@ -120,34 +119,35 @@ export class WordpressClient implements CmsClient { ); } - async getContentItemById(slug: string) { + async getContentItemBySlug({ + slug, + collection, + }: { + slug: string; + collection: string; + }) { const searchParams = new URLSearchParams({ _embed: 'true', slug, }); const endpoints = [ - `/wp-json/wp/v2/pages?${searchParams.toString()}`, `/wp-json/wp/v2/posts?${searchParams.toString()}`, + `/wp-json/wp/v2/pages?${searchParams.toString()}`, ]; - const promises = endpoints.map((endpoint) => - fetch(this.apiUrl + endpoint).then( - (res) => res.json() as Promise, - ), + const endpoint = collection === 'posts' ? endpoints[0] : endpoints[1]; + + const responses = await fetch(this.apiUrl + endpoint).then( + (res) => res.json() as Promise, ); - const responses = await Promise.all(promises).then((values) => - values.filter(Boolean), - ); - - const item = responses[0] ? responses[0][0] : undefined; + const item = responses[0]; if (!item) { return; } - const author = await this.getAuthor(item.author); const categories = await this.getCategoriesByIds(item.categories ?? []); const tags = await this.getTagsByIds(item.tags ?? []); const image = item.featured_media ? this.getFeaturedMedia(item) : ''; @@ -163,7 +163,6 @@ export class WordpressClient implements CmsClient { content: item.content.rendered, slug: item.slug, publishedAt: new Date(item.date), - author: author?.name, categories, tags, parentId: item.parent?.toString(), @@ -313,18 +312,6 @@ export class WordpressClient implements CmsClient { })); } - private async getAuthor(id: number) { - const response = await fetch(`${this.apiUrl}/wp-json/wp/v2/users/${id}`); - - if (!response.ok) { - return undefined; - } - - const data = await response.json(); - - return { name: data.name }; - } - private getFeaturedMedia(post: WP_REST_API_Post) { const embedded = post._embedded ?? { 'wp:featuredmedia': [], diff --git a/packages/features/auth/src/components/oauth-provider-logo-image.tsx b/packages/features/auth/src/components/oauth-provider-logo-image.tsx index 083442c9b..fa4d74872 100644 --- a/packages/features/auth/src/components/oauth-provider-logo-image.tsx +++ b/packages/features/auth/src/components/oauth-provider-logo-image.tsx @@ -31,11 +31,11 @@ function getOAuthProviderLogos(): Record { return { password: , phone: , - google: '/assets/images/google.webp', - facebook: '/assets/images/facebook.webp', - twitter: '/assets/images/twitter.webp', - github: '/assets/images/github.webp', - microsoft: '/assets/images/microsoft.webp', - apple: '/assets/images/apple.webp', + google: '/images/oauth/google.webp', + facebook: '/images/oauth/facebook.webp', + twitter: '/images/oauth/twitter.webp', + github: '/images/oauth/github.webp', + microsoft: '/images/oauth/microsoft.webp', + apple: '/images/oauth/apple.webp', }; }