All projects

Markly

A social bookmarking app: save images, videos, and links, organize them into categories, and browse a community feed laid out as a true masonry grid. Built on ASP.NET Core with vanilla JS on the front.

  • ASP.NET Core
  • Bootstrap
Markly — screenshot
Markly

Overview

Markly is Pinterest-shaped with a bit of GitHub's social layer. Users save bookmarks with text, an image, or a YouTube/Vimeo embed, sort them into public or private categories, and the community can vote and comment. Everything lands in an infinite-scrolling masonry feed with full-text search, trending tags, and profile pages.

There's also an AI assist that suggests tags and categories from a bookmark's title and description, a share modal with a branded QR code, a quick-add bookmarklet, and an admin panel.

The problem

I wanted to see how far the classic server-rendered .NET stack could go without a SPA framework. Could I build a genuinely feature-complete social app and still get the dynamic feel people expect — masonry layout, infinite scroll, live voting and commenting — with no page reloads?

Approach & architecture

It's ASP.NET Core MVC with Razor views, EF Core over PostgreSQL, and ASP.NET Identity for auth, structured as controllers over a small service layer. The front end is deliberately frameworkless: Bootstrap plus modular vanilla JS, with a custom masonry engine that measures each card, drops it into the shortest column, and re-flows on image load, resize, and every AJAX append.

Media lives as JSON on the bookmark. YouTube and Vimeo URLs are parsed into safe embed URLs with auto-generated thumbnails, and anything that doesn't match a known pattern fails closed to null rather than risk an injection.

Challenges & tradeoffs

Combining masonry with infinite scroll was more complex than it sounds. The layout math could run before images had dimensions, so appended cards would jump around. The fix waits for all new images to settle (with a hard timeout), then re-lays out, with a best-effort first pass for images already in cache.

Every AJAX endpoint — votes, comments, AI suggestions — needed CSRF protection, so I wrote a fetch wrapper that injects the antiforgery token and quietly retries when the token has gone stale. Uploaded images get validated by magic bytes, not just extension.

What I'd change

Rate limiting and file storage are both in-process and on-disk, which works fine single-instance but would work even better with Redis and blob storage to scale out.