Skip to content

Commit 485273d

Browse files
authored
feat(blog): comprehensive SEO optimization for blog.multipost.app (#1)
Add complete SEO infrastructure to improve search engine visibility and social media sharing for the MultiPost Blog. Changes: - Add robots.txt with crawler rules and sitemap reference - Create custom OG images (1200x630) for social sharing - og-image.png: Blog branding with MultiPost logo - twitter-image.png: Twitter-optimized sharing image - Enhance global metadata in layout.tsx - Complete Open Graph configuration (zh_CN, en_US, ko_KR, pt_BR) - Twitter Card metadata - Rich keywords and descriptions - RSS feed link in alternates - Robot directives for better indexing - Optimize blog post metadata generation - Article-specific Open Graph type - Published time from frontmatter - Author information support - Keywords integration - Canonical URLs with language prefix - Add JSON-LD structured data (BlogPosting schema) - Headline, description, author, publisher - Publication dates - Main entity reference - Helps search engines understand article structure - Implement RSS feed at /rss.xml - Auto-generate from all blog posts - Sort by publish date (descending) - Include title, description, author, keywords - Multi-language support - 1-hour cache control SEO improvements: ✅ Search engine indexing (sitemap + robots.txt) ✅ Social media optimization (OG + Twitter + images) ✅ Structured data (JSON-LD BlogPosting) ✅ RSS subscription support ✅ Multi-language SEO ✅ Enhanced metadata configuration
1 parent 247b208 commit 485273d

File tree

6 files changed

+227
-18
lines changed

6 files changed

+227
-18
lines changed

app/[lang]/blog/[[...slug]]/page.tsx

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,55 @@ export default async function Page(props: {
1818

1919
const MDXContent = page.data.body;
2020

21+
const langPrefix = params.lang === 'en' ? '' : `/${params.lang}`;
22+
const url = `https://blog.multipost.app${langPrefix}/blog/${params.slug?.join('/') || ''}`;
23+
const publishedTime = page.data.date ? new Date(page.data.date).toISOString() : new Date().toISOString();
24+
25+
const jsonLd = {
26+
'@context': 'https://schema.org',
27+
'@type': 'BlogPosting',
28+
headline: page.data.title,
29+
description: page.data.description,
30+
image: 'https://blog.multipost.app/og-image.png',
31+
datePublished: publishedTime,
32+
dateModified: publishedTime,
33+
author: {
34+
'@type': 'Person',
35+
name: page.data.author || 'MultiPost Team',
36+
},
37+
publisher: {
38+
'@type': 'Organization',
39+
name: 'MultiPost',
40+
logo: {
41+
'@type': 'ImageObject',
42+
url: 'https://blog.multipost.app/og-image.png',
43+
},
44+
},
45+
mainEntityOfPage: {
46+
'@type': 'WebPage',
47+
'@id': url,
48+
},
49+
};
50+
2151
return (
22-
<DocsPage toc={page.data.toc} full={page.data.full}>
23-
<DocsTitle>{page.data.title}</DocsTitle>
24-
<DocsDescription>{page.data.description}</DocsDescription>
25-
<DocsBody>
26-
<MDXContent
27-
components={getMDXComponents({
28-
// this allows you to link to other pages with relative file paths
29-
a: createRelativeLink(source, page),
30-
})}
31-
/>
32-
</DocsBody>
33-
</DocsPage>
52+
<>
53+
<script
54+
type="application/ld+json"
55+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
56+
/>
57+
<DocsPage toc={page.data.toc} full={page.data.full}>
58+
<DocsTitle>{page.data.title}</DocsTitle>
59+
<DocsDescription>{page.data.description}</DocsDescription>
60+
<DocsBody>
61+
<MDXContent
62+
components={getMDXComponents({
63+
// this allows you to link to other pages with relative file paths
64+
a: createRelativeLink(source, page),
65+
})}
66+
/>
67+
</DocsBody>
68+
</DocsPage>
69+
</>
3470
);
3571
}
3672

@@ -45,8 +81,42 @@ export async function generateMetadata(props: {
4581
const page = source.getPage(params.slug, params.lang);
4682
if (!page) notFound();
4783

84+
const langPrefix = params.lang === 'en' ? '' : `/${params.lang}`;
85+
const url = `https://blog.multipost.app${langPrefix}/blog/${params.slug?.join('/') || ''}`;
86+
const publishedTime = page.data.date ? new Date(page.data.date).toISOString() : undefined;
87+
4888
return {
4989
title: page.data.title,
5090
description: page.data.description,
91+
keywords: page.data.keywords,
92+
authors: page.data.author ? [{ name: page.data.author }] : [{ name: 'MultiPost Team' }],
93+
openGraph: {
94+
type: 'article',
95+
locale: params.lang === 'zh-Hans' ? 'zh_CN' : 'en_US',
96+
url: url,
97+
title: page.data.title,
98+
description: page.data.description,
99+
siteName: 'MultiPost Blog',
100+
images: [
101+
{
102+
url: '/og-image.png',
103+
width: 1200,
104+
height: 630,
105+
alt: page.data.title,
106+
},
107+
],
108+
publishedTime,
109+
authors: page.data.author ? [page.data.author] : ['MultiPost Team'],
110+
},
111+
twitter: {
112+
card: 'summary_large_image',
113+
title: page.data.title,
114+
description: page.data.description,
115+
images: ['/twitter-image.png'],
116+
creator: '@multipost_app',
117+
},
118+
alternates: {
119+
canonical: url,
120+
},
51121
};
52122
}

app/layout.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,76 @@ import type { ReactNode } from 'react';
33
import type { Metadata } from 'next';
44

55
export const metadata: Metadata = {
6-
title: 'MultiPost Blog',
7-
keywords: ['blog', 'nextjs', 'react', 'typescript', 'multilingual'],
8-
authors: [{ name: 'Leaperone' }],
9-
creator: 'Leaperone',
6+
metadataBase: new URL('https://blog.multipost.app'),
7+
title: {
8+
default: 'MultiPost Blog - 多平台内容发布技术分享',
9+
template: '%s | MultiPost Blog',
10+
},
11+
description:
12+
'MultiPost 官方博客,分享多平台内容发布技术、社交媒体运营经验、产品动态和最佳实践。帮助内容创作者提升工作效率,实现一键多平台发布。',
13+
keywords: [
14+
'MultiPost',
15+
'多平台发布',
16+
'内容分发',
17+
'社交媒体运营',
18+
'浏览器扩展',
19+
'技术博客',
20+
'一键发布',
21+
'效率工具',
22+
'multi-platform publishing',
23+
'content distribution',
24+
'social media management',
25+
'browser extension',
26+
],
27+
authors: [{ name: 'MultiPost Team', url: 'https://blog.multipost.app' }],
28+
creator: 'MultiPost',
29+
publisher: 'MultiPost',
30+
formatDetection: {
31+
email: false,
32+
address: false,
33+
telephone: false,
34+
},
1035
openGraph: {
1136
type: 'website',
12-
locale: 'en_US',
37+
locale: 'zh_CN',
38+
alternateLocale: ['en_US', 'ko_KR', 'pt_BR'],
1339
url: 'https://blog.multipost.app',
14-
title: 'MultiPost Blog',
40+
title: 'MultiPost Blog - 多平台内容发布技术分享',
41+
description: '分享多平台内容发布技术、社交媒体运营经验和产品动态',
1542
siteName: 'MultiPost Blog',
43+
images: [
44+
{
45+
url: '/og-image.png',
46+
width: 1200,
47+
height: 630,
48+
alt: 'MultiPost Blog',
49+
},
50+
],
51+
},
52+
twitter: {
53+
card: 'summary_large_image',
54+
title: 'MultiPost Blog - 多平台内容发布技术分享',
55+
description: '分享多平台内容发布技术、社交媒体运营经验和产品动态',
56+
images: ['/twitter-image.png'],
57+
creator: '@multipost_app',
58+
},
59+
alternates: {
60+
canonical: 'https://blog.multipost.app',
61+
types: {
62+
'application/rss+xml': 'https://blog.multipost.app/rss.xml',
63+
},
1664
},
1765
robots: {
1866
index: true,
1967
follow: true,
68+
nocache: false,
69+
googleBot: {
70+
index: true,
71+
follow: true,
72+
'max-video-preview': -1,
73+
'max-image-preview': 'large',
74+
'max-snippet': -1,
75+
},
2076
},
2177
};
2278

app/robots.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { MetadataRoute } from 'next';
2+
3+
export default function robots(): MetadataRoute.Robots {
4+
const baseUrl = 'https://blog.multipost.app';
5+
6+
return {
7+
rules: [
8+
{
9+
userAgent: '*',
10+
allow: '/',
11+
disallow: ['/api/'],
12+
},
13+
{
14+
userAgent: 'Googlebot',
15+
allow: '/',
16+
disallow: ['/api/'],
17+
},
18+
{
19+
userAgent: 'Baiduspider',
20+
allow: '/',
21+
disallow: ['/api/'],
22+
},
23+
],
24+
sitemap: `${baseUrl}/sitemap.xml`,
25+
};
26+
}

app/rss.xml/route.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { source } from '@/lib/source';
2+
3+
export async function GET() {
4+
const baseUrl = 'https://blog.multipost.app';
5+
const pages = source.getPages();
6+
7+
const items = pages
8+
.filter((page) => page.data.date) // Only include pages with dates
9+
.sort((a, b) => {
10+
const dateA = a.data.date ? new Date(a.data.date).getTime() : 0;
11+
const dateB = b.data.date ? new Date(b.data.date).getTime() : 0;
12+
return dateB - dateA; // Sort by date descending
13+
})
14+
.map((page) => {
15+
const langPrefix = page.locale === 'en' ? '' : `/${page.locale}`;
16+
const url = `${baseUrl}${langPrefix}/blog/${page.slugs.join('/')}`;
17+
const pubDate = page.data.date
18+
? new Date(page.data.date).toUTCString()
19+
: new Date().toUTCString();
20+
21+
return `
22+
<item>
23+
<title><![CDATA[${page.data.title}]]></title>
24+
<link>${url}</link>
25+
<guid>${url}</guid>
26+
<description><![CDATA[${page.data.description || ''}]]></description>
27+
<pubDate>${pubDate}</pubDate>
28+
${page.data.author ? `<author><![CDATA[${page.data.author}]]></author>` : ''}
29+
${page.data.keywords ? page.data.keywords.map((keyword: string) => `<category><![CDATA[${keyword}]]></category>`).join('\n ') : ''}
30+
</item>`;
31+
})
32+
.join('\n');
33+
34+
const rss = `<?xml version="1.0" encoding="UTF-8"?>
35+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
36+
<channel>
37+
<title>MultiPost Blog - 多平台内容发布技术分享</title>
38+
<link>${baseUrl}</link>
39+
<description>分享多平台内容发布技术、社交媒体运营经验和产品动态</description>
40+
<language>zh-CN</language>
41+
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
42+
<atom:link href="${baseUrl}/rss.xml" rel="self" type="application/rss+xml"/>
43+
<image>
44+
<url>${baseUrl}/og-image.png</url>
45+
<title>MultiPost Blog</title>
46+
<link>${baseUrl}</link>
47+
</image>${items}
48+
</channel>
49+
</rss>`;
50+
51+
return new Response(rss, {
52+
headers: {
53+
'Content-Type': 'application/xml; charset=utf-8',
54+
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
55+
},
56+
});
57+
}

public/og-image.png

317 KB
Loading

public/twitter-image.png

317 KB
Loading

0 commit comments

Comments
 (0)