Skip to content

Commit 6749cfc

Browse files
gmoonclaude
andcommitted
Fix critical SEO issues: OG image, meta alignment, privacy policy
- Convert OG image from SVG to PNG (social platforms don't render SVG) - Update OG SVG tagline to match new copy - Align pre-rendered titles and meta descriptions with React components - Update noscript fallback to match new homepage copy - Add privacy policy page with route, prerender, and noscript fallback - Add privacy link to Footer component (new `links` prop) - Add `dateModified` and `author.url` to BlogPosting JSON-LD schema - Add `<lastmod>` to all sitemap URLs - Update Organization schema description Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e7fc396 commit 6749cfc

11 files changed

Lines changed: 207 additions & 39 deletions

File tree

packages/ui/src/components/Footer.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface FooterProps {
55
repoUrl?: string
66
repoLabel?: string
77
orgName?: string
8+
links?: { label: string; href: string }[]
89
}
910

1011
const styles = {
@@ -23,7 +24,7 @@ const styles = {
2324
},
2425
}
2526

26-
export function Footer({ repoUrl, repoLabel, orgName }: FooterProps) {
27+
export function Footer({ repoUrl, repoLabel, orgName, links }: FooterProps) {
2728
injectGlobalStyles()
2829
return (
2930
<footer style={styles.footer} data-fzui>
@@ -41,6 +42,18 @@ export function Footer({ repoUrl, repoLabel, orgName }: FooterProps) {
4142
<>Built with {repoLabel ?? 'Lattice'}.</>
4243
)}
4344
</p>
45+
{links && links.length > 0 && (
46+
<p>
47+
{links.map((link, i) => (
48+
<span key={link.href}>
49+
{i > 0 && ' · '}
50+
<a href={link.href} style={styles.link}>
51+
{link.label}
52+
</a>
53+
</span>
54+
))}
55+
</p>
56+
)}
4457
</footer>
4558
)
4659
}

public/og-default.png

27 KB
Loading

public/og-default.svg

Lines changed: 1 addition & 1 deletion
Loading

scripts/prerender.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('pre-rendered homepage', () => {
1818

1919
it('has correct title', () => {
2020
const html = readFileSync(file, 'utf-8')
21-
expect(html).toContain('<title>Forkzero — Knowledge Coordination for AI-Native Teams</title>')
21+
expect(html).toContain('<title>Lattice by Forkzero — Knowledge Graph for AI-Native Teams</title>')
2222
})
2323

2424
it('has meta description', () => {
@@ -72,7 +72,7 @@ describe('pre-rendered getting-started', () => {
7272

7373
it('has correct title', () => {
7474
const html = readFileSync(file, 'utf-8')
75-
expect(html).toContain('<title>Getting Started — Forkzero</title>')
75+
expect(html).toContain('<title>Get Started with Lattice — Forkzero</title>')
7676
})
7777

7878
it('has meta description', () => {

scripts/prerender.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,22 @@ const routes: RouteMeta[] = [
3838
})),
3939
{
4040
path: '/getting-started',
41-
title: 'Getting Started — Forkzero',
41+
title: 'Get Started with Lattice — Forkzero',
4242
description: 'Install Lattice and start building a knowledge-coordinated codebase in under five minutes.',
4343
canonical: 'https://forkzero.ai/getting-started',
4444
},
45+
{
46+
path: '/privacy',
47+
title: 'Privacy Policy — Forkzero',
48+
description: 'Forkzero privacy policy. How we handle your data.',
49+
canonical: 'https://forkzero.ai/privacy',
50+
},
4551
// Homepage last — it overwrites dist/index.html (template is already in memory)
4652
{
4753
path: '/',
48-
title: 'Forkzero — Knowledge Coordination for AI-Native Teams',
54+
title: 'Lattice by Forkzero — Knowledge Graph for AI-Native Teams',
4955
description:
50-
'Forkzero builds developer tools that connect research, strategy, requirements, and implementation into a traversable knowledge graph.',
56+
'Lattice captures the research, decisions, and requirements behind your code in a Git-native knowledge graph. Any collaborator — human or AI — picks up where the last one left off.',
5157
canonical: 'https://forkzero.ai/',
5258
},
5359
]
@@ -73,12 +79,12 @@ for (const route of routes) {
7379
`<meta property="og:description" content="${escapeAttr(route.description)}" />`,
7480
`<meta property="og:type" content="${ogType}" />`,
7581
`<meta property="og:url" content="${route.canonical}" />`,
76-
`<meta property="og:image" content="https://forkzero.ai/og-default.svg" />`,
82+
`<meta property="og:image" content="https://forkzero.ai/og-default.png" />`,
7783
`<meta property="og:site_name" content="Forkzero" />`,
7884
`<meta name="twitter:card" content="summary_large_image" />`,
7985
`<meta name="twitter:title" content="${escapeAttr(route.title)}" />`,
8086
`<meta name="twitter:description" content="${escapeAttr(route.description)}" />`,
81-
`<meta name="twitter:image" content="https://forkzero.ai/og-default.svg" />`,
87+
`<meta name="twitter:image" content="https://forkzero.ai/og-default.png" />`,
8288
`<link rel="canonical" href="${route.canonical}" />`,
8389
].join('\n ')
8490

@@ -108,10 +114,12 @@ for (const route of routes) {
108114

109115
// --- Generate sitemap.xml ---
110116
// Note: <changefreq> and <priority> are ignored by Google and omitted per best practice.
111-
const sitemapUrls: { loc: string; lastmod?: string }[] = [
112-
{ loc: 'https://forkzero.ai/' },
113-
{ loc: 'https://forkzero.ai/getting-started' },
114-
{ loc: 'https://forkzero.ai/blog' },
117+
const today = new Date().toISOString().split('T')[0]
118+
const latestPostDate = blogPosts.length > 0 ? blogPosts[0].date : today
119+
const sitemapUrls: { loc: string; lastmod: string }[] = [
120+
{ loc: 'https://forkzero.ai/', lastmod: today },
121+
{ loc: 'https://forkzero.ai/getting-started', lastmod: today },
122+
{ loc: 'https://forkzero.ai/blog', lastmod: latestPostDate },
115123
...blogPosts.map((post) => ({
116124
loc: `https://forkzero.ai/blog/${post.slug}`,
117125
lastmod: post.date,
@@ -121,10 +129,7 @@ const sitemapUrls: { loc: string; lastmod?: string }[] = [
121129
const sitemapXml = [
122130
'<?xml version="1.0" encoding="UTF-8"?>',
123131
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
124-
...sitemapUrls.map((u) => {
125-
const lastmod = u.lastmod ? `\n <lastmod>${u.lastmod}</lastmod>` : ''
126-
return ` <url>\n <loc>${u.loc}</loc>${lastmod}\n </url>`
127-
}),
132+
...sitemapUrls.map((u) => ` <url>\n <loc>${u.loc}</loc>\n <lastmod>${u.lastmod}</lastmod>\n </url>`),
128133
'</urlset>',
129134
'',
130135
].join('\n')
@@ -144,13 +149,13 @@ function buildNoscript(route: RouteMeta): string | null {
144149
.join('\n')
145150
return wrap(
146151
[
147-
`<h1>Forkzero — Knowledge Coordination for AI-Native Teams</h1>`,
148-
`<p>Connect research, strategy, requirements, and implementation into a traversable knowledge graph.</p>`,
149-
`<h2>Why Forkzero</h2>`,
152+
`<h1>Your agent writes the code. Who remembers why?</h1>`,
153+
`<p>The research, reasoning, and requirements behind your code vanish into chat logs. Lattice captures them in a Git-native knowledge graph — so any collaborator, human or AI, can pick up where the last one left off.</p>`,
154+
`<h2>Why Lattice?</h2>`,
150155
`<ul>`,
151-
`<li>Every decision traces back to its source</li>`,
152-
`<li>Agents and humans share the same knowledge layer</li>`,
153-
`<li>Context stays structured, not scattered</li>`,
156+
`<li>Stop losing the &quot;why&quot; — every requirement traces back to the research that motivated it</li>`,
157+
`<li>Requirements before code, not after — give your agent a spec to implement</li>`,
158+
`<li>Onboard anyone in minutes — human or AI</li>`,
154159
`</ul>`,
155160
`<h2>Projects</h2>`,
156161
`<ul>${projectList}</ul>`,
@@ -181,6 +186,16 @@ function buildNoscript(route: RouteMeta): string | null {
181186
return wrap(`<h1>Blog</h1>${listHtml}`)
182187
}
183188

189+
if (route.path === '/privacy') {
190+
return wrap(
191+
[
192+
`<h1>Privacy Policy</h1>`,
193+
`<p>If you subscribe to our mailing list, we collect your email address. We do not use cookies, tracking pixels, or third-party analytics. Lattice is open-source software that runs entirely on your machine and does not collect telemetry.</p>`,
194+
`<p>Contact: privacy@forkzero.ai</p>`,
195+
].join(''),
196+
)
197+
}
198+
184199
// Blog post
185200
const post = blogPosts.find((p) => `/blog/${p.slug}` === route.path)
186201
if (post) {
@@ -214,7 +229,7 @@ function buildJsonLd(route: RouteMeta): string {
214229
},
215230
sameAs: ['https://github.com/forkzero'],
216231
description:
217-
'Forkzero builds developer tools that connect research, strategy, requirements, and implementation into a traversable knowledge graph.',
232+
'Forkzero builds developer tools for AI-native teams. Lattice captures research, decisions, and requirements in a Git-native knowledge graph.',
218233
})
219234
schemas.push({
220235
'@context': 'https://schema.org',
@@ -266,10 +281,12 @@ function buildJsonLd(route: RouteMeta): string {
266281
headline: post.title,
267282
description: post.excerpt,
268283
datePublished: post.date,
269-
image: 'https://forkzero.ai/og-default.svg',
284+
dateModified: post.date,
285+
image: 'https://forkzero.ai/og-default.png',
270286
author: {
271287
'@type': 'Person',
272288
name: post.author.name,
289+
...(post.author.github ? { url: post.author.github } : {}),
273290
},
274291
publisher: {
275292
'@type': 'Organization',
@@ -322,6 +339,24 @@ function buildJsonLd(route: RouteMeta): string {
322339
})
323340
}
324341

342+
if (route.path === '/privacy') {
343+
schemas.push({
344+
'@context': 'https://schema.org',
345+
'@type': 'WebPage',
346+
name: 'Privacy Policy — Forkzero',
347+
description: route.description,
348+
url: route.canonical,
349+
})
350+
schemas.push({
351+
'@context': 'https://schema.org',
352+
'@type': 'BreadcrumbList',
353+
itemListElement: [
354+
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://forkzero.ai/' },
355+
{ '@type': 'ListItem', position: 2, name: 'Privacy Policy', item: 'https://forkzero.ai/privacy' },
356+
],
357+
})
358+
}
359+
325360
if (schemas.length === 0) return ''
326361

327362
return schemas.map((s) => `<script type="application/ld+json">${JSON.stringify(s)}</script>`).join('\n ')

src/App.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ describe('parseRoute', () => {
3535
expect(parseRoute('/design-system/')).toEqual({ page: 'design-system' })
3636
})
3737

38+
it('returns privacy for /privacy', () => {
39+
expect(parseRoute('/privacy')).toEqual({ page: 'privacy' })
40+
})
41+
42+
it('returns privacy for /privacy/ with trailing slash', () => {
43+
expect(parseRoute('/privacy/')).toEqual({ page: 'privacy' })
44+
})
45+
3846
it('returns blog listing for /blog', () => {
3947
expect(parseRoute('/blog')).toEqual({ page: 'blog' })
4048
})

src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { ReaderPage } from './pages/ReaderPage'
33
import { BlogPage } from './pages/BlogPage'
44
import { GettingStartedPage } from './pages/GettingStartedPage'
55
import { DesignSystemPage } from './pages/DesignSystemPage'
6+
import { PrivacyPage } from './pages/PrivacyPage'
67

7-
type PageRoute = 'home' | 'reader' | 'getting-started' | 'blog' | 'blog-post' | 'design-system'
8+
type PageRoute = 'home' | 'reader' | 'getting-started' | 'blog' | 'blog-post' | 'design-system' | 'privacy'
89

910
export function parseRoute(path: string): { page: PageRoute; slug?: string } {
1011
if (path === '/reader' || path === '/reader/') return { page: 'reader' }
1112
if (path === '/getting-started' || path === '/getting-started/') return { page: 'getting-started' }
1213
if (path === '/design-system' || path === '/design-system/') return { page: 'design-system' }
14+
if (path === '/privacy' || path === '/privacy/') return { page: 'privacy' }
1315
if (path === '/blog' || path === '/blog/') return { page: 'blog' }
1416
if (path.startsWith('/blog/')) {
1517
const slug = path.replace(/^\/blog\//, '').replace(/\/$/, '')
@@ -28,6 +30,8 @@ export function App() {
2830
return <GettingStartedPage />
2931
case 'design-system':
3032
return <DesignSystemPage />
33+
case 'privacy':
34+
return <PrivacyPage />
3135
case 'blog':
3236
return <BlogPage />
3337
case 'blog-post':

src/pages/BlogPage.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -731,15 +731,15 @@ function BlogListing() {
731731
)
732732
setOgTag('og:type', 'website')
733733
setOgTag('og:url', 'https://forkzero.ai/blog')
734-
setOgTag('og:image', 'https://forkzero.ai/og-default.svg')
734+
setOgTag('og:image', 'https://forkzero.ai/og-default.png')
735735
setOgTag('og:site_name', 'Forkzero')
736736
setMetaTag('twitter:card', 'summary_large_image')
737737
setMetaTag('twitter:title', 'Blog \u2014 Forkzero')
738738
setMetaTag(
739739
'twitter:description',
740740
'Technical writing on knowledge coordination, context engineering, and AI-first developer tooling.',
741741
)
742-
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.svg')
742+
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.png')
743743
setCanonical('https://forkzero.ai/blog')
744744
}, [])
745745

@@ -764,7 +764,7 @@ function BlogListing() {
764764
</a>
765765
))}
766766
</div>
767-
<Footer repoUrl={GITHUB_REPO_URL} />
767+
<Footer repoUrl={GITHUB_REPO_URL} links={[{ label: 'Privacy', href: '/privacy' }]} />
768768
</div>
769769
)
770770
}
@@ -779,12 +779,12 @@ function BlogPostView({ post }: { post: BlogPost }) {
779779
setOgTag('og:description', post.excerpt)
780780
setOgTag('og:type', 'article')
781781
setOgTag('og:url', `https://forkzero.ai/blog/${post.slug}`)
782-
setOgTag('og:image', 'https://forkzero.ai/og-default.svg')
782+
setOgTag('og:image', 'https://forkzero.ai/og-default.png')
783783
setOgTag('og:site_name', 'Forkzero')
784784
setMetaTag('twitter:card', 'summary_large_image')
785785
setMetaTag('twitter:title', post.title)
786786
setMetaTag('twitter:description', post.excerpt)
787-
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.svg')
787+
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.png')
788788
setCanonical(`https://forkzero.ai/blog/${post.slug}`)
789789
}, [post])
790790

@@ -813,7 +813,7 @@ function BlogPostView({ post }: { post: BlogPost }) {
813813
<BlogComments slug={post.slug} />
814814
</div>
815815
</div>
816-
<Footer repoUrl={GITHUB_REPO_URL} />
816+
<Footer repoUrl={GITHUB_REPO_URL} links={[{ label: 'Privacy', href: '/privacy' }]} />
817817
</div>
818818
)
819819
}
@@ -833,7 +833,7 @@ function BlogNotFound() {
833833
Back to blog
834834
</a>
835835
</div>
836-
<Footer repoUrl={GITHUB_REPO_URL} />
836+
<Footer repoUrl={GITHUB_REPO_URL} links={[{ label: 'Privacy', href: '/privacy' }]} />
837837
</div>
838838
)
839839
}

src/pages/GettingStartedPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,15 +460,15 @@ export function GettingStartedPage() {
460460
)
461461
setOgTag('og:type', 'website')
462462
setOgTag('og:url', 'https://forkzero.ai/getting-started')
463-
setOgTag('og:image', 'https://forkzero.ai/og-default.svg')
463+
setOgTag('og:image', 'https://forkzero.ai/og-default.png')
464464
setOgTag('og:site_name', 'Forkzero')
465465
setMetaTag('twitter:card', 'summary_large_image')
466466
setMetaTag('twitter:title', 'Get Started with Lattice — Forkzero')
467467
setMetaTag(
468468
'twitter:description',
469469
'Install Lattice and start building a knowledge-coordinated codebase in under five minutes.',
470470
)
471-
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.svg')
471+
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.png')
472472
}, [])
473473

474474
return (
@@ -773,7 +773,7 @@ lattice drift`}
773773
</section>
774774
</div>
775775

776-
<Footer repoUrl={GITHUB_REPO_URL} />
776+
<Footer repoUrl={GITHUB_REPO_URL} links={[{ label: 'Privacy', href: '/privacy' }]} />
777777
</div>
778778
)
779779
}

src/pages/HomePage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,12 +856,12 @@ export function HomePage() {
856856
setOgTag('og:description', metaDescription)
857857
setOgTag('og:type', 'website')
858858
setOgTag('og:url', 'https://forkzero.ai/')
859-
setOgTag('og:image', 'https://forkzero.ai/og-default.svg')
859+
setOgTag('og:image', 'https://forkzero.ai/og-default.png')
860860
setOgTag('og:site_name', 'Forkzero')
861861
setMetaTag('twitter:card', 'summary_large_image')
862862
setMetaTag('twitter:title', metaTitle)
863863
setMetaTag('twitter:description', metaDescription)
864-
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.svg')
864+
setMetaTag('twitter:image', 'https://forkzero.ai/og-default.png')
865865
}, [])
866866

867867
return (
@@ -876,7 +876,7 @@ export function HomePage() {
876876
<EmailCapture />
877877
<FeaturedArticle />
878878
<Projects />
879-
<Footer repoUrl={GITHUB_REPO_URL} />
879+
<Footer repoUrl={GITHUB_REPO_URL} links={[{ label: 'Privacy', href: '/privacy' }]} />
880880
</>
881881
)
882882
}

0 commit comments

Comments
 (0)