Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ yarn-error.log*
# rss
public/rss.xml

# search index
public/search-index.json

yarn.lock

5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ This is a Next.js application using:
- Custom Markdoc tags enable rich content layouts
- Content is processed through `app/lib/queries.js` utilities

### Search Index
- A static search index is generated from `app/content/` via `scripts/build-search-index.js`
- The index is written to `public/search-index.json` and refreshed on `npm run dev`/`npm run build`
- The homepage modal search consumes this index at runtime

### Key Directories
- `app/` - Next.js App Router pages and components
- `app/content/` - Markdown content files
Expand Down
3 changes: 2 additions & 1 deletion app/blog/[blog]/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export async function generateMetadata({ params }, parent) {
const { blog } = await params;
const postSlug = `/blog/${blog}.md`;
const postData = await getMarkdownContent(postSlug, "toml");
const description = `${postData.frontMatter.description}`;

const metadata = {
title: `${postData.frontMatter.title} • Blog`,
description: `${postData.frontMatter.description}`,
description,
};

// Only add openGraph image if it exists
Expand Down
2 changes: 1 addition & 1 deletion app/blog/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default async function BlogHome() {
{Object.entries(yearGroups)
.sort(([yearA], [yearB]) => yearB - yearA)
.map(([year, posts]) => (
<section key={year} id={year} className="scroll-mt-[72px] md:scroll-mt-[100px]">
<section key={year} id={year} className="scroll-mt-[90px] md:scroll-mt-[100px]">
{/* Year header */}
{/* <h2 className="text-secondary text-4xl font-serif mb-8 px-4">{year}</h2> */}

Expand Down
35 changes: 10 additions & 25 deletions app/components/BlogNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import { getVisibleAnchorElement } from "../lib/anchorScroll";
import { useLayoutSlots } from "../lib/layoutSlots";

/**
Expand All @@ -23,49 +24,33 @@ export function BlogNav({ sections = [] }) {
}, [setSidebarVisible]);

const handleSectionClick = (sectionId) => {
// Find the visible element (not the first one which might be hidden)
const getVisibleElement = (id) => {
const escapedId = CSS.escape(id);
const elements = document.querySelectorAll(`#${escapedId}`);
return Array.from(elements).find(el => el.getBoundingClientRect().height > 0) || null;
};

const element = getVisibleElement(sectionId);
const element = getVisibleAnchorElement(sectionId);
if (!element) {
console.warn(`Anchor not found for blog section: ${sectionId}`);
return;
}

// Responsive offset: 72px mobile, 100px desktop (matches scroll-mt)
const isMobile = window.innerWidth < 768; // md breakpoint
const offset = isMobile ? 72 : 100;

const isMobile = window.innerWidth < 768;
const offset = isMobile ? 90 : 100;
const rect = element.getBoundingClientRect();
const targetPosition = rect.top + window.scrollY - offset;

window.scrollTo({
top: targetPosition,
behavior: "smooth"
behavior: "smooth",
});
};

// Scroll-spy to track active year
useEffect(() => {
const handleScroll = () => {
// Responsive offset: 72px mobile, 100px desktop (matches scroll-mt)
const isMobile = window.innerWidth < 768; // md breakpoint
const offset = isMobile ? 72 : 100;
const isMobile = window.innerWidth < 768;
const offset = isMobile ? 90 : 100;
let currentSection = "";

// Helper to find visible element
const getVisibleElement = (id) => {
const escapedId = CSS.escape(id);
const elements = document.querySelectorAll(`#${escapedId}`);
return Array.from(elements).find(el => el.getBoundingClientRect().height > 0) || null;
};

// Find active year based on scroll position
for (const section of sections) {
const element = getVisibleElement(section.id);
const element = getVisibleAnchorElement(section.id);
if (element) {
const rect = element.getBoundingClientRect();
const isInRange = rect.top <= offset && rect.bottom >= offset;
Expand All @@ -81,7 +66,7 @@ export function BlogNav({ sections = [] }) {
// Fallback: find the first visible section if none are at offset
if (!currentSection) {
for (const section of sections) {
const element = getVisibleElement(section.id);
const element = getVisibleAnchorElement(section.id);
if (element) {
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
Expand Down
49 changes: 34 additions & 15 deletions app/components/ContentBlurbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ import { useState } from "react";
import Link from "next/link";
import Image from "next/image";

const renderHtml = (content) => ({ __html: content || "" });
const renderContent = (Tag, content, className) => {
if (content === null || content === undefined) {
return null;
}

const Wrapper = Tag;
if (typeof content === "string") {
return (
<Wrapper className={className} dangerouslySetInnerHTML={{ __html: content }} />
);
}

return <Wrapper className={className}>{content}</Wrapper>;
};

const slugify = (value) => {
if (!value) {
Expand Down Expand Up @@ -92,7 +105,7 @@ export const CollapsibleContentBlurb = ({ title, description, content, reference
);
})}
</ul>
<div className="text-base text-gray-87 line-clamp-5" dangerouslySetInnerHTML={renderHtml(content)} />
{renderContent("div", content, "text-base text-gray-87 line-clamp-5")}
</div>
</div>
<button
Expand All @@ -110,7 +123,7 @@ export const CollapsibleContentBlurb = ({ title, description, content, reference

{isExpanded && (
<div className="mt-6 animate-fadeIn">
<article className="prose prose-invert max-w-none" dangerouslySetInnerHTML={renderHtml(content)} />
{renderContent("article", content, "prose prose-invert max-w-none")}

{references && references.length > 0 && (
<div className="mt-8 pt-6 border-t border-gray-87">
Expand All @@ -128,7 +141,7 @@ export const PreviewContentBlurb = ({ id, blurbSlug, title, description, content
const tooltipContext = blurbSlug || id || slugify(title);

return (
<div id={id} className="mb-16 scroll-mt-[72px] md:scroll-mt-[80px] snap-start">
<div id={id} className="mb-16 scroll-mt-[90px] md:scroll-mt-[80px] snap-start">
<div className="flex justify-between items-start gap-4">
<div className="flex-1">
{/* Render images if provided */}
Expand Down Expand Up @@ -182,10 +195,11 @@ export const PreviewContentBlurb = ({ id, blurbSlug, title, description, content
);
})}
</ul>
<div
className={`text-base text-primary transition-transform duration-300 ${isExpanded ? '' : 'line-clamp-3'}`}
dangerouslySetInnerHTML={renderHtml(content)}
/>
{renderContent(
"div",
content,
`text-base text-primary transition-transform duration-300 ${isExpanded ? "" : "line-clamp-3"}`
)}
</div>
</div>
<div className="flex justify-between">
Expand Down Expand Up @@ -256,12 +270,13 @@ export const PreviewContentBlurb = ({ id, blurbSlug, title, description, content
export const ContentBlurb = ({ id, blurbSlug, title, description, content, references, image, imageDark, ctaButton }) => {
const [isDetailsExpanded, setIsDetailsExpanded] = useState(false);
const tooltipContext = blurbSlug || id || slugify(title);
const anchorId = id || blurbSlug;

// Check if there are any details to show
const hasDetails = description || (references && references.some(ref => ref.description));

return (
<div className="">
<div id={anchorId} className="scroll-mt-[90px] md:scroll-mt-[80px]">
<div className="flex justify-between items-start gap-4">
<div className="flex-1">
{/* Render images if provided */}
Expand Down Expand Up @@ -383,7 +398,7 @@ export const ContentBlurb = ({ id, blurbSlug, title, description, content, refer
)}

{/* Full content without line-clamp */}
<article className="prose prose-invert max-w-none mt-6" dangerouslySetInnerHTML={renderHtml(content)} />
{renderContent("article", content, "prose prose-invert max-w-none mt-6")}

{/* CTA Button - smaller for narrow layout */}
{ctaButton && ctaButton.link && ctaButton.label && (
Expand Down Expand Up @@ -425,7 +440,7 @@ export const MicroBlurb = ({
const hasDetails = description || (references && references.some(ref => ref.description));

return (
<div id={id} className="scroll-mt-[72px] md:scroll-mt-[80px]">
<div id={id} className="scroll-mt-[90px] md:scroll-mt-[80px]">
{/* Images - smaller size for narrow layout */}
{(image || imageDark) && (
<div className="mb-3">
Expand Down Expand Up @@ -516,9 +531,9 @@ export const MicroBlurb = ({
{/* Content - either full or line-clamped */}
<div className={`text-sm text-primary mt-3 ${showFullContent ? '' : 'line-clamp-8'}`}>
{showFullContent ? (
<article className="prose prose-sm prose-invert max-w-none" dangerouslySetInnerHTML={renderHtml(content)} />
renderContent("article", content, "prose prose-sm prose-invert max-w-none")
) : (
<div dangerouslySetInnerHTML={renderHtml(content)} />
renderContent("div", content, "")
)}
</div>

Expand Down Expand Up @@ -574,7 +589,7 @@ export function HomepageBlurb({
// Check if there are any details to show
const hasDetails = description || (references && references.some(ref => ref.description));
return (
<div id={id} className="mb-12 md:mb-16 scroll-mt-[72px] md:scroll-mt-[80px] snap-start">
<div id={id} className="mb-12 md:mb-16 scroll-mt-[90px] md:scroll-mt-[80px] snap-start">
{/* Header Images */}
{(image || imageDark) && (
<div className="mb-4 md:mb-6">
Expand Down Expand Up @@ -670,7 +685,11 @@ export function HomepageBlurb({
</div>
)}
{/* Full content */}
<article className="prose prose-lg max-w-none mb-8 text-[#3f3f3f]" dangerouslySetInnerHTML={renderHtml(content)} />
{renderContent(
"article",
content,
"prose prose-lg max-w-none mb-8 text-[#3f3f3f]"
)}

{/* CTA Button - smaller for narrow layout */}
{ctaButton && ctaButton.link && ctaButton.label && (
Expand Down
35 changes: 10 additions & 25 deletions app/components/EcosystemNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import { getVisibleAnchorElement } from "../lib/anchorScroll";
import { useLayoutSlots } from "../lib/layoutSlots";

/**
Expand All @@ -23,49 +24,33 @@ export function EcosystemNav({ sections = [] }) {
}, [setSidebarVisible]);

const handleSectionClick = (sectionId) => {
// Find the visible element (not the first one which might be hidden)
const getVisibleElement = (id) => {
const escapedId = CSS.escape(id);
const elements = document.querySelectorAll(`#${escapedId}`);
return Array.from(elements).find(el => el.getBoundingClientRect().height > 0) || null;
};

const element = getVisibleElement(sectionId);
const element = getVisibleAnchorElement(sectionId);
if (!element) {
console.warn(`Anchor not found for ecosystem section: ${sectionId}`);
return;
}

// Responsive offset: 72px mobile, 100px desktop
const isMobile = window.innerWidth < 768; // md breakpoint
const offset = isMobile ? 72 : 100;

const isMobile = window.innerWidth < 768;
const offset = isMobile ? 90 : 100;
const rect = element.getBoundingClientRect();
const targetPosition = rect.top + window.scrollY - offset;

window.scrollTo({
top: targetPosition,
behavior: "smooth"
behavior: "smooth",
});
};

// Scroll-spy to track active section
useEffect(() => {
const handleScroll = () => {
// Responsive offset: 72px mobile, 100px desktop (matches scroll-mt)
const isMobile = window.innerWidth < 768; // md breakpoint
const offset = isMobile ? 72 : 100;
const isMobile = window.innerWidth < 768;
const offset = isMobile ? 90 : 100;
let currentSection = "";

// Helper to find visible element
const getVisibleElement = (id) => {
const escapedId = CSS.escape(id);
const elements = document.querySelectorAll(`#${escapedId}`);
return Array.from(elements).find(el => el.getBoundingClientRect().height > 0) || null;
};

// Find active section based on scroll position
for (const section of sections) {
const element = getVisibleElement(section.id);
const element = getVisibleAnchorElement(section.id);
if (element) {
const rect = element.getBoundingClientRect();
const isInRange = rect.top <= offset && rect.bottom >= offset;
Expand All @@ -81,7 +66,7 @@ export function EcosystemNav({ sections = [] }) {
// Fallback: find the first visible section if none are at offset
if (!currentSection) {
for (const section of sections) {
const element = getVisibleElement(section.id);
const element = getVisibleAnchorElement(section.id);
if (element) {
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
Expand Down
Loading