fix(daily dev): fixed routes and added one new one (#18021)

* add squads

* style: auto format

* null check

* fix: add null checks for items

* changes

* trigger

* use params

* fix: add categories

---------

Co-authored-by: rjnishant530 <singhnisant530@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Nishant Singh 2025-01-05 20:12:10 +05:30 committed by GitHub
parent 521313ffeb
commit 238955b5fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 699 additions and 175 deletions

View File

@ -3,16 +3,18 @@ import { getData, getList } from './utils';
export const route: Route = {
path: '/',
categories: ['programming'],
radar: [
{
source: ['alistapart.com/articles/'],
target: '',
target: '/',
},
],
name: 'Unknown',
name: 'Home Feed',
maintainers: ['Rjnishant530'],
handler,
url: 'alistapart.com/articles/',
example: '/alistapart',
};
async function handler() {

View File

@ -17,6 +17,7 @@ export const route: Route = {
radar: [
{
source: ['alistapart.com/blog/topic/:topic'],
target: '/:topic',
},
],
name: 'Topics',

View File

@ -1,5 +1,5 @@
import { Route } from '@/types';
import { baseUrl, getData, getList } from './utils.js';
import { Route, ViewType } from '@/types';
import { baseUrl, getData, getList, variables } from './utils.js';
const query = `
query MostDiscussedFeed(
@ -52,8 +52,9 @@ const query = `
`;
export const route: Route = {
path: '/discussed',
example: '/daily/discussed',
path: '/discussed/:period?/:innerSharedContent?/:dateSort?',
example: '/daily/discussed/30',
view: ViewType.Articles,
radar: [
{
source: ['app.daily.dev/discussed'],
@ -63,19 +64,52 @@ export const route: Route = {
maintainers: ['Rjnishant530'],
handler,
url: 'app.daily.dev/discussed',
parameters: {
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
dateSort: {
description: 'Sort posts by publication date instead of popularity',
default: 'true',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
period: {
description: 'Period of Lookup',
default: '7',
options: [
{ value: '7', label: 'Last Week' },
{ value: '30', label: 'Last Month' },
{ value: '365', label: 'Last Year' },
],
},
},
};
async function handler(ctx) {
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const link = `${baseUrl}/discussed`;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true;
const period = ctx.req.param('period') ? Number.parseInt(ctx.req.param('period'), 10) : 7;
const link = `${baseUrl}/posts/discussed`;
const data = await getData({
query,
variables: {
...variables,
first: limit,
period,
},
});
const items = getList(data);
const items = getList(data, innerSharedContent, dateSort);
return {
title: 'Real-time discussions in the developer community | daily.dev',

View File

@ -1,101 +0,0 @@
import { Route } from '@/types';
import { baseUrl, getData, getList } from './utils.js';
const variables = {
version: 11,
ranking: 'POPULARITY',
first: 15,
};
const query = `
query AnonymousFeed(
$first: Int
$ranking: Ranking
$version: Int
$supportedTypes: [String!] = ["article","share","freeform"]
) {
page: anonymousFeed(
first: $first
ranking: $ranking
version: $version
supportedTypes: $supportedTypes
) {
...FeedPostConnection
}
}
fragment FeedPostConnection on PostConnection {
edges {
node {
...FeedPost
contentHtml
}
}
}
fragment FeedPost on Post {
...SharedPostInfo
}
fragment SharedPostInfo on Post {
id
title
image
readTime
permalink
commentsPermalink
summary
createdAt
numUpvotes
numComments
author {
...UserShortInfo
}
tags
}
fragment UserShortInfo on User {
id
name
image
permalink
username
bio
}
`;
const graphqlQuery = {
query,
variables,
};
export const route: Route = {
path: '/',
example: '/daily',
radar: [
{
source: ['app.daily.dev/popular'],
},
],
name: 'Popular',
maintainers: ['Rjnishant530'],
handler,
url: 'app.daily.dev/popular',
};
async function handler() {
const link = `${baseUrl}/popular`;
const data = await getData(graphqlQuery);
const items = getList(data);
return {
title: 'Popular posts on daily.dev',
link,
item: items,
description: 'daily.dev is the easiest way to stay updated on the latest programming news. Get the best content from the top tech publications on any topic you want.',
logo: `${baseUrl}/favicon-32x32.png`,
icon: `${baseUrl}/favicon-32x32.png`,
language: 'en-us',
};
}

182
lib/routes/daily/popular.ts Normal file
View File

@ -0,0 +1,182 @@
import { Route, ViewType } from '@/types';
import { baseUrl, getData, getList, variables } from './utils.js';
const query = `
query AnonymousFeed(
$loggedIn: Boolean! = false
$first: Int
$after: String
$ranking: Ranking
$version: Int
$supportedTypes: [String!] = ["article","share","freeform","video:youtube","collection"]
) {
page: anonymousFeed(
first: $first
after: $after
ranking: $ranking
version: $version
supportedTypes: $supportedTypes
) {
...FeedPostConnection
}
}
fragment FeedPostConnection on PostConnection {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
...FeedPost
contentHtml
...UserPost @include(if: $loggedIn)
}
}
}
fragment FeedPost on Post {
...FeedPostInfo
sharedPost {
id
title
image
readTime
permalink
commentsPermalink
createdAt
type
tags
source {
id
handle
permalink
image
}
slug
clickbaitTitleDetected
}
trending
feedMeta
collectionSources {
handle
image
}
numCollectionSources
updatedAt
slug
}
fragment FeedPostInfo on Post {
id
title
image
readTime
permalink
commentsPermalink
createdAt
commented
bookmarked
views
numUpvotes
numComments
summary
bookmark {
remindAt
}
author {
id
name
image
username
permalink
}
type
tags
source {
id
handle
name
permalink
image
type
}
userState {
vote
flags {
feedbackDismiss
}
}
slug
clickbaitTitleDetected
}
fragment UserPost on Post {
read
upvoted
commented
bookmarked
downvoted
}
`;
export const route: Route = {
path: '/popular/:innerSharedContent?/:dateSort?',
example: '/daily/popular',
view: ViewType.Articles,
radar: [
{
source: ['app.daily.dev/popular'],
},
],
parameters: {
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
dateSort: {
description: 'Sort posts by publication date instead of popularity',
default: 'true',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
},
name: 'Popular',
maintainers: ['Rjnishant530'],
handler,
url: 'app.daily.dev/popular',
};
async function handler(ctx) {
const link = `${baseUrl}/posts`;
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 15;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true;
const data = await getData({
query,
variables: {
...variables,
ranking: 'POPULARITY',
first: limit,
},
});
const items = getList(data, innerSharedContent, dateSort);
return {
title: 'Popular posts on daily.dev',
link,
item: items,
description: 'daily.dev is the easiest way to stay updated on the latest programming news. Get the best content from the top tech publications on any topic you want.',
logo: `${baseUrl}/favicon-32x32.png`,
icon: `${baseUrl}/favicon-32x32.png`,
language: 'en-us',
};
}

View File

@ -1,4 +1,4 @@
import { Route } from '@/types';
import { DataItem, Route } from '@/types';
import { baseUrl, getBuildId, getData, getList } from './utils';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
@ -125,10 +125,18 @@ fragment UserPost on Post {
}`;
export const route: Route = {
path: '/source/:sourceId',
path: '/source/:sourceId/:innerSharedContent?',
example: '/daily/source/hn',
parameters: {
sourceId: 'The source id',
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
},
radar: [
{
@ -144,8 +152,9 @@ export const route: Route = {
async function handler(ctx) {
const sourceId = ctx.req.param('sourceId');
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 10;
const link = `${baseUrl}/sources/${sourceId}`;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const link = `${baseUrl}/sources/${sourceId}`;
const buildId = await getBuildId();
const userData = (await cache.tryGet(`daily:source:${sourceId}`, async () => {
@ -167,7 +176,7 @@ async function handler(ctx) {
loggedIn: false,
},
});
return getList(edges);
return getList(edges, innerSharedContent, true);
},
config.cache.routeExpire,
false
@ -177,7 +186,7 @@ async function handler(ctx) {
title: `${userData.name} posts on daily.dev`,
description: userData.description,
link,
item: items,
item: items as DataItem[],
image: userData.image,
logo: userData.image,
icon: userData.image,

266
lib/routes/daily/squads.ts Normal file
View File

@ -0,0 +1,266 @@
import { Route, ViewType } from '@/types';
import { baseUrl, getData, getList, variables } from './utils.js';
const sourceQuery = `
query Source($handle: ID!) {
source(id: $handle) {
...SquadBaseInfo
moderationPostCount
}
}
fragment SquadBaseInfo on Source {
...SourceBaseInfo
referralUrl
createdAt
flags {
featured
totalPosts
totalViews
totalUpvotes
}
category {
id
title
slug
}
...PrivilegedMembers
}
fragment SourceBaseInfo on Source {
id
active
handle
name
permalink
public
type
description
image
membersCount
currentMember {
...CurrentMember
}
memberPostingRole
memberInviteRole
moderationRequired
}
fragment CurrentMember on SourceMember {
user {
id
}
permissions
role
referralToken
flags {
hideFeedPosts
collapsePinnedPosts
}
}
fragment PrivilegedMembers on Source {
privilegedMembers {
user {
id
name
image
permalink
username
bio
reputation
companies {
name
image
}
contentPreference {
status
}
}
role
}
}
`;
const query = `
query SourceFeed(
$source: ID!
$loggedIn: Boolean! = false
$first: Int
$after: String
$ranking: Ranking
$supportedTypes: [String!]
) {
page: sourceFeed(
source: $source
first: $first
after: $after
ranking: $ranking
supportedTypes: $supportedTypes
) {
...FeedPostConnection
}
}
fragment FeedPostConnection on PostConnection {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
...FeedPost
pinnedAt contentHtml
...UserPost @include(if: $loggedIn)
}
}
}
fragment FeedPost on Post {
...FeedPostInfo
sharedPost {
id
title
image
readTime
permalink
commentsPermalink
createdAt
type
tags
source {
id
handle
permalink
image
}
slug
clickbaitTitleDetected
}
trending
feedMeta
collectionSources {
handle
image
}
numCollectionSources
updatedAt
slug
}
fragment FeedPostInfo on Post {
id
title
image
readTime
permalink
commentsPermalink
createdAt
commented
bookmarked
views
numUpvotes
numComments
summary
bookmark {
remindAt
}
author {
id
name
image
username
permalink
}
type
tags
source {
id
handle
name
permalink
image
type
}
userState {
vote
flags {
feedbackDismiss
}
}
slug
clickbaitTitleDetected
}
fragment UserPost on Post {
read
upvoted
commented
bookmarked
downvoted
}
`;
export const route: Route = {
path: '/squads/:squads/:innerSharedContent?',
example: '/daily/squads/watercooler',
view: ViewType.Articles,
parameters: {
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
},
radar: [
{
source: ['app.daily.dev/squads/:squads'],
},
],
name: 'Squads',
maintainers: ['Rjnishant530'],
handler,
url: 'app.daily.dev/squads/discover',
};
async function handler(ctx) {
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const squads = ctx.req.param('squads');
const link = `${baseUrl}/squads/${squads}`;
const { id, description, name } = await getData(
{
query: sourceQuery,
variables: {
handle: squads,
},
},
true
);
const data = await getData({
query,
variables: {
...variables,
source: id,
ranking: 'TIME',
supportedTypes: ['article', 'share', 'freeform', 'video:youtube', 'collection', 'welcome'],
first: limit,
},
});
const items = getList(data, innerSharedContent, true);
return {
title: `${name} - daily.dev`,
link,
item: items,
description,
logo: `${baseUrl}/favicon-32x32.png`,
icon: `${baseUrl}/favicon-32x32.png`,
language: 'en-us',
};
}

View File

@ -1,84 +1,181 @@
import { Route } from '@/types';
import { baseUrl, getData, getList } from './utils.js';
const variables = {
period: 7,
first: 15,
};
import { Route, ViewType } from '@/types';
import { baseUrl, getData, getList, variables } from './utils.js';
const query = `
query MostUpvotedFeed(
query MostUpvotedFeed(
$loggedIn: Boolean! = false
$first: Int
$after: String
$period: Int
$supportedTypes: [String!] = ["article","share","freeform"]
$supportedTypes: [String!] = ["article","share","freeform","video:youtube","collection"]
$source: ID
$tag: String
) {
page: mostUpvotedFeed(first: $first, period: $period, supportedTypes: $supportedTypes) {
page: mostUpvotedFeed(first: $first, after: $after, period: $period, supportedTypes: $supportedTypes, source: $source, tag: $tag) {
...FeedPostConnection
}
}
fragment FeedPostConnection on PostConnection {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
...FeedPost
contentHtml
...UserPost @include(if: $loggedIn)
}
}
}
fragment FeedPost on Post {
...SharedPostInfo
...FeedPostInfo
sharedPost {
id
title
image
readTime
permalink
commentsPermalink
createdAt
type
tags
source {
id
handle
permalink
image
}
slug
clickbaitTitleDetected
}
trending
feedMeta
collectionSources {
handle
image
}
numCollectionSources
updatedAt
slug
}
fragment SharedPostInfo on Post {
fragment FeedPostInfo on Post {
id
title
image
readTime
permalink
commentsPermalink
summary
createdAt
commented
bookmarked
views
numUpvotes
numComments
author {
...UserShortInfo
summary
bookmark {
remindAt
}
author {
id
name
image
username
permalink
}
type
tags
source {
id
handle
name
permalink
image
type
}
userState {
vote
flags {
feedbackDismiss
}
}
slug
clickbaitTitleDetected
}
fragment UserShortInfo on User {
id
name
image
permalink
username
bio
fragment UserPost on Post {
read
upvoted
commented
bookmarked
downvoted
}
`;
export const route: Route = {
path: '/upvoted',
example: '/daily/upvoted',
path: '/upvoted/:period?/:innerSharedContent?/:dateSort?',
example: '/daily/upvoted/7',
view: ViewType.Articles,
radar: [
{
source: ['app.daily.dev/upvoted'],
},
],
parameters: {
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
dateSort: {
description: 'Sort posts by publication date instead of popularity',
default: 'true',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
period: {
description: 'Period of Lookup',
default: '7',
options: [
{ value: '7', label: 'Last Week' },
{ value: '30', label: 'Last Month' },
{ value: '365', label: 'Last Year' },
],
},
},
name: 'Most upvoted',
maintainers: ['Rjnishant530'],
handler,
url: 'app.daily.dev/upvoted',
};
async function handler() {
const link = `${baseUrl}/upvoted`;
async function handler(ctx) {
const link = `${baseUrl}/posts/upvoted`;
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const dateSort = ctx.req.param('dateSort') ? JSON.parse(ctx.req.param('dateSort')) : true;
const period = ctx.req.param('period') ? Number.parseInt(ctx.req.param('period'), 10) : 7;
const data = await getData({
query,
variables,
variables: {
...variables,
period,
first: limit,
},
});
const items = getList(data);
const items = getList(data, innerSharedContent, dateSort);
return {
title: 'Most upvoted posts for developers | daily.dev',

View File

@ -1,4 +1,4 @@
import { Route } from '@/types';
import { DataItem, Route } from '@/types';
import { baseUrl, getBuildId, getData, getList } from './utils';
import ofetch from '@/utils/ofetch';
import cache from '@/utils/cache';
@ -151,13 +151,23 @@ const userPostQuery = `
}`;
export const route: Route = {
path: '/user/:userId',
path: '/user/:userId/:innerSharedContent?',
example: '/daily/user/kramer',
radar: [
{
source: ['app.daily.dev/:userId/posts', 'app.daily.dev/:userId'],
},
],
parameters: {
innerSharedContent: {
description: 'Where to Fetch inner Shared Posts instead of original',
default: 'false',
options: [
{ value: 'false', label: 'False' },
{ value: 'true', label: 'True' },
],
},
},
name: 'User Posts',
maintainers: ['TonyRL'],
handler,
@ -167,7 +177,7 @@ export const route: Route = {
async function handler(ctx) {
const userId = ctx.req.param('userId');
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 7;
const innerSharedContent = ctx.req.param('innerSharedContent') ? JSON.parse(ctx.req.param('innerSharedContent')) : false;
const buildId = await getBuildId();
const userData = await cache.tryGet(`daily:user:${userId}`, async () => {
@ -191,7 +201,7 @@ async function handler(ctx) {
loggedIn: false,
},
});
return getList(edges);
return getList(edges, innerSharedContent, true);
},
config.cache.routeExpire,
false
@ -201,7 +211,7 @@ async function handler(ctx) {
title: `${user.name} | daily.dev`,
description: user.bio,
link: `${baseUrl}/${userId}/posts`,
item: items,
item: items as DataItem[],
image: user.image,
logo: user.image,
icon: user.image,

View File

@ -5,11 +5,15 @@ import { config } from '@/config';
import { art } from '@/utils/render';
import path from 'node:path';
import { getCurrentPath } from '@/utils/helpers';
import { DataItem } from '@/types';
const __dirname = getCurrentPath(import.meta.url);
export const baseUrl = 'https://app.daily.dev';
const gqlUrl = `https://api.daily.dev/graphql`;
export const variables = {
version: 54,
loggedIn: false,
};
export const getBuildId = () =>
cache.tryGet(
'daily:buildId',
@ -22,30 +26,42 @@ export const getBuildId = () =>
false
);
export const getData = async (graphqlQuery) => {
export const getData = async (graphqlQuery, source = false) => {
const response = await ofetch(gqlUrl, {
method: 'POST',
body: graphqlQuery,
});
return response.data.page.edges;
return source ? response.data.source : response.data.page.edges;
};
const render = (data) => art(path.join(__dirname, 'templates/posts.art'), data);
export const getList = (edges) =>
edges.map(({ node }) => ({
id: node.id,
title: node.title,
link: node.commentsPermalink ?? node.permalink,
guid: node.permalink,
description: render({
image: node.image,
content: node.contentHtml?.replaceAll('\n', '<br>') ?? node.summary,
}),
author: node.author?.name,
itunes_item_image: node.image,
pubDate: parseDate(node.createdAt),
upvotes: node.numUpvotes,
comments: node.numComments,
category: node.tags,
}));
export const getList = (edges, innerSharedContent: boolean, dateSort: boolean) =>
edges.map(({ node }) => {
let link: string;
let title: string;
if (innerSharedContent && node.type === 'share') {
link = node.sharedPost.permalink;
title = node.sharedPost.title;
} else {
link = node.commentsPermalink ?? node.permalink;
title = node.title;
}
return {
id: node.id,
title,
link,
guid: node.permalink,
description: render({
image: node.image,
content: node.contentHtml?.replaceAll('\n', '<br>') ?? node.summary,
}),
author: node.author?.name,
itunes_item_image: node.image,
pubDate: dateSort ? parseDate(node.createdAt) : '',
upvotes: node.numUpvotes,
comments: node.numComments,
category: node.tags,
} as DataItem;
});

View File

@ -37,6 +37,9 @@ async function handler() {
const items = await Promise.all(
list.map((item) =>
cache.tryGet(item.link, async () => {
if (!item.securityID) {
return item;
}
const info = await getData(`${apiInfo}/${item.securityID}`);
item.description = item.description + '<br><br>' + info?.AboutCompanyText;
return item;

View File

@ -1,3 +1,4 @@
import { DataItem } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
@ -15,16 +16,20 @@ const getData = (url) =>
const getList = (data) =>
data.map((value) => {
const { ID, Description: title, Date: createdOn, NewsitemSecurities, NewsitemSectors, NewsitemIndustries } = value;
const securityID = NewsitemSecurities[0].SecurityID;
const securityID = NewsitemSecurities?.[0]?.SecurityID;
const securitySlug = NewsitemSecurities?.[0]?.SecuritySlug;
const sectors = NewsitemSectors.map((v) => v.SectorName);
const industries = NewsitemIndustries.map((v) => v.IndustryName);
return {
id: ID,
title: `${title} [${NewsitemSectors.map((v) => v.SectorName).join(', ')}]`,
title: `${title} [${sectors.join(', ')}]`,
description: title,
securityID,
link: `${baseUrl}${NewsitemSecurities[0].SecuritySlug}/${securityID}`,
link: NewsitemSecurities?.length === 0 ? '' : `${baseUrl}${securitySlug}/${securityID}?section=news`,
guid: NewsitemSecurities?.length === 0 ? ID : `${baseUrl}${securitySlug}/${securityID}`,
pubDate: parseDate(createdOn),
category: [...NewsitemIndustries.map((v) => v.IndustryName), ...NewsitemSectors.map((v) => v.SectorName)],
};
category: [...industries, ...sectors],
} as DataItem;
});
export { getData, getList };