Skip to content

Commit 317bc29

Browse files
updates
1 parent 1135d89 commit 317bc29

208 files changed

Lines changed: 7316 additions & 5391 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/[slug]/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { Metadata } from 'next';
22
import { notFound } from 'next/navigation';
33
import { getAllPostSlugs, getPostBySlug } from '@/lib/posts';
4+
import { getCanonicalUrlForPost } from '@/lib/post-canonical';
45
import PostLayout from '@/components/PostLayout';
56
import ArticleStructuredData from '@/components/ArticleStructuredData';
67
import { defaultOgImage } from '@/lib/site-seo';
78

8-
/** Job-support service pages are canonical at /:slug/ (root). */
99
const isJobSupportSlug = (slug: string) =>
1010
slug.includes('job-support') || slug.includes('job-help');
1111

@@ -23,7 +23,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
2323
const post = await getPostBySlug(slug);
2424
if (!post) return {};
2525

26-
const canonical = `https://proxytechsupport.com/${slug}/`;
26+
const canonical = getCanonicalUrlForPost(post);
2727
const title = `${post.title} | Proxy Tech Support`;
2828
const published = post.date ? `${post.date}T12:00:00.000Z` : undefined;
2929

@@ -58,7 +58,8 @@ export default async function SlugPage({ params }: Props) {
5858
const post = await getPostBySlug(slug);
5959
if (!post) notFound();
6060

61-
const url = `https://proxytechsupport.com/${slug}/`;
61+
const Article = post.Article;
62+
const url = getCanonicalUrlForPost(post);
6263

6364
const breadcrumbs = isJobSupportSlug(slug)
6465
? [
@@ -83,11 +84,12 @@ export default async function SlugPage({ params }: Props) {
8384
/>
8485
<PostLayout
8586
title={post.title}
86-
content={post.content}
8787
date={post.date}
8888
breadcrumbs={breadcrumbs}
8989
showInterviewBanner={isJobSupportSlug(slug)}
90-
/>
90+
>
91+
<Article />
92+
</PostLayout>
9193
</>
9294
);
9395
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Metadata } from 'next';
2+
import LandingPageTemplate from '@/components/LandingPageTemplate';
3+
import { agenticAiRagMlopsJobSupportUSA } from '@/data/landing-pages';
4+
import { landingPageMetadata } from '@/lib/site-seo';
5+
6+
export const metadata: Metadata = landingPageMetadata(agenticAiRagMlopsJobSupportUSA);
7+
8+
export default function AgenticAiRagMlopsJobSupportUSAPage() {
9+
return <LandingPageTemplate config={agenticAiRagMlopsJobSupportUSA} />;
10+
}

app/blog/[slug]/page.tsx

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import type { Metadata } from 'next';
2-
import { notFound, permanentRedirect } from 'next/navigation';
2+
import { notFound } from 'next/navigation';
33
import { getAllPostSlugs, getPostBySlug } from '@/lib/posts';
4+
import { getCanonicalUrlForPost } from '@/lib/post-canonical';
45
import PostLayout from '@/components/PostLayout';
56
import ArticleStructuredData from '@/components/ArticleStructuredData';
67
import { defaultOgImage } from '@/lib/site-seo';
78

89
/**
9-
* Job-support service pages are canonical at /:slug/ (root level).
10-
* If a legacy /blog/:slug/ URL arrives for one of these, we 308-redirect
11-
* it back to the root so rankings stay at the correct canonical URL.
12-
*
13-
* Pure blog articles (no "job-support" in slug) ARE canonical here at
14-
* /blog/:slug/ and are rendered normally.
10+
* Duplicate URLs: many posts are reachable at both /blog/:slug/ and /:slug/
11+
* (static export cannot rely on HTTP redirects on GitHub Pages).
12+
* We render full content here and in app/[slug]/page.tsx and set the same
13+
* canonical URL (from meta.permalink when present) on both.
1514
*/
1615
const isJobSupportSlug = (slug: string) =>
1716
slug.includes('job-support') || slug.includes('job-help');
@@ -22,20 +21,15 @@ type Props = {
2221

2322
export async function generateStaticParams() {
2423
const slugs = await getAllPostSlugs();
25-
// Static export (`output: 'export'`): emit every slug so legacy /blog/:slug/ URLs
26-
// that point at job-support posts still get HTML redirect pages to /:slug/.
2724
return slugs.map((slug) => ({ slug }));
2825
}
2926

3027
export async function generateMetadata({ params }: Props): Promise<Metadata> {
3128
const { slug } = await params;
32-
// Metadata is irrelevant for slugs that will redirect
33-
if (isJobSupportSlug(slug)) return {};
34-
3529
const post = await getPostBySlug(slug);
3630
if (!post) return {};
3731

38-
const canonical = `https://proxytechsupport.com/blog/${slug}/`;
32+
const canonical = getCanonicalUrlForPost(post);
3933
const title = `${post.title} | Proxy Tech Support`;
4034
const published = post.date ? `${post.date}T12:00:00.000Z` : undefined;
4135

@@ -67,16 +61,11 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
6761

6862
export default async function BlogArticlePage({ params }: Props) {
6963
const { slug } = await params;
70-
71-
// Job-support pages belong at root level — redirect legacy /blog/:slug/ hits
72-
if (isJobSupportSlug(slug)) {
73-
permanentRedirect(`/${slug}/`);
74-
}
75-
7664
const post = await getPostBySlug(slug);
7765
if (!post) notFound();
7866

79-
const url = `https://proxytechsupport.com/blog/${slug}/`;
67+
const Article = post.Article;
68+
const url = getCanonicalUrlForPost(post);
8069

8170
const breadcrumbs = [
8271
{ label: 'Home', href: '/' },
@@ -95,11 +84,12 @@ export default async function BlogArticlePage({ params }: Props) {
9584
/>
9685
<PostLayout
9786
title={post.title}
98-
content={post.content}
9987
date={post.date}
10088
breadcrumbs={breadcrumbs}
101-
showInterviewBanner
102-
/>
89+
showInterviewBanner={isJobSupportSlug(slug)}
90+
>
91+
<Article />
92+
</PostLayout>
10393
</>
10494
);
10595
}

app/blog/page.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from 'next';
22
import Link from 'next/link';
33
import { getAllPosts } from '@/lib/posts';
4+
import { getPostPublicHref } from '@/lib/post-canonical';
45
import TopBar from '@/components/TopBar';
56
import Navbar from '@/components/Navbar';
67
import Footer from '@/components/Footer';
@@ -40,13 +41,9 @@ export const metadata: Metadata = {
4041
},
4142
};
4243

43-
/**
44-
* Job-support service pages live at their root canonical URL /:slug/.
45-
* Pure blog articles (no "job-support" in slug) live at /blog/:slug/.
46-
*/
47-
function getPostUrl(slug: string) {
48-
const isJobSupport = slug.includes('job-support') || slug.includes('job-help');
49-
return isJobSupport ? `/${slug}/` : `/blog/${slug}/`;
44+
/** Uses each post's permalink when set (matches indexed URLs). */
45+
function getPostUrl(post: Awaited<ReturnType<typeof getAllPosts>>[number]) {
46+
return getPostPublicHref(post);
5047
}
5148

5249
export default async function BlogPage() {
@@ -79,7 +76,7 @@ export default async function BlogPage() {
7976

8077
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
8178
{posts.map((post) => {
82-
const href = getPostUrl(post.slug);
79+
const href = getPostUrl(post);
8380
return (
8481
<article
8582
key={post.slug}

app/interviews/[slug]/page.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from 'next';
22
import { notFound } from 'next/navigation';
33
import { getAllInterviewSlugs, getInterviewBySlug } from '@/lib/interviews';
4+
import { getCanonicalInterviewUrl } from '@/lib/interview-canonical';
45
import PostLayout from '@/components/PostLayout';
56
import ArticleStructuredData from '@/components/ArticleStructuredData';
67
import { defaultOgImage } from '@/lib/site-seo';
@@ -19,13 +20,16 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
1920
const interview = await getInterviewBySlug(slug);
2021
if (!interview) return {};
2122

22-
const canonical = `https://proxytechsupport.com/interviews/${slug}/`;
23+
const canonical = getCanonicalInterviewUrl(interview.slug);
2324
const title = `${interview.title} | Proxy Tech Support`;
2425
const published = interview.date ? `${interview.date}T12:00:00.000Z` : undefined;
2526

2627
return {
2728
title,
2829
description: interview.description,
30+
...(interview.keywords
31+
? { keywords: interview.keywords.split(',').map((k) => k.trim()) }
32+
: {}),
2933
alternates: { canonical },
3034
robots: { index: true, follow: true },
3135
openGraph: {
@@ -53,7 +57,8 @@ export default async function InterviewPostPage({ params }: Props) {
5357
const interview = await getInterviewBySlug(slug);
5458
if (!interview) notFound();
5559

56-
const url = `https://proxytechsupport.com/interviews/${slug}/`;
60+
const Article = interview.Article;
61+
const url = getCanonicalInterviewUrl(interview.slug);
5762

5863
return (
5964
<>
@@ -66,15 +71,17 @@ export default async function InterviewPostPage({ params }: Props) {
6671
/>
6772
<PostLayout
6873
title={interview.title}
69-
content={interview.content}
7074
date={interview.date}
7175
showInterviewBanner
76+
wrapWithBlogShell
7277
breadcrumbs={[
7378
{ label: 'Home', href: '/' },
7479
{ label: 'Interview Questions', href: '/interviews/' },
7580
{ label: interview.title },
7681
]}
77-
/>
82+
>
83+
<Article />
84+
</PostLayout>
7885
</>
7986
);
8087
}

app/sitemap.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { MetadataRoute } from 'next';
22
import { getAllPosts } from '@/lib/posts';
3+
import { getCanonicalUrlForPost } from '@/lib/post-canonical';
34
import { getAllInterviews } from '@/lib/interviews';
5+
import { getCanonicalInterviewUrl } from '@/lib/interview-canonical';
46
import { allLandingPages } from '@/data/landing-pages';
57

68
/** Required so `output: 'export'` can emit `/sitemap.xml` at build time. */
@@ -33,9 +35,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
3335
];
3436

3537
const postRoutes: MetadataRoute.Sitemap = posts.map((post) => {
36-
const isJobSupport =
37-
post.slug.includes('job-support') || post.slug.includes('job-help');
38-
const url = isJobSupport ? `${BASE}/${post.slug}/` : `${BASE}/blog/${post.slug}/`;
38+
const url = getCanonicalUrlForPost(post);
3939
const last =
4040
post.date && post.date.length >= 10 ? post.date : today;
4141
return {
@@ -49,7 +49,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
4949
const interviewRoutes: MetadataRoute.Sitemap = interviews.map((i) => {
5050
const last = i.date && i.date.length >= 10 ? i.date : today;
5151
return {
52-
url: `${BASE}/interviews/${i.slug}/`,
52+
url: getCanonicalInterviewUrl(i.slug),
5353
lastModified: last,
5454
changeFrequency: 'weekly' as const,
5555
priority: prio('0.7'),

components/BlogArticleShell.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { ReactNode } from 'react';
2+
3+
export type BlogArticleShellProps = {
4+
children: ReactNode;
5+
/** default = subtle card wrap; feature = dark hero band + tags */
6+
variant?: 'default' | 'feature';
7+
hero?: {
8+
eyebrow?: string;
9+
subtitle?: string;
10+
tags?: string[];
11+
};
12+
};
13+
14+
/**
15+
* Wraps blog article HTML/TSX bodies for richer layouts than flat markdown prose.
16+
*/
17+
export default function BlogArticleShell({ children, variant = 'default', hero }: BlogArticleShellProps) {
18+
if (variant === 'feature' && hero) {
19+
return (
20+
<div className="blog-article-shell blog-article-shell--feature">
21+
<div
22+
className="blog-article-shell-hero"
23+
style={{
24+
margin: '-0.25rem -0.5rem 1.75rem',
25+
padding: '2rem 1.25rem 1.75rem',
26+
borderRadius: 'var(--pts-card-radius)',
27+
background: 'linear-gradient(135deg, var(--pts-nav-bg) 0%, var(--pts-surface-dark-raised) 55%, var(--pts-dashboard-bg) 100%)',
28+
border: '1px solid rgba(0, 223, 130, 0.2)',
29+
boxShadow: 'var(--pts-shadow-lp)',
30+
}}
31+
>
32+
{hero.eyebrow && (
33+
<p
34+
style={{
35+
fontSize: '0.72rem',
36+
letterSpacing: '0.14em',
37+
textTransform: 'uppercase',
38+
color: 'var(--pts-accent)',
39+
fontWeight: 700,
40+
marginBottom: '0.75rem',
41+
}}
42+
>
43+
{hero.eyebrow}
44+
</p>
45+
)}
46+
{hero.subtitle && (
47+
<p
48+
style={{
49+
fontSize: '1.05rem',
50+
lineHeight: 1.65,
51+
color: 'rgba(255,255,255,0.86)',
52+
maxWidth: '44rem',
53+
fontWeight: 500,
54+
}}
55+
>
56+
{hero.subtitle}
57+
</p>
58+
)}
59+
{hero.tags && hero.tags.length > 0 && (
60+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.45rem', marginTop: '1.1rem' }}>
61+
{hero.tags.map((t) => (
62+
<span
63+
key={t}
64+
style={{
65+
fontSize: '0.68rem',
66+
letterSpacing: '0.06em',
67+
textTransform: 'uppercase',
68+
padding: '0.25rem 0.6rem',
69+
borderRadius: '999px',
70+
background: 'rgba(0, 223, 130, 0.12)',
71+
color: 'var(--pts-accent)',
72+
border: '1px solid rgba(0, 223, 130, 0.28)',
73+
}}
74+
>
75+
{t}
76+
</span>
77+
))}
78+
</div>
79+
)}
80+
</div>
81+
<div
82+
className="blog-article-shell-body"
83+
style={{
84+
padding: '0.25rem 0.15rem 0.5rem',
85+
}}
86+
>
87+
{children}
88+
</div>
89+
</div>
90+
);
91+
}
92+
93+
return (
94+
<div
95+
className="blog-article-shell blog-article-shell--default"
96+
style={{
97+
padding: '1.25rem 1.25rem 1.5rem',
98+
borderRadius: 'var(--pts-card-radius)',
99+
background: 'linear-gradient(180deg, var(--pts-tech-header) 0%, var(--pts-card-bg) 35%)',
100+
border: '1px solid var(--pts-border)',
101+
boxShadow: 'var(--pts-shadow-lp)',
102+
}}
103+
>
104+
{children}
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)