feat(content): public Blog + Learn sections and static CMS pages (full-stack)
Adds the missing public-facing content pages and their admin authoring, all powered by the existing content-svc Blog entity discriminated by `kind`. Backend (content-svc): - BlogKind enum += Learn, Page (reuses Blog CRUD/SEO/slug/publish for all three). - SQL migration services/content/migrations/001_blog_kind_learn_page.sql (ALTER TYPE content.blog_kind ADD VALUE 'Learn','Page'). Frontend (public, Next.js): - lib/content-api.ts: fetchArticles(kind) / fetchArticle(slug) / fetchPage(slug) with safe empty/null fallbacks. - components/content: article-ui (card/list/detail + RTL prose), CmsPageContent, CmsRoute (admin-authored page or localized built-in fallback copy). - Routes: /blog, /blog/[slug], /learn, /learn/[slug] and static pages /about /contact /careers /privacy /terms /cookies /help. - Navbar "tutorials" → /learn; all footer links now resolve. Admin: - AdminResource: new `fixedValues` option (injects kind on create/update). - learnConfig (kind=Learn) + pagesConfig (kind=Page) reuse the /v1/blogs endpoint; /admin/learn + /admin/pages routes + nav items. i18n: blog, learn and 7 *Page namespaces added to both fa.json and en.json (verified key parity); admin nav labels learn/pages. Frontend tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("aboutPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/about" });
|
||||
}
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="about" ns="aboutPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,8 @@ export default async function AdminLayout({
|
||||
{ href: "/admin/fonts", label: t("fonts") },
|
||||
{ href: "/admin/music", label: t("music") },
|
||||
{ href: "/admin/blogs", label: t("blogs") },
|
||||
{ href: "/admin/learn", label: t("learn") },
|
||||
{ href: "/admin/pages", label: t("pages") },
|
||||
{ href: "/admin/slides", label: t("slides") },
|
||||
{ href: "/admin/home-events", label: t("homeEvents") },
|
||||
{ href: "/admin/routes", label: t("routes") },
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { AdminResource } from "@/components/admin/AdminResource";
|
||||
import { learnConfig } from "@/components/admin/admin-resources";
|
||||
|
||||
export default function Page() {
|
||||
return <AdminResource config={learnConfig} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { AdminResource } from "@/components/admin/AdminResource";
|
||||
import { pagesConfig } from "@/components/admin/admin-resources";
|
||||
|
||||
export default function Page() {
|
||||
return <AdminResource config={pagesConfig} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { ArticleDetailContent } from "@/components/content/article-ui";
|
||||
import { fetchArticle } from "@/lib/content-api";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const a = await fetchArticle(slug);
|
||||
if (!a) return {};
|
||||
return {
|
||||
title: a.metaTitle || a.title,
|
||||
description: a.metaDescription || a.shortDescription || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogDetailPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const a = await fetchArticle(slug);
|
||||
if (!a || !a.isPublished) notFound();
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<ArticleDetailContent section="blog" article={a} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { ArticleListContent } from "@/components/content/article-ui";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
import { fetchArticles } from "@/lib/content-api";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("blog");
|
||||
return createPageMetadata({
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
path: "/blog",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
const { items } = await fetchArticles("Blog", { pageSize: 24 });
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<ArticleListContent section="blog" articles={items} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("careersPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/careers" });
|
||||
}
|
||||
|
||||
export default function CareersPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="careers" ns="careersPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("contactPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/contact" });
|
||||
}
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="contact" ns="contactPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("cookiesPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/cookies" });
|
||||
}
|
||||
|
||||
export default function CookiesPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="cookies" ns="cookiesPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("helpPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/help" });
|
||||
}
|
||||
|
||||
export default function HelpPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="help" ns="helpPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { ArticleDetailContent } from "@/components/content/article-ui";
|
||||
import { fetchArticle } from "@/lib/content-api";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const a = await fetchArticle(slug);
|
||||
if (!a) return {};
|
||||
return {
|
||||
title: a.metaTitle || a.title,
|
||||
description: a.metaDescription || a.shortDescription || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LearnDetailPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const a = await fetchArticle(slug);
|
||||
if (!a || !a.isPublished) notFound();
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<ArticleDetailContent section="learn" article={a} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { ArticleListContent } from "@/components/content/article-ui";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
import { fetchArticles } from "@/lib/content-api";
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("learn");
|
||||
return createPageMetadata({
|
||||
title: t("metaTitle"),
|
||||
description: t("metaDescription"),
|
||||
path: "/learn",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function LearnPage() {
|
||||
const { items } = await fetchArticles("Learn", { pageSize: 24 });
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<ArticleListContent section="learn" articles={items} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("privacyPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/privacy" });
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="privacy" ns="privacyPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
import { CmsRoute } from "@/components/content/CmsRoute";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("termsPage");
|
||||
return createPageMetadata({ title: t("title"), description: t("lead"), path: "/terms" });
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<CmsRoute slug="terms" ns="termsPage" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user