Make Bookmark Dashboard sharable online

Published: (December 17, 2025 at 05:40 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

Introduction

Recently, I often needed to share bookmarks with teammates—code repositories, documentation links, UI designs on Figma, etc. That’s when an idea struck me:

What if I could organize those bookmarks into a dashboard and generate a public webpage to share everything at once?

If I could make this happen, I’d be able to prepare a dashboard from one or more bookmark collections and then simply generate a corresponding webpage. Instead of copying and pasting bookmarks manually, I could just send the URL to my teammates, and they’d see a clean, static dashboard with all the resources. So convenient!

Excited by the idea, I jumped right into building it. Since the Bookmark Dashboard (a browser extension I previously developed) already existed, the main task was adding a “Generate Sharing Link” button. Clicking it would:

  1. Save the current dashboard layout.
  2. Produce a unique URL that, when opened, shows an exact replica of that dashboard at that moment.

Dashboard sharing UI

Updating the UI was the easy part; the real work happens on the backend when the button is clicked. After some consideration, I chose the simplest and fastest way to get this working.

Below, I’ll walk through how I implemented the backend using Next.js and MongoDB.

Implementation Approach

The dashboard shows the same content as long as the layout passed in is identical. Therefore, the sharing feature boils down to two steps:

  1. Save the layout when generating the sharing link.
  2. Restore the layout when the link is opened.

The backend implementation is straightforward:

  • Create an API to receive the dashboard layout and store it in the database.
  • Create a page that retrieves the stored layout and renders it when visited.
    The URL of this page becomes the sharing link.

My backend service is built using Next.js App Router. To define an API endpoint, I just need to follow the folder‑based routing convention.

Since I wanted the API path to be /dashboard/link, I created the following folder structure and placed a route.js inside:

src/app/dashboard
└── link
    └── route.js

route.js

import { NextResponse } from 'next/server';
// database util
import connectMongo from '@/db/mongo';
// database model for dashboard layout data
import LinkData from '@/db/entity/link_data';

export async function PUT(req) {
  // TODO: read current user's account (e.g., from auth token)
  const account = /* ... */;

  // Read the dashboard layout from the request body
  const { content } = await req.json();

  // Connect to MongoDB and upsert the layout
  await connectMongo();
  const item = await LinkData.findOneAndUpdate(
    { account },
    { content },
    { upsert: true, new: true }
  );

  // Return the generated document ID
  return NextResponse.json(
    { re: item._id },
    { status: 200 }
  );
}

The PUT function automatically handles incoming PUT requests to /dashboard/link. After connecting to MongoDB, it saves the layout (content) under the user’s account. The frontend (the “Generate” button’s click handler) can call this API; the returned _id is used to construct the sharing link.

Dashboard Page

To reference a dashboard by its _id, the sharing link should look like /dashboard/link/[id]. I created a page.js under app/dashboard/link/[id]:

src/app/dashboard
└── link
    ├── [id]
    │   ├── Dashboard.js
    │   └── page.js
    └── route.js

page.js

// database util
import connectMongo from '@/db/mongo';
// database model for dashboard layout data
import LinkData from '@/db/entity/link_data';
// the component that actually renders the dashboard
import Dashboard from './Dashboard';

async function loadData(id) {
  await connectMongo();
  if (!id) return null;
  return await LinkData.findById(id).exec();
}

export default async function DashboardLinkPage({ params }) {
  const { id } = params;
  const data = await loadData(id);

  let layout = undefined;
  if (data?.content) {
    try {
      const parsed = JSON.parse(data.content);
      layout = parsed?.layout;
    } catch (e) {
      console.error('Failed to parse layout JSON', e);
    }
  }

  // This is a server‑side component (no "use client" directive)
  return <Dashboard layout={layout} />;
}

The page extracts the id from the route parameters, fetches the stored layout from MongoDB, parses it, and passes it to the Dashboard component (built with React‑Grid‑Layout, as introduced in an earlier post).

Replace [id] in /dashboard/link/[id] with the actual _id value returned by the API. That URL becomes the sharing link you can send to others. When they open it, they’ll see a static dashboard that looks exactly like the one used to generate the link.

A Note on MongoDB Connection

Both route.js and page.js use the same connectMongo helper to reuse a single connection across concurrent requests and hot reloads:

import mongoose from 'mongoose';

// Reuse the connection across requests (module re‑imports)
let cached = global.mongoose;
if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function connectMongo() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const uri = process.env.MONGODB_URI;
    const opts = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    };
    cached.promise = mongoose.connect(uri, opts).then((mongoose) => mongoose);
  }

  cached.conn = await cached.promise;
  return cached.conn;
}

export default connectMongo;

This pattern ensures that the MongoDB client is instantiated only once per server instance, improving performance and preventing connection‑exhaustion errors.

Conclusion

With just a few lines of backend code, the Bookmark Dashboard now supports generating shareable, static dashboard pages. The workflow is:

  1. Click Generate Sharing Link → UI calls the PUT /dashboard/link API.
  2. API stores the layout and returns an _id.
  3. Construct the URL /dashboard/link/ and share it.
  4. Anyone visiting that URL sees the exact dashboard layout, rendered server‑side.

The solution is lightweight, leverages Next.js’s App Router, and uses MongoDB for persistent storage—perfect for quickly sharing curated sets of resources with teammates.

if (!cached.promise) {
  cached.promise = mongoose
    .connect(MONGO_URI, {
      serverApi: { version: '1', strict: true, deprecationErrors: true },
    })
    .then((mongoose) => {
      return mongoose;
    });
}

cached.conn = await cached.promise;
await mongoose.connection.db.admin().command({ ping: 1 });
console.log('Successfully connected to db!');
} catch (e) {
  cached.promise = null;
  throw e;
}

return cached.conn;
}

Full‑screen Controls

  • Enter fullscreen mode
  • Exit fullscreen mode

Since both route.js and page.js run on the server side, they can share the same connection logic and benefit from connection reuse.

This pattern of querying the database directly from server‑side pages is one of Next.js’s standout features. It enables server‑side data fetching and rendering, which differs significantly from client‑side frameworks like React or Vue.

Wrapping Up

So the backend is all set. Here’s how the flow works:

  1. Generate a dashboard sharing link – The frontend (from the “Generate” button’s click handler) calls the /dashboard/link API.
  2. Save the layout – The API stores the layout and returns an _id.
  3. Construct the sharing link – The _id is used to build the link /dashboard/link/_id.
  4. Open the link – The page extracts the _id from the path, fetches the saved layout, and renders the dashboard content.

Example

Dashboard generation preview

Open the sharing link

Shared dashboard view

This sharable dashboard feature is now live in the latest version of Bookmark Dashboard. It’s already saving me a lot of time when sharing resources with others. It’s free, and I hope it can help more people share their collections quickly and easily.

Thanks for reading!

Back to Blog

Related posts

Read more »