Skip to content

feat(blog): orchestration architecture post + hero blog page layout (ZE-1246)#168

Open
vern-dt wants to merge 7 commits intomainfrom
feat/ZE-1246-orchestration-architecture-blog-post
Open

feat(blog): orchestration architecture post + hero blog page layout (ZE-1246)#168
vern-dt wants to merge 7 commits intomainfrom
feat/ZE-1246-orchestration-architecture-blog-post

Conversation

@vern-dt
Copy link
Copy Markdown

@vern-dt vern-dt commented Mar 24, 2026

What's added in this PR?

  • New blog post: Why Orchestration is the Missing Layer in Your Deployment Stack
  • Vern Tremble author entry with avatar
  • Hero image for the orchestration post
  • Per-post SEO meta tags injected on blog post pages
  • External MDX links auto-get target=_blank and rel=noopener noreferrer
  • Internal and external backlinks added to the post
  • Blog page layout: single hero featured post (full-width horizontal card) at the top, followed by a "Latest Posts" grid

What's the issue related to this PR?

ZE-1246

How to test this PR?

  1. Visit /blog — confirm the hero post renders as a wide horizontal card at the top
  2. Confirm remaining posts appear in a 3-column grid below under "Latest Posts"
  3. Visit the orchestration post — confirm SEO meta tags are present in <head>
  4. Confirm external links in MDX open in a new tab

Checklist

  • I have added explanation of the changes I made
  • UI related: this PR has been visually reviewed for both web, mobile, and tablet

Adds full blog post "Why Orchestration is the Missing Layer in Your
Deployment Stack" targeting enterprise C-level readers as part of the
Zephyr Orchestration Architecture marketing campaign.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 24, 2026

🚀 Preview Environment Ready!

Name Status URL
zephyr-landing ✅ Active https://zackary-chapple-14106-zephyr-landing-zephyr-websi-04e1b18... ↗

Details:

  • Latest Commit: 3a1b7a8
  • Updated at: 3/26/2026, 5:41:26 PM

Vern Tremble and others added 6 commits March 24, 2026 17:44
…hestration post (ZE-1246)

- Add custom dark-theme SVG hero image with 3-stage pipeline diagram
- Add Vern Tremble as author with avatar
- Wire hero image into images.ts and loader.ts authorMap
- Update post frontmatter: author → Vern Tremble

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds dynamic document head management to the blog post route:
- document.title: "[Post Title] | Zephyr Cloud"
- meta description, og:title, og:description, og:image, og:type, og:url
- twitter:card, twitter:title, twitter:description, twitter:image
- canonical link element

Fixes Lighthouse "Document does not have a meta description" for all
blog posts. Cleans up title on unmount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… (ZE-1246)

Internal links:
- /blog/module-federation-vs-native-esm — micro-frontend architectures
- /blog/whos-your-cloud-daddy — CDN vendor lock-in
- /blog/cloudflare-workers-aws-outage — single-provider outage risk
- /blog/three-sdlcs-one-zephyr — tag/environment routing workflows
- /blog/all-the-pipelines — multi-CDN publishing mechanics
- /blog/serve-time — edge serving / Serve Time concept
- /blog/aws-byoc — AWS as CDN provider
- /blog/soc2 — compliance and audit trail
- /blog/dora-metrics — deployment frequency and DORA research

External links:
- kubernetes.io — Kubernetes deployment docs
- aws.amazon.com/ecs — AWS ECS
- workers.cloudflare.com — Cloudflare Workers
- rspack.dev — Rspack bundler
- docs.zephyr-cloud.io/general/getting-started — supported toolchains
- akamai.com, fastly.com, netlify.com — CDN providers
- dora.dev/research — DORA research on elite engineering teams
- module-federation.io — Module Federation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rnal MDX links

Updates the global MDX <a> component to detect external URLs (http/https)
and automatically apply target="_blank" rel="noopener noreferrer".
Internal links (relative paths) are unaffected.

Applies site-wide to all blog posts via the MDXProvider in __root.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vern-dt vern-dt self-assigned this Mar 26, 2026
@vern-dt vern-dt changed the title docs: add orchestration architecture blog post (ZE-1246) feat(blog): orchestration architecture post + hero blog page layout (ZE-1246) Mar 26, 2026
@arthurfiorette arthurfiorette requested a review from Copilot March 26, 2026 17:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new orchestration architecture blog post and updates the blog UI/MDX rendering to support a hero featured post, improved outbound link handling, and per-post SEO meta tags.

Changes:

  • Updates /blog to render a single hero post followed by a “Latest Posts” grid.
  • Injects per-post SEO meta tags/canonical URL on blog post pages.
  • Adds new post/author assets and registers the post + author in the blog loader.

Reviewed changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/routes/blog/index.tsx Switches blog listing to hero + latest grid layout logic.
src/routes/blog/$slug.tsx Adds client-side meta tag + canonical injection based on post content.
src/routes/__root.tsx Updates MDX <a> to open external links in a new tab safely.
src/lib/blog/loader.ts Registers new author mapping and blog post module loader entry.
src/lib/blog/images.ts Registers hero/listing image for the new orchestration post.
src/images/blog/orchestration-architecture/hero.svg Adds new hero SVG asset for the orchestration post.
src/images/authors/vern.svg Adds new author avatar SVG asset.
src/data/blog/authors.ts Adds new author entry for Vern (avatar + socials).
src/content/blog/orchestration-architecture.mdx Adds new orchestration architecture blog post content.
src/components/BlogCard.tsx Introduces a new featured/hero BlogCard layout.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/routes/blog/$slug.tsx
Comment on lines +39 to +82
useEffect(() => {
if (!post) return;

const prevTitle = document.title;
document.title = `${post.title} | Zephyr Cloud`;

const setMeta = (selector: string, attr: string, value: string) => {
let el = document.querySelector<HTMLMetaElement>(selector);
if (!el) {
el = document.createElement('meta');
const [attrName, attrValue] = selector.replace('meta[', '').replace(']', '').split('=');
el.setAttribute(attrName, attrValue.replace(/"/g, ''));
document.head.appendChild(el);
}
el.setAttribute(attr, value);
};

const origin = typeof window !== 'undefined' ? window.location.origin : 'https://zephyr-cloud.io';
const url = `${origin}/blog/${post.slug}`;
const heroImage = typeof post.heroImage === 'string' ? post.heroImage : '';

setMeta('meta[name="description"]', 'content', post.description);
setMeta('meta[property="og:type"]', 'content', 'article');
setMeta('meta[property="og:title"]', 'content', post.title);
setMeta('meta[property="og:description"]', 'content', post.description);
setMeta('meta[property="og:url"]', 'content', url);
if (heroImage) setMeta('meta[property="og:image"]', 'content', heroImage);
setMeta('meta[name="twitter:card"]', 'content', 'summary_large_image');
setMeta('meta[name="twitter:title"]', 'content', post.title);
setMeta('meta[name="twitter:description"]', 'content', post.description);
if (heroImage) setMeta('meta[name="twitter:image"]', 'content', heroImage);

let canonical = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!canonical) {
canonical = document.createElement('link');
canonical.rel = 'canonical';
document.head.appendChild(canonical);
}
canonical.href = url;

return () => {
document.title = prevTitle;
};
}, [post]);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta tags/canonical are mutated but not restored on cleanup, and og:image / twitter:image are only set when heroImage is truthy. Navigating from a post with an image to one without (or to a non-post route) will leave stale image/canonical tags in <head>. Track previous values (or elements created) and restore/remove them in the effect cleanup; also explicitly remove or blank og:image + twitter:image when heroImage is empty.

Copilot uses AI. Check for mistakes.
Comment thread src/routes/blog/$slug.tsx

const origin = typeof window !== 'undefined' ? window.location.origin : 'https://zephyr-cloud.io';
const url = `${origin}/blog/${post.slug}`;
const heroImage = typeof post.heroImage === 'string' ? post.heroImage : '';
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open Graph/Twitter image URLs are typically expected to be absolute (many scrapers won’t reliably resolve relative URLs). If post.heroImage is a relative asset URL, build an absolute URL using origin (e.g., only prepend when it starts with /) before setting og:image / twitter:image.

Suggested change
const heroImage = typeof post.heroImage === 'string' ? post.heroImage : '';
const rawHeroImage = typeof post.heroImage === 'string' ? post.heroImage : '';
const heroImage = rawHeroImage.startsWith('/') ? `${origin}${rawHeroImage}` : rawHeroImage;

Copilot uses AI. Check for mistakes.
Comment thread src/routes/blog/$slug.tsx
setMeta('meta[property="og:title"]', 'content', post.title);
setMeta('meta[property="og:description"]', 'content', post.description);
setMeta('meta[property="og:url"]', 'content', url);
if (heroImage) setMeta('meta[property="og:image"]', 'content', heroImage);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open Graph/Twitter image URLs are typically expected to be absolute (many scrapers won’t reliably resolve relative URLs). If post.heroImage is a relative asset URL, build an absolute URL using origin (e.g., only prepend when it starts with /) before setting og:image / twitter:image.

Copilot uses AI. Check for mistakes.
Comment thread src/routes/blog/$slug.tsx
setMeta('meta[name="twitter:card"]', 'content', 'summary_large_image');
setMeta('meta[name="twitter:title"]', 'content', post.title);
setMeta('meta[name="twitter:description"]', 'content', post.description);
if (heroImage) setMeta('meta[name="twitter:image"]', 'content', heroImage);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open Graph/Twitter image URLs are typically expected to be absolute (many scrapers won’t reliably resolve relative URLs). If post.heroImage is a relative asset URL, build an absolute URL using origin (e.g., only prepend when it starts with /) before setting og:image / twitter:image.

Copilot uses AI. Check for mistakes.
Comment thread src/routes/__root.tsx
Comment on lines +132 to +137
return (
<a
href={href}
className="text-emerald-400 hover:text-emerald-300 underline"
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
{...props}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{...props} is spread after the external-link target/rel, which allows MDX-provided props to override them (e.g., removing rel=\"noopener noreferrer\"). To ensure the security attributes can’t be overridden, spread props first and then enforce/merge target + rel for external links (ensuring noopener noreferrer is preserved if a custom rel is provided).

Suggested change
return (
<a
href={href}
className="text-emerald-400 hover:text-emerald-300 underline"
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
{...props}
const externalAttrs = isExternal
? {
target: '_blank',
rel: Array.from(
new Set(
['noopener', 'noreferrer']
.concat(
typeof props.rel === 'string'
? props.rel.split(/\s+/).filter(Boolean)
: [],
),
),
).join(' '),
}
: {};
return (
<a
href={href}
className="text-emerald-400 hover:text-emerald-300 underline"
{...props}
{...externalAttrs}

Copilot uses AI. Check for mistakes.
Comment thread src/data/blog/authors.ts
@@ -1,4 +1,5 @@
import loisAvatar from '@/images/authors/lois.webp';
import vernAvatar from '@/images/authors/vern.webp';
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This imports vern.webp, but the PR adds src/images/authors/vern.svg (not a .webp). This will fail at build time unless vern.webp exists elsewhere. Either add the expected vern.webp asset or update the import to the actual file type/path being added.

Suggested change
import vernAvatar from '@/images/authors/vern.webp';
import vernAvatar from '@/images/authors/vern.svg';

Copilot uses AI. Check for mistakes.
Comment thread src/routes/blog/index.tsx
);
// First featured post is the hero; everything else follows as latest
const featuredPost = filteredPosts.find((post) => post.featured) ?? filteredPosts[0];
const latestPosts = filteredPosts.filter((post) => post !== featuredPost);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering latestPosts by object identity (post !== featuredPost) is brittle if posts are ever re-hydrated/cloned (identity changes) or if a similar object reference is used. Prefer filtering by a stable identifier (e.g., slug) and handle the undefined case explicitly.

Suggested change
const latestPosts = filteredPosts.filter((post) => post !== featuredPost);
const featuredPostSlug = featuredPost?.slug;
const latestPosts =
featuredPostSlug != null
? filteredPosts.filter((post) => post.slug !== featuredPostSlug)
: filteredPosts;

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60
<div className="flex -space-x-2">
{post.authors?.map((author, index) => (
<img
key={index}
src={author.avatar}
alt={author.displayName}
className="w-9 h-9 rounded-full ring-2 ring-neutral-900"
/>
))}
</div>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index as a React key can cause incorrect UI reconciliation if the authors array changes order or items are inserted/removed. Use a stable key such as author.displayName (or another unique author id/link) instead.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should regenerate this image. Seems a bit broken

@Nsttt Nsttt marked this pull request as ready for review March 26, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants