Add Astro.request.canonicalURL and Astro.site to global (#199)
This commit is contained in:
parent
d2eb413a6e
commit
7184149514
25 changed files with 234 additions and 98 deletions
5
.changeset/cold-windows-exercise.md
Normal file
5
.changeset/cold-windows-exercise.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Added canonical URL and site globals for .astro files
|
22
docs/api.md
22
docs/api.md
|
@ -4,14 +4,6 @@
|
|||
|
||||
The `Astro` global is available in all contexts in `.astro` files. It has the following functions:
|
||||
|
||||
#### `config`
|
||||
|
||||
`Astro.config` returns an object with the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
| :----- | :------- | :--------------------------------------------------------------------------------------------------------- |
|
||||
| `site` | `string` | Your website’s public root domain. Set it with `site: "https://mysite.com"` in your [Astro config][config] |
|
||||
|
||||
#### `fetchContent()`
|
||||
|
||||
`Astro.fetchContent()` is a way to load local `*.md` files into your static site setup. You can either use this on its own, or within [Astro Collections][docs-collections].
|
||||
|
@ -47,9 +39,16 @@ const data = Astro.fetchContent('../pages/post/*.md'); // returns an array of po
|
|||
|
||||
`Astro.request` returns an object with the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
| :---- | :---- | :------------------------------------- |
|
||||
| `url` | `URL` | The URL of the request being rendered. |
|
||||
| Name | Type | Description |
|
||||
| :------------- | :---- | :---------------------------------------------- |
|
||||
| `url` | `URL` | The URL of the request being rendered. |
|
||||
| `canonicalURL` | `URL` | [Canonical URL][canonical] of the current page. |
|
||||
|
||||
⚠️ Temporary restriction: this is only accessible in top-level pages and not in sub-components.
|
||||
|
||||
#### `site`
|
||||
|
||||
`Astro.site` returns a `URL` made from `buildOptions.site` in your Astro config. If undefined, this will return a URL generated from `localhost`.
|
||||
|
||||
### `collection`
|
||||
|
||||
|
@ -147,6 +146,7 @@ Astro will generate an RSS 2.0 feed at `/feed/[collection].xml` (for example, `/
|
|||
<link rel="alternate" type="application/rss+xml" title="My RSS Feed" href="/feed/podcast.xml" />
|
||||
```
|
||||
|
||||
[canonical]: https://en.wikipedia.org/wiki/Canonical_link_element
|
||||
[config]: ../README.md#%EF%B8%8F-configuration
|
||||
[docs-collections]: ./collections.md
|
||||
[rss]: #-rss-feed
|
||||
|
|
|
@ -4,6 +4,9 @@ export let title: string;
|
|||
export let description: string;
|
||||
export let image: string | undefined;
|
||||
export let type: string | undefined;
|
||||
export let next: string | undefined;
|
||||
export let prev: string | undefined;
|
||||
export let canonicalURL: string | undefined;
|
||||
|
||||
// internal data
|
||||
const OG_TYPES = {
|
||||
|
@ -17,6 +20,10 @@ const OG_TYPES = {
|
|||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="stylesheet" href="/global.css" />
|
||||
<link rel="sitemap" href="/sitemap.xml" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
{next && <link rel="next" href={next} />}
|
||||
{prev && <link rel="prev" href={prev} />}
|
||||
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:title" content={title} />
|
||||
|
|
|
@ -21,7 +21,11 @@ let firstThree = allPosts.slice(0, 3);
|
|||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<MainHead title={title} description={description} />
|
||||
<MainHead
|
||||
title={title}
|
||||
description={description}
|
||||
canonicalURL={Astro.request.canonicalURL.href}
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -17,7 +17,7 @@ import { buildCollectionPage, buildStaticPage, getPageType } from './build/page.
|
|||
import { generateSitemap } from './build/sitemap.js';
|
||||
import { logURLStats, collectBundleStats, mapBundleStatsToURLStats } from './build/stats.js';
|
||||
import { getDistPath, sortSet, stopTimer } from './build/util.js';
|
||||
import { debug, defaultLogDestination, error, info, trapWarn } from './logger.js';
|
||||
import { debug, defaultLogDestination, error, info, warn, trapWarn } from './logger.js';
|
||||
import { createRuntime } from './runtime.js';
|
||||
|
||||
const logging: LogOptions = {
|
||||
|
@ -55,6 +55,9 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
|
|||
dest: defaultLogDestination,
|
||||
};
|
||||
|
||||
// warn users if missing config item in build that may result in broken SEO (can’t disable, as they should provide this)
|
||||
warn(logging, 'config', `Set "buildOptions.site" to generate correct canonical URLs and sitemap`);
|
||||
|
||||
const mode: RuntimeMode = 'production';
|
||||
const runtime = await createRuntime(astroConfig, { mode, logging: runtimeLogging });
|
||||
const { runtimeConfig } = runtime;
|
||||
|
@ -170,8 +173,6 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
|
|||
await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
|
||||
info(logging, 'build', green('✔'), 'sitemap built.');
|
||||
debug(logging, 'build', `built sitemap [${stopTimer(timer.sitemap)}]`);
|
||||
} else if (astroConfig.buildOptions.sitemap) {
|
||||
info(logging, 'tip', `Set "buildOptions.site" in astro.config.mjs to generate a sitemap.xml, or set "buildOptions.sitemap: false" to disable this message.`);
|
||||
}
|
||||
|
||||
// write to disk and free up memory
|
||||
|
|
|
@ -27,7 +27,7 @@ export function generateRSS<T>(input: { data: T[]; site: string } & CollectionRS
|
|||
// title, description, customData
|
||||
xml += `<title><![CDATA[${input.title}]]></title>`;
|
||||
xml += `<description><![CDATA[${input.description}]]></description>`;
|
||||
xml += `<link>${canonicalURL('/feed/' + filename + '.xml', input.site)}</link>`;
|
||||
xml += `<link>${canonicalURL('/feed/' + filename + '.xml', input.site).href}</link>`;
|
||||
if (typeof input.customData === 'string') xml += input.customData;
|
||||
|
||||
// items
|
||||
|
@ -40,7 +40,7 @@ export function generateRSS<T>(input: { data: T[]; site: string } & CollectionRS
|
|||
if (!result.title) throw new Error(`[${filename}] rss.item() returned object but required "title" is missing.`);
|
||||
if (!result.link) throw new Error(`[${filename}] rss.item() returned object but required "link" is missing.`);
|
||||
xml += `<title><![CDATA[${result.title}]]></title>`;
|
||||
xml += `<link>${canonicalURL(result.link, input.site)}</link>`;
|
||||
xml += `<link>${canonicalURL(result.link, input.site).href}</link>`;
|
||||
if (result.description) xml += `<description><![CDATA[${result.description}]]></description>`;
|
||||
if (result.pubDate) {
|
||||
// note: this should be a Date, but if user provided a string or number, we can work with that, too.
|
||||
|
|
|
@ -4,18 +4,18 @@ import { canonicalURL } from './util';
|
|||
|
||||
/** Construct sitemap.xml given a set of URLs */
|
||||
export function generateSitemap(buildState: BuildOutput, site: string): string {
|
||||
const pages: string[] = [];
|
||||
const uniqueURLs = new Set<string>();
|
||||
|
||||
// TODO: find way to respect <link rel="canonical"> URLs here
|
||||
// TODO: find way to exclude pages from sitemap
|
||||
|
||||
// look through built pages, only add HTML
|
||||
for (const id of Object.keys(buildState)) {
|
||||
if (buildState[id].contentType !== 'text/html' || id.endsWith('/1/index.html')) continue; // note: exclude auto-generated "page 1" pages (duplicates of index)
|
||||
let url = canonicalURL(id.replace(/index\.html$/, ''), site);
|
||||
pages.push(url);
|
||||
if (buildState[id].contentType !== 'text/html') continue;
|
||||
uniqueURLs.add(canonicalURL(id, site).href);
|
||||
}
|
||||
|
||||
const pages = [...uniqueURLs];
|
||||
pages.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })); // sort alphabetically so sitemap is same each time
|
||||
|
||||
let sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;
|
||||
|
|
|
@ -5,11 +5,11 @@ import path from 'path';
|
|||
import { fileURLToPath, URL } from 'url';
|
||||
|
||||
/** Normalize URL to its canonical form */
|
||||
export function canonicalURL(url: string, base?: string): string {
|
||||
return new URL(
|
||||
path.extname(url) ? url : url.replace(/(\/+)?$/, '/'), // add trailing slash if there’s no extension
|
||||
base
|
||||
).href;
|
||||
export function canonicalURL(url: string, base?: string): URL {
|
||||
let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical
|
||||
pathname = pathname.replace(/\/1\/?$/, ''); // neither is a trailing /1/ (impl. detail of collections)
|
||||
if (!path.extname(pathname)) pathname = pathname.replace(/(\/+)?$/, '/'); // add trailing slash if there’s no extension
|
||||
return new URL(pathname, base);
|
||||
}
|
||||
|
||||
/** Sort a Set */
|
||||
|
|
|
@ -65,7 +65,7 @@ function printHelp() {
|
|||
${colors.bold('Flags:')}
|
||||
--config <path> Specify the path to the Astro config file.
|
||||
--project-root <path> Specify the path to the project root folder.
|
||||
--no-sitemap Disable sitemap generation (build only).
|
||||
--no-sitemap Disable sitemap generation (build only).
|
||||
--version Show the version number and exit.
|
||||
--help Show this help message.
|
||||
`);
|
||||
|
|
|
@ -3,16 +3,18 @@ import type { AstroConfig, ValidExtensionPlugins } from '../../@types/astro';
|
|||
import type { Ast, Script, Style, TemplateNode } from 'astro-parser';
|
||||
import type { TransformResult } from '../../@types/astro';
|
||||
|
||||
import 'source-map-support/register.js';
|
||||
import eslexer from 'es-module-lexer';
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { walk } from 'estree-walker';
|
||||
import _babelGenerator from '@babel/generator';
|
||||
import babelParser from '@babel/parser';
|
||||
import { codeFrameColumns } from '@babel/code-frame';
|
||||
import * as babelTraverse from '@babel/traverse';
|
||||
import { ImportDeclaration, ExportNamedDeclaration, VariableDeclarator, Identifier } from '@babel/types';
|
||||
import { warn } from '../../logger.js';
|
||||
import { error, warn } from '../../logger.js';
|
||||
import { fetchContent } from './content.js';
|
||||
import { isFetchContent } from './utils.js';
|
||||
import { yellow } from 'kleur/colors';
|
||||
|
@ -77,7 +79,12 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
|||
switch (val.type) {
|
||||
case 'MustacheTag': {
|
||||
// FIXME: this won't work when JSX element can appear in attributes (rare but possible).
|
||||
result[attr.name] = '(' + val.expression.codeChunks[0] + ')';
|
||||
const codeChunks = val.expression.codeChunks[0];
|
||||
if (codeChunks) {
|
||||
result[attr.name] = '(' + codeChunks + ')';
|
||||
} else {
|
||||
throw new Error(`Parse error: ${attr.name}={}`); // if bad codeChunk, throw error
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'Text':
|
||||
|
@ -388,7 +395,7 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
|
|||
if (!id || !init || id.type !== 'Identifier') continue;
|
||||
if (init.type === 'AwaitExpression') {
|
||||
init = init.argument;
|
||||
const shortname = path.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
|
||||
const shortname = path.posix.relative(compileOptions.astroConfig.projectRoot.pathname, state.filename);
|
||||
warn(compileOptions.logging, shortname, yellow('awaiting Astro.fetchContent() not necessary'));
|
||||
}
|
||||
if (init.type !== 'CallExpression') continue;
|
||||
|
@ -572,29 +579,36 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
|
|||
if (!name) {
|
||||
throw new Error('AHHHH');
|
||||
}
|
||||
const attributes = getAttributes(node.attributes);
|
||||
try {
|
||||
const attributes = getAttributes(node.attributes);
|
||||
|
||||
outSource += outSource === '' ? '' : ',';
|
||||
if (node.type === 'Slot') {
|
||||
outSource += `(children`;
|
||||
return;
|
||||
}
|
||||
const COMPONENT_NAME_SCANNER = /^[A-Z]/;
|
||||
if (!COMPONENT_NAME_SCANNER.test(name)) {
|
||||
outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
return;
|
||||
}
|
||||
const [componentName, componentKind] = name.split(':');
|
||||
const componentImportData = components[componentName];
|
||||
if (!componentImportData) {
|
||||
throw new Error(`Unknown Component: ${componentName}`);
|
||||
}
|
||||
const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
|
||||
if (wrapperImport) {
|
||||
importExportStatements.add(wrapperImport);
|
||||
}
|
||||
outSource += outSource === '' ? '' : ',';
|
||||
if (node.type === 'Slot') {
|
||||
outSource += `(children`;
|
||||
return;
|
||||
}
|
||||
const COMPONENT_NAME_SCANNER = /^[A-Z]/;
|
||||
if (!COMPONENT_NAME_SCANNER.test(name)) {
|
||||
outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
return;
|
||||
}
|
||||
const [componentName, componentKind] = name.split(':');
|
||||
const componentImportData = components[componentName];
|
||||
if (!componentImportData) {
|
||||
throw new Error(`Unknown Component: ${componentName}`);
|
||||
}
|
||||
const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
|
||||
if (wrapperImport) {
|
||||
importExportStatements.add(wrapperImport);
|
||||
}
|
||||
|
||||
outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
|
||||
} catch (err) {
|
||||
// handle errors in scope with filename
|
||||
const rel = filename.replace(fileURLToPath(astroConfig.projectRoot), '');
|
||||
// TODO: return actual codeframe here
|
||||
error(compileOptions.logging, rel, err.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'Attribute': {
|
||||
|
|
|
@ -124,6 +124,7 @@ export async function compileComponent(
|
|||
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
|
||||
): Promise<CompileResult> {
|
||||
const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
|
||||
const site = compileOptions.astroConfig.buildOptions.site || `http://localhost:${compileOptions.astroConfig.devOptions.port}`;
|
||||
|
||||
// return template
|
||||
let modJsx = `
|
||||
|
@ -137,7 +138,8 @@ import { h, Fragment } from '${internalImport('h.js')}';
|
|||
const __astroRequestSymbol = Symbol('astro.request');
|
||||
async function __render(props, ...children) {
|
||||
const Astro = {
|
||||
request: props[__astroRequestSymbol]
|
||||
request: props[__astroRequestSymbol] || {},
|
||||
site: new URL('/', ${JSON.stringify(site)}),
|
||||
};
|
||||
|
||||
${result.script}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'source-map-support/register.js';
|
||||
import type { AstroConfig } from './@types/astro';
|
||||
|
||||
import 'source-map-support/register.js';
|
||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
|
@ -9,25 +10,36 @@ const type = (thing: any): string => (Array.isArray(thing) ? 'Array' : typeof th
|
|||
/** Throws error if a user provided an invalid config. Manually-implemented to avoid a heavy validation library. */
|
||||
function validateConfig(config: any): void {
|
||||
// basic
|
||||
if (config === undefined || config === null) throw new Error(`[astro config] Config empty!`);
|
||||
if (typeof config !== 'object') throw new Error(`[astro config] Expected object, received ${typeof config}`);
|
||||
if (config === undefined || config === null) throw new Error(`[config] Config empty!`);
|
||||
if (typeof config !== 'object') throw new Error(`[config] Expected object, received ${typeof config}`);
|
||||
|
||||
// strings
|
||||
for (const key of ['projectRoot', 'astroRoot', 'dist', 'public', 'site']) {
|
||||
for (const key of ['projectRoot', 'astroRoot', 'dist', 'public']) {
|
||||
if (config[key] !== undefined && config[key] !== null && typeof config[key] !== 'string') {
|
||||
throw new Error(`[astro config] ${key}: ${JSON.stringify(config[key])}\n Expected string, received ${type(config[key])}.`);
|
||||
throw new Error(`[config] ${key}: ${JSON.stringify(config[key])}\n Expected string, received ${type(config[key])}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// booleans
|
||||
for (const key of ['sitemap']) {
|
||||
if (config[key] !== undefined && config[key] !== null && typeof config[key] !== 'boolean') {
|
||||
throw new Error(`[astro config] ${key}: ${JSON.stringify(config[key])}\n Expected boolean, received ${type(config[key])}.`);
|
||||
throw new Error(`[config] ${key}: ${JSON.stringify(config[key])}\n Expected boolean, received ${type(config[key])}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// buildOptions
|
||||
if (config.buildOptions && config.buildOptions.site !== undefined) {
|
||||
if (typeof config.buildOptions.site !== 'string') throw new Error(`[config] buildOptions.site is not a string`);
|
||||
try {
|
||||
new URL(config.buildOptions.site);
|
||||
} catch (err) {
|
||||
throw new Error('[config] buildOptions.site must be a valid URL');
|
||||
}
|
||||
}
|
||||
|
||||
// devOptions
|
||||
if (typeof config.devOptions?.port !== 'number') {
|
||||
throw new Error(`[astro config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`);
|
||||
throw new Error(`[config] devOptions.port: Expected number, received ${type(config.devOptions?.port)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ import type { AstroConfig } from './@types/astro';
|
|||
import type { LogOptions } from './logger.js';
|
||||
|
||||
import { logger as snowpackLogger } from 'snowpack';
|
||||
import { bold, green } from 'kleur/colors';
|
||||
import { green } from 'kleur/colors';
|
||||
import http from 'http';
|
||||
import { relative as pathRelative } from 'path';
|
||||
import path from 'path';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { defaultLogDestination, error, info, parseError } from './logger.js';
|
||||
import { createRuntime } from './runtime.js';
|
||||
|
@ -63,7 +63,7 @@ export default async function dev(astroConfig: AstroConfig) {
|
|||
switch (result.type) {
|
||||
case 'parse-error': {
|
||||
const err = result.error;
|
||||
err.filename = pathRelative(projectRoot.pathname, err.filename);
|
||||
if (err.filename) err.filename = path.posix.relative(projectRoot.pathname, err.filename);
|
||||
parseError(logging, err);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -114,6 +114,10 @@ export function table(opts: LogOptions, columns: number[]) {
|
|||
|
||||
/** Pretty format error for display */
|
||||
export function parseError(opts: LogOptions, err: CompileError) {
|
||||
if (!err.frame) {
|
||||
return error(opts, 'parse-error', err.message || err);
|
||||
}
|
||||
|
||||
let frame = err.frame
|
||||
// Switch colons for pipes
|
||||
.replace(/^([0-9]+)(:)/gm, `${bold('$1')} │`)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import 'source-map-support/register.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, SnowpackConfig } from 'snowpack';
|
||||
import type { AstroConfig, CollectionResult, CollectionRSS, CreateCollection, Params, RuntimeMode } from './@types/astro';
|
||||
import type { LogOptions } from './logger';
|
||||
import type { CompileError } from 'astro-parser';
|
||||
import { debug, info } from './logger.js';
|
||||
import { searchForPage } from './search.js';
|
||||
import type { LogOptions } from './logger';
|
||||
import type { AstroConfig, CollectionResult, CollectionRSS, CreateCollection, Params, RuntimeMode } from './@types/astro';
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack';
|
||||
import { canonicalURL } from './build/util.js';
|
||||
import { debug, info } from './logger.js';
|
||||
import { searchForPage } from './search.js';
|
||||
|
||||
// We need to use require.resolve for snowpack plugins, so create a require function here.
|
||||
import { createRequire } from 'module';
|
||||
|
@ -49,9 +50,10 @@ snowpackLogger.level = 'silent';
|
|||
/** Pass a URL to Astro to resolve and build */
|
||||
async function load(config: RuntimeConfig, rawPathname: string | undefined): Promise<LoadResult> {
|
||||
const { logging, backendSnowpackRuntime, frontendSnowpack } = config;
|
||||
const { astroRoot } = config.astroConfig;
|
||||
const { astroRoot, buildOptions, devOptions } = config.astroConfig;
|
||||
|
||||
const fullurl = new URL(rawPathname || '/', 'https://example.org/');
|
||||
let origin = buildOptions.site ? new URL(buildOptions.site).origin : `http://localhost:${devOptions.port}`;
|
||||
const fullurl = new URL(rawPathname || '/', origin);
|
||||
|
||||
const reqPath = decodeURI(fullurl.pathname);
|
||||
info(logging, 'access', reqPath);
|
||||
|
@ -208,6 +210,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
|
|||
request: {
|
||||
// params should go here when implemented
|
||||
url: requestURL,
|
||||
canonicalURL: canonicalURL(requestURL.pathname, requestURL.origin),
|
||||
},
|
||||
children: [],
|
||||
props: { collection },
|
||||
|
|
45
packages/astro/test/astro-global.test.js
Normal file
45
packages/astro/test/astro-global.test.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
|
||||
const Global = suite('Astro.*');
|
||||
|
||||
setup(Global, './fixtures/astro-global');
|
||||
|
||||
Global('Astro.request.url', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
|
||||
assert.equal(result.statusCode, 200);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#pathname').text(), '/');
|
||||
});
|
||||
|
||||
Global('Astro.request.canonicalURL', async (context) => {
|
||||
// given a URL, expect the following canonical URL
|
||||
const canonicalURLs = {
|
||||
'/': 'https://mysite.dev/',
|
||||
'/post/post': 'https://mysite.dev/post/post/',
|
||||
'/posts': 'https://mysite.dev/posts/',
|
||||
'/posts/1': 'https://mysite.dev/posts/', // should be the same as /posts
|
||||
'/posts/2': 'https://mysite.dev/posts/2/',
|
||||
};
|
||||
|
||||
for (const [url, canonicalURL] of Object.entries(canonicalURLs)) {
|
||||
const result = await context.runtime.load(url);
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('link[rel="canonical"]').attr('href'), canonicalURL);
|
||||
}
|
||||
});
|
||||
|
||||
Global('Astro.site', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
|
||||
assert.equal(result.statusCode, 200);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('#site').attr('href'), 'https://mysite.dev');
|
||||
});
|
||||
|
||||
Global.run();
|
|
@ -1,19 +0,0 @@
|
|||
import { suite } from 'uvu';
|
||||
import * as assert from 'uvu/assert';
|
||||
import { doc } from './test-utils.js';
|
||||
import { setup } from './helpers.js';
|
||||
|
||||
const Request = suite('Astro.request');
|
||||
|
||||
setup(Request, './fixtures/astro-request');
|
||||
|
||||
Request('Astro.request available', async (context) => {
|
||||
const result = await context.runtime.load('/');
|
||||
|
||||
assert.equal(result.statusCode, 200);
|
||||
|
||||
const $ = doc(result.contents);
|
||||
assert.equal($('h1').text(), '/');
|
||||
});
|
||||
|
||||
Request.run();
|
6
packages/astro/test/fixtures/astro-global/astro.config.mjs
vendored
Normal file
6
packages/astro/test/fixtures/astro-global/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
buildOptions: {
|
||||
site: 'https://mysite.dev',
|
||||
sitemap: false,
|
||||
},
|
||||
};
|
12
packages/astro/test/fixtures/astro-global/src/layouts/post.astro
vendored
Normal file
12
packages/astro/test/fixtures/astro-global/src/layouts/post.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
export let content;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>{content.title}</title>
|
||||
<link rel="canonical" href={Astro.request.canonicalURL.href}>
|
||||
</head>
|
||||
<body>
|
||||
<slot></slot>
|
||||
</body>
|
||||
</html>
|
28
packages/astro/test/fixtures/astro-global/src/pages/$posts.astro
vendored
Normal file
28
packages/astro/test/fixtures/astro-global/src/pages/$posts.astro
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
export let collection;
|
||||
|
||||
export function createCollection() {
|
||||
return {
|
||||
async data() {
|
||||
const data = Astro.fetchContent('./post/*.md');
|
||||
return data;
|
||||
},
|
||||
pageSize: 1,
|
||||
};
|
||||
}
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>All Posts</title>
|
||||
<link rel="canonical" href={Astro.request.canonicalURL.href} />
|
||||
</head>
|
||||
<body>
|
||||
{collection.data.map((data) => (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
<a href={data.url}>Read</a>
|
||||
</div>
|
||||
))}
|
||||
</body>
|
||||
</html>
|
10
packages/astro/test/fixtures/astro-global/src/pages/index.astro
vendored
Normal file
10
packages/astro/test/fixtures/astro-global/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<link rel="canonical" href={Astro.request.canonicalURL.href}>
|
||||
</head>
|
||||
<body>
|
||||
<div id="pathname">{Astro.request.url.pathname}</div>
|
||||
<a id="site" href={Astro.site.origin}>Home</a>
|
||||
</body>
|
||||
</html>
|
6
packages/astro/test/fixtures/astro-global/src/pages/post/post-2.md
vendored
Normal file
6
packages/astro/test/fixtures/astro-global/src/pages/post/post-2.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: 'My Post 2'
|
||||
layout: '../../layouts/post.astro'
|
||||
---
|
||||
|
||||
# Post 2
|
6
packages/astro/test/fixtures/astro-global/src/pages/post/post.md
vendored
Normal file
6
packages/astro/test/fixtures/astro-global/src/pages/post/post.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: 'My Post'
|
||||
layout: '../../layouts/post.astro'
|
||||
---
|
||||
|
||||
# My Post
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
let path = Astro.request.url.pathname;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body>
|
||||
<h1>{path}</h1>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue