New Markdown API (#2862)

* Implement new markdown plugin with deferred markdown rendering

* feat: switch from `getContent()` fn to `<Content />` API

* update types

* Update packages/astro/src/@types/astro.ts

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

* update types

* Create forty-coins-attend.md

Co-authored-by: Nate Moore <nate@skypack.dev>
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
Fred K. Schott 2022-03-28 17:16:06 -07:00 committed by GitHub
parent 7d29feace1
commit 4299ab303b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 279 additions and 195 deletions

View file

@ -0,0 +1,16 @@
---
"astro": minor
---
Implement RFC [#0017](https://github.com/withastro/rfcs/blob/main/proposals/0017-markdown-content-redesign.md)
- New Markdown API
- New `Astro.glob()` API
- **BREAKING CHANGE:** Removed `Astro.fetchContent()` (replaced by `Astro.glob()`)
```diff
// v0.25
- let allPosts = Astro.fetchContent('./posts/*.md');
// v0.26+
+ let allPosts = await Astro.glob('./posts/*.md');
```

View file

@ -4,6 +4,7 @@ export interface Props {
author: string;
}
const { post, author } = Astro.props;
const { frontmatter } = post;
function formatDate(date) {
return new Date(date).toUTCString().replace(/(\d\d\d\d) .*/, '$1'); // remove everything after YYYY
@ -12,12 +13,12 @@ function formatDate(date) {
<article class="post">
<div class="data">
<h2>{post.title}</h2>
<a class="author" href={`/authors/${post.author}`}>{author.name}</a>
<time class="date" datetime={post.date}>{formatDate(post.date)}</time>
<h2>{frontmatter.title}</h2>
<a class="author" href={`/authors/${frontmatter.author}`}>{author.name}</a>
<time class="date" datetime={frontmatter.date}>{formatDate(frontmatter.date)}</time>
<p class="description">
{post.description}
<a class="link" href={post.url} aria-label={`Read ${post.title}`}>Read</a>
{frontmatter.description}
<a class="link" href={post.url} aria-label={`Read ${frontmatter.title}`}>Read</a>
</p>
</div>
</article>

View file

@ -2,36 +2,28 @@
import MainHead from '../../components/MainHead.astro';
import Nav from '../../components/Nav.astro';
import PostPreview from '../../components/PostPreview.astro';
import Pagination from '../../components/Pagination.astro';
import authorData from '../../data/authors.json';
export function getStaticPaths() {
const allPosts = Astro.fetchContent<MarkdownFrontmatter>('../post/*.md');
let allAuthorsUnique = [...new Set(allPosts.map((p) => p.author))];
export async function getStaticPaths() {
const allPosts = await Astro.glob('../post/*.md');
let allAuthorsUnique = [...new Set(allPosts.map((p) => p.frontmatter.author))];
return allAuthorsUnique.map((author) => ({ params: { author }, props: { allPosts } }));
}
interface MarkdownFrontmatter {
date: number;
description: string;
title: string;
author: string;
}
const { allPosts } = Astro.props;
const { params, canonicalURL } = Astro.request;
const title = 'Dons Blog';
const description = 'An example blog on Astro';
/** filter posts by author, sort by date */
const posts = allPosts.filter((post) => post.author === params.author).sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
const author = authorData[posts[0].author];
const posts = allPosts.filter((post) => post.frontmatter.author === params.author).sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
const author = authorData[posts[0].frontmatter.author];
---
<html lang="en">
<head>
<title>{title}</title>
<MainHead {title} {description} image={posts[0].image} canonicalURL={canonicalURL.toString()} />
<MainHead {title} {description} image={posts[0].frontmatter.image} canonicalURL={canonicalURL.toString()} />
<style lang="scss">
.title {

View file

@ -6,12 +6,6 @@ import PostPreview from '../components/PostPreview.astro';
import Pagination from '../components/Pagination.astro';
import authorData from '../data/authors.json';
interface MarkdownFrontmatter {
date: number;
image: string;
author: string;
}
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
@ -21,10 +15,9 @@ let description = 'An example blog on Astro';
let canonicalURL = Astro.request.canonicalURL;
// Data Fetching: List all Markdown posts in the repo.
let allPosts = Astro.fetchContent<MarkdownFrontmatter>('./post/*.md');
allPosts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
let allPosts = await Astro.glob('./post/*.md');
allPosts.sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
let firstPage = allPosts.slice(0, 2);
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
---
@ -32,14 +25,14 @@ let firstPage = allPosts.slice(0, 2);
<html lang="en">
<head>
<title>{title}</title>
<MainHead {title} {description} image={allPosts[0].image} {canonicalURL} />
<MainHead {title} {description} image={allPosts[0].frontmatter.image} {canonicalURL} />
</head>
<body>
<Nav {title} />
<main class="wrapper">
{allPosts.map((post) => <PostPreview post={post} author={authorData[post.author]} />)}
{allPosts.map((post) => <PostPreview post={post} author={authorData[post.frontmatter.author]} />)}
</main>
<footer>

View file

@ -6,8 +6,8 @@ import Pagination from '../../components/Pagination.astro';
import authorData from '../../data/authors.json';
export async function getStaticPaths({ paginate, rss }) {
const allPosts = Astro.fetchContent<MarkdownFrontmatter>('../post/*.md');
const sortedPosts = allPosts.sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf());
const allPosts = await Astro.glob('../post/*.md');
const sortedPosts = allPosts.sort((a, b) => new Date(b.frontmatter.date).valueOf() - new Date(a.frontmatter.date).valueOf());
// Generate an RSS feed from this collection of posts.
// NOTE: This is disabled by default, since it requires `buildOptions.site` to be set in your "astro.config.mjs" file.
@ -31,21 +31,13 @@ export async function getStaticPaths({ paginate, rss }) {
let title = 'Dons Blog';
let description = 'An example blog on Astro';
let canonicalURL = Astro.request.canonicalURL;
// collection
interface MarkdownFrontmatter {
date: number;
description: string;
title: string;
}
const { page } = Astro.props;
---
<html lang="en">
<head>
<title>{title}</title>
<MainHead {title} {description} image={page.data[0].image} canonicalURL={canonicalURL.toString()} prev={page.url.prev} next={page.url.next} />
<MainHead {title} {description} image={page.data[0].frontmatter.image} canonicalURL={canonicalURL.toString()} prev={page.url.prev} next={page.url.next} />
<style lang="scss">
.title {
@ -70,7 +62,7 @@ const { page } = Astro.props;
<main class="wrapper">
<h2 class="title">All Posts</h2>
<small class="count">{page.start + 1}{page.end + 1} of {page.total}</small>
{page.data.map((post) => <PostPreview post={post} author={authorData[post.author]} />)}
{page.data.map((post) => <PostPreview post={post} author={authorData[post.frontmatter.author]} />)}
</main>
<footer>

View file

@ -8,10 +8,10 @@ const { post } = Astro.props;
<article class="post-preview">
<header>
<p class="publish-date">{post.publishDate}</p>
<a href={post.url}><h1 class="title">{post.title}</h1></a>
<p class="publish-date">{post.frontmatter.publishDate}</p>
<a href={post.url}><h1 class="title">{post.frontmatter.title}</h1></a>
</header>
<p>{post.description}</p>
<p>{post.frontmatter.description}</p>
<a href={post.url}>Read more</a>
</article>

View file

@ -4,10 +4,6 @@ import BaseHead from '../components/BaseHead.astro';
import BlogHeader from '../components/BlogHeader.astro';
import BlogPostPreview from '../components/BlogPostPreview.astro';
interface MarkdownFrontmatter {
publishDate: number;
}
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
@ -18,8 +14,8 @@ let permalink = 'https://example.com/';
// Data Fetching: List all Markdown posts in the repo.
let allPosts = await Astro.fetchContent('./posts/*.md');
allPosts = allPosts.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
let allPosts = await Astro.glob('./posts/*.md');
allPosts = allPosts.sort((a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf());
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/

View file

@ -2,16 +2,17 @@ import { h } from 'preact';
import Styles from './styles.module.scss';
function PortfolioPreview({ project }) {
const { frontmatter } = project;
return (
<div className={Styles.card}>
<div className={Styles.titleCard} style={`background-image:url(${project.img})`}>
<h1 className={Styles.title}>{project.title}</h1>
<div className={Styles.titleCard} style={`background-image:url(${frontmatter.img})`}>
<h1 className={Styles.title}>{frontmatter.title}</h1>
</div>
<div className="pa3">
<p className={`${Styles.desc} mt0 mb2`}>{project.description}</p>
<p className={`${Styles.desc} mt0 mb2`}>{frontmatter.description}</p>
<div className={Styles.tags}>
Tagged:
{project.tags.map((t) => (
{frontmatter.tags.map((t) => (
<div className={Styles.tag} data-tag={t}>
{t}
</div>

View file

@ -7,7 +7,7 @@ import Footer from '../components/Footer/index.jsx';
import PortfolioPreview from '../components/PortfolioPreview/index.jsx';
// Data Fetching: List all Markdown posts in the repo.
const projects = Astro.fetchContent('./project/**/*.md');
const projects = await Astro.glob('./project/**/*.md');
const featuredProject = projects[0];
// Full Astro Component Syntax:

View file

@ -4,13 +4,9 @@ import Footer from '../components/Footer/index.jsx';
import Nav from '../components/Nav/index.jsx';
import PortfolioPreview from '../components/PortfolioPreview/index.jsx';
interface MarkdownFrontmatter {
publishDate: number;
}
const projects = Astro.fetchContent<MarkdownFrontmatter>('./project/**/*.md')
.filter(({ publishDate }) => !!publishDate)
.sort((a, b) => new Date(b.publishDate).valueOf() - new Date(a.publishDate).valueOf());
const projects = (await Astro.glob('./project/**/*.md'))
.filter(({ frontmatter }) => !!frontmatter.publishDate)
.sort((a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf());
---
<html lang="en">

View file

@ -1,6 +1,6 @@
/// <reference types="vite/client" />
type Astro = import('./dist/types/@types/astro').AstroGlobal;
type Astro = import('astro').AstroGlobal;
// We duplicate the description here because editors won't show the JSDoc comment from the imported type (but will for its properties, ex: Astro.request will show the AstroGlobal.request description)
/**

View file

@ -85,6 +85,7 @@
"@proload/core": "^0.2.2",
"@proload/plugin-tsm": "^0.1.1",
"@web/parse5-utils": "^1.3.0",
"ast-types": "^0.14.2",
"boxen": "^6.2.1",
"ci-info": "^3.3.0",
"common-ancestor-path": "^1.0.1",
@ -112,6 +113,7 @@
"preferred-pm": "^3.0.3",
"prismjs": "^1.27.0",
"prompts": "^2.4.2",
"recast": "^0.20.5",
"rehype-slug": "^5.0.1",
"resolve": "^1.22.0",
"rollup": "^2.70.1",

View file

@ -1,7 +1,7 @@
import type { AddressInfo } from 'net';
import type * as babel from '@babel/core';
import type * as vite from 'vite';
import type { z } from 'zod';
import { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type { AstroRequest } from '../core/render/request';
@ -60,11 +60,15 @@ export interface AstroGlobal extends AstroGlobalPartial {
}
export interface AstroGlobalPartial {
fetchContent<T = any>(globStr: string): Promise<FetchContentResult<T>[]>;
/**
* @deprecated since version 0.24. See the {@link https://astro.build/deprecated/resolve upgrade guide} for more details.
*/
resolve: (path: string) => string;
/** @deprecated Use `Astro.glob()` instead. */
fetchContent(globStr: string): Promise<any[]>;
glob(globStr: `${any}.astro`): Promise<ComponentInstance[]>;
glob<T extends Record<string, any>>(globStr: `${any}.md`): Promise<MarkdownInstance<T>[]>;
glob<T extends Record<string, any>>(globStr: string): Promise<T[]>;
site: URL;
}
@ -508,20 +512,13 @@ export interface ComponentInstance {
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}
/**
* Astro.fetchContent() result
* Docs: https://docs.astro.build/reference/api-reference/#astrofetchcontent
*/
export type FetchContentResult<T> = FetchContentResultBase & T;
export type FetchContentResultBase = {
astro: {
headers: string[];
source: string;
html: string;
};
url: string;
};
export interface MarkdownInstance<T extends Record<string, any>> {
frontmatter: T;
file: string;
url: string | undefined;
Content: AstroComponentFactory;
getHeaders(): Promise<{ depth: number, slug: string, text: string }[]>;
}
export type GetHydrateCallback = () => Promise<(element: Element, innerHTML: string | null) => void>;

View file

@ -29,10 +29,15 @@ export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer):
: // Otherwise, you are following an import in the module import tree.
// You are safe to use getModuleById() here because Vite has already
// resolved the correct `id` for you, by creating the import you followed here.
new Set([viteServer.moduleGraph.getModuleById(id)!]);
new Set([viteServer.moduleGraph.getModuleById(id)]);
// Collect all imported modules for the module(s).
for (const entry of moduleEntriesForId) {
// Handle this in case an module entries weren't found for ID
// This seems possible with some virtual IDs (ex: `astro:markdown/*.md`)
if (!entry) {
continue;
}
if (id === entry.id) {
scanned.add(id);
for (const importedModule of entry.importedModules) {

View file

@ -277,34 +277,25 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
}
/** Create the Astro.fetchContent() runtime function. */
function createFetchContentFn(url: URL, site: URL) {
let sitePathname = site.pathname;
const fetchContent = (importMetaGlobResult: Record<string, any>) => {
let allEntries = [...Object.entries(importMetaGlobResult)];
function createDeprecatedFetchContentFn() {
return () => {
throw new Error('Deprecated: Astro.fetchContent() has been replaced with Astro.glob().');
};
}
/** Create the Astro.glob() runtime function. */
function createAstroGlobFn() {
const globHandler = (importMetaGlobResult: Record<string, any>, globValue: () => any) => {
let allEntries = [...Object.values(importMetaGlobResult)];
if (allEntries.length === 0) {
throw new Error(`[${url.pathname}] Astro.fetchContent() no matches found.`);
throw new Error(`Astro.glob(${JSON.stringify(globValue())}) - no matches found.`);
}
return allEntries
.map(([spec, mod]) => {
// Only return Markdown files for now.
if (!mod.frontmatter) {
return;
}
const urlSpec = new URL(spec, url).pathname;
return {
...mod.frontmatter,
Content: mod.default,
content: mod.metadata,
file: new URL(spec, url),
url: urlSpec.includes('/pages/') ? urlSpec.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '') : undefined,
// Map over the `import()` promises, calling to load them.
return Promise.all(allEntries.map(fn => fn()));
};
})
.filter(Boolean);
};
// This has to be cast because the type of fetchContent is the type of the function
// that receives the import.meta.glob result, but the user is using it as
// another type.
return fetchContent as unknown as AstroGlobalPartial['fetchContent'];
// Cast the return type because the argument that the user sees (string) is different from the argument
// that the runtime sees post-compiler (Record<string, Module>).
return globHandler as unknown as AstroGlobalPartial['glob'];
}
// This is used to create the top-level Astro global; the one that you can use
@ -313,10 +304,10 @@ export function createAstro(filePathname: string, _site: string, projectRootStr:
const site = new URL(_site);
const url = new URL(filePathname, site);
const projectRoot = new URL(projectRootStr);
const fetchContent = createFetchContentFn(url, site);
return {
site,
fetchContent,
fetchContent: createDeprecatedFetchContentFn(),
glob: createAstroGlobFn(),
// INVESTIGATE is there a use-case for multi args?
resolve(...segments: string[]) {
let resolved = segments.reduce((u, segment) => new URL(segment, u), url).pathname;

View file

@ -1,10 +1,12 @@
import type * as t from '@babel/types';
import { parse as babelParser } from '@babel/parser';
import type { ArrowFunctionExpressionKind, CallExpressionKind, StringLiteralKind } from 'ast-types/gen/kinds';
import type { NodePath } from 'ast-types/lib/node-path';
import { parse, print, types, visit } from "recast";
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
import * as babelTraverse from '@babel/traverse';
import * as babel from '@babel/core';
// Check for `Astro.glob()`. Be very forgiving of whitespace. False positives are okay.
const ASTRO_GLOB_REGEX = /Astro2?\s*\.\s*glob\s*\(/;
interface AstroPluginOptions {
config: AstroConfig;
}
@ -21,55 +23,56 @@ export default function astro({ config }: AstroPluginOptions): Plugin {
return null;
}
// Optimization: only run on a probably match
// Open this up if need for post-pass extends past fetchContent
if (!code.includes('fetchContent')) {
// Optimization: Detect usage with a quick string match.
// Only perform the transform if this function is found
if (!ASTRO_GLOB_REGEX.test(code)) {
return null;
}
// Handle the second-pass JS AST Traversal
const result = await babel.transformAsync(code, {
sourceType: 'module',
sourceMaps: true,
plugins: [
() => {
return {
visitor: {
StringLiteral(path: babelTraverse.NodePath<t.StringLiteral>) {
const ast = parse(code, {
// We need to use the babel parser because `import.meta.hot` is not
// supported by esprima (default parser). In the future, we should
// experiment with other parsers if Babel is too slow or heavy.
parser: { parse: babelParser },
});
visit(ast, {
visitCallExpression: function (path) {
// Filter out anything that isn't `Astro.glob()` or `Astro2.glob()`
if (
path.parent.type !== 'CallExpression' ||
path.parent.callee.type !== 'MemberExpression' ||
!validAstroGlobalNames.has((path.parent.callee.object as any).name) ||
(path.parent.callee.property as any).name !== 'fetchContent'
!types.namedTypes.MemberExpression.check(path.node.callee) ||
!types.namedTypes.Identifier.check(path.node.callee.property) ||
!(path.node.callee.property.name === 'glob') ||
!types.namedTypes.Identifier.check(path.node.callee.object) ||
!(path.node.callee.object.name === 'Astro' || path.node.callee.object.name === 'Astro2')
) {
this.traverse(path);
return;
}
const { value } = path.node;
if (/[a-z]\:\/\//.test(value)) {
return;
}
path.replaceWith({
// Wrap the `Astro.glob()` argument with `import.meta.glob`.
const argsPath = path.get('arguments', 0) as NodePath;
const args = argsPath.value;
argsPath.replace({
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'MetaProperty', meta: { type: 'Identifier', name: 'import' }, property: { type: 'Identifier', name: 'meta' } },
property: { type: 'Identifier', name: 'globEager' },
property: { type: 'Identifier', name: 'glob' },
computed: false,
},
arguments: [path.node],
} as any);
arguments: [args],
} as CallExpressionKind,
{
type: 'ArrowFunctionExpression',
body: args,
params: []
} as ArrowFunctionExpressionKind);
return false;
},
},
};
},
],
});
// Undocumented baby behavior, but possible according to Babel types.
if (!result || !result.code) {
return null;
}
const result = print(ast);
return { code: result.code, map: result.map };
},
};

View file

@ -2,14 +2,20 @@ import { transform } from '@astrojs/compiler';
import ancestor from 'common-ancestor-path';
import esbuild from 'esbuild';
import fs from 'fs';
import matter from 'gray-matter';
import { fileURLToPath } from 'url';
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
import { PAGE_SSR_SCRIPT_ID } from '../vite-plugin-scripts/index.js';
import { virtualModuleId as pagesVirtualModuleId } from '../core/build/vite-plugin-pages.js';
interface AstroPluginOptions {
config: AstroConfig;
}
const VIRTUAL_MODULE_ID_PREFIX = 'astro:markdown';
const VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID_PREFIX;
// TODO: Clean up some of the shared logic between this Markdown plugin and the Astro plugin.
// Both end up connecting a `load()` hook to the Astro compiler, and share some copy-paste
// logic in how that is done.
@ -23,14 +29,88 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
return filename;
}
// Weird Vite behavior: Vite seems to use a fake "index.html" importer when you
// have `enforce: pre`. This can probably be removed once the vite issue is fixed.
// see: https://github.com/vitejs/vite/issues/5981
const fakeRootImporter = fileURLToPath(new URL('index.html', config.projectRoot));
function isRootImport(importer: string | undefined) {
if (!importer) {
return true;
}
if (importer === fakeRootImporter) {
return true;
}
if (importer === '\0' + pagesVirtualModuleId) {
return true;
}
return false;
}
return {
name: 'astro:markdown',
enforce: 'pre', // run transforms before other plugins can
enforce: 'pre',
async resolveId(id, importer, options) {
// Resolve virtual modules as-is.
if (id.startsWith(VIRTUAL_MODULE_ID)) {
return id;
}
// Resolve any .md files with the `?content` cache buster. This should only come from
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
// Unclear if this is expected or if cache busting is just working around a Vite bug.
if (id.endsWith('.md?content')) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
return resolvedId?.id.replace('?content', '');
}
// If the markdown file is imported from another file via ESM, resolve a JS representation
// that defers the markdown -> HTML rendering until it is needed. This is especially useful
// when fetching and then filtering many markdown files, like with import.meta.glob() or Astro.glob().
// Otherwise, resolve directly to the actual component.
if (id.endsWith('.md') && !isRootImport(importer)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
if (resolvedId) {
return VIRTUAL_MODULE_ID + resolvedId.id;
}
}
// In all other cases, we do nothing and rely on normal Vite resolution.
return undefined;
},
async load(id) {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
if (id.startsWith(VIRTUAL_MODULE_ID)) {
const sitePathname = config.buildOptions.site ? new URL(config.buildOptions.site).pathname : '/';
const fileId = id.substring(VIRTUAL_MODULE_ID.length);
const fileUrl = fileId.includes('/pages/') ? fileId.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '') : undefined;
const source = await fs.promises.readFile(fileId, 'utf8');
const { data: frontmatter } = matter(source);
return {
code: `
// Static
export const frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
// Deferred
export default async function load() {
return (await import(${JSON.stringify(fileId + '?content')}));
};
export function Content(...args) {
return load().then((m) => m.default(...args))
}
Content.isAstroComponentFactory = true;
export function getHeaders() {
return load().then((m) => m.metadata.headers)
};`,
map: null,
};
}
// A markdown file is being rendered! This markdown file was either imported
// directly as a page in Vite, or it was a deferred render from a JS module.
// This returns the compiled markdown -> astro component that renders to HTML.
if (id.endsWith('.md')) {
const source = await fs.promises.readFile(id, 'utf8');
// Transform from `.md` to valid `.astro`
let render = config.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(render)) {
@ -40,8 +120,6 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
if (typeof render === 'string') {
({ default: render } = await import(render));
}
let renderResult = await render(source, renderOpts);
let { frontmatter, metadata, code: astroResult } = renderResult;
const filename = normalizeFilename(id);
const fileUrl = new URL(`file://${filename}`);
@ -49,6 +127,9 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
const hasInjectedScript = isPage && config._ctx.scripts.some((s) => s.stage === 'page-ssr');
// Extract special frontmatter keys
const { data: frontmatter, content: markdownContent } = matter(source);
let renderResult = await render(markdownContent, renderOpts);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
content.astro = metadata;
const prelude = `---
@ -83,8 +164,7 @@ export const frontmatter = ${JSON.stringify(content)};
${tsResult}`;
// Compile from `.ts` to `.js`
const { code, map } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: 'inline', sourcefile: id });
const { code } = await esbuild.transform(tsResult, { loader: 'ts', sourcemap: false, sourcefile: id });
return {
code,
map: null,

View file

@ -48,7 +48,7 @@ describe('Astro.*', () => {
expect($('#site').attr('href')).to.equal('https://mysite.dev/blog/');
});
it('Astro.fetchContent() returns the correct "url" property, including buildOptions.site subpath', async () => {
it('Astro.glob() correctly returns an array of all posts', async () => {
const html = await fixture.readFile('/posts/1/index.html');
const $ = cheerio.load(html);
expect($('.post-url').attr('href')).to.equal('/blog/post/post-2');

View file

@ -1,6 +1,6 @@
---
export function getStaticPaths({paginate}) {
const data = Astro.fetchContent('../post/*.md');
export async function getStaticPaths({paginate}) {
const data = await Astro.glob('../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;
@ -15,7 +15,7 @@ const { params, canonicalURL} = Astro.request;
<body>
{page.data.map((data) => (
<div>
<h1>{data.title}</h1>
<h1>{data.frontmatter.title}</h1>
<a class="post-url" href={data.url}>Read</a>
</div>
))}

View file

@ -1,8 +1,8 @@
---
export function getStaticPaths({paginate}) {
const allPosts = Astro.fetchContent('../../post/*.md');
export async function getStaticPaths({paginate}) {
const allPosts = await Astro.glob('../../post/*.md');
return ['red', 'blue'].map((filter) => {
const filteredPosts = allPosts.filter((post) => post.tag === filter);
const filteredPosts = allPosts.filter((post) => post.frontmatter.tag === filter);
return paginate(filteredPosts, {
params: { slug: filter },
props: { filter },

View file

@ -1,6 +1,6 @@
---
export async function getStaticPaths({paginate}) {
const data = Astro.fetchContent('../../post/*.md');
const data = await Astro.glob('../../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;

View file

@ -1,6 +1,6 @@
---
export async function getStaticPaths({paginate}) {
const data = Astro.fetchContent('../../post/*.md');
const data = await Astro.glob('../../post/*.md');
return paginate(data, {pageSize: 1});
}
const { page } = Astro.props;

View file

@ -1,6 +1,6 @@
---
export function getStaticPaths({paginate, rss}) {
const episodes = Astro.fetchContent('../episode/*.md').sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
export async function getStaticPaths({paginate, rss}) {
const episodes = (await Astro.glob('../episode/*.md')).sort((a, b) => new Date(b.frontmatter.pubDate) - new Date(a.frontmatter.pubDate));
rss({
title: 'MF Doomcast',
description: 'The podcast about the things you find on a picnic, or at a picnic table',
@ -11,13 +11,13 @@ export function getStaticPaths({paginate, rss}) {
customData: `<language>en-us</language>` +
`<itunes:author>MF Doom</itunes:author>`,
items: episodes.map((episode) => ({
title: episode.title,
title: episode.frontmatter.title,
link: episode.url,
description: episode.description,
pubDate: episode.pubDate + 'Z',
customData: `<itunes:episodeType>${episode.type}</itunes:episodeType>` +
`<itunes:duration>${episode.duration}</itunes:duration>` +
`<itunes:explicit>${episode.explicit || false}</itunes:explicit>`,
description: episode.frontmatter.description,
pubDate: episode.frontmatter.pubDate + 'Z',
customData: `<itunes:episodeType>${episode.frontmatter.type}</itunes:episodeType>` +
`<itunes:duration>${episode.frontmatter.duration}</itunes:duration>` +
`<itunes:explicit>${episode.frontmatter.explicit || false}</itunes:explicit>`,
})),
dest: '/custom/feed.xml',
});
@ -31,13 +31,13 @@ export function getStaticPaths({paginate, rss}) {
customData: `<language>en-us</language>` +
`<itunes:author>MF Doom</itunes:author>`,
items: episodes.map((episode) => ({
title: episode.title,
title: episode.frontmatter.title,
link: `https://example.com${episode.url}/`,
description: episode.description,
pubDate: episode.pubDate + 'Z',
customData: `<itunes:episodeType>${episode.type}</itunes:episodeType>` +
`<itunes:duration>${episode.duration}</itunes:duration>` +
`<itunes:explicit>${episode.explicit || false}</itunes:explicit>`,
description: episode.frontmatter.description,
pubDate: episode.frontmatter.pubDate + 'Z',
customData: `<itunes:episodeType>${episode.frontmatter.type}</itunes:episodeType>` +
`<itunes:duration>${episode.frontmatter.duration}</itunes:duration>` +
`<itunes:explicit>${episode.frontmatter.explicit || false}</itunes:explicit>`,
})),
dest: '/custom/feed-pregenerated-urls.xml',
});
@ -53,6 +53,6 @@ const { page } = Astro.props;
<link rel="alternate" type="application/rss+2.0" href="/rss.xml" />
</head>
<body>
{page.data.map((ep) => (<li>{ep.title}</li>))}
{page.data.map((ep) => (<li>{ep.frontmatter.title}</li>))}
</body>
</html>

View file

@ -3,15 +3,13 @@ import Debug from 'astro/debug';
// all the content that should be generated
export async function getStaticPaths() {
const data = Astro.fetchContent('../../data/posts/*.md')
const data = await Astro.glob('../../data/posts/*.md')
const allArticles = data.map(({ astro, file, url, ...article }) => {
const allArticles = data.map((article) => {
return {
params: { slug: article.slug },
params: { slug: article.frontmatter.slug },
props: {
article: article,
content: astro.html,
md: astro.source,
}
}
})

View file

@ -2,7 +2,7 @@
import MainHead from '../components/MainHead.astro';
import Nav from '../components/Nav/index.jsx';
import { test as ssrConfigTest } from '@test/static-build-pkg';
let allPosts = await Astro.fetchContent('./posts/*.md');
let allPosts = await Astro.glob('./posts/*.md');
---
<html>
<head>

View file

@ -21,7 +21,7 @@ describe('Static build', () => {
expect(html).to.be.a('string');
});
it('can build pages using fetchContent', async () => {
it('can build pages using Astro.glob()', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerioLoad(html);
const link = $('.posts a');

View file

@ -473,6 +473,7 @@ importers:
'@types/send': ^0.17.1
'@types/yargs-parser': ^21.0.0
'@web/parse5-utils': ^1.3.0
ast-types: ^0.14.2
astro-scripts: workspace:*
boxen: ^6.2.1
chai: ^4.3.6
@ -504,6 +505,7 @@ importers:
preferred-pm: ^3.0.3
prismjs: ^1.27.0
prompts: ^2.4.2
recast: ^0.20.5
rehype-slug: ^5.0.1
resolve: ^1.22.0
rollup: ^2.70.1
@ -536,6 +538,7 @@ importers:
'@proload/core': 0.2.2
'@proload/plugin-tsm': 0.1.1_@proload+core@0.2.2
'@web/parse5-utils': 1.3.0
ast-types: 0.14.2
boxen: 6.2.1
ci-info: 3.3.0
common-ancestor-path: 1.0.1
@ -563,6 +566,7 @@ importers:
preferred-pm: 3.0.3
prismjs: 1.27.0
prompts: 2.4.2
recast: 0.20.5
rehype-slug: 5.0.1
resolve: 1.22.0
rollup: 2.70.1
@ -4615,6 +4619,13 @@ packages:
tslib: 2.3.1
dev: true
/ast-types/0.14.2:
resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==}
engines: {node: '>=4'}
dependencies:
tslib: 2.3.1
dev: false
/async/0.9.2:
resolution: {integrity: sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=}
dev: true
@ -8807,6 +8818,16 @@ packages:
dependencies:
picomatch: 2.3.1
/recast/0.20.5:
resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==}
engines: {node: '>= 4'}
dependencies:
ast-types: 0.14.2
esprima: 4.0.1
source-map: 0.6.1
tslib: 2.3.1
dev: false
/redent/3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}