diff --git a/app/controllers/content_distribution_controller.ts b/app/controllers/content_distribution_controller.ts new file mode 100644 index 0000000..e8f1cff --- /dev/null +++ b/app/controllers/content_distribution_controller.ts @@ -0,0 +1,13 @@ +import { inject } from '@adonisjs/core' +import { HttpContext } from '@adonisjs/core/http' + +import { SitemapGenerator } from '#services/sitemap_generator' + +export default class ContentDistributionController { + @inject() + async getSitemap({ response }: HttpContext, sitemapGenerator: SitemapGenerator) { + response.header('Content-Type', 'application/xml') + + return await sitemapGenerator.generate() + } +} diff --git a/app/services/sitemap_generator.ts b/app/services/sitemap_generator.ts new file mode 100644 index 0000000..ce416d9 --- /dev/null +++ b/app/services/sitemap_generator.ts @@ -0,0 +1,27 @@ +import cache from '@adonisjs/cache/services/main' +import { SitemapStream, streamToPromise } from 'sitemap' + +import type { PackageInfo } from '#types/main' + +export class SitemapGenerator { + constructor(protected packages: PackageInfo[]) {} + + async #generateSitemap() { + const sitemap = new SitemapStream({ hostname: 'https://packages.adonisjs.com' }) + + sitemap.write({ url: '/', changefreq: 'daily', priority: 1 }) + + this.packages.forEach((pkg) => + sitemap.write({ url: `/packages/${pkg.slug}`, changefreq: 'weekly', priority: 0.7 }), + ) + + sitemap.end() + + const buffer = await streamToPromise(sitemap) + return buffer.toString() + } + + async generate() { + return cache.getOrSet('sitemap', () => this.#generateSitemap(), { ttl: '1d' }) + } +} diff --git a/package.json b/package.json index 2140c37..4e9ae01 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "sanitize-html": "^2.11.0", "satori": "^0.10.11", "satori-html": "^0.3.2", + "sitemap": "^7.1.1", "sqlite3": "^5.1.7", "vue": "^3.4.7" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ec3ce2..490deda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ dependencies: satori-html: specifier: ^0.3.2 version: 0.3.2 + sitemap: + specifier: ^7.1.1 + version: 7.1.1 sqlite3: specifier: ^5.1.7 version: 5.1.7 @@ -2249,6 +2252,10 @@ packages: /@types/mdurl@1.0.5: resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + /@types/node@17.0.45: + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + dev: false + /@types/node@20.11.5: resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} dependencies: @@ -2270,6 +2277,12 @@ packages: htmlparser2: 8.0.2 dev: true + /@types/sax@1.2.7: + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + dependencies: + '@types/node': 20.11.5 + dev: false + /@types/semver@7.5.6: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} dev: true @@ -3019,6 +3032,10 @@ packages: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -6560,6 +6577,10 @@ packages: yoga-wasm-web: 0.3.3 dev: false + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -6692,6 +6713,17 @@ packages: totalist: 3.0.1 dev: true + /sitemap@7.1.1: + resolution: {integrity: sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.3.0 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} diff --git a/providers/app_provider.ts b/providers/app_provider.ts index cf5175a..b38be37 100644 --- a/providers/app_provider.ts +++ b/providers/app_provider.ts @@ -7,6 +7,7 @@ import type { ApplicationService } from '@adonisjs/core/types' import { PackageFetcher } from '#services/package_fetcher' import { PackagesFetcher } from '#services/packages_fetcher' +import { SitemapGenerator } from '#services/sitemap_generator' import { PackagesDataRefresher } from '#services/packages_data_refresher' export default class AppProvider { @@ -32,6 +33,8 @@ export default class AppProvider { return new PackagesFetcher(await resolver.make(PackageFetcher), packagesFile) }) + this.app.container.bind(SitemapGenerator, async () => new SitemapGenerator(packagesFile)) + /** * Helper for removing double slashes from urls */ diff --git a/start/routes.ts b/start/routes.ts index 646b7cf..a823724 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -11,9 +11,11 @@ import router from '@adonisjs/core/services/router' const PackagesController = () => import('#controllers/packages_controller') const OgImagesController = () => import('#controllers/og_images_controller') +const ContentDistributionController = () => import('#controllers/content_distribution_controller') router.get('/', [PackagesController, 'getHome']) router.get('/packages/:name', [PackagesController, 'getPackage']) router.get('/packages/:name/og.png', [OgImagesController]).as('og_image') +router.get('/sitemap.xml', [ContentDistributionController, 'getSitemap']) router.get('/healthcheck', ({ response }) => response.noContent())