Major site overhaul: resources hub, content migration, new blog posts, forms

- Redesign /resources as sectioned hub with category pages
- Migrate 645 Squarespace CDN images to local /images/content/
- Create 9 new news/blog posts with event photos
- Fix blog post slugs (rename gibberish filenames)
- Rename Design Blog to Design Blogs across site
- Remove education page, replace with Platform in nav
- Redesign rover repair request form with dynamic rover entries
- Add school search combobox to contact, store, and repair forms
- Extract shared KNOWN_SCHOOLS data
- Make /rover-expansion-3d-printing dynamically pull from MDX
- Add related resources sections to product pages
- Fix homepage broken /quote links to /store
- Store page: sample kit cards, inline quote builder, mailing list opt-in
This commit is contained in:
Tim Hadwen
2026-03-01 17:14:05 +10:00
parent 707c49dd3f
commit ae3ae18585
1212 changed files with 2477 additions and 6948 deletions

View File

@@ -5,24 +5,94 @@ import { MDXRemote } from "next-mdx-remote/rsc";
import { Container } from "@/components/layout/Container";
import { Button } from "@/components/ui/Button";
import { getAllResources, getResourceBySlug } from "@/lib/resources";
import { ResourcesGrid } from "../resources-client";
function MdxLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const { href, children, ...rest } = props;
const isExternal = href?.startsWith("http");
return (
<a
href={href}
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
{...rest}
>
{children}
</a>
);
}
const mdxComponents = {
a: MdxLink,
};
const NEWS_CATEGORIES = ["News & Updates", "Customer Stories"];
const CATEGORY_SLUGS: Record<string, {
name: string;
description: string;
matchCategories?: string[];
filterCategories?: string[];
}> = {
"getting-started": {
name: "Getting Started",
description: "Everything you need to get up and running with the Micromelon platform.",
},
activities: {
name: "Activities",
description: "Hands-on activities for the Micromelon Rover and Robot Simulator.",
matchCategories: ["Activities", "Simulator Activities"],
filterCategories: ["Activities", "Simulator Activities"],
},
"sensor-guides": {
name: "Sensor Guides",
description: "Learn how each sensor on the Micromelon Rover works and how to code it.",
},
"3d-printing-guides": {
name: "3D Printing Guides",
description: "Guides for designing, printing, and troubleshooting rover attachments.",
},
"advanced-guides": {
name: "Advanced Guides",
description: "I2C, UART, OpenMV, and other advanced topics for experienced users.",
},
"build-guides": {
name: "Build Guides",
description: "Step-by-step instructions for building rover attachments.",
},
"design-blogs": {
name: "Design Blogs",
description: "Read about the design process behind rover attachments.",
},
};
interface ResourcePageProps {
params: Promise<{ slug: string }>;
}
export function generateStaticParams() {
const resources = getAllResources();
return resources.map((resource) => ({
const resourceParams = resources.map((resource) => ({
slug: resource.slug,
}));
const categoryParams = Object.keys(CATEGORY_SLUGS).map((slug) => ({
slug,
}));
return [...resourceParams, ...categoryParams];
}
export async function generateMetadata({
params,
}: ResourcePageProps): Promise<Metadata> {
const { slug } = await params;
const category = CATEGORY_SLUGS[slug];
if (category) {
return {
title: `${category.name} - Resources`,
description: category.description,
};
}
const resource = getResourceBySlug(slug);
if (!resource) return {};
return {
@@ -33,6 +103,35 @@ export async function generateMetadata({
export default async function ResourcePage({ params }: ResourcePageProps) {
const { slug } = await params;
/* Category page */
const category = CATEGORY_SLUGS[slug];
if (category) {
const cats = category.matchCategories || [category.name];
const resources = getAllResources()
.filter((r) => r.categories.some((c) => cats.includes(c)))
.map(({ content, ...meta }) => meta);
return (
<>
<section className="bg-white pt-16 sm:pt-20">
<Container>
<Button href="/resources" variant="outline" size="sm" className="mb-8">
&larr; All Resources
</Button>
</Container>
</section>
<ResourcesGrid
resources={resources}
title={category.name}
subtitle={category.description}
filterCategories={category.filterCategories}
/>
</>
);
}
/* Individual resource page */
const resource = getResourceBySlug(slug);
if (!resource) {
@@ -94,8 +193,19 @@ export default async function ResourcePage({ params }: ResourcePageProps) {
{/* MDX Content */}
<article className="mdx-content">
<MDXRemote source={resource.content} />
<MDXRemote source={resource.content} components={mdxComponents} />
</article>
{/* Return button */}
<div className="mt-12 border-t border-border pt-8">
<Button
href={isNews ? "/news" : "/resources"}
variant="outline"
size="sm"
>
&larr; {isNews ? "Return to News" : "Return to Resources"}
</Button>
</div>
</Container>
</section>
);