Skip to content

Commit 4c6d537

Browse files
committed
feat: generate english blog og images
1 parent a320129 commit 4c6d537

7 files changed

Lines changed: 90 additions & 37 deletions

File tree

23.1 KB
Loading
19 KB
Loading

scripts/lib/build-ogp-core.js

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import matter from 'gray-matter';
1313
* @typedef {{ title: string; isBasicPage: boolean }} OgpCardTarget
1414
* @typedef {{ slug: string; filename: string; title: string; isBasicPage: boolean }} OgpTarget
1515
* @typedef {{ name: string; data: Buffer | Uint8Array; weight: number; style: string }} FontDefinition
16-
* @typedef {{ name: string; isFile(): boolean }} BlogDirEntry
16+
* @typedef {{ name: string; isFile(): boolean; isDirectory?(): boolean }} BlogDirEntry
1717
* @typedef {{ data: Record<string, unknown> }} FrontmatterResult
1818
* @typedef {{ log?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }} Logger
1919
* @typedef {(directory: string) => Promise<string[]>} FontDirReader
@@ -297,26 +297,36 @@ export async function getBlogEntries({
297297
logger = console,
298298
} = {}) {
299299
try {
300-
const entries = await readdirImpl(contentDir, { withFileTypes: true });
300+
const localeDirs = await readdirImpl(contentDir, { withFileTypes: true });
301301
const posts = [];
302302

303-
for (const entry of entries) {
304-
if (!entry.isFile()) continue;
305-
if (!entry.name.endsWith('.md') && !entry.name.endsWith('.mdx')) continue;
306-
if (entry.name.startsWith('_')) continue;
307-
308-
const slug = entry.name.replace(/\.mdx?$/, '');
309-
const filePath = path.join(contentDir, entry.name);
310-
const fileContents = await readFileImpl(filePath, 'utf-8');
311-
const { data } = matterImpl(fileContents);
312-
const title = typeof data.title === 'string' && data.title.length > 0 ? data.title : slug;
313-
314-
posts.push({
315-
slug,
316-
filename: slug,
317-
title,
318-
isBasicPage: false,
319-
});
303+
for (const localeDir of localeDirs) {
304+
const locale = localeDir.name;
305+
const isLocaleDirectory =
306+
(locale === 'ja' || locale === 'en') && localeDir.isDirectory?.() === true;
307+
308+
if (!isLocaleDirectory) continue;
309+
310+
const entries = await readdirImpl(path.join(contentDir, locale), { withFileTypes: true });
311+
312+
for (const entry of entries) {
313+
if (!entry.isFile()) continue;
314+
if (!entry.name.endsWith('.md') && !entry.name.endsWith('.mdx')) continue;
315+
if (entry.name.startsWith('_')) continue;
316+
317+
const slug = entry.name.replace(/\.mdx?$/, '');
318+
const filePath = path.join(contentDir, locale, entry.name);
319+
const fileContents = await readFileImpl(filePath, 'utf-8');
320+
const { data } = matterImpl(fileContents);
321+
const title = typeof data.title === 'string' && data.title.length > 0 ? data.title : slug;
322+
323+
posts.push({
324+
slug,
325+
filename: locale === 'en' ? `en/${slug}` : slug,
326+
title,
327+
isBasicPage: false,
328+
});
329+
}
320330
}
321331

322332
return posts;
@@ -371,7 +381,9 @@ export async function runBuildOgp({
371381
}
372382

373383
const png = await createOgpImageImpl(target, fonts);
374-
await writeFileImpl(path.join(outputDir, `${target.filename}.png`), png);
384+
const outputPath = path.join(outputDir, `${target.filename}.png`);
385+
await mkdirImpl(path.dirname(outputPath), { recursive: true });
386+
await writeFileImpl(outputPath, png);
375387
logger.log(` ✓ ${target.filename}.png を保存しました`);
376388
}
377389

src/layouts/Layout.astro

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface Props {
1616
title: string;
1717
description?: string;
1818
blogTitle?: string; // ブログ記事のタイトルを渡すためのプロパティを追加
19+
blogContentLocale?: Locale;
1920
robots?: string;
2021
locale?: Locale;
2122
}
@@ -24,6 +25,7 @@ const {
2425
title,
2526
description,
2627
blogTitle, // ブログタイトルを受け取る
28+
blogContentLocale,
2729
robots = 'index, follow',
2830
locale: localeProp,
2931
} = Astro.props;
@@ -48,7 +50,8 @@ const ogpPathname =
4850
if (blogTitle) {
4951
// ブログ記事の場合、スラッグに基づいたOGP画像を使用
5052
const slug = Astro.url.pathname.split('/').filter(Boolean).pop() || 'index';
51-
ogImageUrl = `/images/ogp/${slug}.png`;
53+
ogImageUrl =
54+
blogContentLocale === 'en' ? `/images/ogp/en/${slug}.png` : `/images/ogp/${slug}.png`;
5255
} else if (ogpPathname === '/' || ogpPathname === '') {
5356
// トップページ
5457
ogImageUrl = '/images/ogp/default.png';

src/pages/blog/[...slug].astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const locale = 'ja';
2929
title={`${post.data.title} | SAKUSPACE`}
3030
description={post.data.description}
3131
blogTitle={post.data.title}
32+
blogContentLocale={locale}
3233
locale={locale}
3334
>
3435
<div

src/pages/en/blog/[...slug].astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export async function getStaticPaths() {
2525
2626
const { post } = Astro.props;
2727
const locale = 'en';
28-
const isFallbackJapanesePost = getBlogEntryLocale(post) !== locale;
28+
const blogContentLocale = getBlogEntryLocale(post) ?? 'ja';
29+
const isFallbackJapanesePost = blogContentLocale !== locale;
2930
3031
const { Content, headings } = await render(post);
3132
const estimatedReadingTimeMinutes = getEstimatedReadingTimeMinutes(post.body ?? '');
@@ -35,6 +36,7 @@ const estimatedReadingTimeMinutes = getEstimatedReadingTimeMinutes(post.body ??
3536
title={`${post.data.title} | SAKUSPACE`}
3637
description={post.data.description}
3738
blogTitle={post.data.title}
39+
blogContentLocale={blogContentLocale}
3840
locale={locale}
3941
>
4042
<div

tests/build-ogp-core.test.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,35 @@ describe('createOgpElement', () => {
140140
});
141141

142142
describe('getBlogEntries', () => {
143-
it('collects md and mdx files, skips underscored entries, and falls back to the slug', async () => {
144-
const readdirImpl = vi.fn(async () => [
145-
{ name: 'first-post.mdx', isFile: () => true },
146-
{ name: 'second-post.md', isFile: () => true },
147-
{ name: '_draft-post.mdx', isFile: () => true },
148-
{ name: 'images', isFile: () => false },
149-
{ name: 'notes.txt', isFile: () => true },
150-
]);
143+
it('collects localized md and mdx files, skips underscored entries, and falls back to the slug', async () => {
144+
const readdirImpl = vi
145+
.fn()
146+
.mockResolvedValueOnce([
147+
{ name: 'ja', isFile: () => false, isDirectory: () => true },
148+
{ name: 'en', isFile: () => false, isDirectory: () => true },
149+
{ name: 'notes.txt', isFile: () => true, isDirectory: () => false },
150+
])
151+
.mockResolvedValueOnce([
152+
{ name: 'first-post.mdx', isFile: () => true },
153+
{ name: 'second-post.md', isFile: () => true },
154+
{ name: '_draft-post.mdx', isFile: () => true },
155+
{ name: 'images', isFile: () => false },
156+
{ name: 'notes.txt', isFile: () => true },
157+
])
158+
.mockResolvedValueOnce([
159+
{ name: 'first-post.mdx', isFile: () => true },
160+
{ name: '_english-draft.mdx', isFile: () => true },
161+
]);
151162
const readFileImpl = vi
152163
.fn()
153164
.mockResolvedValueOnce('---\ntitle: First Post\n---')
154-
.mockResolvedValueOnce('---\ndescription: No title\n---');
165+
.mockResolvedValueOnce('---\ndescription: No title\n---')
166+
.mockResolvedValueOnce('---\ntitle: First Post in English\n---');
155167
const matterImpl = vi
156168
.fn()
157169
.mockReturnValueOnce({ data: { title: 'First Post' } })
158-
.mockReturnValueOnce({ data: {} });
170+
.mockReturnValueOnce({ data: {} })
171+
.mockReturnValueOnce({ data: { title: 'First Post in English' } });
159172

160173
await expect(
161174
getBlogEntries({
@@ -177,7 +190,16 @@ describe('getBlogEntries', () => {
177190
title: 'second-post',
178191
isBasicPage: false,
179192
},
193+
{
194+
slug: 'first-post',
195+
filename: 'en/first-post',
196+
title: 'First Post in English',
197+
isBasicPage: false,
198+
},
180199
]);
200+
201+
expect(readFileImpl).toHaveBeenCalledWith('/virtual/blog/ja/first-post.mdx', 'utf-8');
202+
expect(readFileImpl).toHaveBeenCalledWith('/virtual/blog/en/first-post.mdx', 'utf-8');
181203
});
182204

183205
it('returns an empty list when reading blog entries fails', async () => {
@@ -375,18 +397,25 @@ describe('runBuildOgp', () => {
375397
);
376398
});
377399

378-
it('generates default pages and blog entries in normal mode', async () => {
400+
it('generates default pages and blog entries in normal mode, including nested output paths', async () => {
379401
const writeFileImpl = vi.fn();
402+
const mkdirImpl = vi.fn();
380403
const createOgpImage = vi.fn(async (target) => Buffer.from(target.filename));
381404
const logger = createLogger();
382405
const getBlogEntriesMock = vi.fn(async () => [
383406
{ slug: 'first-post', filename: 'first-post', title: 'First Post', isBasicPage: false },
407+
{
408+
slug: 'first-post',
409+
filename: 'en/first-post',
410+
title: 'First Post in English',
411+
isBasicPage: false,
412+
},
384413
]);
385414

386415
const result = await runBuildOgp({
387416
argv: ['node', 'scripts/build-ogp.js'],
388417
outputDir: '/virtual/output',
389-
mkdirImpl: vi.fn(),
418+
mkdirImpl,
390419
writeFileImpl,
391420
loadFonts: vi.fn(async () => [createFont()]),
392421
createOgpImage,
@@ -395,13 +424,15 @@ describe('runBuildOgp', () => {
395424
});
396425

397426
expect(getBlogEntriesMock).toHaveBeenCalledOnce();
398-
expect(result.generatedCount).toBe(basicPages.length + 1);
427+
expect(result.generatedCount).toBe(basicPages.length + 2);
399428
expect(result.targets.at(-1)).toEqual({
400429
slug: 'first-post',
401-
filename: 'first-post',
402-
title: 'First Post',
430+
filename: 'en/first-post',
431+
title: 'First Post in English',
403432
isBasicPage: false,
404433
});
434+
expect(mkdirImpl).toHaveBeenCalledWith('/virtual/output', { recursive: true });
435+
expect(mkdirImpl).toHaveBeenCalledWith('/virtual/output/en', { recursive: true });
405436
expect(writeFileImpl).toHaveBeenCalledWith(
406437
'/virtual/output/default.png',
407438
Buffer.from('default')
@@ -410,5 +441,9 @@ describe('runBuildOgp', () => {
410441
'/virtual/output/first-post.png',
411442
Buffer.from('first-post')
412443
);
444+
expect(writeFileImpl).toHaveBeenCalledWith(
445+
'/virtual/output/en/first-post.png',
446+
Buffer.from('en/first-post')
447+
);
413448
});
414449
});

0 commit comments

Comments
 (0)