diff --git a/public/trade-barriers/buildcanada-logo.svg b/public/trade-barriers/buildcanada-logo.svg new file mode 100644 index 0000000..5145156 --- /dev/null +++ b/public/trade-barriers/buildcanada-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/trade-barriers/og.png b/public/trade-barriers/og.png new file mode 100644 index 0000000..f03b6e7 Binary files /dev/null and b/public/trade-barriers/og.png differ diff --git a/src/app/memos/MemoResultsList.tsx b/src/app/memos/MemoResultsList.tsx index db5d1dd..a7770e0 100644 --- a/src/app/memos/MemoResultsList.tsx +++ b/src/app/memos/MemoResultsList.tsx @@ -8,37 +8,49 @@ import { useMemosFilter } from "./store"; import { MemoItem, formatDate, shortenName } from "./types"; function MemoGridRow({ memo, basePath }: { memo: MemoItem; basePath: string }) { + const author = memo.author; + const hasMedia = Boolean(author?.photo); return ( -
-
- {memo.author.photo && ( - {memo.author.name} - )} -
+
+ {hasMedia && ( +
+ {author?.photo && ( + {author.name} + )} +
+ )}

{memo.title}

-

- {memo.author.name} - - {shortenName(memo.author.name)} - -

- · + {author && ( + <> +

+ {author.name} + {shortenName(author.name)} +

+ · + + )}

{formatDate(memo.publishedAt, memo.createdAt)}

@@ -69,9 +81,11 @@ function MemoGridRow({ memo, basePath }: { memo: MemoItem; basePath: string }) { export default function MemoResultsList({ memos, basePath = "/memos", + resultsLabel = "Memos", }: { memos: MemoItem[]; basePath?: string; + resultsLabel?: string; }) { const search = useMemosFilter((s) => s.search); const activeCategory = useMemosFilter((s) => s.activeCategory); @@ -87,7 +101,7 @@ export default function MemoResultsList({ list = list.filter( (m) => m.title.toLowerCase().includes(q) || - m.author.name.toLowerCase().includes(q) || + m.author?.name.toLowerCase().includes(q) || m.keyMessage1?.toLowerCase().includes(q) ); } @@ -95,20 +109,22 @@ export default function MemoResultsList({ return list; }, [memos, search, activeCategory]); + const lowerLabel = resultsLabel.toLowerCase(); + return (
{activeCategory - ? `${activeCategory.replace(/-/g, " ")} Memos` - : "All Memos"} + ? `${activeCategory.replace(/-/g, " ")} ${resultsLabel}` + : `All ${resultsLabel}`} {filtered.length === 0 && (

{memos.length === 0 - ? "No memos yet." - : "No memos match your filters."} + ? `No ${lowerLabel} yet.` + : `No ${lowerLabel} match your filters.`}

)}
diff --git a/src/app/memos/MemoSearch.tsx b/src/app/memos/MemoSearch.tsx index 9273504..cc6529c 100644 --- a/src/app/memos/MemoSearch.tsx +++ b/src/app/memos/MemoSearch.tsx @@ -3,7 +3,7 @@ import SectionLabel from "@/components/SectionLabel"; import { useMemosFilter } from "./store"; -export default function MemoSearch() { +export default function MemoSearch({ placeholder = "Search memos..." }: { placeholder?: string } = {}) { const search = useMemosFilter((s) => s.search); const setSearch = useMemosFilter((s) => s.setSearch); @@ -38,7 +38,7 @@ export default function MemoSearch() { type="text" value={search} onChange={(e) => setSearch(e.target.value)} - placeholder="Search memos..." + placeholder={placeholder} className="flex-1 bg-transparent font-mono text-base tracking-normal outline-none placeholder:text-text-secondary" /> {search && ( diff --git a/src/app/memos/MemosListClient.tsx b/src/app/memos/MemosListClient.tsx index eeb1434..1583acc 100644 --- a/src/app/memos/MemosListClient.tsx +++ b/src/app/memos/MemosListClient.tsx @@ -28,9 +28,13 @@ function CategoryFromSearchParams({ categories }: { categories: string[] }) { export default function MemosListClient({ memos, basePath = "/memos", + showCategoryFilter = true, + resultsLabel, }: { memos: MemoItem[]; basePath?: string; + showCategoryFilter?: boolean; + resultsLabel?: string; }) { const categories = useMemo(() => { const cats = new Set(); @@ -42,13 +46,15 @@ export default function MemosListClient({ return ( <> - - - + {showCategoryFilter && ( + + + + )} - - - + {showCategoryFilter && } + + ); } diff --git a/src/app/memos/types.ts b/src/app/memos/types.ts index 95dd14c..1708082 100644 --- a/src/app/memos/types.ts +++ b/src/app/memos/types.ts @@ -5,7 +5,7 @@ export interface MemoItem { author: { name: string; photo: string | null; - }; + } | null; keyMessage1: string | null; keyMessage2: string | null; keyMessage3: string | null; diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx new file mode 100644 index 0000000..583b27f --- /dev/null +++ b/src/app/posts/[slug]/page.tsx @@ -0,0 +1,149 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { fetchPost, fetchPosts, getSiteConfig } from "@/lib/api"; +import { extractHeadings } from "@/lib/extract-headings"; +import { ShareSection } from "@/components/share"; +import { Signpost } from "@/components/custom/signpost"; +import { SubscribeButton } from "@/components/ui/subscribe-button"; +import { buildGraph } from "@/lib/schemas/graph"; +import { generateBreadcrumbSchema } from "@/lib/schemas/generators/breadcrumb"; +import { generateOrganizationSchema } from "@/lib/schemas/generators/organization"; + +export async function generateStaticParams() { + try { + const posts = await fetchPosts(); + return posts.map((p) => ({ slug: p.slug })); + } catch { + return []; + } +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + let post; + try { + post = await fetchPost(slug); + } catch { + return { title: "Post Not Found | Build Canada" }; + } + + const title = `${post.title} | Build Canada`; + const description = post.summary ?? undefined; + const image = post.seoImage || undefined; + + return { + title, + description, + openGraph: { + title, + description, + type: "article", + publishedTime: post.publishedAt ?? post.createdAt, + ...(image && { images: [{ url: image }] }), + }, + twitter: { + card: "summary_large_image", + title, + description, + ...(image && { images: [image] }), + }, + }; +} + +export default async function PostDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + let post; + try { + post = await fetchPost(slug); + } catch { + notFound(); + } + + const date = new Date(post.publishedAt || post.createdAt).toLocaleDateString( + "en-CA", + { year: "numeric", month: "long", day: "numeric" }, + ); + + const configData = getSiteConfig(); + const fullUrl = `${configData.siteUrl}/posts/${post.slug}`; + + const jsonLd = buildGraph( + generateOrganizationSchema(configData), + generateBreadcrumbSchema(`/posts/${post.slug}`, post.title, configData.siteUrl), + ); + + const { headings, html: bodyHtml } = extractHeadings(post.body ?? ""); + + const sidebar = ( +
+ + + Subscribe + +
+ ); + + return ( +
+