Integrate Keystatic to edit twenty.com content (#10709)
This PR introduces Keystatic to let us edit twenty.com's content with a CMS. For now, we'll focus on creating release notes through Keystatic as it uses quite simple Markdown. Other types of content will need some refactoring to work with Keystatic. https://github.com/user-attachments/assets/e9f85bbf-daff-4b41-bc97-d1baf63758b2 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
6b4d3ed025
commit
2c465bd42e
4
.github/workflows/ci-website.yaml
vendored
4
.github/workflows/ci-website.yaml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
website-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 3
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
|
||||
55
packages/twenty-website/keystatic.config.ts
Normal file
55
packages/twenty-website/keystatic.config.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { collection, config, fields } from '@keystatic/core';
|
||||
import { wrapper } from '@keystatic/core/content-components';
|
||||
|
||||
export default config({
|
||||
storage: {
|
||||
kind:
|
||||
process.env.KEYSTATIC_STORAGE_KIND === ''
|
||||
? 'local'
|
||||
: ((process.env.KEYSTATIC_STORAGE_KIND || 'local') as
|
||||
| 'local'
|
||||
| 'github'
|
||||
| 'cloud'),
|
||||
repo: {
|
||||
owner: 'twentyhq',
|
||||
name: 'twenty',
|
||||
},
|
||||
},
|
||||
collections: {
|
||||
developers: collection({
|
||||
label: 'Technical documentation',
|
||||
slugField: 'title',
|
||||
path: 'src/content/developers/**',
|
||||
format: { contentField: 'content' },
|
||||
schema: {
|
||||
title: fields.slug({ name: { label: 'Title' } }),
|
||||
icon: fields.text({ label: 'Icon' }),
|
||||
image: fields.text({ label: 'Image' }),
|
||||
info: fields.text({ label: 'Info' }),
|
||||
content: fields.mdx({
|
||||
label: 'Content',
|
||||
components: {
|
||||
ArticleEditContent: wrapper({
|
||||
label: 'ArticleEditContent',
|
||||
schema: {},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
releases: collection({
|
||||
label: 'Releases',
|
||||
slugField: 'release',
|
||||
path: 'src/content/releases/*',
|
||||
format: { contentField: 'content' },
|
||||
schema: {
|
||||
release: fields.slug({ name: { label: 'Release' } }),
|
||||
// TODO: Define the date with a normalized format
|
||||
Date: fields.text({ label: 'Date' }),
|
||||
content: fields.mdx({
|
||||
label: 'Content',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
@ -15,6 +15,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/react": "^3.6.2",
|
||||
"@keystatic/core": "^0.5.45",
|
||||
"@keystatic/next": "^5.0.3",
|
||||
"@markdoc/markdoc": "^0.5.1",
|
||||
"@nivo/calendar": "^0.87.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"next-runtime-env": "^3.2.2",
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return null;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { makePage } from '@keystatic/next/ui/app';
|
||||
import config from '../../../../keystatic.config';
|
||||
|
||||
export default makePage(config);
|
||||
@ -0,0 +1,5 @@
|
||||
import KeystaticApp from './keystatic';
|
||||
|
||||
export default function Layout() {
|
||||
return <KeystaticApp />;
|
||||
}
|
||||
21
packages/twenty-website/src/app/(cms)/layout.tsx
Normal file
21
packages/twenty-website/src/app/(cms)/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Twenty.com',
|
||||
description: 'Open Source CRM',
|
||||
icons: '/images/core/logo.svg',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<div className="container">{children}</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { getContributorActivity } from '@/app/(public)/contributors/utils/get-contributor-activity';
|
||||
import { ActivityLog } from '@/app/_components/contributors/ActivityLog';
|
||||
import { Breadcrumb } from '@/app/_components/contributors/Breadcrumb';
|
||||
import { ContentContainer } from '@/app/_components/contributors/ContentContainer';
|
||||
@ -11,7 +12,6 @@ import { ProfileSharing } from '@/app/_components/contributors/ProfileSharing';
|
||||
import { PullRequests } from '@/app/_components/contributors/PullRequests';
|
||||
import { ThankYou } from '@/app/_components/contributors/ThankYou';
|
||||
import { Background } from '@/app/_components/oss-friends/Background';
|
||||
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';
|
||||
|
||||
export function generateMetadata({
|
||||
params,
|
||||
@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const GraphQlPlayground = dynamic(
|
||||
() => import('../../../_components/playground/graphql-playground'),
|
||||
() => import('@/app/_components/playground/graphql-playground'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const GraphQlPlayground = dynamic(
|
||||
() => import('../../../_components/playground/graphql-playground'),
|
||||
() => import('@/app/_components/playground/graphql-playground'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const CoreGraphql = () => {
|
||||
const MetadataGraphql = () => {
|
||||
return <GraphQlPlayground subDoc={'metadata'} />;
|
||||
};
|
||||
|
||||
export default CoreGraphql;
|
||||
export default MetadataGraphql;
|
||||
@ -1,11 +1,11 @@
|
||||
import { Metadata } from 'next';
|
||||
import { Gabarito, Inter } from 'next/font/google';
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
import { Gabarito, Inter } from 'next/font/google';
|
||||
|
||||
import { AppHeader } from '@/app/_components/ui/layout/header';
|
||||
|
||||
import { FooterDesktop } from './_components/ui/layout/FooterDesktop';
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry';
|
||||
import { FooterDesktop } from '../_components/ui/layout/FooterDesktop';
|
||||
import EmotionRootStyleRegistry from '../emotion-root-style-registry';
|
||||
|
||||
import './layout.css';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ContentContainer } from './_components/ui/layout/ContentContainer';
|
||||
import { ContentContainer } from '../_components/ui/layout/ContentContainer';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getReleases } from '@/app/releases/utils/get-releases';
|
||||
import { getReleases } from '@/app/(public)/releases/utils/get-releases';
|
||||
|
||||
export interface ReleaseNote {
|
||||
slug: string;
|
||||
@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import { desc } from 'drizzle-orm';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { ReleaseContainer } from '@/app/_components/releases/ReleaseContainer';
|
||||
import { Title } from '@/app/_components/releases/StyledTitle';
|
||||
import { ContentContainer } from '@/app/_components/ui/layout/ContentContainer';
|
||||
import {
|
||||
getMdxReleasesContent,
|
||||
getReleases,
|
||||
} from '@/app/releases/utils/get-releases';
|
||||
import { getVisibleReleases } from '@/app/releases/utils/get-visible-releases';
|
||||
} from '@/app/(public)/releases/utils/get-releases';
|
||||
import { getVisibleReleases } from '@/app/(public)/releases/utils/get-visible-releases';
|
||||
import { ReleaseContainer } from '@/app/_components/releases/ReleaseContainer';
|
||||
import { Title } from '@/app/_components/releases/StyledTitle';
|
||||
import { ContentContainer } from '@/app/_components/ui/layout/ContentContainer';
|
||||
import { findAll } from '@/database/database';
|
||||
import { GithubReleases, githubReleasesModel } from '@/database/model';
|
||||
import { pgGithubReleasesModel } from '@/database/schema-postgres';
|
||||
@ -4,7 +4,7 @@ import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import { JSXElementConstructor, ReactElement } from 'react';
|
||||
import gfm from 'remark-gfm';
|
||||
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
import { ReleaseNote } from '@/app/(public)/releases/api/route';
|
||||
import { compareSemanticVersions } from '@/shared-utils/compareSemanticVersions';
|
||||
|
||||
// WARNING: This API is used by twenty-front, not just by twenty-website
|
||||
@ -1,5 +1,5 @@
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
import { getFormattedReleaseNumber } from '@/app/releases/utils/get-formatted-release-number';
|
||||
import { ReleaseNote } from '@/app/(public)/releases/api/route';
|
||||
import { getFormattedReleaseNumber } from '@/app/(public)/releases/utils/get-formatted-release-number';
|
||||
|
||||
export const getVisibleReleases = (
|
||||
releaseNotes: ReleaseNote[],
|
||||
@ -3,9 +3,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { TimeRange } from '@nivo/calendar';
|
||||
|
||||
import { getActivityEndDate } from '@/app/(public)/contributors/utils/get-activity-end-date';
|
||||
import { CardContainer } from '@/app/_components/contributors/CardContainer';
|
||||
import { Title } from '@/app/_components/contributors/Title';
|
||||
import { getActivityEndDate } from '@/app/contributors/utils/get-activity-end-date';
|
||||
|
||||
const CalendarContentContainer = styled.div`
|
||||
@media (max-width: 890px) {
|
||||
|
||||
@ -11,7 +11,7 @@ import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticle
|
||||
import { getSectionIcon } from '@/shared-utils/getSectionIcons';
|
||||
|
||||
import '@docsearch/css';
|
||||
import '../../user-guide/algolia.css';
|
||||
import '../../(public)/user-guide/algolia.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${mq({
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useHeadsObserver } from '@/app/(public)/user-guide/hooks/useHeadsObserver';
|
||||
import ClientOnly from '@/app/_components/docs/ClientOnly';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { useHeadsObserver } from '@/app/user-guide/hooks/useHeadsObserver';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${mq({
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { JSXElementConstructor, ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Gabarito } from 'next/font/google';
|
||||
import { JSXElementConstructor, ReactElement } from 'react';
|
||||
|
||||
import { ReleaseNote } from '@/app/(public)/releases/api/route';
|
||||
import { ArticleContent } from '@/app/_components/ui/layout/articles/ArticleContent';
|
||||
import MotionContainer from '@/app/_components/ui/layout/LoaderAnimation';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 810px;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { ReleaseNote } from '@/app/(public)/releases/api/route';
|
||||
import { getGithubReleaseDateFromReleaseNote } from '@/app/(public)/releases/utils/get-github-release-date-from-release-note';
|
||||
import { Line } from '@/app/_components/releases/Line';
|
||||
import { Release } from '@/app/_components/releases/Release';
|
||||
import { ReleaseNote } from '@/app/releases/api/route';
|
||||
import { getGithubReleaseDateFromReleaseNote } from '@/app/releases/utils/get-github-release-date-from-release-note';
|
||||
import { GithubReleases } from '@/database/model';
|
||||
|
||||
interface ReleaseProps {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { format } from 'date-fns';
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { getContributorActivity } from '@/app/(public)/contributors/utils/get-contributor-activity';
|
||||
import {
|
||||
backgroundImage,
|
||||
container,
|
||||
@ -16,7 +17,6 @@ import {
|
||||
profileUsernameHeader,
|
||||
styledContributorAvatar,
|
||||
} from '@/app/api/contributors/[slug]/og.png/style';
|
||||
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';
|
||||
|
||||
const GABARITO_FONT_CDN_URL =
|
||||
'https://fonts.cdnfonts.com/s/105143/Gabarito-Medium-BF651cdf1f3f18e.woff';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';
|
||||
import { getContributorActivity } from '@/app/(public)/contributors/utils/get-contributor-activity';
|
||||
import { executePartialSync } from '@/github/execute-partial-sync';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { makeRouteHandler } from '@keystatic/next/route-handler';
|
||||
import config from '../../../../../keystatic.config';
|
||||
|
||||
export const { POST, GET } = makeRouteHandler({
|
||||
config,
|
||||
});
|
||||
@ -1,8 +1,8 @@
|
||||
import { desc } from 'drizzle-orm';
|
||||
|
||||
import { getGithubReleaseDateFromReleaseNote } from '@/app/releases/utils/get-github-release-date-from-release-note';
|
||||
import { getReleases } from '@/app/releases/utils/get-releases';
|
||||
import { getVisibleReleases } from '@/app/releases/utils/get-visible-releases';
|
||||
import { getGithubReleaseDateFromReleaseNote } from '@/app/(public)/releases/utils/get-github-release-date-from-release-note';
|
||||
import { getReleases } from '@/app/(public)/releases/utils/get-releases';
|
||||
import { getVisibleReleases } from '@/app/(public)/releases/utils/get-visible-releases';
|
||||
import { findAll } from '@/database/database';
|
||||
import { GithubReleases, githubReleasesModel } from '@/database/model';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user