diff --git a/.changeset/twelve-oranges-approve.md b/.changeset/twelve-oranges-approve.md new file mode 100644 index 000000000..25ee7464c --- /dev/null +++ b/.changeset/twelve-oranges-approve.md @@ -0,0 +1,11 @@ +--- +'astro': patch +--- + +Provides first-class support for a site deployed to a subpath + +Now you can deploy your site to a subpath more easily. Astro will use your `buildOptions.site` URL and host the dev server from there. + +If your site config is `http://example.com/blog` you will need to go to `http://localhost:3000/blog/` in dev and when using `astro preview`. + +Includes a helpful 404 page when encountering this in dev and preview. \ No newline at end of file diff --git a/examples/subpath/README.md b/examples/subpath/README.md new file mode 100644 index 000000000..d1175d61a --- /dev/null +++ b/examples/subpath/README.md @@ -0,0 +1,45 @@ +# Astro Starter Kit: A site deployed to a subpath + +``` +npm init astro -- --template subpath +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/snowpackjs/astro/tree/latest/examples/subpath) + +> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! + +## 🚀 Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +``` +/ +├── public/ +├── src/ +│ └── components/ +│ └── Time.jsx +│ └── pages/ +│ └── index.astro +└── package.json +``` + +Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + +Any static assets, like images, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +|:---------------- |:-------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:3000` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | + +## 👀 Want to learn more? + +Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/subpath/astro.config.mjs b/examples/subpath/astro.config.mjs new file mode 100644 index 000000000..989f60121 --- /dev/null +++ b/examples/subpath/astro.config.mjs @@ -0,0 +1,16 @@ +// Full Astro Configuration API Documentation: +// https://docs.astro.build/reference/configuration-reference + +// @type-check enabled! +// VSCode and other TypeScript-enabled text editors will provide auto-completion, +// helpful tooltips, and warnings if your exported object is invalid. +// You can disable this by removing "@ts-check" and `@type` comments below. + +// @ts-check +export default /** @type {import('astro').AstroUserConfig} */ ({ + // Comment out "renderers: []" to enable Astro's default component support. + buildOptions: { + site: 'http://example.com/blog' + }, + renderers: ['@astrojs/renderer-react'], +}); diff --git a/examples/subpath/package.json b/examples/subpath/package.json new file mode 100644 index 000000000..2aaf016c5 --- /dev/null +++ b/examples/subpath/package.json @@ -0,0 +1,14 @@ +{ + "name": "@example/subpath", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "devDependencies": { + "astro": "^0.21.0-next.3" + } +} diff --git a/examples/subpath/public/favicon.ico b/examples/subpath/public/favicon.ico new file mode 100644 index 000000000..578ad458b Binary files /dev/null and b/examples/subpath/public/favicon.ico differ diff --git a/examples/subpath/public/images/penguin.png b/examples/subpath/public/images/penguin.png new file mode 100644 index 000000000..bc9523bd4 Binary files /dev/null and b/examples/subpath/public/images/penguin.png differ diff --git a/examples/subpath/public/robots.txt b/examples/subpath/public/robots.txt new file mode 100644 index 000000000..1f53798bb --- /dev/null +++ b/examples/subpath/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/examples/subpath/sandbox.config.json b/examples/subpath/sandbox.config.json new file mode 100644 index 000000000..9178af77d --- /dev/null +++ b/examples/subpath/sandbox.config.json @@ -0,0 +1,11 @@ +{ + "infiniteLoopProtection": true, + "hardReloadOnChange": false, + "view": "browser", + "template": "node", + "container": { + "port": 3000, + "startScript": "start", + "node": "14" + } +} diff --git a/examples/subpath/src/components/Time.jsx b/examples/subpath/src/components/Time.jsx new file mode 100644 index 000000000..8172b77dd --- /dev/null +++ b/examples/subpath/src/components/Time.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function() { + const date = new Date(); + const format = new Intl.DateTimeFormat('en-US'); + return +} \ No newline at end of file diff --git a/examples/subpath/src/pages/index.astro b/examples/subpath/src/pages/index.astro new file mode 100644 index 000000000..b08f2e06f --- /dev/null +++ b/examples/subpath/src/pages/index.astro @@ -0,0 +1,32 @@ +--- +import Time from '../components/Time.jsx' +--- + + + + + + + + + Welcome to Astro + + + +

Welcome to Astro

+ +
+ Today:
+ +
+

Animals

+ +
+ +
A penguin
+
+
+ + + diff --git a/examples/subpath/src/styles/main.scss b/examples/subpath/src/styles/main.scss new file mode 100644 index 000000000..573fc4396 --- /dev/null +++ b/examples/subpath/src/styles/main.scss @@ -0,0 +1,5 @@ +body { + #app { + color: tomato; + } +} \ No newline at end of file diff --git a/packages/astro/package.json b/packages/astro/package.json index 88ccde6f8..71a7f9b4a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,7 +53,7 @@ "test": "mocha --parallel --timeout 15000" }, "dependencies": { - "@astrojs/compiler": "^0.2.26", + "@astrojs/compiler": "^0.2.27", "@astrojs/language-server": "^0.7.16", "@astrojs/markdown-remark": "^0.4.0-next.1", "@astrojs/markdown-support": "0.3.1", diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index ed8529a77..b3a514e88 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -184,6 +184,7 @@ class AstroBuilder { publicDir: viteConfig.publicDir, root: viteConfig.root, server: viteConfig.server, + base: this.config.buildOptions.site ? new URL(this.config.buildOptions.site).pathname : '/' }); debug(logging, 'build', timerMessage('Vite build finished', timer.buildStart)); diff --git a/packages/astro/src/core/dev/index.ts b/packages/astro/src/core/dev/index.ts index 3774a9995..5c81f4101 100644 --- a/packages/astro/src/core/dev/index.ts +++ b/packages/astro/src/core/dev/index.ts @@ -19,7 +19,7 @@ import { collectResources } from '../ssr/html.js'; import { createRouteManifest, matchRoute } from '../ssr/routing.js'; import { createVite } from '../create-vite.js'; import * as msg from './messages.js'; -import notFoundTemplate from './template/4xx.js'; +import notFoundTemplate, { subpathNotUsedTemplate } from './template/4xx.js'; import serverErrorTemplate from './template/5xx.js'; export interface DevOptions { @@ -59,6 +59,9 @@ export class AstroDevServer { private logging: LogOptions; private manifest: ManifestData; private mostRecentRoute?: RouteData; + private site: URL | undefined; + private pathname: string; + private url: URL; private origin: string; private routeCache: RouteCache = {}; private viteServer: vite.ViteDevServer | undefined; @@ -69,6 +72,9 @@ export class AstroDevServer { this.logging = options.logging; this.port = config.devOptions.port; this.origin = `http://localhost:${this.port}`; + this.site = config.buildOptions.site ? new URL(config.buildOptions.site) : undefined; + this.pathname = this.site ? this.site.pathname + '/' : '/'; + this.url = new URL(this.pathname, this.origin); this.manifest = createRouteManifest({ config }); } @@ -195,7 +201,7 @@ export class AstroDevServer { const listen = () => { this.httpServer = this.app.listen(this.port, this.hostname, () => { info(this.logging, 'astro', msg.devStart({ startupTime: performance.now() - devStart })); - info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}` })); + info(this.logging, 'astro', msg.devHost({ host: `http://${this.hostname}:${this.port}${this.pathname}` })); resolve(); }); this.httpServer?.on('error', onError); @@ -228,7 +234,7 @@ export class AstroDevServer { server: { middlewareMode: 'ssr', host: this.hostname, - }, + } }, this.config.vite || {} ), @@ -271,10 +277,19 @@ export class AstroDevServer { const reqStart = performance.now(); let filePath: URL | undefined; try { - const route = matchRoute(pathname, this.manifest); + let routePathname = pathname.startsWith(this.pathname) ? pathname.substr(this.pathname.length) || '/' : undefined; + if(!routePathname) { + next(); + return; + } + + const route = matchRoute(routePathname, this.manifest); // 404: continue to Vite if (!route) { + // Send through, stripping off the `/blog/` part so that Vite matches it. + const newPathname = routePathname.startsWith('/') ? routePathname : '/' + routePathname; + req.url = newPathname; next(); return; } @@ -348,7 +363,11 @@ export class AstroDevServer { } // if not found, fall back to default template else { - html = notFoundTemplate({ statusCode, title: 'Not found', tabTitle: '404: Not Found', pathname }); + if(pathname === '/' && !pathname.startsWith(this.pathname)) { + html = subpathNotUsedTemplate(this.pathname, pathname); + } else { + html = notFoundTemplate({ statusCode, title: 'Not found', tabTitle: '404: Not Found', pathname }); + } } info(this.logging, 'astro', msg.req({ url: pathname, statusCode, reqTime: performance.now() - reqStart })); res.writeHead(statusCode, { diff --git a/packages/astro/src/core/dev/template/4xx.ts b/packages/astro/src/core/dev/template/4xx.ts index 99c022963..b24e87098 100644 --- a/packages/astro/src/core/dev/template/4xx.ts +++ b/packages/astro/src/core/dev/template/4xx.ts @@ -1,5 +1,6 @@ import { encode } from 'html-entities'; import { baseCSS } from './css.js'; + interface ErrorTemplateOptions { /** a short description of the error */ pathname: string; @@ -9,10 +10,12 @@ interface ErrorTemplateOptions { tabTitle: string; /** page title */ title: string; + /** The body of the message, if one is provided */ + body?: string; } /** Display all errors */ -export default function template({ title, pathname, statusCode = 404, tabTitle }: ErrorTemplateOptions): string { +export default function template({ title, pathname, statusCode = 404, tabTitle, body }: ErrorTemplateOptions): string { return ` @@ -44,8 +47,23 @@ export default function template({ title, pathname, statusCode = 404, tabTitle }

${statusCode ? `${statusCode}: ` : ''}${title}

-
Path: ${encode(pathname)}
+ ${body || ` +
Path: ${encode(pathname)}
+ `}
`; } + +export function subpathNotUsedTemplate(base: string, pathname: string) { + return template({ + pathname, + statusCode: 404, + title: 'Not found', + tabTitle: '404: Not Found', + body: ` +

In your buildOptions.site you have your base path set to ${base}. Do you want to go there instead?

+

Come to our Discord if you need help.

+ ` + }); +} \ No newline at end of file diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts index bb2cf33ad..28f568827 100644 --- a/packages/astro/src/core/preview/index.ts +++ b/packages/astro/src/core/preview/index.ts @@ -7,6 +7,7 @@ import send from 'send'; import { fileURLToPath } from 'url'; import * as msg from '../dev/messages.js'; import { error, info } from '../logger.js'; +import { subpathNotUsedTemplate } from '../dev/template/4xx.js'; interface PreviewOptions { logging: LogOptions; @@ -22,10 +23,17 @@ interface PreviewServer { /** The primary dev action */ export default async function preview(config: AstroConfig, { logging }: PreviewOptions): Promise { const startServerTime = performance.now(); + const base = config.buildOptions.site ? new URL(config.buildOptions.site).pathname + '/' : '/'; // Create the preview server, send static files out of the `dist/` directory. const server = http.createServer((req, res) => { - send(req, req.url!, { + if(!req.url!.startsWith(base)) { + res.statusCode = 404; + res.end(subpathNotUsedTemplate(base, req.url!)); + return; + } + + send(req, req.url!.substr(base.length - 1), { root: fileURLToPath(config.dist), }).pipe(res); }); @@ -48,7 +56,7 @@ export default async function preview(config: AstroConfig, { logging }: PreviewO server .listen(port, hostname, () => { info(logging, 'preview', msg.devStart({ startupTime: performance.now() - startServerTime })); - info(logging, 'preview', msg.devHost({ host: `http://${hostname}:${port}/` })); + info(logging, 'preview', msg.devHost({ host: `http://${hostname}:${port}${base}` })); resolve(server); }) .on('error', (err: NodeJS.ErrnoException) => {