diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
index 582075d..4f30177 100644
--- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
+++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
@@ -41,6 +41,7 @@
+
diff --git a/DysonNetwork.Sphere/PageData/PostPageData.cs b/DysonNetwork.Sphere/PageData/PostPageData.cs
new file mode 100644
index 0000000..af10475
--- /dev/null
+++ b/DysonNetwork.Sphere/PageData/PostPageData.cs
@@ -0,0 +1,75 @@
+using System.Net;
+using DysonNetwork.Shared.PageData;
+using DysonNetwork.Shared.Proto;
+using DysonNetwork.Sphere.Post;
+using DysonNetwork.Sphere.Publisher;
+using Microsoft.EntityFrameworkCore;
+using OpenGraphNet;
+
+namespace DysonNetwork.Sphere.PageData;
+
+public class PostPageData(
+ AppDatabase db,
+ AccountService.AccountServiceClient accounts,
+ PublisherService pub,
+ PostService ps,
+ IConfiguration configuration
+)
+ : IPageDataProvider
+{
+ private readonly string _siteUrl = configuration["SiteUrl"]!;
+
+ public bool CanHandlePath(PathString path) =>
+ path.StartsWithSegments("/posts");
+
+ public async Task> GetAppDataAsync(HttpContext context)
+ {
+ var path = context.Request.Path.Value!;
+ var startIndex = "/posts/".Length;
+ var endIndex = path.IndexOf('/', startIndex);
+ var slug = endIndex == -1 ? path[startIndex..] : path.Substring(startIndex, endIndex - startIndex);
+ slug = WebUtility.UrlDecode(slug);
+
+ var postId = Guid.TryParse(slug, out var postIdGuid) ? postIdGuid : Guid.Empty;
+ if (postId == Guid.Empty) return new Dictionary();
+
+ context.Items.TryGetValue("CurrentUser", out var currentUserValue);
+ var currentUser = currentUserValue as Account;
+ List userFriends = [];
+ if (currentUser != null)
+ {
+ var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
+ { AccountId = currentUser.Id });
+ userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList();
+ }
+
+ var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id));
+
+ var post = await db.Posts
+ .Where(e => e.Id == postId)
+ .Include(e => e.Publisher)
+ .Include(e => e.Tags)
+ .Include(e => e.Categories)
+ .FilterWithVisibility(currentUser, userFriends, userPublishers)
+ .FirstOrDefaultAsync();
+ post = await ps.LoadPostInfo(post, currentUser);
+
+ // Track view - use the account ID as viewer ID if user is logged in
+ await ps.IncreaseViewCount(post.Id, currentUser?.Id);
+
+ var og = OpenGraph.MakeGraph(
+ title: post.Title ?? $"Post from {post.Publisher.Name}",
+ type: "article",
+ image: $"{_siteUrl}/cgi/drive/files/{post.Publisher.Background?.Id}?original=true",
+ url: $"{_siteUrl}/@{slug}",
+ description: post.Description ?? post.Content?[..80] ?? "Posted with some media",
+ siteName: "Solar Network"
+ );
+
+ return new Dictionary()
+ {
+ ["Post"] = post,
+ ["OpenGraph"] = og
+ };
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs
index 9521387..532b65b 100644
--- a/DysonNetwork.Sphere/Program.cs
+++ b/DysonNetwork.Sphere/Program.cs
@@ -1,7 +1,9 @@
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
+using DysonNetwork.Shared.PageData;
using DysonNetwork.Shared.Registry;
using DysonNetwork.Sphere;
+using DysonNetwork.Sphere.PageData;
using DysonNetwork.Sphere.Startup;
using Microsoft.EntityFrameworkCore;
@@ -33,6 +35,8 @@ builder.Services.AddAppBusinessServices(builder.Configuration);
// Add scheduled jobs
builder.Services.AddAppScheduledJobs();
+builder.Services.AddTransient();
+
var app = builder.Build();
// Run database migrations
diff --git a/DysonNetwork.Sphere/appsettings.json b/DysonNetwork.Sphere/appsettings.json
index c318c8c..0f22668 100644
--- a/DysonNetwork.Sphere/appsettings.json
+++ b/DysonNetwork.Sphere/appsettings.json
@@ -1,6 +1,7 @@
{
"Debug": true,
"BaseUrl": "http://localhost:5071",
+ "SiteUrl": "https://solian.app",
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user
index 7ac5870..e0ed2bb 100644
--- a/DysonNetwork.sln.DotSettings.user
+++ b/DysonNetwork.sln.DotSettings.user
@@ -54,6 +54,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded