Skip to content
Merged
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
23 changes: 23 additions & 0 deletions web/app/file/og.png/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ImageResponse } from 'next/og';

import { loadBrandFonts, OG_SIZE, RelayfileVariant } from '../../../lib/og/template';

export const runtime = 'nodejs';
// Prerender at build time — the Relayfile card never changes between deploys.
export const dynamic = 'force-static';

/**
* The Relayfile landing page's Open Graph card: the same composition as the
* homepage and Pear cards (left copy column, a panel bled into the bottom-right)
* with the page's own badge, headline, and subtitle beside the "relayfile
* workspace" tree from its hero. Served as a `.png` route handler so the URL
* ends in a real image extension.
*/
export async function GET() {
const { fonts, headingFamily, bodyFamily } = await loadBrandFonts();

return new ImageResponse(<RelayfileVariant headingFamily={headingFamily} bodyFamily={bodyFamily} />, {
...OG_SIZE,
...(fonts.length > 0 ? { fonts } : {}),
});
}
31 changes: 25 additions & 6 deletions web/app/file/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import type { Metadata } from 'next';

import { GitHubStarsBadge } from '../../components/GitHubStars';
import { relayfileMetadata, RELAYFILE_PATH } from '../../lib/relayfile-meta';
import { absoluteUrl } from '../../lib/site';

import { RelayfileContent } from './RelayfileContent';

export const metadata: Metadata = {
title: 'Relayfile | Integration filesystem for AI agents',
description:
'Mount SaaS integrations as files so agents can read, write, watch, and coordinate through one realtime workspace.',
};
export const metadata: Metadata = relayfileMetadata(RELAYFILE_PATH);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The openGraph.title for /file won't carry the '| Agent Relay' brand suffix, while the browser <title> does (via the layout's '%s | Agent Relay' template, which doesn't apply to openGraph). Social/OG shares will show the un-branded title 'Relayfile — Integration filesystem for AI agents', diverging from the stated PR title and from /enterprise, whose openGraph.title includes the brand. Consider appending the brand to openGraph.title in relayfile-meta rather than relying on the template.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/app/file/page.tsx, line 9:

<comment>The openGraph.title for /file won't carry the '| Agent Relay' brand suffix, while the browser <title> does (via the layout's '%s | Agent Relay' template, which doesn't apply to openGraph). Social/OG shares will show the un-branded title 'Relayfile — Integration filesystem for AI agents', diverging from the stated PR title and from /enterprise, whose openGraph.title includes the brand. Consider appending the brand to openGraph.title in relayfile-meta rather than relying on the template.</comment>

<file context>
@@ -1,15 +1,34 @@
-  description:
-    'Mount SaaS integrations as files so agents can read, write, watch, and coordinate through one realtime workspace.',
-};
+export const metadata: Metadata = relayfileMetadata(RELAYFILE_PATH);
 
 export default function FilePage() {
</file context>


export default function FilePage() {
return <RelayfileContent navActions={<GitHubStarsBadge />} />;
const structuredData = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Relayfile',
applicationCategory: 'DeveloperApplication',
description:
'An integration filesystem for AI agents. Mount Linear, GitHub, Notion, Slack, and other SaaS systems as a realtime filesystem that agents read with bash and write back to by saving files.',
url: absoluteUrl(RELAYFILE_PATH),
operatingSystem: 'macOS, Linux',
provider: {
'@type': 'Organization',
name: 'Agent Relay',
url: absoluteUrl('/'),
},
};

return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
<RelayfileContent navActions={<GitHubStarsBadge />} />
</>
);
}
6 changes: 6 additions & 0 deletions web/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: absoluteUrl('/file'),
lastModified: now,
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: absoluteUrl('/skill'),
lastModified: now,
Expand Down
266 changes: 266 additions & 0 deletions web/lib/og/template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1149,3 +1149,269 @@ export function BlogVariant({
</Frame>
);
}

// ───────────────────────────────────────────────────────────────────────────
// Variant: RELAYFILE — left copy column + the workspace tree window pinned into
// the bottom-right corner.
//
// Same composition as the landing and Pear cards (left copy, a panel bled off
// the right + bottom edges) but the panel is a synthetic Relayfile mount: the
// "relayfile workspace" window from the /file hero, showing provider directories
// and files with the WATCH/WRITE/READ activity labels from the hero's rail.
// ───────────────────────────────────────────────────────────────────────────

/** Colors for a tree row's trailing activity label. */
const ACTIVITY_COLOR = {
read: PALETTE.primary,
write: PALETTE.green,
watch: PALETTE.primaryHover,
} as const;

type TreeRow = {
label: string;
/** Directories render a folder glyph and brighter text. */
dir?: boolean;
/** Nested one level under the preceding directory. */
indent?: boolean;
activity?: keyof typeof ACTIVITY_COLOR;
};

/**
* The workspace tree shown on the card: a provider mount with the deterministic
* digest at the top, mirroring the paths used across the /file page copy.
*/
const RELAYFILE_TREE: TreeRow[] = [
{ label: 'LAYOUT.md' },
{ label: 'digests', dir: true },
{ label: 'yesterday.md', indent: true, activity: 'read' },
{ label: 'linear/issues', dir: true },
{ label: 'AGE-12.json', indent: true, activity: 'write' },
{ label: 'github/prs', dir: true, activity: 'watch' },
{ label: '4821.json', indent: true },
{ label: 'notion/pages', dir: true },
{ label: 'roadmap.md', indent: true },
];

function FolderGlyph({ size = 22 }: { size?: number }): ReactElement {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ display: 'flex' }}>
<path
d="M3 6.6C3 5.7 3.7 5 4.6 5h4.1l2 2.2h8.7c.9 0 1.6.7 1.6 1.6v9.6c0 .9-.7 1.6-1.6 1.6H4.6c-.9 0-1.6-.7-1.6-1.6z"
fill={PALETTE.primary}
fillOpacity="0.85"
/>
</svg>
);
}

function FileGlyph({ size = 22 }: { size?: number }): ReactElement {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ display: 'flex' }}>
<path d="M6 3.6h7.2L19 9.4v11c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1V4.6c0-.6.4-1 1-1z" fill={PALETTE.faint} />
<path d="M13.2 3.6 19 9.4h-5.8z" fill={PALETTE.fg} fillOpacity="0.55" />
</svg>
);
}

/**
* The "relayfile workspace" window. Anchored to its cropping wrapper's top-left
* and rendered slightly larger than it, so the card bleeds off the canvas's
* right + bottom edges and the tree reads as continuing below the fold.
*/
function WorkspacePanel({ headingFamily, scale = 1 }: { headingFamily: string; scale?: number }): ReactElement {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
position: 'absolute',
left: 0,
top: 0,
width: 640 * scale,
height: 600 * scale,
background: PALETTE.surface,
border: `2px solid ${PALETTE.line}`,
borderRadius: `${18 * scale}px 0 0 0`,
overflow: 'hidden',
boxShadow: '-18px 18px 48px rgba(0,0,0,0.5)',
}}
>
{/* Title bar: traffic lights + the window title from the live hero. */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 14 * scale,
height: 52 * scale,
flexShrink: 0,
padding: `0 ${24 * scale}px`,
borderBottom: `1px solid ${PALETTE.line}`,
background: 'rgba(116,184,226,0.05)',
}}
>
<div style={{ display: 'flex', gap: 7 * scale }}>
<div style={{ display: 'flex', width: 11 * scale, height: 11 * scale, borderRadius: 999, background: '#ff5f57' }} />
<div style={{ display: 'flex', width: 11 * scale, height: 11 * scale, borderRadius: 999, background: '#febc2e' }} />
<div style={{ display: 'flex', width: 11 * scale, height: 11 * scale, borderRadius: 999, background: PALETTE.green }} />
</div>
<span
style={{
display: 'flex',
fontFamily: headingFamily,
fontSize: 17 * scale,
fontWeight: 700,
letterSpacing: '-0.01em',
color: PALETTE.muted,
}}
>
relayfile workspace
</span>
</div>

{/* The mounted tree. */}
<div style={{ display: 'flex', flexDirection: 'column', padding: `${18 * scale}px 0` }}>
{RELAYFILE_TREE.map((row) => (
<div
key={row.label}
style={{
display: 'flex',
alignItems: 'center',
gap: 12 * scale,
height: 55 * scale,
flexShrink: 0,
padding: `0 ${26 * scale}px 0 ${(row.indent ? 62 : 26) * scale}px`,
}}
>
{row.dir ? <FolderGlyph size={22 * scale} /> : <FileGlyph size={22 * scale} />}
<span
style={{
display: 'flex',
fontSize: 21 * scale,
fontWeight: row.dir ? 500 : 400,
color: row.dir ? PALETTE.fg : PALETTE.muted,
}}
>
{row.dir ? `${row.label}/` : row.label}
</span>
{row.activity ? (
<span
style={{
display: 'flex',
marginLeft: 'auto',
padding: `${4 * scale}px ${10 * scale}px`,
borderRadius: 6 * scale,
background: 'rgba(116,184,226,0.10)',
fontSize: 14 * scale,
fontWeight: 500,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: ACTIVITY_COLOR[row.activity],
}}
>
{row.activity}
</span>
) : null}
</div>
))}
</div>
</div>
);
}

export function RelayfileVariant({
headingFamily,
bodyFamily,
}: {
headingFamily: string;
bodyFamily: string;
}): ReactElement {
return (
<Frame bodyFamily={bodyFamily}>
<SwoopLines top={430} opacity={0.16} />

{/* LEFT: copy, mirroring the /file hero badge, headline, and subtitle. */}
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: 620,
flexShrink: 0,
padding: '0 0 0 80px',
position: 'relative',
}}
>
<div style={{ display: 'flex', marginBottom: 34 }}>
<BrandLockup scale={1.25} />
</div>

<div
style={{
display: 'flex',
alignSelf: 'flex-start',
padding: '7px 15px',
borderRadius: 999,
background: 'rgba(116,184,226,0.12)',
border: `1px solid ${PALETTE.line}`,
color: PALETTE.primaryHover,
fontSize: 15,
fontWeight: 500,
letterSpacing: '0.14em',
textTransform: 'uppercase',
marginBottom: 22,
}}
>
Integration filesystem
</div>

<div
style={{
display: 'flex',
flexDirection: 'column',
fontFamily: headingFamily,
fontWeight: 800,
fontSize: 60,
lineHeight: 1.0,
letterSpacing: '-0.04em',
color: PALETTE.fg,
}}
>
<span style={{ display: 'flex', fontWeight: 800 }}>Relayfile</span>
<span style={{ display: 'flex', fontWeight: 800 }}>
<span style={{ display: 'flex', fontWeight: 800 }}>gives agents&nbsp;</span>
<span style={{ display: 'flex', fontWeight: 800, color: PALETTE.primary }}>files</span>
</span>
</div>

<div
style={{
display: 'flex',
fontSize: 22,
lineHeight: 1.5,
color: PALETTE.muted,
marginTop: 24,
maxWidth: 490,
}}
>
Mount Linear, GitHub, Notion, Slack, and your own systems as a realtime filesystem that agents can
read, write, and watch.
</div>
</div>

{/* RIGHT: the workspace window bled off the right + bottom edges. */}
<div
style={{
display: 'flex',
position: 'absolute',
right: 0,
bottom: 0,
width: 578,
height: 540,
overflow: 'hidden',
}}
>
<WorkspacePanel headingFamily={headingFamily} scale={0.92} />
</div>
</Frame>
);
}
Loading
Loading