Add Strapi as CMS for SEO content

Add Strapi as CMS for SEO content
This commit is contained in:
Johannes
2023-08-17 17:39:42 +02:00
committed by GitHub
13 changed files with 594 additions and 263 deletions

View File

@@ -1,3 +1,6 @@
NEXT_PUBLIC_FORMBRICKS_COM_API_HOST=http://localhost:3000
NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID=
NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID=
# Strapi API Key
STRAPI_API_KEY=

View File

@@ -1,8 +1,8 @@
import { useEffect } from "react";
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
import { Prose } from "./Prose";
import { useEffect } from "react";
const useExternalLinks = (selector: string) => {
useEffect(() => {

View File

@@ -4,6 +4,7 @@ interface Props {
title: string;
description: string;
publishedTime?: string;
updatedTime?: string;
authors?: string[];
section?: string;
tags?: string[];
@@ -13,6 +14,7 @@ export default function MetaInformation({
title,
description,
publishedTime,
updatedTime,
authors,
section,
tags,
@@ -31,9 +33,10 @@ export default function MetaInformation({
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Source Experience Management, Privacy-first" />
<meta property="article:publisher" content="Formbricks" />
<meta property="og:site_name" content="Formbricks Privacy-first Experience Management Solution" />
<meta property="article:publisher" content="Formbricks GmbH" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{updatedTime && <meta property="article:updated_time" content={updatedTime} />}
{authors && <meta property="article:author" content={authors.join(", ")} />}
{section && <meta property="article:section" content={section} />}
{tags && <meta property="article:tag" content={tags.join(", ")} />}

View File

@@ -1,14 +1,23 @@
/** @type {import('next').NextConfig} */
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import { withPlausibleProxy } from "next-plausible";
import remarkGfm from "remark-gfm";
import rehypePrism from "@mapbox/rehype-prism";
const nextConfig = {
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "seo-strapi-aws-s3.s3.eu-central-1.amazonaws.com",
port: "",
},
],
},
async redirects() {
return [
{

View File

@@ -27,12 +27,15 @@
"lottie-web": "^5.12.2",
"next": "13.4.12",
"next-plausible": "^3.10.1",
"next-seo": "^6.1.0",
"next-sitemap": "^4.1.8",
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.0.6",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.10.1",
"react-markdown": "^8.0.7",
"react-responsive-embed": "^2.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.4"

View File

@@ -1,12 +1,12 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import AuthorBox from "@/components/shared/AuthorBox";
import Formbricks from "./open-source-survey-software-free-2023-formbricks-typeform-alternative.png";
import Typebot from "./typebot-open-source-free-conversational-form-builder-survey-software-opensource.jpg";
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "5 Open Source Survey and Form Tools maintained in 2023",

View File

@@ -0,0 +1,141 @@
import LayoutMdx from "@/components/shared/LayoutMdx";
import { FAQPageJsonLd } from "next-seo";
import Image from "next/image";
import fetch from "node-fetch";
import ReactMarkdown from "react-markdown";
type Article = {
id?: number;
attributes?: {
author?: string;
title?: string;
text?: string;
slug?: string;
createdAt?: string;
updatedAt?: string;
publishedAt?: string;
meta?: {
id?: number;
description?: string;
title?: string;
publisher?: string;
section?: string;
tags?: {
id?: number;
tag?: string;
}[];
};
faq?: {
id?: number;
question?: string;
answer?: string;
}[];
};
};
type ArticlePageProps = {
article?: Article;
};
interface ArticleResponse {
data: Article[];
meta: {
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
}
export async function getStaticPaths() {
const response = await fetch(
"https://strapi.formbricks.com/api/articles?populate[meta][populate]=*&filters[category][name][$eq]=learn",
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const articles = (await response.json()) as ArticleResponse;
const paths = articles.data.map((article) => ({
params: { slug: article.attributes.slug },
}));
return { paths, fallback: true };
}
export async function getStaticProps({ params }) {
const res = await fetch(
`https://strapi.formbricks.com/api/articles?populate[meta][populate]=*&populate[faq][populate]=*&filters[slug][$eq]=${params.slug}`,
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
},
}
);
if (!res.ok) {
throw new Error("Something went wrong");
}
const resData = (await res.json()) as ArticleResponse;
const article = resData.data[0];
return { props: { article } };
}
export default function ArticlePage({ article = {} }: ArticlePageProps) {
if (!article || !article.attributes) return <div>Loading...</div>;
// Use next/image to render images in markdown
const renderers = {
img: (image) => {
return <Image src={image.src} alt={image.alt} width={1000} height={500} />;
},
};
const {
attributes: {
author,
publishedAt,
text,
faq,
meta: {
title,
description,
section,
tags = [], // default empty array if tags are not provided
} = {}, // default empty object if meta is not provided
} = {}, // default empty object if attributes are not provided
} = article;
const metaTags = tags.map((tag) => tag.tag);
const meta = {
title,
description,
publishedTime: publishedAt,
authors: [author],
section,
tags: metaTags,
};
// Convert the FAQ details into the desired format for FAQPageJsonLd
const faqEntities = faq.map(({ question, answer }) => ({
questionName: question,
acceptedAnswerText: answer,
}));
return (
<LayoutMdx meta={meta}>
<>
<ReactMarkdown components={renderers}>{text}</ReactMarkdown>
<FAQPageJsonLd mainEntity={faqEntities} />
</>
</LayoutMdx>
);
}

View File

@@ -1,15 +1,15 @@
"use client";
import toast from "react-hot-toast";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useState, Dispatch, SetStateAction } from "react";
import { useRouter } from "next/navigation";
import { useMembers } from "@/lib/members";
import { useProfile } from "@/lib/profile";
import { Button, ErrorComponent, Input } from "@formbricks/ui";
import { useTeam, deleteTeam } from "@/lib/teams/teams";
import { useMemberships } from "@/lib/memberships";
import { useProfile } from "@/lib/profile";
import { deleteTeam, useTeam } from "@/lib/teams/teams";
import { Button, ErrorComponent, Input } from "@formbricks/ui";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
export default function DeleteTeam({ environmentId }) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

View File

@@ -3,6 +3,7 @@
import DeleteDialog from "@/components/shared/DeleteDialog";
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
import { formbricksLogout } from "@/lib/formbricks";
import { TProfile } from "@formbricks/types/v1/profile";
import { Button, Input, ProfileAvatar } from "@formbricks/ui";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
@@ -10,7 +11,6 @@ import Image from "next/image";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
import { profileDeleteAction } from "./actions";
import { TProfile } from "@formbricks/types/v1/profile";
export function EditAvatar({ session }) {
return (

View File

@@ -11,7 +11,7 @@
"eslint": "^8.46.0",
"eslint-config-next": "^13.4.12",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-react": "7.33.1",
"eslint-config-turbo": "latest"
"eslint-config-turbo": "latest",
"eslint-plugin-react": "7.33.1"
}
}

View File

@@ -15,12 +15,12 @@
"@formbricks/database": "*",
"@formbricks/errors": "*",
"@formbricks/types": "*",
"@paralleldrive/cuid2": "^2.2.1",
"date-fns": "^2.30.0",
"markdown-it": "^13.0.1",
"posthog-node": "^3.1.1",
"server-only": "^0.0.1",
"tailwind-merge": "^1.14.0",
"@paralleldrive/cuid2": "^2.2.1"
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
"@formbricks/tsconfig": "*",

663
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,7 @@
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"SENTRY_DSN",
"STRAPI_API_KEY",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"TELEMETRY_DISABLED",