diff --git a/CHANGELOG.md b/CHANGELOG.md index be3381b..159c773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [Untagged] + +### Features +- Display tags page navigation and filtering +- Improve page title display +- Add hero images to blog posts and pages +- Enhance cards and SEO in /pages section +- Update post configuration for better population +- Add more posts to /blog with date sorting +- Display links in homepage + +### Fixed +- Fix search page functionality +- Fix bug in Strapi local setup +- Remove unnecessary Dockerfile + ## v4.3.1 (2024-07-27) ### Fix @@ -74,7 +91,7 @@ All notable changes to this project will be documented in this file. See [standa ### Others -* adds blog post for how to add a social icon ([#221](https://github.com/satnaing/astro-paper/issues/221)) +* adds blog post for how to add a social icon ([#221](https://github.com/satnaing/astro-paper/issues/221)) * updates the hook post with a smarter updateHook ([#222](https://github.com/satnaing/astro-paper/issues/222)) * update breadcrumbs delimiter to "»" ([#213](https://github.com/satnaing/astro-paper/issues/213)) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 711a252..08b70f7 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,35 +1,75 @@ -import { slugifyStr } from "@utils/slugify"; -import Datetime from "./Datetime"; -import type { CollectionEntry } from "astro:content"; +import Tag from './Tag.astro'; +import type { SEO } from '../interfaces/Article'; export interface Props { href?: string; - frontmatter: CollectionEntry<"blog">["data"]; + author?: string; + tags: string[]; + frontmatter: { + title: string; + pubDatetime: Date; + modDatetime: Date; + description: string; + SEO?: SEO; + author?: string; + }; secHeading?: boolean; } -export default function Card({ href, frontmatter, secHeading = true }: Props) { - const { title, pubDatetime, modDatetime, description } = frontmatter; - - const headerProps = { - style: { viewTransitionName: slugifyStr(title) }, - className: "text-lg font-medium decoration-dashed hover:underline", - }; +export default function Card({ href, frontmatter, tags, secHeading = true }: Props) { + const { title, pubDatetime, description, SEO } = frontmatter; + const imageUrl = SEO?.socialImage?.url; return ( -
  • - - {secHeading ? ( -

    {title}

    - ) : ( -

    {title}

    - )} -
    - -

    {description}

    +
  • +
    + + {imageUrl && ( +
    + {SEO?.socialImage?.alternativeText +
    +
    + )} +
    + {secHeading ? ( +

    + {title} +

    + ) : ( +

    + {title} +

    + )} +
    + + {new Date(pubDatetime).toLocaleDateString()} + +
    +

    + {description} +

    +
    + Read more + + + +
    +
      + {tags?.map((tag) => { + if (!tag) return null; + + })} +
    +
    +
    +
  • ); } diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 4137dad..4695a08 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -6,19 +6,26 @@ const currentYear = new Date().getFullYear(); export interface Props { noMarginTop?: boolean; + activeNav?: string; } -const { noMarginTop = false } = Astro.props; +const { activeNav, noMarginTop = false } = Astro.props; --- diff --git a/src/components/Header.astro b/src/components/Header.astro index 899cf8d..305005b 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -5,7 +5,7 @@ import LinkButton from "./LinkButton.astro"; import type Store from "../interfaces/Store"; export interface Props { - activeNav?: "posts" | "tags" | "about" | "search"; + activeNav?: "posts" | "tags" | "about" | "search" | "pages"; store?: Store; } diff --git a/src/components/HeroImage.astro b/src/components/HeroImage.astro new file mode 100644 index 0000000..a6f65ae --- /dev/null +++ b/src/components/HeroImage.astro @@ -0,0 +1,29 @@ +--- +interface Props { + image: { + url: string; + alternativeText?: string | null; + width?: number; + height?: number; + }; + title: string; +} + +const { image, title } = Astro.props; +--- + +{ + image && ( +
    + {image.alternativeText +
    +
    + ) +} diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 3192d2b..065e6ce 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,3 +1,4 @@ +import React from "react"; import Fuse from "fuse.js"; import { useEffect, useRef, useState, useMemo } from "react"; import Card from "@components/Card"; @@ -113,13 +114,14 @@ export default function SearchBar({ searchList }: Props) { searchResults.map(({ item, refIndex }) => ( tag.Label) || []} frontmatter={{ author: "x", - tags: item.data.Tags.map(tag => tag.Label), title: item.data.Title, pubDatetime: new Date(item.data.createdAt), modDatetime: new Date(item.data.updatedAt), description: item.data.SEO?.metaDescription || "", + SEO: item.data?.SEO, }} key={`${refIndex}-${item.slug}`} /> diff --git a/src/config.ts b/src/config.ts index 748a9fb..abd3671 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,15 +6,15 @@ export const markketplace = { }; export const SITE: Site = { - website: "https://morirsoniando.com/", + website: "https://markket.place/", author: "Club Calima", profile: "https://caliman.org/", desc: "A minimal, responsive and SEO-friendly Markketplace theme.", title: "Morir Soñando", - ogImage: "astropaper-og.jpg", + ogImage: "https://markketplace.nyc3.digitaloceanspaces.com/uploads/3852868ed9aad1e45e4ee4992fe43177.png", lightAndDarkMode: true, postPerIndex: 4, - postPerPage: 3, + postPerPage: 8, scheduledPostMargin: 15 * 60 * 1000, // 15 minutes }; @@ -56,12 +56,6 @@ export const SOCIALS: SocialObjects = [ // active: false, // }, // { - // name: "Twitter", - // href: "https://github.com/satnaing/astro-paper", - // linkTitle: `${SITE.title} on Twitter`, - // active: false, - // }, - // { // name: "Twitch", // href: "https://github.com/satnaing/astro-paper", // linkTitle: `${SITE.title} on Twitch`, @@ -145,4 +139,9 @@ export const SOCIALS: SocialObjects = [ // linkTitle: `${SITE.title} on Mastodon`, // active: false, // }, + // name: "BlueSky", + // href: "https://bsky.app/profile/markketplace.bsky.social", + // linkTitle: `${SITE.title} on BlueSKy`, + // active: false, + // }, ]; diff --git a/src/content/config.ts b/src/content/config.ts index 8f2444e..f125dc3 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -3,16 +3,24 @@ import { defineCollection } from "astro:content"; import { strapiLoader } from "../lib/strapi-loader"; -const posts = defineCollection({ - loader: strapiLoader({ contentType: "article", filter: `filters[store][slug][$eq]=${markketplace.STORE_SLUG}` }), -}); const pages = defineCollection({ - loader: strapiLoader({ contentType: "page", filter: `filters[store][slug][$eq]=${markketplace.STORE_SLUG}` }), + loader: strapiLoader({ + contentType: "page", filter: `filters[store][slug][$eq]=${markketplace.STORE_SLUG}`, + populate: 'SEO.socialImage,store' + }), + }); const stores = defineCollection({ loader: strapiLoader({ contentType: "store", filter: `filters[slug][$eq]=${markketplace.STORE_SLUG}` }), }); +const posts = defineCollection({ + loader: strapiLoader({ + contentType: "article", filter: `filters[store][slug][$eq]=${markketplace.STORE_SLUG}`, + populate: 'SEO.socialImage,Tags,store' + }), +}); + export const collections = { posts, pages, stores }; diff --git a/src/interfaces/Article.ts b/src/interfaces/Article.ts new file mode 100644 index 0000000..e10cd7d --- /dev/null +++ b/src/interfaces/Article.ts @@ -0,0 +1,17 @@ +export interface Tag { + Label: string; + Color: string; +} + +export interface SEOImage { + url: string; + alternativeText: string | null; + width: number; + height: number; +} + +export interface SEO { + metaTitle: string; + metaDescription: string; + socialImage?: SEOImage; +} diff --git a/src/interfaces/Page.ts b/src/interfaces/Page.ts index 8962861..267e0ce 100644 --- a/src/interfaces/Page.ts +++ b/src/interfaces/Page.ts @@ -29,6 +29,7 @@ export default interface Page { SEO: { metaDescription: string, metaTitle: string, + socialImage: {}, }, createdAt: string; updatedAt: string; diff --git a/src/interfaces/Store.ts b/src/interfaces/Store.ts index 0816c6a..4b3a7d1 100644 --- a/src/interfaces/Store.ts +++ b/src/interfaces/Store.ts @@ -8,6 +8,7 @@ export default interface Store { slug: string; products: { data: [] }; URLS: { Label: string, URL: string, }[]; + SEO: {}; Logo: { id: string, url: string, diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 4763b1a..b6c1d45 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,7 +1,8 @@ --- -import { LOCALE, SITE } from "@config"; +import { LOCALE, SITE, markketplace } from "@config"; import "@styles/base.css"; import { ViewTransitions } from "astro:transitions"; +import { getCollection } from "astro:content"; const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION; @@ -21,7 +22,7 @@ const { title = SITE.title, author = SITE.author, profile = SITE.profile, - description = SITE.desc, + description = Astro.props.description ?? SITE.desc, ogImage = SITE.ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, pubDatetime, @@ -49,6 +50,11 @@ const structuredData = { }, ], }; + +const stores = await getCollection("stores"); +const store = stores.find( + store => store.data.slug == markketplace.STORE_SLUG +)?.data; --- @@ -64,17 +70,26 @@ const structuredData = { - {title} - - + {store?.title || title} + + - - - - + + + + { diff --git a/src/layouts/PostDetails.astro b/src/layouts/PostDetails.astro index a81e344..15a69bd 100644 --- a/src/layouts/PostDetails.astro +++ b/src/layouts/PostDetails.astro @@ -9,6 +9,8 @@ import { slugifyStr } from "@utils/slugify"; import ShareLinks from "@components/ShareLinks.astro"; import { SITE } from "@config"; +import HeroImage from "@components/HeroImage.astro"; + import { BlocksRenderer } from "@strapi/blocks-react-renderer"; import { getCollection } from "astro:content"; import type Store from "../interfaces/Store"; @@ -30,7 +32,8 @@ const { canonicalURL, pubDatetime, modDatetime, - tags, + Tags, + SEO, } = post.data; const ogImageUrl = typeof ogImage === "string" ? ogImage : ogImage?.src; @@ -68,6 +71,7 @@ const layoutProps = {
    +

    {Title}

      - {post.data.Tags.map(tag => )} + {Tags.map(tag => )}
    ( tag.Label)} frontmatter={{ author: "x", - tags: ["x"], title: data.Title || "---", pubDatetime: new Date(data.createdAt), modDatetime: new Date(data.updatedAt), description: data.SEO?.metaDescription || "", + SEO: { ...data.SEO }, }} /> )) @@ -53,5 +54,5 @@ const { currentPage, totalPages, paginatedPosts } = Astro.props; nextUrl={`/posts/${currentPage + 1}/`} /> -
    1} /> +
    1} activeNav="x" /> diff --git a/src/layouts/TagPosts.astro b/src/layouts/TagPosts.astro index cdd5343..820cc16 100644 --- a/src/layouts/TagPosts.astro +++ b/src/layouts/TagPosts.astro @@ -6,13 +6,15 @@ import Header from "@components/Header.astro"; import Footer from "@components/Footer.astro"; import Card from "@components/Card"; import Pagination from "@components/Pagination.astro"; -import { SITE } from "@config"; +import { SITE, markketplace } from "@config"; import { getCollection } from "astro:content"; import type Store from "../interfaces/Store"; import { slugifyStr } from "@utils/slugify"; const stores = await getCollection("stores"); -const store = stores?.[0]?.data as unknown as Store; +const store = stores?.find( + (store: { data: Store }) => store.data.slug === markketplace.STORE_SLUG +)?.data; export interface Props { currentPage: number; @@ -22,7 +24,13 @@ export interface Props { tagName: string; } -const { currentPage, totalPages, paginatedPosts, tag, tagName } = Astro.props; +const { + currentPage, + totalPages, + paginatedPosts, + tag = "all", + tagName, +} = Astro.props; --- @@ -38,9 +46,9 @@ const { currentPage, totalPages, paginatedPosts, tag, tagName } = Astro.props; paginatedPosts.map(({ data, id }) => ( tag.Label)} frontmatter={{ author: "x", - tags: data.Tags.map(tag => tag.Label), title: data.Title, pubDatetime: new Date(data.createdAt), modDatetime: new Date(data.updatedAt), diff --git a/src/lib/strapi-loader.ts b/src/lib/strapi-loader.ts index 83cd4fa..8d88439 100644 --- a/src/lib/strapi-loader.ts +++ b/src/lib/strapi-loader.ts @@ -14,9 +14,9 @@ const SYNC_INTERVAL = 60 * 1000; // 1 minute in milliseconds * @param filter The filter to apply to the content &filters[store][id][$eq]=${STRAPI_STORE_ID} * @returns An Astro loader for the specified content type */ -export function strapiLoader({ contentType, filter }: { contentType: string, filter?: string }): Loader { +export function strapiLoader({ contentType, filter, populate = 'SEO.socialImage' }: { contentType: string, filter?: string, populate?: string }): Loader { return { - name: "strapi-posts", + name: `strapi-${contentType}`, load: async function (this: Loader, { store, meta, logger }) { const lastSynced = meta.get("lastSynced"); @@ -43,13 +43,19 @@ export function strapiLoader({ contentType, filter }: { contentType: string, fil [filterKey, filterValue] = filter.split('='); } - const data = await fetchFromStrapi(`/api/${contentType}s?`, { [filterKey]: filterValue, populate: '*' }); - const posts = data?.data; + const data = await fetchFromStrapi(`/api/${contentType}s?`, { + [filterKey]: filterValue, + populate, + }); + let posts = data?.data; if (!posts || !Array.isArray(posts)) { throw new Error("Invalid data received from Strapi"); } + // Sort posts by creation date in descending order + posts = posts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + // Get the schema const schemaOrFn = this.schema; if (!schemaOrFn) { @@ -172,11 +178,29 @@ async function fetchFromStrapi( const url = new URL(path, baseUrl || STRAPI_BASE_URL); if (params) { + // Handle populate parameters + if (params.populate) { + const populateFields = params.populate.split(','); + populateFields.forEach((field, index) => { + if (field.includes('.')) { + const [parent, child] = field.split('.'); + url.searchParams.append('populate', parent); + url.searchParams.append(`populate[${index + 1}]`, `${parent}.${child}`); + } else { + url.searchParams.append('populate', field); + } + }); + delete params.populate; + } + + // Handle remaining parameters (including filters) Object.entries(params).forEach(([key, value]) => { url.searchParams.set(key, value); }); } + console.log('Fetching URL:', url.toString()); + try { const response = await fetch(url.href); if (!response.ok) { diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index 101fb21..c677b6f 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,6 +1,7 @@ --- import { type CollectionEntry, getCollection } from "astro:content"; -import type { Page, Store } from "@interfaces/index"; +import type { Store } from "@interfaces/index"; +import HeroImage from "@components/HeroImage.astro"; import { BlocksRenderer, type BlocksContent, @@ -12,13 +13,13 @@ import Breadcrumbs from "@components/Breadcrumbs.astro"; import Hr from "@components/Hr.astro"; export interface Props { - page: CollectionEntry<"page">; + page: CollectionEntry<"pages">; } export async function getStaticPaths() { const pages = await getCollection("pages"); - return pages.map((page: { data: CollectionEntry<"page"> }) => ({ + return pages.map((page: CollectionEntry<"pages">) => ({ params: { slug: page.data.slug }, props: { page: page.data }, })); @@ -28,14 +29,20 @@ const stores = await getCollection("stores"); const store = stores?.[0]?.data as unknown as Store; const { page } = Astro.props; +console.log({ x: page }); +// const { SEO } = page.data; --- -
    - +
    + { + page?.SEO?.socialImage && ( + + ) + }

    {page.Title}

    @@ -45,4 +52,4 @@ const { page } = Astro.props;
    -
    +
    diff --git a/src/pages/index.astro b/src/pages/index.astro index e35bff9..5e65ff4 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -27,14 +27,14 @@ const pages = await getCollection("pages"); const homePage = pages.find( (page: { data: Page }) => page.data.slug === "home" -) as unknown as { data: Page }; +) as { data: Page }; const links = store?.URLS || []; // const products = store?.products?.data || []; --- - +
    @@ -121,33 +121,6 @@ const links = store?.URLS || []; } -->
    - { - pages.length > 0 && ( - <> - - {posts.length > 0 &&
    } - - ) - } - { posts.length > 0 && (
    @@ -160,11 +133,12 @@ const links = store?.URLS || []; href={`/posts/${id}-${slugifyStr(data.Title)}`} frontmatter={{ author: "x", - tags: data.Tags.map(tag => tag.Label), + tags: data.Tags?.map(tag => tag.Label), title: data.Title, pubDatetime: new Date(data.createdAt), modDatetime: new Date(data.updatedAt), description: data.SEO?.metaDescription || "", + SEO: { ...data.SEO }, }} secHeading={false} /> @@ -188,18 +162,25 @@ const links = store?.URLS || [];
    -

    - - Built with Markketplace-astro - - - a minimal, responsive, accessible and SEO-friendly Astro blog theme, compatible - with Markketplace/Strapi. This theme follows best practices and provides - accessibility out of the box. Light and dark mode are supported by default. - Moreover, additional color schemes can also be configured. -

    + { + !store?.Description && ( + <> +

    + + Built with Markketplace-astro - + + a minimal, responsive, accessible and SEO-friendly Astro blog + theme, compatible with Markketplace/Strapi. This theme follows + best practices and provides accessibility out of the box. Light + and dark mode are supported by default. Moreover, additional + color schemes can also be configured. +

    + + ) + }

    diff --git a/src/pages/pages.astro b/src/pages/pages.astro new file mode 100644 index 0000000..89d44b5 --- /dev/null +++ b/src/pages/pages.astro @@ -0,0 +1,43 @@ +--- +import { getCollection } from "astro:content"; +import Layout from "@layouts/Layout.astro"; +import Main from "@layouts/Main.astro"; +import Card from "@components/Card"; +import { slugifyStr } from "@utils/slugify"; +import { SITE } from "@config"; +import Footer from "@components/Footer.astro"; +import Header from "@components/Header.astro"; +import type Store from "@interfaces/Store"; + +const stores = await getCollection("stores"); +const store = stores?.[0]?.data as Store; + +let pages = await getCollection("pages"); +pages = pages.sort( + (a, b) => + new Date(b.data.createdAt).getTime() - new Date(a.data.createdAt).getTime() +); +--- + + +
    +
    +
      + { + pages.map(page => ( + + )) + } +
    +
    +
    + diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index a50b5e8..c5239e7 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -6,6 +6,7 @@ import { slugifyStr } from "@utils/slugify"; export async function GET() { const posts = await getCollection("posts"); const pages = await getCollection("pages"); + // const products = const items = []; diff --git a/src/pages/search.astro b/src/pages/search.astro index 5f6d151..9a206ca 100644 --- a/src/pages/search.astro +++ b/src/pages/search.astro @@ -29,5 +29,5 @@ const searchList = posts.map(({ data, id }) => ({
    -