From fdb014c516188ea21ab22776260c4341b039b4fa Mon Sep 17 00:00:00 2001 From: Jan Simecek Date: Tue, 28 Apr 2026 11:42:22 +0200 Subject: [PATCH] fix: visual fixes and missing features --- .../content-types/blog-post/lifecycles.ts | 8 + .../content-types/blog-post/schema.json | 4 +- .../content-types/case-study/lifecycles.ts | 8 + .../content-types/not-found/schema.json | 63 + .../api/not-found/controllers/not-found.ts | 3 + .../src/api/not-found/routes/not-found.ts | 3 + .../src/api/not-found/services/not-found.ts | 3 + .../src/components/blog/related-posts.json | 3 + .../src/components/cards/feature-card.json | 2 +- .../elements/how-it-works-item.json | 2 +- .../src/components/testimonials/quote.json | 2 +- .../utilities/link-decorations.json | 2 +- apps/strapi/types/generated/components.d.ts | 9 +- apps/strapi/types/generated/contentTypes.d.ts | 69 +- apps/ui/src/app/[locale]/blog/page.tsx | 11 +- apps/ui/src/app/[locale]/layout.tsx | 12 + apps/ui/src/app/[locale]/not-found.tsx | 65 +- apps/ui/src/app/robots.ts | 19 +- apps/ui/src/components/blog/BlogNavbar.tsx | 23 +- .../ui/src/components/blog/BlogNavbarTabs.tsx | 137 +- .../ui/src/components/blog/BlogPostHeader.tsx | 2 +- apps/ui/src/components/blog/BlogPostRow.tsx | 10 +- .../src/components/blog/FeaturedBlogPost.tsx | 8 +- apps/ui/src/components/elementary/AppLink.tsx | 2 +- .../newsletter/NewsletterSignup.tsx | 35 +- .../components/blog/StrapiRelatedPosts.tsx | 2 +- .../navigation/navbar/DesktopNavbar.tsx | 12 +- .../sections/StrapiTwoColumnsBenefits.tsx | 3 +- .../components/utilities/StrapiLink.tsx | 2 +- .../single-types/footer/StrapiFooterCta.tsx | 10 +- apps/ui/src/components/ui/button.tsx | 2 +- apps/ui/src/lib/blog-utils.ts | 4 +- apps/ui/src/lib/metadata/helpers.ts | 20 +- apps/ui/src/lib/strapi-api/base.ts | 1 + apps/ui/src/lib/strapi-api/content/server.ts | 36 +- packages/migration/src/clients/target.ts | 5 +- packages/migration/src/index.ts | 29 + .../scripts/backfill-original-published-at.ts | 159 + packages/migration/state/media-cache.json | 5875 +---------------- packages/migration/state/migration-state.json | 99 +- 40 files changed, 649 insertions(+), 6115 deletions(-) create mode 100644 apps/strapi/src/api/not-found/content-types/not-found/schema.json create mode 100644 apps/strapi/src/api/not-found/controllers/not-found.ts create mode 100644 apps/strapi/src/api/not-found/routes/not-found.ts create mode 100644 apps/strapi/src/api/not-found/services/not-found.ts create mode 100644 packages/migration/src/scripts/backfill-original-published-at.ts diff --git a/apps/strapi/src/api/blog-post/content-types/blog-post/lifecycles.ts b/apps/strapi/src/api/blog-post/content-types/blog-post/lifecycles.ts index 9319cbf..95be7e2 100644 --- a/apps/strapi/src/api/blog-post/content-types/blog-post/lifecycles.ts +++ b/apps/strapi/src/api/blog-post/content-types/blog-post/lifecycles.ts @@ -9,6 +9,14 @@ function backfillOriginalPublishedAt( const publishedAt = data["publishedAt"] + // Strapi v5 sometimes hands publishedAt to lifecycle hooks as a Date, + // sometimes as an ISO string — normalize both. + if (publishedAt instanceof Date) { + data["originalPublishedAt"] = publishedAt.toISOString() + + return + } + if (typeof publishedAt === "string" && publishedAt.length > 0) { data["originalPublishedAt"] = publishedAt } diff --git a/apps/strapi/src/api/blog-post/content-types/blog-post/schema.json b/apps/strapi/src/api/blog-post/content-types/blog-post/schema.json index 87dc618..316aa6b 100644 --- a/apps/strapi/src/api/blog-post/content-types/blog-post/schema.json +++ b/apps/strapi/src/api/blog-post/content-types/blog-post/schema.json @@ -38,9 +38,9 @@ "component": "media.image", "repeatable": false }, - "timelineImage": { + "coverImage": { "type": "component", - "component": "media.image", + "component": "utilities.basic-image", "repeatable": false }, "author": { diff --git a/apps/strapi/src/api/case-study/content-types/case-study/lifecycles.ts b/apps/strapi/src/api/case-study/content-types/case-study/lifecycles.ts index 9319cbf..95be7e2 100644 --- a/apps/strapi/src/api/case-study/content-types/case-study/lifecycles.ts +++ b/apps/strapi/src/api/case-study/content-types/case-study/lifecycles.ts @@ -9,6 +9,14 @@ function backfillOriginalPublishedAt( const publishedAt = data["publishedAt"] + // Strapi v5 sometimes hands publishedAt to lifecycle hooks as a Date, + // sometimes as an ISO string — normalize both. + if (publishedAt instanceof Date) { + data["originalPublishedAt"] = publishedAt.toISOString() + + return + } + if (typeof publishedAt === "string" && publishedAt.length > 0) { data["originalPublishedAt"] = publishedAt } diff --git a/apps/strapi/src/api/not-found/content-types/not-found/schema.json b/apps/strapi/src/api/not-found/content-types/not-found/schema.json new file mode 100644 index 0000000..23a3a07 --- /dev/null +++ b/apps/strapi/src/api/not-found/content-types/not-found/schema.json @@ -0,0 +1,63 @@ +{ + "kind": "singleType", + "collectionName": "not_founds", + "info": { + "singularName": "not-found", + "pluralName": "not-founds", + "displayName": "404 / Not Found", + "description": "Customizable 404 page content" + }, + "options": { + "draftAndPublish": false + }, + "pluginOptions": { + "i18n": { + "localized": true + } + }, + "attributes": { + "title": { + "type": "string", + "pluginOptions": { + "i18n": { + "localized": true + } + } + }, + "backButtonText": { + "type": "string", + "pluginOptions": { + "i18n": { + "localized": true + } + } + }, + "image": { + "type": "component", + "component": "utilities.basic-image", + "repeatable": false, + "pluginOptions": { + "i18n": { + "localized": true + } + } + }, + "content": { + "type": "dynamiczone", + "pluginOptions": { + "i18n": { + "localized": true + } + }, + "components": [ + "blog.related-posts", + "blog.editors-picks", + "blog.resource-cta", + "sections.cta-banner", + "sections.community-banner", + "sections.section-header", + "sections.feature-card-grid" + ] + } + } +} diff --git a/apps/strapi/src/api/not-found/controllers/not-found.ts b/apps/strapi/src/api/not-found/controllers/not-found.ts new file mode 100644 index 0000000..d997e9a --- /dev/null +++ b/apps/strapi/src/api/not-found/controllers/not-found.ts @@ -0,0 +1,3 @@ +import { factories } from "@strapi/strapi" + +export default factories.createCoreController("api::not-found.not-found") diff --git a/apps/strapi/src/api/not-found/routes/not-found.ts b/apps/strapi/src/api/not-found/routes/not-found.ts new file mode 100644 index 0000000..f5c24ec --- /dev/null +++ b/apps/strapi/src/api/not-found/routes/not-found.ts @@ -0,0 +1,3 @@ +import { factories } from "@strapi/strapi" + +export default factories.createCoreRouter("api::not-found.not-found") diff --git a/apps/strapi/src/api/not-found/services/not-found.ts b/apps/strapi/src/api/not-found/services/not-found.ts new file mode 100644 index 0000000..2806212 --- /dev/null +++ b/apps/strapi/src/api/not-found/services/not-found.ts @@ -0,0 +1,3 @@ +import { factories } from "@strapi/strapi" + +export default factories.createCoreService("api::not-found.not-found") diff --git a/apps/strapi/src/components/blog/related-posts.json b/apps/strapi/src/components/blog/related-posts.json index 8f9d270..e2837a0 100644 --- a/apps/strapi/src/components/blog/related-posts.json +++ b/apps/strapi/src/components/blog/related-posts.json @@ -7,6 +7,9 @@ }, "options": {}, "attributes": { + "title": { + "type": "string" + }, "blogPosts": { "type": "relation", "relation": "oneToMany", diff --git a/apps/strapi/src/components/cards/feature-card.json b/apps/strapi/src/components/cards/feature-card.json index bda0165..703bf72 100644 --- a/apps/strapi/src/components/cards/feature-card.json +++ b/apps/strapi/src/components/cards/feature-card.json @@ -12,7 +12,7 @@ "required": true }, "description": { - "type": "text" + "type": "richtext" }, "icon": { "type": "component", diff --git a/apps/strapi/src/components/elements/how-it-works-item.json b/apps/strapi/src/components/elements/how-it-works-item.json index 400006c..76f8198 100644 --- a/apps/strapi/src/components/elements/how-it-works-item.json +++ b/apps/strapi/src/components/elements/how-it-works-item.json @@ -26,7 +26,7 @@ } }, "description": { - "type": "text", + "type": "richtext", "required": true, "pluginOptions": { "i18n": { diff --git a/apps/strapi/src/components/testimonials/quote.json b/apps/strapi/src/components/testimonials/quote.json index 39d6840..09fef14 100644 --- a/apps/strapi/src/components/testimonials/quote.json +++ b/apps/strapi/src/components/testimonials/quote.json @@ -8,7 +8,7 @@ "options": {}, "attributes": { "quote": { - "type": "text", + "type": "richtext", "required": true }, "authorName": { diff --git a/apps/strapi/src/components/utilities/link-decorations.json b/apps/strapi/src/components/utilities/link-decorations.json index 41581a1..e8f8c4d 100644 --- a/apps/strapi/src/components/utilities/link-decorations.json +++ b/apps/strapi/src/components/utilities/link-decorations.json @@ -9,7 +9,7 @@ "variant": { "type": "enumeration", "required": true, - "default": "link", + "default": "default", "enum": [ "default", "destructive", diff --git a/apps/strapi/types/generated/components.d.ts b/apps/strapi/types/generated/components.d.ts index 7dbad4f..8d47ef6 100644 --- a/apps/strapi/types/generated/components.d.ts +++ b/apps/strapi/types/generated/components.d.ts @@ -63,6 +63,7 @@ export interface BlogRelatedPosts extends Struct.ComponentSchema { "oneToOne", "api::post-category.post-category" > + title: Schema.Attribute.String } } @@ -121,7 +122,7 @@ export interface CardsFeatureCard extends Struct.ComponentSchema { } attributes: { ctaLinks: Schema.Attribute.Component<"utilities.link", true> - description: Schema.Attribute.Text + description: Schema.Attribute.RichText icon: Schema.Attribute.Component<"utilities.basic-image", false> image: Schema.Attribute.Component<"utilities.basic-image", false> imagePosition: Schema.Attribute.Enumeration<["left", "right"]> & @@ -271,7 +272,7 @@ export interface ElementsHowItWorksItem extends Struct.ComponentSchema { icon: "lightbulb" } attributes: { - description: Schema.Attribute.Text & + description: Schema.Attribute.RichText & Schema.Attribute.Required & Schema.Attribute.SetPluginOptions<{ i18n: { @@ -1310,7 +1311,7 @@ export interface TestimonialsQuote extends Struct.ComponentSchema { authorRole: Schema.Attribute.String companyLogo: Schema.Attribute.Component<"utilities.basic-image", false> image: Schema.Attribute.Component<"utilities.basic-image", false> - quote: Schema.Attribute.Text & Schema.Attribute.Required + quote: Schema.Attribute.RichText & Schema.Attribute.Required variant: Schema.Attribute.Enumeration<["boxed", "image"]> & Schema.Attribute.DefaultTo<"boxed"> } @@ -1384,7 +1385,7 @@ export interface UtilitiesLinkDecorations extends Struct.ComponentSchema { ["default", "destructive", "outline", "secondary", "ghost", "link"] > & Schema.Attribute.Required & - Schema.Attribute.DefaultTo<"link"> + Schema.Attribute.DefaultTo<"default"> } } diff --git a/apps/strapi/types/generated/contentTypes.d.ts b/apps/strapi/types/generated/contentTypes.d.ts index 9b347c8..719bc05 100644 --- a/apps/strapi/types/generated/contentTypes.d.ts +++ b/apps/strapi/types/generated/contentTypes.d.ts @@ -509,6 +509,7 @@ export interface ApiBlogPostBlogPost extends Struct.CollectionTypeSchema { "plugin::users-permissions.user" > content: Schema.Attribute.RichText + coverImage: Schema.Attribute.Component<"utilities.basic-image", false> createdAt: Schema.Attribute.DateTime createdBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> & Schema.Attribute.Private @@ -565,7 +566,6 @@ export interface ApiBlogPostBlogPost extends Struct.CollectionTypeSchema { seo: Schema.Attribute.Component<"shared.seo", false> slug: Schema.Attribute.UID<"title"> & Schema.Attribute.Required tags: Schema.Attribute.Relation<"manyToMany", "api::post-tag.post-tag"> - timelineImage: Schema.Attribute.Component<"media.image", false> title: Schema.Attribute.String & Schema.Attribute.Required updatedAt: Schema.Attribute.DateTime updatedBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> & @@ -1147,6 +1147,72 @@ export interface ApiNewsItemNewsItem extends Struct.CollectionTypeSchema { } } +export interface ApiNotFoundNotFound extends Struct.SingleTypeSchema { + collectionName: "not_founds" + info: { + description: "Customizable 404 page content" + displayName: "404 / Not Found" + pluralName: "not-founds" + singularName: "not-found" + } + options: { + draftAndPublish: false + } + pluginOptions: { + i18n: { + localized: true + } + } + attributes: { + backButtonText: Schema.Attribute.String & + Schema.Attribute.SetPluginOptions<{ + i18n: { + localized: true + } + }> + content: Schema.Attribute.DynamicZone< + [ + "blog.related-posts", + "blog.editors-picks", + "blog.resource-cta", + "sections.cta-banner", + "sections.community-banner", + "sections.section-header", + "sections.feature-card-grid", + ] + > & + Schema.Attribute.SetPluginOptions<{ + i18n: { + localized: true + } + }> + createdAt: Schema.Attribute.DateTime + createdBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> & + Schema.Attribute.Private + image: Schema.Attribute.Component<"utilities.basic-image", false> & + Schema.Attribute.SetPluginOptions<{ + i18n: { + localized: true + } + }> + locale: Schema.Attribute.String + localizations: Schema.Attribute.Relation< + "oneToMany", + "api::not-found.not-found" + > + publishedAt: Schema.Attribute.DateTime + title: Schema.Attribute.String & + Schema.Attribute.SetPluginOptions<{ + i18n: { + localized: true + } + }> + updatedAt: Schema.Attribute.DateTime + updatedBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> & + Schema.Attribute.Private + } +} + export interface ApiPagePage extends Struct.CollectionTypeSchema { collectionName: "pages" info: { @@ -2071,6 +2137,7 @@ declare module "@strapi/strapi" { "api::hubspot-form.hubspot-form": ApiHubspotFormHubspotForm "api::internal-job.internal-job": ApiInternalJobInternalJob "api::news-item.news-item": ApiNewsItemNewsItem + "api::not-found.not-found": ApiNotFoundNotFound "api::page.page": ApiPagePage "api::plan-feature.plan-feature": ApiPlanFeaturePlanFeature "api::plan.plan": ApiPlanPlan diff --git a/apps/ui/src/app/[locale]/blog/page.tsx b/apps/ui/src/app/[locale]/blog/page.tsx index 6cbe360..6f29a07 100644 --- a/apps/ui/src/app/[locale]/blog/page.tsx +++ b/apps/ui/src/app/[locale]/blog/page.tsx @@ -6,7 +6,6 @@ import { use } from "react" import { BlogNavbar } from "@/components/blog/BlogNavbar" import { BlogPostsList } from "@/components/blog/BlogPostsList" import { FeaturedBlogPost } from "@/components/blog/FeaturedBlogPost" -import { Container } from "@/components/elementary/Container" import { HeroContainer, HeroContainerContent, @@ -63,15 +62,9 @@ export default function BlogIndexPage(props: PageProps<"/[locale]/blog">) { - {featuredPost && ( - - - - )} + {featuredPost && } - - - + diff --git a/apps/ui/src/app/[locale]/layout.tsx b/apps/ui/src/app/[locale]/layout.tsx index 0ae3d37..a519326 100644 --- a/apps/ui/src/app/[locale]/layout.tsx +++ b/apps/ui/src/app/[locale]/layout.tsx @@ -32,6 +32,18 @@ export const metadata: Metadata = { template: "%s / Notum Technologies", default: "", }, + // TODO: REMOVE BEFORE PRODUCTION DEPLOY — site-wide noindex/nofollow while + // hosted on a non-production URL. Drop this `robots` field entirely. + robots: { + index: false, + follow: false, + nocache: true, + googleBot: { + index: false, + follow: false, + noimageindex: true, + }, + }, } export default async function RootLayout({ diff --git a/apps/ui/src/app/[locale]/not-found.tsx b/apps/ui/src/app/[locale]/not-found.tsx index a19a436..228c7ee 100644 --- a/apps/ui/src/app/[locale]/not-found.tsx +++ b/apps/ui/src/app/[locale]/not-found.tsx @@ -1,25 +1,56 @@ -import { LinkBreakIcon } from "@phosphor-icons/react/ssr" -import { getTranslations } from "next-intl/server" +import type { Locale } from "next-intl" +import { getLocale } from "next-intl/server" +import { Container } from "@/components/elementary/Container" +import { + SectionHeader, + SectionTitle, +} from "@/components/elementary/section-header" +import { StrapiBasicImage } from "@/components/page-builder/components/utilities/StrapiBasicImage" +import { DynamicZoneRenderer } from "@/components/page-builder/DynamicZoneRenderer" +import { buttonVariants } from "@/components/ui/button" import { Link } from "@/lib/navigation" +import { fetchNotFound } from "@/lib/strapi-api/content/server" +import { cn } from "@/lib/styles" export default async function NotFound() { - const t = await getTranslations("errors.notFound") + const locale = (await getLocale()) as Locale + const notFound = (await fetchNotFound(locale))?.data + + const title = notFound?.title ?? "Page not found" + const backButtonText = notFound?.backButtonText ?? "Back to home" + const image = notFound?.image + const content = notFound?.content ?? [] return ( -
- -
-

{t("title")}

-

{t("description")}

-
-

{t("solution")}

- - {t("redirect")} - -
+ <> +
+ + + {title} + + {image && ( + + )} + + + {backButtonText} + + + +
+ + {content.length > 0 && ( + + )} + ) } diff --git a/apps/ui/src/app/robots.ts b/apps/ui/src/app/robots.ts index 99ce15f..f9b5e7c 100644 --- a/apps/ui/src/app/robots.ts +++ b/apps/ui/src/app/robots.ts @@ -1,19 +1,8 @@ import type { MetadataRoute } from "next" -import { getEnvVar } from "@/lib/env-vars" -import { isProduction } from "@/lib/general-helpers" - export default function robots(): MetadataRoute.Robots { - const baseUrl = getEnvVar("APP_PUBLIC_URL") - - if (!isProduction()) { - return { rules: { userAgent: "*", disallow: "/" } } - } - - return { - rules: { userAgent: "*", allow: "/" }, - ...(baseUrl - ? { sitemap: new URL("./sitemap.xml", baseUrl).toString() } - : {}), - } + // TODO: REMOVE BEFORE PRODUCTION DEPLOY — temporary site-wide noindex while + // hosted on a non-production URL. Revert this file to its prior version + // (env-aware allow + sitemap) — see git history. + return { rules: { userAgent: "*", disallow: "/" } } } diff --git a/apps/ui/src/components/blog/BlogNavbar.tsx b/apps/ui/src/components/blog/BlogNavbar.tsx index bbd0677..a5b270b 100644 --- a/apps/ui/src/components/blog/BlogNavbar.tsx +++ b/apps/ui/src/components/blog/BlogNavbar.tsx @@ -1,7 +1,6 @@ import { MagnifyingGlassIcon } from "@phosphor-icons/react/ssr" import type { Locale } from "next-intl" -import { Container } from "@/components/elementary/Container" import { getBlogNewsletterHubspot, type BlogNavbarCategory, @@ -25,19 +24,17 @@ export async function BlogNavbar({ locale }: { readonly locale: Locale }) { const hubspotForm = getBlogNewsletterHubspot(response) return ( -