mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-27 18:58:46 -06:00
Add Strapi as CMS for SEO content
Add Strapi as CMS for SEO content
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(", ")} />}
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
141
apps/formbricks-com/pages/learn/[slug].tsx
Normal file
141
apps/formbricks-com/pages/learn/[slug].tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
663
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,7 @@
|
||||
"RAILWAY_STATIC_URL",
|
||||
"RENDER_EXTERNAL_URL",
|
||||
"SENTRY_DSN",
|
||||
"STRAPI_API_KEY",
|
||||
"STRIPE_SECRET_KEY",
|
||||
"STRIPE_WEBHOOK_SECRET",
|
||||
"TELEMETRY_DISABLED",
|
||||
|
||||
Reference in New Issue
Block a user