All projects

Markly

Save images, videos, and links, sort them into categories. A social bookmarking app built on ASP.NET Core with vanilla JS on the front end.

  • ASP.NET Core
  • Bootstrap
Markly — screenshot
Markly

What it is

Think Pinterest, with votes and comments. Bookmarks can be plain text, an image, or a YouTube/Vimeo embed; they live in public or private categories, and everything flows into an infinite-scroll masonry feed with full-text search, trending tags, and profile pages.

Around the edges: AI suggestions for tags and categories based on a bookmark's title and description, a share modal with a branded QR code, a bookmarklet for quick saving, and an admin panel.

The problem

I wanted to see how far the classic server-rendered .NET stack could go without a SPA framework. Masonry layout, infinite scroll, voting and commenting that update in place, and never a full page reload.

How it's put together

ASP.NET Core MVC with Razor views, EF Core over PostgreSQL, ASP.NET Identity for auth, controllers sitting on a small service layer. The front end is Bootstrap plus small vanilla JS modules. The masonry engine is mine: it measures every card, drops each one into the currently shortest column, and re-flows whenever an image loads, the window resizes, or an AJAX page gets appended.

Media is stored as JSON on the bookmark. YouTube and Vimeo URLs get parsed into known-safe embed URLs with auto-generated thumbnails, and anything that doesn't match a pattern I recognize becomes null, because a missing thumbnail beats an injected iframe.

Challenges & tradeoffs

Masonry and infinite scroll conspire against each other. Layout math would run before freshly appended images knew their own dimensions, and cards visibly jumped. The eventual fix waits for the new batch of images to settle, with a hard timeout so one dead image can't stall the feed, plus a best-effort early pass for anything already in cache.

Since every interaction is an AJAX call, every endpoint needed CSRF protection, so all requests go through a fetch wrapper that injects the antiforgery token and silently retries once when it has expired. Uploads are checked by magic bytes, not just the file extension.

What I'd change

Rate limiting is in-process and files sit on local disk. For one instance that's fine. A second instance would force both to move, to Redis and blob storage respectively, and that's the first refactor on the list.