Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unset charset=utf-8 content-type for md/mdx pages #12231

Merged
merged 10 commits into from
Oct 24, 2024
9 changes: 9 additions & 0 deletions .changeset/dirty-cooks-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/mdx': major
---

Handles the breaking change in Astro where content pages (including `.mdx` pages) no longer respond with `charset=utf-8` in the `Content-Type` header.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

For MDX pages without layouts, `@astrojs/mdx` will automatically add the `<meta charset="utf-8">` tag to the page by default. This reduces the boilerplate needed to write with non-ASCII characters. If your MDX pages have a layout, the layout component should include the `<meta charset="utf-8">` tag.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

If you require `charset=utf-8` to render your page correctly, make sure that your layout components have the `<meta charset="utf-8">` tag added.
9 changes: 9 additions & 0 deletions .changeset/strong-months-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'astro': major
---

Updates Markdown page handling to no longer respond with `charset=utf-8` in the `Content-Type` header. This matches the rendering behaviour of other non-content pages.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

Instead, for Markdown pages without layouts, Astro will automatically add the `<meta charset="utf-8">` tag to the page by default. This reduces the boilerplate needed to write with non-ASCII characters. If your Markdown pages have a layout, the layout component should include the `<meta charset="utf-8">` tag.
bluwy marked this conversation as resolved.
Show resolved Hide resolved

If you require `charset=utf-8` to render your page correctly, make sure that your layout components have the `<meta charset="utf-8">` tag added.
bluwy marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function vitePluginAstroPreview(settings: AstroSettings): Plugin {
const errorPagePath = fileURLToPath(outDir + '/404.html');
if (fs.existsSync(errorPagePath)) {
res.statusCode = 404;
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.setHeader('Content-Type', 'text/html');
res.end(fs.readFileSync(errorPagePath));
} else {
res.statusCode = 404;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function default404Page({ pathname }: { pathname: string }) {
tabTitle: '404: Not Found',
pathname,
}),
{ status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
{ status: 404, headers: { 'Content-Type': 'text/html' } },
);
}
// mark the function as an AstroComponentFactory for the rendering internals
Expand Down
7 changes: 1 addition & 6 deletions packages/astro/src/runtime/server/render/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function renderPage(

return new Response(bytes, {
headers: new Headers([
['Content-Type', 'text/html; charset=utf-8'],
['Content-Type', 'text/html'],
['Content-Length', bytes.byteLength.toString()],
]),
});
Expand Down Expand Up @@ -80,11 +80,6 @@ export async function renderPage(
body = encoder.encode(body);
headers.set('Content-Length', body.byteLength.toString());
}
// TODO: Revisit if user should manually set charset by themselves in Astro 4
// This code preserves the existing behaviour for markdown pages since Astro 2
if (route?.component.endsWith('.md')) {
headers.set('Content-Type', 'text/html; charset=utf-8');
}
let status = init.status;
// Custom 404.astro and 500.astro are particular routes that must return a fixed status code
if (route?.route === '/404') {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/vite-plugin-astro-server/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function handle500Response(

export function writeHtmlResponse(res: http.ServerResponse, statusCode: number, html: string) {
res.writeHead(statusCode, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(html, 'utf-8'),
});
res.write(html);
Expand Down
8 changes: 6 additions & 2 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { normalizePath } from 'vite';
import { safeParseFrontmatter } from '../content/utils.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import { isMarkdownFile, isPage } from '../core/util.js';
import { shorthash } from '../runtime/server/shorthash.js';
import type { AstroSettings } from '../types/astro.js';
import { createDefaultAstroMetadata } from '../vite-plugin-astro/metadata.js';
Expand Down Expand Up @@ -77,6 +77,10 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
let html = renderResult.code;
const { headings, imagePaths: rawImagePaths, frontmatter } = renderResult.metadata;

// Add default charset for markdown pages
const isMarkdownPage = isPage(fileURL, settings);
const charset = isMarkdownPage ? '<meta charset="utf-8">' : '';

// Resolve all the extracted images from the content
const imagePaths: MarkdownImagePath[] = [];
for (const imagePath of rawImagePaths) {
Expand Down Expand Up @@ -141,7 +145,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
}, {
'default': () => render\`\${unescapeHTML(html())}\`
})}\`;`
: `render\`\${maybeRenderHead(result)}\${unescapeHTML(html())}\`;`
: `render\`${charset}\${maybeRenderHead(result)}\${unescapeHTML(html())}\`;`
}
});
export default Content;
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/test/astro-basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe('Astro basic development', () => {
assert.equal($('h1').text(), '我的第一篇博客文章');
const isUtf8 =
res.headers.get('content-type').includes('charset=utf-8') ||
html.includes('<meta charset="utf-8">');
html.includes('<meta charset="utf-8"');
assert.ok(isUtf8);
});

Expand All @@ -221,7 +221,7 @@ describe('Astro basic development', () => {
assert.equal($('h1').text(), '我的第一篇博客文章');
const isUtf8 =
res.headers.get('content-type').includes('charset=utf-8') ||
html.includes('<meta charset="utf-8">');
html.includes('<meta charset="utf-8"');
assert.ok(isUtf8);
});

Expand Down
13 changes: 8 additions & 5 deletions packages/integrations/mdx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { PluggableList } from 'unified';
import type { OptimizeOptions } from './rehype-optimize-static.js';
import { ignoreStringPlugins, safeParseFrontmatter } from './utils.js';
import { vitePluginMdxPostprocess } from './vite-plugin-mdx-postprocess.js';
import { vitePluginMdx } from './vite-plugin-mdx.js';
import { type VitePluginMdxOptions, vitePluginMdx } from './vite-plugin-mdx.js';

export type MdxOptions = Omit<typeof markdownConfigDefaults, 'remarkPlugins' | 'rehypePlugins'> & {
extendMarkdownConfig: boolean;
Expand Down Expand Up @@ -43,7 +43,7 @@ export function getContainerRenderer(): ContainerRenderer {
export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroIntegration {
// @ts-expect-error Temporarily assign an empty object here, which will be re-assigned by the
// `astro:config:done` hook later. This is so that `vitePluginMdx` can get hold of a reference earlier.
let mdxOptions: MdxOptions = {};
let vitePluginMdxOptions: VitePluginMdxOptions = {};

return {
name: '@astrojs/mdx',
Expand Down Expand Up @@ -79,7 +79,7 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI

updateConfig({
vite: {
plugins: [vitePluginMdx(mdxOptions), vitePluginMdxPostprocess(config)],
plugins: [vitePluginMdx(vitePluginMdxOptions), vitePluginMdxPostprocess(config)],
},
});
},
Expand All @@ -98,10 +98,13 @@ export default function mdx(partialMdxOptions: Partial<MdxOptions> = {}): AstroI
});

// Mutate `mdxOptions` so that `vitePluginMdx` can reference the actual options
Object.assign(mdxOptions, resolvedMdxOptions);
Object.assign(vitePluginMdxOptions, {
mdxOptions: resolvedMdxOptions,
srcDir: config.srcDir,
});
// @ts-expect-error After we assign, we don't need to reference `mdxOptions` in this context anymore.
// Re-assign it so that the garbage can be collected later.
mdxOptions = {};
vitePluginMdxOptions = {};
},
},
};
Expand Down
64 changes: 60 additions & 4 deletions packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { isFrontmatterValid } from '@astrojs/markdown-remark';
import type { Root, RootContent } from 'hast';
import type { VFile } from 'vfile';
import { jsToTreeNode } from './utils.js';

// Passed metadata to help determine adding charset utf8 by default
declare module 'vfile' {
interface DataMap {
applyFrontmatterExport?: {
srcDir?: URL;
};
}
}

const exportConstPartialTrueRe = /export\s+const\s+partial\s*=\s*true/;

export function rehypeApplyFrontmatterExport() {
return function (tree: any, vfile: VFile) {
return function (tree: Root, vfile: VFile) {
const frontmatter = vfile.data.astro?.frontmatter;
if (!frontmatter || !isFrontmatterValid(frontmatter))
throw new Error(
// Copied from Astro core `errors-data`
// TODO: find way to import error data from core
'[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
);
const exportNodes = [
const extraChildren: RootContent[] = [
jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
];
if (frontmatter.layout) {
exportNodes.unshift(
extraChildren.unshift(
jsToTreeNode(
// NOTE: Use `__astro_*` import names to prevent conflicts with user code
/** @see 'vite-plugin-markdown' for layout props reference */
Expand All @@ -39,7 +53,49 @@ export default function ({ children }) {
};`,
),
);
} else if (shouldAddCharset(tree, vfile)) {
extraChildren.unshift({
type: 'mdxJsxFlowElement',
name: 'meta',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'charset',
value: 'utf-8',
},
],
children: [],
});
}
tree.children = exportNodes.concat(tree.children);
tree.children = extraChildren.concat(tree.children);
};
}

/**
* If this is a page (e.g. in src/pages), has no layout, and `partial` isn't set to true, we default to
* adding charset=utf-8 like markdown so that users don't have to worry about it for MDX pages without layouts.
*/
function shouldAddCharset(tree: Root, vfile: VFile) {
const srcDirUrl = vfile.data.applyFrontmatterExport?.srcDir;
if (!srcDirUrl) return false;

const hasConstPartialTrue = tree.children.some(
(node) => node.type === 'mdxjsEsm' && exportConstPartialTrueRe.test(node.value),
);
if (hasConstPartialTrue) return false;

// NOTE: the pages directory is a non-configurable Astro behaviour
const pagesDir = path.join(fileURLToPath(srcDirUrl), 'pages').replace(/\\/g, '/');
// `vfile.path` comes from Vite, which is a normalized path (no backslashes)
const filePath = vfile.path;
if (!filePath.startsWith(pagesDir)) return false;

const hasLeadingUnderscoreInPath = filePath
.slice(pagesDir.length)
.replace(/\\/g, '/')
.split('/')
.some((part) => part.startsWith('_'));
if (hasLeadingUnderscoreInPath) return false;

return true;
}
13 changes: 11 additions & 2 deletions packages/integrations/mdx/src/vite-plugin-mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type { MdxOptions } from './index.js';
import { createMdxProcessor } from './plugins.js';
import { safeParseFrontmatter } from './utils.js';

export function vitePluginMdx(mdxOptions: MdxOptions): Plugin {
export interface VitePluginMdxOptions {
mdxOptions: MdxOptions;
srcDir: URL;
}

// NOTE: Do not destructure `opts` as we're assigning a reference that will be mutated later
export function vitePluginMdx(opts: VitePluginMdxOptions): Plugin {
let processor: ReturnType<typeof createMdxProcessor> | undefined;
let sourcemapEnabled: boolean;

Expand Down Expand Up @@ -47,12 +53,15 @@ export function vitePluginMdx(mdxOptions: MdxOptions): Plugin {
astro: {
frontmatter,
},
applyFrontmatterExport: {
srcDir: opts.srcDir,
},
},
});

// Lazily initialize the MDX processor
if (!processor) {
processor = createMdxProcessor(mdxOptions, { sourcemap: sourcemapEnabled });
processor = createMdxProcessor(opts.mdxOptions, { sourcemap: sourcemapEnabled });
}

try {
Expand Down
Loading