Skip to content

Commit b3b2c7e

Browse files
authored
Merge pull request #28 from maboudharam/feat/opengraph-meta-tags
feat: add OpenGraph meta tags configuration in settings
2 parents e9ab1f1 + fc4862b commit b3b2c7e

File tree

2 files changed

+293
-1
lines changed

2 files changed

+293
-1
lines changed

components/SettingsModal.tsx

Lines changed: 281 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
AlertCircle,
1515
Loader2,
1616
Database,
17+
Globe,
1718
} from 'lucide-react';
1819
import type { SocialPlatform, UserProfile, BlockData } from '../types';
1920
import { AVATAR_PLACEHOLDER } from '../constants';
@@ -37,7 +38,7 @@ type SettingsModalProps = {
3738
onBlocksChange?: (blocks: BlockData[]) => void;
3839
};
3940

40-
type TabType = 'general' | 'social' | 'analytics' | 'json';
41+
type TabType = 'general' | 'social' | 'seo' | 'analytics' | 'json';
4142

4243
const SettingsModal: React.FC<SettingsModalProps> = ({
4344
isOpen,
@@ -256,6 +257,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
256257
const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
257258
{ id: 'general', label: 'General', icon: <User size={16} /> },
258259
{ id: 'social', label: 'Social', icon: <Share2 size={16} /> },
260+
{ id: 'seo', label: 'SEO & Social Sharing', icon: <Globe size={16} /> },
259261
{ id: 'analytics', label: 'Analytics', icon: <BarChart3 size={16} /> },
260262
{ id: 'json', label: 'Raw JSON', icon: <Code size={16} /> },
261263
];
@@ -902,6 +904,284 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
902904
</section>
903905
)}
904906

907+
{/* SEO TAB */}
908+
{activeTab === 'seo' && (
909+
<section className="space-y-6">
910+
<div>
911+
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">
912+
OpenGraph & Social Sharing
913+
</h3>
914+
<p className="text-sm text-gray-500 mb-4">
915+
Configure how your page appears when shared on social media platforms.
916+
</p>
917+
</div>
918+
919+
{/* OG Title */}
920+
<div className="space-y-2">
921+
<label htmlFor="og-title" className="block text-sm font-medium text-gray-700">
922+
Title
923+
</label>
924+
<input
925+
id="og-title"
926+
type="text"
927+
value={profile.openGraph?.title || ''}
928+
onChange={(e) =>
929+
setProfile({
930+
...profile,
931+
openGraph: { ...profile.openGraph, title: e.target.value },
932+
})
933+
}
934+
placeholder={profile.name || 'Your page title'}
935+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
936+
/>
937+
<p className="text-xs text-gray-400">Leave empty to use your profile name</p>
938+
</div>
939+
940+
{/* OG Description */}
941+
<div className="space-y-2">
942+
<label
943+
htmlFor="og-description"
944+
className="block text-sm font-medium text-gray-700"
945+
>
946+
Description
947+
</label>
948+
<textarea
949+
id="og-description"
950+
value={profile.openGraph?.description || ''}
951+
onChange={(e) => {
952+
const value = e.target.value.slice(0, 200);
953+
setProfile({
954+
...profile,
955+
openGraph: { ...profile.openGraph, description: value },
956+
});
957+
}}
958+
placeholder="A brief description of your page..."
959+
rows={3}
960+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
961+
/>
962+
<p className="text-xs text-gray-400">
963+
{profile.openGraph?.description?.length || 0}/200 characters
964+
</p>
965+
</div>
966+
967+
{/* OG Image */}
968+
<div className="space-y-2">
969+
<label htmlFor="og-image" className="block text-sm font-medium text-gray-700">
970+
Image
971+
</label>
972+
<div className="flex gap-2">
973+
<input
974+
id="og-image"
975+
type="url"
976+
value={
977+
profile.openGraph?.image?.startsWith('data:')
978+
? ''
979+
: profile.openGraph?.image || ''
980+
}
981+
onChange={(e) =>
982+
setProfile({
983+
...profile,
984+
openGraph: { ...profile.openGraph, image: e.target.value },
985+
})
986+
}
987+
placeholder="https://example.com/image.png"
988+
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
989+
/>
990+
<label
991+
className="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg cursor-pointer transition-colors flex items-center gap-2 text-sm font-medium text-gray-700"
992+
title="Upload image"
993+
>
994+
<Upload size={16} />
995+
<span>Upload</span>
996+
<input
997+
type="file"
998+
accept="image/*"
999+
aria-label="Upload OpenGraph image"
1000+
className="hidden"
1001+
onChange={(e) => {
1002+
const file = e.target.files?.[0];
1003+
if (file) {
1004+
const reader = new FileReader();
1005+
reader.onload = () => {
1006+
setProfile({
1007+
...profile,
1008+
openGraph: {
1009+
...profile.openGraph,
1010+
image: reader.result as string,
1011+
},
1012+
});
1013+
};
1014+
reader.readAsDataURL(file);
1015+
}
1016+
}}
1017+
/>
1018+
</label>
1019+
{profile.openGraph?.image && (
1020+
<button
1021+
type="button"
1022+
aria-label="Remove image"
1023+
onClick={() =>
1024+
setProfile({
1025+
...profile,
1026+
openGraph: { ...profile.openGraph, image: undefined },
1027+
})
1028+
}
1029+
className="px-3 py-2 bg-red-50 hover:bg-red-100 rounded-lg text-red-600 transition-colors"
1030+
title="Remove image"
1031+
>
1032+
<X size={16} />
1033+
</button>
1034+
)}
1035+
</div>
1036+
<p className="text-xs text-gray-400">Recommended size: 1200x630 pixels</p>
1037+
{profile.openGraph?.image && (
1038+
<div className="mt-2 p-2 bg-gray-50 rounded-lg">
1039+
<img
1040+
src={profile.openGraph.image}
1041+
alt="OG Preview"
1042+
className="w-full max-w-xs h-auto rounded border border-gray-200"
1043+
onError={(e) => {
1044+
(e.target as HTMLImageElement).style.display = 'none';
1045+
}}
1046+
/>
1047+
</div>
1048+
)}
1049+
</div>
1050+
1051+
{/* Site Name */}
1052+
<div className="space-y-2">
1053+
<label
1054+
htmlFor="og-site-name"
1055+
className="block text-sm font-medium text-gray-700"
1056+
>
1057+
Site Name
1058+
</label>
1059+
<input
1060+
id="og-site-name"
1061+
type="text"
1062+
value={profile.openGraph?.siteName || ''}
1063+
onChange={(e) =>
1064+
setProfile({
1065+
...profile,
1066+
openGraph: { ...profile.openGraph, siteName: e.target.value },
1067+
})
1068+
}
1069+
placeholder="My Bento"
1070+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
1071+
/>
1072+
</div>
1073+
1074+
{/* Twitter Section */}
1075+
<div className="pt-4 border-t border-gray-100">
1076+
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">
1077+
Twitter / X
1078+
</h4>
1079+
1080+
{/* Twitter Handle */}
1081+
<div className="space-y-2 mb-4">
1082+
<label
1083+
htmlFor="twitter-handle"
1084+
className="block text-sm font-medium text-gray-700"
1085+
>
1086+
Twitter Handle
1087+
</label>
1088+
<div className="flex">
1089+
<span className="inline-flex items-center px-3 border border-r-0 border-gray-200 rounded-l-lg bg-gray-50 text-gray-500 text-sm">
1090+
@
1091+
</span>
1092+
<input
1093+
id="twitter-handle"
1094+
type="text"
1095+
value={profile.openGraph?.twitterHandle || ''}
1096+
onChange={(e) => {
1097+
const value = e.target.value.replace(/^@/, '');
1098+
setProfile({
1099+
...profile,
1100+
openGraph: { ...profile.openGraph, twitterHandle: value },
1101+
});
1102+
}}
1103+
placeholder="username"
1104+
className="flex-1 px-3 py-2 border border-gray-200 rounded-r-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
1105+
/>
1106+
</div>
1107+
</div>
1108+
1109+
{/* Twitter Card Type */}
1110+
<div className="space-y-2">
1111+
<label
1112+
htmlFor="twitter-card-type"
1113+
className="block text-sm font-medium text-gray-700"
1114+
>
1115+
Card Type
1116+
</label>
1117+
<select
1118+
id="twitter-card-type"
1119+
value={profile.openGraph?.twitterCardType || 'summary_large_image'}
1120+
onChange={(e) =>
1121+
setProfile({
1122+
...profile,
1123+
openGraph: {
1124+
...profile.openGraph,
1125+
twitterCardType: e.target.value as 'summary' | 'summary_large_image',
1126+
},
1127+
})
1128+
}
1129+
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
1130+
>
1131+
<option value="summary_large_image">Large Image Card</option>
1132+
<option value="summary">Summary Card</option>
1133+
</select>
1134+
<p className="text-xs text-gray-400">
1135+
Large image cards show a bigger preview image
1136+
</p>
1137+
</div>
1138+
</div>
1139+
1140+
{/* Live Preview */}
1141+
<div className="pt-4 border-t border-gray-100">
1142+
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">
1143+
Preview
1144+
</h4>
1145+
<p className="text-sm text-gray-500 mb-4">
1146+
How your link will appear when shared on social media
1147+
</p>
1148+
<div className="border border-gray-200 rounded-xl overflow-hidden bg-white max-w-md">
1149+
{/* Image Preview */}
1150+
<div
1151+
className="w-full bg-gray-100 flex items-center justify-center"
1152+
style={{ aspectRatio: '1200/630' }}
1153+
>
1154+
{profile.openGraph?.image ? (
1155+
<img
1156+
src={profile.openGraph.image}
1157+
alt="Preview"
1158+
className="w-full h-full object-cover"
1159+
onError={(e) => {
1160+
(e.target as HTMLImageElement).style.display = 'none';
1161+
}}
1162+
/>
1163+
) : (
1164+
<div className="text-gray-400 text-sm">No image set</div>
1165+
)}
1166+
</div>
1167+
{/* Content Preview */}
1168+
<div className="p-3">
1169+
<p className="text-xs text-gray-500 mb-1">
1170+
{profile.openGraph?.siteName || 'yourdomain.com'}
1171+
</p>
1172+
<p className="font-semibold text-gray-900 text-sm leading-tight mb-1 line-clamp-1">
1173+
{profile.openGraph?.title || profile.name || 'Page Title'}
1174+
</p>
1175+
<p className="text-xs text-gray-500 line-clamp-2">
1176+
{profile.openGraph?.description ||
1177+
'Your page description will appear here...'}
1178+
</p>
1179+
</div>
1180+
</div>
1181+
</div>
1182+
</section>
1183+
)}
1184+
9051185
{/* ANALYTICS TAB */}
9061186
{activeTab === 'analytics' && (
9071187
<section className="space-y-6">

types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ export interface AvatarStyle {
8888
borderWidth?: number; // border width in pixels (default: 3)
8989
}
9090

91+
// OpenGraph meta tags for social sharing
92+
export interface OpenGraphData {
93+
title?: string; // Title for social previews (defaults to profile name)
94+
description?: string; // Description (max 200 chars recommended)
95+
image?: string; // Image URL (1200x630px recommended)
96+
siteName?: string; // Site name
97+
twitterHandle?: string; // Twitter/X handle (without @)
98+
twitterCardType?: 'summary' | 'summary_large_image'; // Twitter card type
99+
}
100+
91101
export interface UserProfile {
92102
name: string;
93103
bio: string;
@@ -109,6 +119,8 @@ export interface UserProfile {
109119
};
110120
// Centralized social accounts configuration
111121
socialAccounts?: SocialAccount[];
122+
// OpenGraph meta tags for social sharing
123+
openGraph?: OpenGraphData;
112124
}
113125

114126
export interface SiteData {

0 commit comments

Comments
 (0)