diff --git a/app.config.ts b/app.config.ts index a75738a..a9f684b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,8 +1,11 @@ import { defineConfig } from '@solidjs/start/config'; export default defineConfig({ - ssr: false, + ssr: true, server: { - static: true, + preset: 'github-pages', + prerender: { + crawlLinks: true, + }, }, }); diff --git a/package.json b/package.json index ce74970..30d2796 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "type": "module", "scripts": { "dev": "vinxi dev", - "build": "vinxi build", - "start": "vinxi start" + "build": "pnpm run initdata && vinxi build", + "start": "vinxi start", + "initdata": "node ./src/initdata.js" }, "dependencies": { "@iconify-icon/solid": "^2.1.1", diff --git a/src/initdata.js b/src/initdata.js new file mode 100644 index 0000000..129238c --- /dev/null +++ b/src/initdata.js @@ -0,0 +1,80 @@ +import fs from 'node:fs/promises'; + +await fs.rm('./.output/data', { recursive: true, force: true }); +await fs.mkdir('./.output/data', { recursive: true }); + +const initServerData = async () => { + const fediverseObserver = await ( + await fetch('https://api.fediverse.observer/', { + body: '{"query":"{nodes(softwarename:\\"mbin\\" status: \\"UP\\"){domain}}"}', + method: 'POST', + }) + ).json(); + + /** @type string[] */ + const servers = fediverseObserver.data.nodes.map((v) => v.domain); + + const serversJson = ( + await Promise.allSettled( + servers.map(async (serverHost) => { + console.log('START:', serverHost); + + const jsonNodeInfo = await ( + await fetch(`https://${serverHost}/nodeinfo/2.1.json`) + ).json(); + + if (jsonNodeInfo.software.name != 'mbin') { + throw new Error( + `${serverHost} software does not match mbin (skipped)`, + ); + } + + const jsonApiInfo = await ( + await fetch(`https://${serverHost}/api/info`) + ).json(); + if (jsonApiInfo.websiteDomain != serverHost) { + throw new Error(`${serverHost} api not setup correctly (skipped)`); + } + const jsonApiInstance = await ( + await fetch(`https://${serverHost}/api/instance`) + ).json(); + const jsonApiDefederated = await ( + await fetch(`https://${serverHost}/api/defederated`) + ).json(); + + console.log('FINISH:', serverHost); + + /** @type import('./routes/servers').Server */ + const server = { + domain: serverHost, + version: jsonNodeInfo.software.version, + name: jsonNodeInfo.metadata.nodeName, + description: jsonNodeInfo.metadata.nodeDescription, + openRegistrations: jsonNodeInfo.openRegistrations, + federationEnabled: jsonApiInfo.websiteFederationEnabled, + language: jsonApiInfo.websiteDefaultLang ?? 'en', + contactEmail: jsonApiInfo.websiteContactEmail, + totalUsers: jsonNodeInfo.usage.users.total, + activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear, + activeMonthUsers: jsonNodeInfo.usage.users.activeMonth, + localPosts: jsonNodeInfo.usage.localPosts, + localComments: jsonNodeInfo.usage.localComments, + pages: jsonApiInstance, + defederated: jsonApiDefederated.instances ?? [], + }; + + return server; + }), + ) + ) + .filter((v) => v.status == 'fulfilled') + .map((v) => v.value); + + fs.writeFile( + './.output/data/servers.json', + JSON.stringify(serversJson), + 'utf8', + ); +}; + +await initServerData(); diff --git a/src/routes/servers.tsx b/src/routes/servers.tsx index 26f19ee..7e4e3fa 100644 --- a/src/routes/servers.tsx +++ b/src/routes/servers.tsx @@ -1,5 +1,4 @@ import { For, Show, createSignal } from 'solid-js'; -import { createAsync, cache } from '@solidjs/router'; import Markdown from '~/components/Markdown'; import Chip from '~/components/Chip'; import { Button } from '~/components/ui/button'; @@ -18,8 +17,11 @@ import { AccordionItem, AccordionTrigger, } from '~/components/ui/accordion'; +import serversJson from '../../.output/data/servers.json'; -type Server = { +const servers = serversJson as Server[]; + +export interface Server { domain: string; version: string; name: string; @@ -41,7 +43,7 @@ type Server = { terms?: string; }; defederated: string[]; -}; +} const pageNames: Required = { about: 'About', @@ -51,92 +53,16 @@ const pageNames: Required = { terms: 'Terms of Service', }; -const getServers = cache(async () => { - 'use server'; - - const fediverseObserver = await ( - await fetch('https://api.fediverse.observer/', { - body: '{"query":"{nodes(softwarename:\\"mbin\\" status: \\"UP\\"){domain}}"}', - method: 'POST', - }) - ).json(); - - const servers: string[] = fediverseObserver.data.nodes.map((v) => v.domain); - - return ( - await Promise.allSettled( - servers.map(async (serverHost) => { - console.log('START:', serverHost); - - const jsonNodeInfo = await ( - await fetch(`https://${serverHost}/nodeinfo/2.1.json`) - ).json(); - - if (jsonNodeInfo.software.name != 'mbin') { - throw new Error( - `${serverHost} software does not match mbin (skipped)`, - ); - } - - const jsonApiInfo = await ( - await fetch(`https://${serverHost}/api/info`) - ).json(); - if (jsonApiInfo.websiteDomain != serverHost) { - throw new Error(`${serverHost} api not setup correctly (skipped)`); - } - const jsonApiInstance = await ( - await fetch(`https://${serverHost}/api/instance`) - ).json(); - const jsonApiDefederated = await ( - await fetch(`https://${serverHost}/api/defederated`) - ).json(); - - console.log('FINISH:', serverHost); - - const server: Server = { - domain: serverHost, - version: jsonNodeInfo.software.version, - name: jsonNodeInfo.metadata.nodeName, - description: jsonNodeInfo.metadata.nodeDescription, - openRegistrations: jsonNodeInfo.openRegistrations, - federationEnabled: jsonApiInfo.websiteFederationEnabled, - language: jsonApiInfo.websiteDefaultLang ?? 'en', - contactEmail: jsonApiInfo.websiteContactEmail, - totalUsers: jsonNodeInfo.usage.users.total, - activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear, - activeMonthUsers: jsonNodeInfo.usage.users.activeMonth, - localPosts: jsonNodeInfo.usage.localPosts, - localComments: jsonNodeInfo.usage.localComments, - pages: jsonApiInstance, - defederated: jsonApiDefederated.instances ?? [], - }; - - return server; - }), - ) - ) - .filter((v) => v.status == 'fulfilled') - .map((v) => v.value as Server); -}, 'users'); - -export const route = { - load: () => getServers(), -}; - const languageNames = new Intl.DisplayNames(['en'], { type: 'language', }); export default function ServersPage() { - const servers = createAsync(() => getServers()); - const [filterRegistration, setFilterRegistration] = createSignal(true); const [langFilter, setLangFilter] = createSignal(''); const resultServers = () => { - if (!servers()) return undefined; - - return servers()!.filter( + return servers.filter( (server) => (!filterRegistration() || server.openRegistrations) && (!langFilter() || server.language == langFilter()), @@ -158,7 +84,7 @@ export default function ServersPage() { Language: