npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

simple-link-aggregator

v0.0.1

Published

A simple link aggregator built on Apps Script

Downloads

28

Readme

simple-link-aggregator

A simple link aggregator built to work with a custom backend, or a backend powered by AppsScript.

Why might you want this? Imagine you have your data source for links in your aggregator coming from a Google Sheets document.

Screen recording 2024-03-22 09.15.32.webm

Install

npm install simple-link-aggregator

Usage

The idea is to have a backend that stores each story, then use this library to render the frontend and use a url for handling sorting by topic or domain, and a POST request for upvoting.

Generating client side html

renderStories should be your main entry point in the story.ts folder.

const stories = [
  Story(
    1,
    new Date(),
    "https://github.com/eeue56/jump-to/",
    "Click on links without needing to leave the keyboard",
    ["Workflow"],
    "A Vimium inspired keyboard shortcut script for browsers. Highlights all clickable elements on a page, then provides shortcuts to click them so you never have to leave your keyboard again!",
    "A handy way to learn about making Chrome extensions, testing them with Playwright, and using JSDoc",
    1
  ),
];

const storyHtml = renderStories(stories, "default", "/upvote", "/topic", "/domain");

The CSS and JS needed is included when using renderStories. If the google window object exists, then it will use google.script.run.upvoteStory(storyId: string) to handle the upvoting. Otherwise, it defaults to a POST via fetch.

AppsScript backend

If you want to hook this up to AppsScript, then create an AppsScript attachment to a spreadsheet. Write a function to get all the stories, then add doGet and deploy as a web app. I recommend using the caching service too. You'll also want upvoteStory(storyId: string), which is the default thing called in the client-side js if the google (more specifically: google.script.run) script exists.

Here's an example:

import { story } from "simple-link-aggregator";

function getStories(): story.Story[] {}

export function doGet(
  event: GoogleAppsScript.Events.DoGet
): GoogleAppsScript.HTML.HtmlOutput {
  let pathInfo = "/";
  if (!event.pathInfo) {
    pathInfo = "/";
  } else {
    pathInfo = `/${event.pathInfo}`;
  }

  console.log("Using path", pathInfo);

  let cachedPage = cache.get(pathInfo);
  if (cachedPage === null) {
    console.log("Cache is empty, populating...");
    cachedPage = doGetCached(pathInfo);
    cache.put(pathInfo, cachedPage, 60 * 5);
  }

  return HtmlService.createHtmlOutput(cachedPage).addMetaTag(
    "viewport",
    "width=device-width, initial-scale=1.0"
  );
}

function getScriptUrl(): string {
  var url = ScriptApp.getService().getUrl();
  return url;
}

function doGetCached(pathInfo: string): string {
  const index = `<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>{contents}</body>
</html>`;
  const baseUrl = getScriptUrl();

  let stories = getStories();
  let sort: "default" | "new" | "top" = "default";

  const root = matches(/\/$/, pathInfo);
  const topic = matches(/\/topic\/(.+)$/, pathInfo);
  const domain = matches(/\/domain\/(.+)$/, pathInfo);
  const new_ = matches(/\/new$/, pathInfo);
  const top = matches(/\/top$/, pathInfo);

  if (root) {
  } else if (topic) {
    stories = stories.filter((story) => story.topic.includes(topic[1]));
  } else if (domain) {
    stories = stories.filter((story) => story.link.includes(domain[1]));
  } else if (new_) {
    sort = "new";
  } else if (top) {
    sort = "top";
  }

  return index.replace(
    "{contents}",
    story.renderStories(
      stories,
      sort,
      `${baseUrl}/upvote`,
      `${baseUrl}/topic`,
      `${baseUrl}/domain`
    )
  );
}

// you may want this in a separate file
function upvoteStory(storyId) {
    // grab the current score and increment it in the spreadsheet
}

Suggested backend server API

There isn't a backend server included, but this is the API I'd recommend.

|url format|output|format| |:---:|:---:|:---:| |/| Last 7 days sorted by score - older stories sorted by date| GET | |/new| Stories sorted by date - newest first| GET | |/top| Stories sorted by score - top scoring first| GET | |/topic/:topic| Only fetch articles with that topic| GET | |/domain/:domain| Only fetch articles with that domain| GET | |/upvote/:story_id| Upvote that story, and return the new count| POST |

For a comprehensive example, check out the example folder that implements this (without a permanent data store) that also happens to have all the tests.

Stories

Every submission is made up of stories. Every story is a link, with a title, a summary, and a description of how it relates to the reader. For more details, this blog post covers why we'd want that.

|sort|output| |:---:|:---:| |default| Last 7 days sorted by score - older stories sorted by date| |new| Stories sorted by date - newest first| |top| Stories sorted by score - top scoring first|

A visualization of how that looks, on the date 20/05/2024 (en_GB format):

flowchart TD
    stories(
        Imagine you have the following stories:
        date: score
        16/05/2024: 115
        11/05/2024: 030
        12/05/2024: 002
        13/05/2024: 001
        18/05/2024: 086
        20/05/2024: 123
        ) --> renderStories

    renderStories -->|sort: default| frontpage(
        Last 7 days sorted by score,
        older stories sorted by date
    )

    frontpage --> exampleFrontPage(
        1. 20/05/2024: 123
        2. 16/05/2024: 115
        3. 18/05/2024: 086
        4. 13/05/2024: 001
        5. 12/05/2024: 002
        6. 11/05/2024: 030
    )

    renderStories -->|sort: new| new(
        Stories sorted by date,
        newest first
    )

    new --> exampleNew(
        1. 20/05/2024: 123
        2. 18/05/2024: 086
        3. 16/05/2024: 115
        4. 13/05/2024: 001
        5. 12/05/2024: 002
        6. 11/05/2024: 030
    )

    renderStories -->|sort: top| top(
        Stories sorted by score,
        top scoring first
    )

    top --> exampleTop(
        1. 20/05/2024: 123
        2. 16/05/2024: 115
        3. 18/05/2024: 086
        4. 11/05/2024: 030
        5. 12/05/2024: 002
        6. 13/05/2024: 001
    )

Novelities

Because AppsScript's runtime has some quirks (like not support URL), and the Web App is also weird (iframe), this project does some strange things. In particular:

  • Re-implementing hostname parsing in strings.
  • Detecting if the script is being run from an AppsScript, switching between AppsScript's remote function calls to a simple fetch request if not.
  • Avoid as many imports as possible, as deploying packages with imports on AppsScript complicates things.
  • Avoid reading files, prefering to do replacements as part of the build step.
  • Remain backend agnositic.