From 9cbc4185491e95bd28cd2d726ad96644f998bbed Mon Sep 17 00:00:00 2001 From: marcobottaro <39835990+marcobottaro@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:12:07 +0100 Subject: [PATCH] [DEV-1418] Allow fetching from the CMS the "In evidenza" section on the home page (#667) * Allow fetching from the CMS the "In evidenza" section on the home page * Add changeset * Fix homepage test * Fix type to prevent error when newsShowcase is null * Fixes after code review * Set newsItem image as optional * Fix publishedAt type in NewsItemCodec --------- Co-authored-by: tommaso1 --- .changeset/selfish-items-worry.md | 5 + .../src/_contents/appIo/tutorialLists.ts | 13 ++- .../src/_contents/ioSign/tutorialLists.ts | 8 +- .../src/_contents/pagoPa/tutorialLists.ts | 9 +- .../src/_contents/send/tutorialLists.ts | 6 +- .../src/_contents/translations.ts | 52 ++++++---- .../src/app/[productSlug]/tutorials/page.tsx | 6 +- apps/nextjs-website/src/app/page.tsx | 8 +- .../atoms/LinkButton/LinkButton.tsx | 1 - .../NewsShowcase.tsx} | 43 +++++---- .../TutorialsOverview/TutorialsOverview.tsx | 13 ++- .../editorialComponents/Newsroom/Newsroom.tsx | 11 ++- apps/nextjs-website/src/lib/homepage.ts | 49 +++++++--- .../src/lib/strapi/__tests__/homepage.test.ts | 94 +++++++++++++++++++ .../nextjs-website/src/lib/strapi/homepage.ts | 29 +++++- .../src/lib/types/tutorialData.ts | 6 +- 16 files changed, 267 insertions(+), 86 deletions(-) create mode 100644 .changeset/selfish-items-worry.md rename apps/nextjs-website/src/components/organisms/{News/News.tsx => NewsShowcase/NewsShowcase.tsx} (54%) diff --git a/.changeset/selfish-items-worry.md b/.changeset/selfish-items-worry.md new file mode 100644 index 000000000..5e7dfeef6 --- /dev/null +++ b/.changeset/selfish-items-worry.md @@ -0,0 +1,5 @@ +--- +"nextjs-website": minor +--- + +Allow fetching from the CMS the "In evidenza" section on the home page diff --git a/apps/nextjs-website/src/_contents/appIo/tutorialLists.ts b/apps/nextjs-website/src/_contents/appIo/tutorialLists.ts index d037e005c..a44b47f5f 100644 --- a/apps/nextjs-website/src/_contents/appIo/tutorialLists.ts +++ b/apps/nextjs-website/src/_contents/appIo/tutorialLists.ts @@ -9,6 +9,10 @@ export const appIoTutorials: readonly Tutorial[] = [ title: 'Quali sono i possibili accordi di adesione all’app IO', path: `${appIoTutorialListsPath.path}/quale-accordo-di-adesione-scegliere`, name: 'Quale accordo di adesione scegliere', + image: { + url: '/images/news.png', + alternativeText: 'Immagine: Come allegare documenti a un messaggio', + }, showInOverview: true, }, { @@ -17,8 +21,9 @@ export const appIoTutorials: readonly Tutorial[] = [ name: 'Come allegare documenti a un messaggio', image: { url: '/images/app-io-come-allegare-documenti.png', - alt: 'Immagine: Come allegare documenti a un messaggio', + alternativeText: 'Immagine: Come allegare documenti a un messaggio', }, + showInOverview: true, }, { @@ -27,7 +32,8 @@ export const appIoTutorials: readonly Tutorial[] = [ name: 'Come sapere se un cittadino può ricevere messaggi da un servizio', image: { url: '/images/app-io-ricevere-messaggi.png', - alt: 'Immagine: Come sapere se un cittadino può ricevere messaggi da un servizio', + alternativeText: + 'Immagine: Come sapere se un cittadino può ricevere messaggi da un servizio', }, showInOverview: true, }, @@ -37,7 +43,8 @@ export const appIoTutorials: readonly Tutorial[] = [ name: 'Come sapere se un cittadino può ricevere messaggi da un servizio', image: { url: '/images/app-io-come-inviare-avviso-pagamento.png', - alt: 'Immagine: Come spedire un avviso di pagamento in un messaggio', + alternativeText: + 'Immagine: Come spedire un avviso di pagamento in un messaggio', }, showInOverview: false, }, diff --git a/apps/nextjs-website/src/_contents/ioSign/tutorialLists.ts b/apps/nextjs-website/src/_contents/ioSign/tutorialLists.ts index 010c8cbb4..a5d982197 100644 --- a/apps/nextjs-website/src/_contents/ioSign/tutorialLists.ts +++ b/apps/nextjs-website/src/_contents/ioSign/tutorialLists.ts @@ -10,7 +10,8 @@ export const ioSignTutorials: readonly Tutorial[] = [ path: `${ioSignTutorialListsPath.path}/come-creare-e-preparare-il-documento-da-firmare-digitalmente-con-firma-con-io`, name: 'Come creare e preparare il documento da firmare digitalmente', image: { - alt: 'Immagine: Come creare e preparare il documento da firmare digitalmente', + alternativeText: + 'Immagine: Come creare e preparare il documento da firmare digitalmente', url: '/images/io-sign-firmare-documento.png', }, showInOverview: true, @@ -20,7 +21,8 @@ export const ioSignTutorials: readonly Tutorial[] = [ path: `${ioSignTutorialListsPath.path}/come-creare-il-dossier-per-la-richiesta-di-firma`, name: 'Come creare il Dossier per la richiesta di firma', image: { - alt: 'Immagine: Come creare il Dossier per la richiesta di firma', + alternativeText: + 'Immagine: Come creare il Dossier per la richiesta di firma', url: '/images/io-sign-creare-dossier.png', }, showInOverview: true, @@ -30,7 +32,7 @@ export const ioSignTutorials: readonly Tutorial[] = [ path: `${ioSignTutorialListsPath.path}/upload-dei-documenti`, name: 'Come effettuare l’upload dei documenti', image: { - alt: 'Immagine: Come effettuare l’upload dei documenti', + alternativeText: 'Immagine: Come effettuare l’upload dei documenti', url: '/images/io-sign-effettuare-upload-documento.png', }, showInOverview: true, diff --git a/apps/nextjs-website/src/_contents/pagoPa/tutorialLists.ts b/apps/nextjs-website/src/_contents/pagoPa/tutorialLists.ts index 2f21584bb..9cde52432 100644 --- a/apps/nextjs-website/src/_contents/pagoPa/tutorialLists.ts +++ b/apps/nextjs-website/src/_contents/pagoPa/tutorialLists.ts @@ -11,7 +11,8 @@ export const pagoPaTutorials: readonly Tutorial[] = [ name: 'Come richiedere pagamenti che contengono marca da bollo digitale', image: { url: '/images/pago-pa-marca-bollo.png', - alt: 'Immagine: Come richiedere pagamenti che contengono marca da bollo digitale', + alternativeText: + 'Immagine: Come richiedere pagamenti che contengono marca da bollo digitale', }, showInOverview: true, }, @@ -21,7 +22,8 @@ export const pagoPaTutorials: readonly Tutorial[] = [ name: 'Come avviare un esercizio come Ente Creditore su pagoPA', image: { url: '/images/pago-pa-creare-esercizio-ente-creditore.png', - alt: 'Immagine: Come avviare un esercizio come Ente Creditore su pagoPA', + alternativeText: + 'Immagine: Come avviare un esercizio come Ente Creditore su pagoPA', }, showInOverview: true, }, @@ -31,7 +33,8 @@ export const pagoPaTutorials: readonly Tutorial[] = [ name: 'Come stampare un avviso di pagamento in formato PDF', image: { url: '/images/pago-pa-stampare-avviso-pagamento.png', - alt: 'Immagine: Come stampare un avviso di pagamento in formato PDF', + alternativeText: + 'Immagine: Come stampare un avviso di pagamento in formato PDF', }, showInOverview: true, }, diff --git a/apps/nextjs-website/src/_contents/send/tutorialLists.ts b/apps/nextjs-website/src/_contents/send/tutorialLists.ts index d318b6d2f..9d81e4666 100644 --- a/apps/nextjs-website/src/_contents/send/tutorialLists.ts +++ b/apps/nextjs-website/src/_contents/send/tutorialLists.ts @@ -10,7 +10,7 @@ export const sendTutorials: readonly Tutorial[] = [ path: `${sendTutorialListsPath.path}/come-inserire-una-notifica-via-curl`, name: 'Inserisci una notifica via curl', image: { - alt: 'Immagine: Inserisci una notifica via curl', + alternativeText: 'Immagine: Inserisci una notifica via curl', url: '/images/send-tutorial-1.png', }, showInOverview: true, @@ -20,7 +20,7 @@ export const sendTutorials: readonly Tutorial[] = [ path: `${sendTutorialListsPath.path}/come-generare-il-tuo-api-client-per-le-api-di-send`, name: 'Genera il tuo client', image: { - alt: 'Immagine: Genera il tuo client', + alternativeText: 'Immagine: Genera il tuo client', url: '/images/send-tutorial-0.png', }, showInOverview: true, @@ -30,7 +30,7 @@ export const sendTutorials: readonly Tutorial[] = [ path: `${sendTutorialListsPath.path}/configurare-laccesso-ad-interoperabilita-per-i-servizi-send`, name: 'Genera il tuo client', image: { - alt: 'Immagine: Genera il tuo client', + alternativeText: 'Immagine: Genera il tuo client', url: '/images/send-tutorial-2.png', }, showInOverview: true, diff --git a/apps/nextjs-website/src/_contents/translations.ts b/apps/nextjs-website/src/_contents/translations.ts index 6c5b4263b..b707552ea 100644 --- a/apps/nextjs-website/src/_contents/translations.ts +++ b/apps/nextjs-website/src/_contents/translations.ts @@ -45,45 +45,63 @@ export const translations = { onThisPage: 'In questa pagina', }, homepage: { - news: { + newsShowcase: { title: 'In evidenza', - list: [ + items: [ { + comingSoon: false, title: 'Usa il validatore di SEND per fare una verifica sull’integrazione', - href: { - label: 'Vai al validatore', - link: `${sendGuideListsPath.path}/validatore/v1.0`, - title: 'Vai al validatore', + link: { + url: `${sendGuideListsPath.path}/validatore/v1.0`, + text: 'Vai al validatore', }, image: { + name: 'homepage-validatore.png', + alternativeText: + 'Immagine: Usa il validatore di SEND per fare una verifica sull’integrazione', + width: 1156, + height: 580, + ext: '.svg', + mime: 'image/svg+xml', url: '/images/homepage-validatore.png', - alt: 'Immagine: Usa il validatore di SEND per fare una verifica sull’integrazione', }, }, { + comingSoon: false, title: 'Scopri i nuovi tutorial di Firma con IO', - href: { - label: 'Vai ai tutorial', - link: `${ioSignTutorialListsPath.path}`, - title: 'Vai ai tutorial', + link: { + url: `${ioSignTutorialListsPath.path}`, + text: 'Vai ai tutorial', }, image: { + name: 'homepage-io-sign.png', + alternativeText: + 'Immagine: Scopri i nuovi tutorial di Firma con IO', + width: 1156, + height: 580, + ext: '.svg', + mime: 'image/svg+xml', url: '/images/homepage-io-sign.png', - alt: 'Immagine: Scopri i nuovi tutorial di Firma con IO', }, }, { + comingSoon: false, title: 'Scopri la Quick Start di piattaforma pagoPA: l’integrazione in pochi semplici step', - href: { - label: 'Vai alla guida', - link: `${pagoPaQuickStartGuidePath.path}`, - title: 'Vai alla guida', + link: { + url: `${pagoPaQuickStartGuidePath.path}`, + text: 'Vai alla guida', }, image: { + name: 'homepage-pago-pa.png', + alternativeText: + 'Immagine: Scopri la Quick Start di piattaforma pagoPA: l’integrazione in pochi semplici step', + width: 1156, + height: 580, + ext: '.svg', + mime: 'image/svg+xml', url: '/images/homepage-pago-pa.png', - alt: 'Immagine: Scopri la Quick Start di piattaforma pagoPA: l’integrazione in pochi semplici step', }, }, ], diff --git a/apps/nextjs-website/src/app/[productSlug]/tutorials/page.tsx b/apps/nextjs-website/src/app/[productSlug]/tutorials/page.tsx index 887d7fe14..fc677d518 100644 --- a/apps/nextjs-website/src/app/[productSlug]/tutorials/page.tsx +++ b/apps/nextjs-website/src/app/[productSlug]/tutorials/page.tsx @@ -75,9 +75,7 @@ const TutorialsPage = async ({ params }: ProductParams) => { : shared.comingSoon, title: tutorial.title, date: { - date: tutorial.dateString - ? new Date(tutorial.dateString) - : undefined, + date: tutorial.publishedAt, }, href: { label: shared.readTutorial, @@ -85,7 +83,7 @@ const TutorialsPage = async ({ params }: ProductParams) => { title: shared.readTutorial, }, img: { - alt: tutorial.image?.alt || '', + alt: tutorial.image?.alternativeText || '', src: tutorial.image?.url || '/images/news.png', }, }))} diff --git a/apps/nextjs-website/src/app/page.tsx b/apps/nextjs-website/src/app/page.tsx index 9d2c75010..8ce8800a2 100644 --- a/apps/nextjs-website/src/app/page.tsx +++ b/apps/nextjs-website/src/app/page.tsx @@ -2,7 +2,7 @@ import { translations } from '@/_contents/translations'; import SiteLabel from '@/components/atoms/SiteLabel/SiteLabel'; import HeroSwiper from '@/components/molecules/HeroSwiper/HeroSwiper'; import RelatedLinks from '@/components/atoms/RelatedLinks/RelatedLinks'; -import News from '@/components/organisms/News/News'; +import NewsShowcase from '@/components/organisms/NewsShowcase/NewsShowcase'; import ProductsShowcase from '@/components/organisms/ProductsShowcase/ProductsShowcase'; import { Metadata } from 'next'; import { makeMetadata } from '@/helpers/metadata.helpers'; @@ -49,10 +49,10 @@ const Home = async () => { ) : undefined, }))} /> - { +const NewsShowcase = ({ + title, + subtitle, + cta, + marginTop, + items, +}: NewsShowcaseProps) => { const t = useTranslations('shared'); const coomingSoonLabel = t('comingSoon'); return ( @@ -37,16 +42,16 @@ const News = ({ title, subtitle, cta, marginTop, cards }: NewsProps) => { ({ - comingSoonLabel: !card.comingSoon ? undefined : coomingSoonLabel, - title: card.title, + items={items.map((item) => ({ + comingSoonLabel: !item.comingSoon ? undefined : coomingSoonLabel, + title: item.title, date: { - date: card.dateString ? new Date(card.dateString) : undefined, + date: item.publishedAt, }, - href: card.href, + href: { link: item.link.url, label: item.link.text }, img: { - alt: card.image?.alt || '', - src: card.image?.url || '/images/news.png', + alt: (item.image && item.image.alternativeText) || '', + src: (item.image && item.image.url) || '/images/news.png', }, }))} /> @@ -55,4 +60,4 @@ const News = ({ title, subtitle, cta, marginTop, cards }: NewsProps) => { ); }; -export default News; +export default NewsShowcase; diff --git a/apps/nextjs-website/src/components/organisms/TutorialsOverview/TutorialsOverview.tsx b/apps/nextjs-website/src/components/organisms/TutorialsOverview/TutorialsOverview.tsx index 1beb81d79..766e70f4d 100644 --- a/apps/nextjs-website/src/components/organisms/TutorialsOverview/TutorialsOverview.tsx +++ b/apps/nextjs-website/src/components/organisms/TutorialsOverview/TutorialsOverview.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Tutorial } from '@/lib/types/tutorialData'; import { Path } from '@/lib/types/path'; -import News from '@/components/organisms/News/News'; +import NewsShowcase from '@/components/organisms/NewsShowcase/NewsShowcase'; import { useTranslations } from 'next-intl'; type TutorialsOverviewProps = { @@ -24,7 +24,7 @@ const TutorialsOverview = ({ const t = useTranslations('shared'); const label = t('readTutorial'); return ( - ({ + items={tutorials.map((tutorial) => ({ ...tutorial, - href: { - label, - link: tutorial.path, - title: label, + link: { + url: tutorial.path, + text: label, }, }))} /> diff --git a/apps/nextjs-website/src/editorialComponents/Newsroom/Newsroom.tsx b/apps/nextjs-website/src/editorialComponents/Newsroom/Newsroom.tsx index 6ea4f43f2..f61a1f093 100644 --- a/apps/nextjs-website/src/editorialComponents/Newsroom/Newsroom.tsx +++ b/apps/nextjs-website/src/editorialComponents/Newsroom/Newsroom.tsx @@ -58,7 +58,7 @@ const Item = (props: INewsroomItem) => { > {comingSoonLabel && ( { width={0} height={0} sizes='100vw' - style={{ borderRadius: 16, width: '100%', height: 'auto' }} + style={{ + borderRadius: 16, + width: '100%', + height: 'auto', + marginBottom: '16px', + }} /> )} @@ -93,7 +98,7 @@ const Item = (props: INewsroomItem) => { color='text.secondary' fontSize={16} fontWeight={400} - my={2} + mb={2} > {new Intl.DateTimeFormat(locale, options).format(date)} diff --git a/apps/nextjs-website/src/lib/homepage.ts b/apps/nextjs-website/src/lib/homepage.ts index d7f189eee..0ba58d3c4 100644 --- a/apps/nextjs-website/src/lib/homepage.ts +++ b/apps/nextjs-website/src/lib/homepage.ts @@ -8,21 +8,26 @@ export type HomepageProps = { readonly boldTitle: string; readonly cards: readonly CtaSlideProps[]; }; - readonly news: { + readonly newsShowcase: { readonly title: string; - readonly cards: readonly { + readonly items: readonly { readonly comingSoon?: boolean; readonly title: string; - readonly dateString?: string; - readonly image?: { + readonly publishedAt?: Date; + readonly link: { + readonly text: string; readonly url: string; - readonly alt: string; - }; - readonly href: { - readonly label: string; - readonly link: string; - readonly title: string; + readonly target?: '_self' | '_blank' | '_parent' | '_top' | null; }; + readonly image: { + readonly name: string; + readonly alternativeText: string | null; + readonly width: number; + readonly height: number; + readonly ext: string; + readonly mime: string; + readonly url: string; + } | null; }[]; }; readonly productsShowcase: { @@ -61,6 +66,25 @@ export const makeHomepageProps = ( ...makeHomepagePropsFromStatic(staticHeader, staticHomepage), comingsoonDocumentation: strapiHomepage.data.attributes.comingsoonDocumentation, + ...(strapiHomepage.data.attributes.newsShowcase && { + newsShowcase: { + title: strapiHomepage.data.attributes.newsShowcase.title, + items: strapiHomepage.data.attributes.newsShowcase.items.data.map( + (item) => ({ + comingSoon: item.attributes.comingSoon || undefined, + title: item.attributes.title, + publishedAt: item.attributes.publishedAt, + link: { + text: item.attributes.link.text, + url: item.attributes.link.href, + target: item.attributes.link.target, + }, + image: + item.attributes.image.data && item.attributes.image.data.attributes, + }) + ), + }, + }), productsShowcase: { title: strapiHomepage.data.attributes.productsShowcase.title, products: strapiHomepage.data.attributes.productsShowcase.products.data.map( @@ -83,10 +107,7 @@ export const makeHomepagePropsFromStatic = ( boldTitle: staticHeader.boldTitle, cards: staticHomepage.heroItems, }, - news: { - title: staticHomepage.news.title, - cards: staticHomepage.news.list, - }, + newsShowcase: staticHomepage.newsShowcase, productsShowcase: staticHomepage.productsShowcase, comingsoonDocumentation: staticHomepage.comingsoonDocumentation, }); diff --git a/apps/nextjs-website/src/lib/strapi/__tests__/homepage.test.ts b/apps/nextjs-website/src/lib/strapi/__tests__/homepage.test.ts index a3da8bfe9..89033c692 100644 --- a/apps/nextjs-website/src/lib/strapi/__tests__/homepage.test.ts +++ b/apps/nextjs-website/src/lib/strapi/__tests__/homepage.test.ts @@ -44,6 +44,100 @@ const makeStrapiResponseJson = () => ({ }, ], }, + newsShowcase: { + id: 1, + title: 'aText', + subTitle: null, + items: { + data: [ + { + id: 2, + attributes: { + title: 'aText', + createdAt: '2024-02-19T12:12:07.580Z', + updatedAt: '2024-02-21T10:24:18.509Z', + publishedAt: '2024-02-19T12:12:08.278Z', + locale: 'it', + comingSoon: false, + link: { + id: 4, + text: 'aText', + href: 'aText', + target: '_self', + }, + image: { + data: { + id: 5, + attributes: { + name: 'aText', + alternativeText: null, + caption: null, + width: 1156, + height: 580, + formats: { + thumbnail: { + name: 'aText', + hash: 'aText', + ext: '.png', + mime: 'image/png', + path: null, + width: 245, + height: 123, + size: 14.83, + url: 'aText', + }, + small: { + name: 'aText', + hash: 'aText', + ext: '.png', + mime: 'image/png', + path: null, + width: 500, + height: 251, + size: 50.41, + url: 'aText', + }, + medium: { + name: 'aText', + hash: 'aText', + ext: '.png', + mime: 'image/png', + path: null, + width: 750, + height: 376, + size: 108.32, + url: 'aText', + }, + large: { + name: 'aText', + hash: 'aText', + ext: '.png', + mime: 'image/png', + path: null, + width: 1000, + height: 502, + size: 188.94, + url: 'aText', + }, + }, + hash: 'aText', + ext: '.png', + mime: 'image/png', + size: 42.4, + url: 'aText', + previewUrl: null, + provider: 'local', + provider_metadata: null, + createdAt: '2024-02-19T12:12:03.952Z', + updatedAt: '2024-02-19T12:12:03.952Z', + }, + }, + }, + }, + }, + ], + }, + }, productsShowcase: { id: 1, title: 'aText', diff --git a/apps/nextjs-website/src/lib/strapi/homepage.ts b/apps/nextjs-website/src/lib/strapi/homepage.ts index 06b8214ca..8d4fc2f78 100644 --- a/apps/nextjs-website/src/lib/strapi/homepage.ts +++ b/apps/nextjs-website/src/lib/strapi/homepage.ts @@ -1,8 +1,9 @@ import * as t from 'io-ts/lib'; +import * as tt from 'io-ts-types'; import * as qs from 'qs'; import { fetchFromStrapi } from './fetchFromStrapi'; -const LinkHomepageCodec = t.strict({ +const LinkCodec = t.strict({ text: t.string, href: t.string, target: t.union([ @@ -17,6 +18,8 @@ const LinkHomepageCodec = t.strict({ const MediaCodec = t.strict({ attributes: t.strict({ name: t.string, + alternativeText: t.union([t.null, t.string]), + caption: t.union([t.null, t.string]), width: t.number, height: t.number, ext: t.string, @@ -34,13 +37,32 @@ const ProductCodec = t.strict({ }), }); +const NewsItemCodec = t.strict({ + attributes: t.strict({ + comingSoon: t.boolean, + title: t.string, + link: LinkCodec, + publishedAt: tt.DateFromISOString, + image: t.strict({ data: t.union([t.null, MediaCodec]) }), + }), +}); + export const StrapiHomepageCodec = t.strict({ data: t.strict({ attributes: t.strict({ comingsoonDocumentation: t.type({ title: t.string, - links: t.array(LinkHomepageCodec), + links: t.array(LinkCodec), }), + newsShowcase: t.union([ + t.null, + t.strict({ + title: t.string, + items: t.strict({ + data: t.array(NewsItemCodec), + }), + }), + ]), productsShowcase: t.strict({ title: t.string, products: t.strict({ @@ -59,6 +81,9 @@ const makeStrapiHomepagePopulate = () => comingsoonDocumentation: { populate: ['links'], }, + newsShowcase: { + populate: ['items.image', 'items.link'], + }, productsShowcase: { populate: ['products.logo'], }, diff --git a/apps/nextjs-website/src/lib/types/tutorialData.ts b/apps/nextjs-website/src/lib/types/tutorialData.ts index 656c2b8d7..c2be328bb 100644 --- a/apps/nextjs-website/src/lib/types/tutorialData.ts +++ b/apps/nextjs-website/src/lib/types/tutorialData.ts @@ -3,10 +3,10 @@ import { Path } from '@/lib/types/path'; export type Tutorial = { readonly comingSoon?: boolean; readonly showInOverview?: boolean; - readonly image?: { + readonly image: { readonly url: string; - readonly alt: string; + readonly alternativeText: string; }; readonly title: string; - readonly dateString?: string; + readonly publishedAt?: Date; } & Path;