diff options
Diffstat (limited to 'src/build.ts')
-rw-r--r-- | src/build.ts | 66 |
1 files changed, 50 insertions, 16 deletions
diff --git a/src/build.ts b/src/build.ts index 4c693ca07..9f9f8f3ea 100644 --- a/src/build.ts +++ b/src/build.ts @@ -3,25 +3,29 @@ import type { LogOptions } from './logger'; import type { AstroRuntime, LoadResult } from './runtime'; import { existsSync, promises as fsPromises } from 'fs'; -import { relative as pathRelative } from 'path'; +import path from 'path'; +import cheerio from 'cheerio'; import { fileURLToPath } from 'url'; import { fdir } from 'fdir'; -import { defaultLogDestination, error } from './logger.js'; +import { defaultLogDestination, error, info } from './logger.js'; import { createRuntime } from './runtime.js'; import { bundle, collectDynamicImports } from './build/bundle.js'; +import { generateSitemap } from './build/sitemap.js'; import { collectStatics } from './build/static.js'; -const { mkdir, readdir, readFile, stat, writeFile } = fsPromises; +const { mkdir, readFile, writeFile } = fsPromises; interface PageBuildOptions { astroRoot: URL; dist: URL; filepath: URL; runtime: AstroRuntime; + sitemap: boolean; statics: Set<string>; } interface PageResult { + canonicalURLs: string[]; statusCode: number; } @@ -49,14 +53,14 @@ function mergeSet(a: Set<string>, b: Set<string>) { } /** Utility for writing to file (async) */ -async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf-8' | null) { +async function writeFilep(outPath: URL, bytes: string | Buffer, encoding: 'utf8' | null) { const outFolder = new URL('./', outPath); await mkdir(outFolder, { recursive: true }); await writeFile(outPath, bytes, encoding || 'binary'); } /** Utility for writing a build result to disk */ -async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'utf-8') { +async function writeResult(result: LoadResult, outPath: URL, encoding: null | 'utf8') { if (result.statusCode === 500 || result.statusCode === 404) { error(logging, 'build', result.error || result.statusCode); } else if (result.statusCode !== 200) { @@ -75,7 +79,7 @@ function getPageType(filepath: URL): 'collection' | 'static' { /** Build collection */ async function buildCollectionPage({ astroRoot, dist, filepath, runtime, statics }: PageBuildOptions): Promise<PageResult> { - const rel = pathRelative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro + const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro const pagePath = `/${rel.replace(/\$([^.]+)\.astro$/, '$1')}`; const builtURLs = new Set<string>(); // !important: internal cache that prevents building the same URLs @@ -86,8 +90,8 @@ async function buildCollectionPage({ astroRoot, dist, filepath, runtime, statics builtURLs.add(url); if (result.statusCode === 200) { const outPath = new URL('./' + url + '/index.html', dist); - await writeResult(result, outPath, 'utf-8'); - mergeSet(statics, collectStatics(result.contents.toString('utf-8'))); + await writeResult(result, outPath, 'utf8'); + mergeSet(statics, collectStatics(result.contents.toString('utf8'))); } return result; } @@ -103,6 +107,7 @@ async function buildCollectionPage({ astroRoot, dist, filepath, runtime, statics [...result.collectionInfo.additionalURLs].map(async (url) => { // for the top set of additional URLs, we render every new URL generated const addlResult = await loadCollection(url); + builtURLs.add(url); if (addlResult && addlResult.collectionInfo) { // believe it or not, we may still have a few unbuilt pages left. this is our last crawl: await Promise.all([...addlResult.collectionInfo.additionalURLs].map(async (url2) => loadCollection(url2))); @@ -112,14 +117,16 @@ async function buildCollectionPage({ astroRoot, dist, filepath, runtime, statics } return { + canonicalURLs: [...builtURLs].filter((url) => !url.endsWith('/1')), // note: canonical URLs are controlled by the collection, so these are canonical (but exclude "/1" pages as those are duplicates of the index) statusCode: result.statusCode, }; } /** Build static page */ -async function buildStaticPage({ astroRoot, dist, filepath, runtime, statics }: PageBuildOptions): Promise<PageResult> { - const rel = pathRelative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro +async function buildStaticPage({ astroRoot, dist, filepath, runtime, sitemap, statics }: PageBuildOptions): Promise<PageResult> { + const rel = path.relative(fileURLToPath(astroRoot) + '/pages', fileURLToPath(filepath)); // pages/index.astro const pagePath = `/${rel.replace(/\.(astro|md)$/, '')}`; + let canonicalURLs: string[] = []; let relPath = './' + rel.replace(/\.(astro|md)$/, '.html'); if (!relPath.endsWith('index.html')) { @@ -129,12 +136,21 @@ async function buildStaticPage({ astroRoot, dist, filepath, runtime, statics }: const outPath = new URL(relPath, dist); const result = await runtime.load(pagePath); - await writeResult(result, outPath, 'utf-8'); + await writeResult(result, outPath, 'utf8'); + if (result.statusCode === 200) { - mergeSet(statics, collectStatics(result.contents.toString('utf-8'))); + mergeSet(statics, collectStatics(result.contents.toString('utf8'))); + + // get Canonical URL (if user has specified one manually, use that) + if (sitemap) { + const $ = cheerio.load(result.contents); + const canonicalTag = $('link[rel="canonical"]'); + canonicalURLs.push(canonicalTag.attr('href') || pagePath.replace(/index$/, '')); + } } return { + canonicalURLs, statusCode: result.statusCode, }; } @@ -162,6 +178,7 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const collectImportsOptions = { astroConfig, logging, resolvePackageUrl, mode }; const pages = await allPages(pageRoot); + let builtURLs: string[] = []; try { await Promise.all( @@ -169,11 +186,13 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const filepath = new URL(`file://${pathname}`); const pageType = getPageType(filepath); - const pageOptions: PageBuildOptions = { astroRoot, dist, filepath, runtime, statics }; + const pageOptions: PageBuildOptions = { astroRoot, dist, filepath, runtime, sitemap: astroConfig.sitemap, statics }; if (pageType === 'collection') { - await buildCollectionPage(pageOptions); + const { canonicalURLs } = await buildCollectionPage(pageOptions); + builtURLs.push(...canonicalURLs); } else { - await buildStaticPage(pageOptions); + const { canonicalURLs } = await buildStaticPage(pageOptions); + builtURLs.push(...canonicalURLs); } mergeSet(imports, await collectDynamicImports(filepath, collectImportsOptions)); @@ -211,7 +230,7 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { const publicFiles = (await new fdir().withFullPaths().crawl(fileURLToPath(pub)).withPromise()) as string[]; for (const filepath of publicFiles) { const fileUrl = new URL(`file://${filepath}`); - const rel = pathRelative(pub.pathname, fileUrl.pathname); + const rel = path.relative(pub.pathname, fileUrl.pathname); const outUrl = new URL('./' + rel, dist); const bytes = await readFile(fileUrl); @@ -219,6 +238,21 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> { } } + // build sitemap + if (astroConfig.sitemap && astroConfig.site) { + const sitemap = generateSitemap( + builtURLs.map((url) => ({ + canonicalURL: new URL( + path.extname(url) ? url : url.replace(/\/?$/, '/'), // add trailing slash if there’s no extension + astroConfig.site + ).href, + })) + ); + await writeFile(new URL('./sitemap.xml', dist), sitemap, 'utf8'); + } else if (astroConfig.sitemap) { + info(logging, 'tip', `Set your "site" in astro.config.mjs to generate a sitemap.xml`); + } + await runtime.shutdown(); return 0; } |