Skip to content
Open
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
29 changes: 20 additions & 9 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ export default async function BlogPost({ params }: BlogPostProps) {
const title = metadata.title;
const description = metadata.description || `${title} — an engineer's take on building real software. From CyberWorld Builders.`;

// Schema image must be an absolute URL — relative paths fail structured-data validation.
const toAbsoluteUrl = (path?: string) =>
!path ? null : path.startsWith('http') ? path : `https://cyberworldbuilders.com${path}`;
const schemaImageUrl =
toAbsoluteUrl(metadata.socialImage) ||
toAbsoluteUrl(metadata.headerImage) ||
'https://cyberworldbuilders.com/images/logo.png';

// Build allPosts list for related posts and navigation
const allPostEntries = getAllPosts();
const allPosts = allPostEntries.map(p => ({
Expand Down Expand Up @@ -153,14 +161,12 @@ export default async function BlogPost({ params }: BlogPostProps) {
{new Date(metadata.publishedDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
</p>
{metadata.modifiedDate && metadata.modifiedDate !== metadata.publishedDate && (
<p className="text-[#00ff00]/50 text-xs">
Updated{' '}
<time dateTime={metadata.modifiedDate}>
{new Date(metadata.modifiedDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
</p>
)}
<p className="text-[#00ff00]/50 text-xs">
Last updated{' '}
<time dateTime={metadata.modifiedDate || metadata.publishedDate}>
{new Date(metadata.modifiedDate || metadata.publishedDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</time>
</p>
</div>
</div>
</div>
Expand All @@ -174,7 +180,12 @@ export default async function BlogPost({ params }: BlogPostProps) {
"@type": "BlogPosting",
headline: title,
description,
image: metadata.socialImage || "https://cyberworldbuilders.com/images/logo.png",
image: {
"@type": "ImageObject",
url: schemaImageUrl,
width: 1200,
height: 630,
},
author: {
"@type": "Person",
name: "Jay Long",
Expand Down
28 changes: 25 additions & 3 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,14 @@ export default async function BlogIndex({ searchParams }: BlogIndexProps) {
</div>
)}

{/* CollectionPage Schema with freshness signals */}
{/* Blog schema with embedded BlogPosting entries — gives crawlers explicit
BlogPosting markup on the listing page in addition to per-post pages. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "CollectionPage",
"@type": "Blog",
name: tagFilter
? `Posts tagged with "${tagFilter}" - CyberWorld Builders Blog`
: "Blog — Real Engineering, Not AI Fluff | CyberWorld Builders",
Expand All @@ -201,8 +202,29 @@ export default async function BlogIndex({ searchParams }: BlogIndexProps) {
publisher: {
"@type": "Organization",
name: "CyberWorld Builders",
logo: { "@type": "ImageObject", url: "https://cyberworldbuilders.com/images/logo.png" },
logo: { "@type": "ImageObject", url: "https://cyberworldbuilders.com/images/logo.png", width: 250, height: 250 },
},
blogPost: posts.map((p) => {
const headerImage = p.headerImage && p.headerImage.trim() !== ''
? (p.headerImage.startsWith('http') ? p.headerImage : `https://cyberworldbuilders.com${p.headerImage}`)
: 'https://cyberworldbuilders.com/images/logo.png';
return {
"@type": "BlogPosting",
headline: p.title,
description: p.description,
url: `https://cyberworldbuilders.com/blog/${p.slug}`,
mainEntityOfPage: { "@type": "WebPage", "@id": `https://cyberworldbuilders.com/blog/${p.slug}` },
datePublished: p.mtime.toISOString(),
dateModified: new Date(p.modifiedDate || p.mtime.toISOString()).toISOString(),
image: { "@type": "ImageObject", url: headerImage, width: 1200, height: 630 },
author: { "@type": "Person", name: "Jay Long", url: "https://cyberworldbuilders.com" },
publisher: {
"@type": "Organization",
name: "CyberWorld Builders",
logo: { "@type": "ImageObject", url: "https://cyberworldbuilders.com/images/logo.png", width: 250, height: 250 },
},
};
}),
mainEntity: {
"@type": "ItemList",
numberOfItems: allPosts.length,
Expand Down
8 changes: 6 additions & 2 deletions app/blog/posts/revenant-hollow-never-stopped-being-alive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export default function Post() {
<img
src="/images/revenant-hollow-arduino-node-prototype.png"
alt="A node prototype on a workbench: Arduino, breadboard, jumper wires, glowing blue LED"
className="w-full max-w-md mx-auto rounded"
width={793}
height={900}
className="w-full max-w-md mx-auto rounded h-auto"
/>
<figcaption className="text-center text-sm text-gray-400 mt-2">
A node prototype from years back. Still on the workbench.
Expand Down Expand Up @@ -215,7 +217,9 @@ export default function Post() {
<img
src="/images/revenant-hollow-haunt-fog-night.png"
alt="Night-time haunt environment: a dimly lit structure with heavy fog drifting under a single warm overhead light"
className="w-full max-w-md mx-auto rounded"
width={510}
height={900}
className="w-full max-w-md mx-auto rounded h-auto"
/>
<figcaption className="text-center text-sm text-gray-400 mt-2">
The work doesn&apos;t live online. It lives outside, after dark, in the fog.
Expand Down
5 changes: 4 additions & 1 deletion app/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,12 @@ export default function PrivacyPolicy() {
<h2 className="text-xl font-bold text-[#00ff00] mb-3">Contact</h2>
<p>
For privacy-related questions or requests, contact us at{' '}
{/* email_off opts out of Cloudflare's email obfuscation, which otherwise rewrites mailto links to /cdn-cgi/l/email-protection — a page Cloudflare serves without a meta description, flagged by SEO crawlers. */}
<span dangerouslySetInnerHTML={{ __html: '<!--email_off-->' }} />
<a href="mailto:contact@cyberworldbuilders.com" className="text-[#00ff00] underline">
contact@cyberworldbuilders.com
</a>.
</a>
<span dangerouslySetInnerHTML={{ __html: '<!--/email_off-->' }} />.
</p>
</section>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function robots(): MetadataRoute.Robots {
'cohere-ai',
],
allow: '/',
disallow: ['/admin/', '/api/'],
disallow: ['/admin/', '/api/', '/cdn-cgi/'],
},
],
sitemap: 'https://cyberworldbuilders.com/sitemap.xml',
Expand Down
3 changes: 3 additions & 0 deletions app/services/ServicesIndexContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ export default function ServicesIndexContent() {
Tell us what you&apos;re trying to solve. We&apos;ll figure out which combination
of services gets you there fastest.
</p>
{/* email_off opts out of Cloudflare's email obfuscation, which otherwise rewrites mailto links to /cdn-cgi/l/email-protection — a page Cloudflare serves without a meta description, flagged by SEO crawlers. */}
<span dangerouslySetInnerHTML={{ __html: '<!--email_off-->' }} />
<a
href="mailto:contact@cyberworldbuilders.com"
className="inline-block px-8 py-4 bg-[#00ff00] text-[#1a1a1a] font-bold rounded-lg hover:bg-[#00cc00] transition-colors text-lg"
>
Start a Conversation
</a>
<span dangerouslySetInnerHTML={{ __html: '<!--/email_off-->' }} />
</div>
</section>
</main>
Expand Down
2 changes: 1 addition & 1 deletion components/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function PostCard({ slug }: PostCardProps) {
<div className="relative flex-shrink-0 w-32 h-20 sm:w-40 sm:h-24 rounded overflow-hidden bg-black">
<Image
src={post.headerImage}
alt=""
alt={post.title}
fill
className="object-cover"
sizes="(max-width: 640px) 128px, 160px"
Expand Down
3 changes: 3 additions & 0 deletions components/SiteFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ export default function SiteFooter() {
<p className="text-sm font-bold text-[#00ff00] uppercase tracking-widest mb-4">Connect</p>
<SocialLinks />
<p className="mt-4 text-sm text-[#00ff00]/60">
{/* email_off opts out of Cloudflare's email obfuscation, which otherwise rewrites mailto links to /cdn-cgi/l/email-protection — a page Cloudflare serves without a meta description, flagged by SEO crawlers. */}
<span dangerouslySetInnerHTML={{ __html: '<!--email_off-->' }} />
<a
href="mailto:contact@cyberworldbuilders.com"
className="hover:text-[#00ff00] transition-colors"
>
contact@cyberworldbuilders.com
</a>
<span dangerouslySetInnerHTML={{ __html: '<!--/email_off-->' }} />
</p>
</div>
</div>
Expand Down
Loading