From ebd7d1e8697830bae23d05e87083a83ee96bb1bb Mon Sep 17 00:00:00 2001 From: Gui <51219838+gui-ace@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:19:02 +0900 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E7=BF=BB=E8=A8=B3=E3=83=96=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AESEO=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astro.config.mjs | 39 +++++++++++++++++++++++++ public/_redirects | 32 ++++++++++++++++++++ src/layouts/BaseLayout.astro | 13 ++++++--- src/pages/[locale]/blog/[...slug].astro | 16 ++++++---- src/views/BlogPostPage.astro | 23 +++++++++++++-- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 1a7179d..fc40af9 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,6 +4,44 @@ import sitemap from '@astrojs/sitemap' import rehypeExternalLinks from 'rehype-external-links' import rehypeOptimizeImages from './src/utils/rehype-optimize-images' import rehypeInjectAds from './src/utils/rehype-inject-ads' +import { existsSync, readdirSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +const defaultLocale = 'ja' +const locales = ['ja', 'en', 'zh-cn', 'es', 'pt', 'fr', 'ko', 'de', 'ru'] +const translatedBlogSlugsByLocale = Object.fromEntries( + locales + .filter((locale) => locale !== defaultLocale) + .map((locale) => { + const dir = fileURLToPath( + new URL(`./src/content/blog/${locale}/`, import.meta.url), + ) + return [ + locale, + new Set( + existsSync(dir) + ? readdirSync(dir) + .filter((file) => file.endsWith('.md')) + .map((file) => file.replace(/\.md$/, '')) + : [], + ), + ] + }), +) + +function isMissingLocalizedBlogPost(page) { + const { pathname } = new URL(page) + const parts = pathname.split('/').filter(Boolean) + const [locale, section, slug] = parts + return ( + locale && + locale !== defaultLocale && + translatedBlogSlugsByLocale[locale] && + section === 'blog' && + parts.length === 3 && + !translatedBlogSlugsByLocale[locale].has(decodeURIComponent(slug)) + ) +} export default defineConfig({ site: 'https://acecore.net', @@ -33,6 +71,7 @@ export default defineConfig({ lastmod: new Date(), filter(page) { return ( + !isMissingLocalizedBlogPost(page) && !page.includes('/blog/tags/') && !page.includes('/blog/archive/') && !page.includes('/blog/authors/') && diff --git a/public/_redirects b/public/_redirects index 00c1a63..f6b8a0a 100644 --- a/public/_redirects +++ b/public/_redirects @@ -52,6 +52,38 @@ /de/blog/business-system-implementation-tips/ https://systems.acecore.net/guide/ 301 /ru/blog/business-system-implementation-tips https://systems.acecore.net/guide/ 301 /ru/blog/business-system-implementation-tips/ https://systems.acecore.net/guide/ 301 +/en/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/en/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/zh-cn/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/zh-cn/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/es/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/es/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/pt/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/pt/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/fr/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/fr/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/ko/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/ko/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/de/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/de/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/ru/blog/homepage-production-cost-guide https://acecore.net/blog/homepage-production-cost-guide/ 301 +/ru/blog/homepage-production-cost-guide/ https://acecore.net/blog/homepage-production-cost-guide/ 301 +/en/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/en/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/zh-cn/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/zh-cn/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/es/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/es/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/pt/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/pt/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/fr/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/fr/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/ko/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/ko/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/de/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/de/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 +/ru/blog/service-cta-contact-prefill https://acecore.net/blog/service-cta-contact-prefill/ 301 +/ru/blog/service-cta-contact-prefill/ https://acecore.net/blog/service-cta-contact-prefill/ 301 /blog/tags/システム開発 https://systems.acecore.net/guide/ 301 /blog/tags/システム開発/ https://systems.acecore.net/guide/ 301 /en/blog/tags/システム開発 https://systems.acecore.net/guide/ 301 diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 4bb5f4e..3e6ca4d 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -25,6 +25,8 @@ interface Props { locale?: Locale breadcrumbItems?: { name: string; item?: string }[] languageSwitcherPath?: string + canonicalPath?: string + alternateUrls?: { locale: Locale; url: string }[] } const locale = Astro.props.locale ?? getLocaleFromUrl(Astro.url) @@ -44,12 +46,14 @@ const { preloadImage, breadcrumbItems, languageSwitcherPath, + canonicalPath, + alternateUrls, } = Astro.props const isHome = title === t(locale, 'nav.home') const gaFallbackDelay = isHome ? 12000 : 4000 const pageTitle = isHome ? t(locale, 'meta.siteTitle') : `${title} | Acecore` -const canonicalURL = new URL(Astro.url.pathname, Astro.site) +const canonicalURL = new URL(canonicalPath ?? Astro.url.pathname, Astro.site) const ogImage = image || new URL('/uploads/og-default.png', Astro.site).href const isMonetizedPage = Astro.url.pathname === '/blog/' || Astro.url.pathname.startsWith('/blog/') @@ -146,7 +150,8 @@ const breadcrumbLd = } : null) -const alternateUrls = getAlternateUrls(Astro.url.pathname, siteOrigin) +const alternateUrlEntries = + alternateUrls ?? getAlternateUrls(Astro.url.pathname, siteOrigin) import { ClientRouter } from 'astro:transitions' import Header from '../components/Header.astro' @@ -220,14 +225,14 @@ import { SITE } from '../data/site' href={locale === defaultLocale ? '/rss.xml' : `/${locale}/rss.xml`} /> { - alternateUrls.map(({ locale: l, url }) => ( + alternateUrlEntries.map(({ locale: l, url }) => ( )) } a.locale === defaultLocale)?.url} + href={alternateUrlEntries.find((a) => a.locale === defaultLocale)?.url} /> { diff --git a/src/pages/[locale]/blog/[...slug].astro b/src/pages/[locale]/blog/[...slug].astro index ead9105..a777704 100644 --- a/src/pages/[locale]/blog/[...slug].astro +++ b/src/pages/[locale]/blog/[...slug].astro @@ -5,16 +5,22 @@ import { locales, defaultLocale, type Locale } from '../../../i18n' import { isBasePost } from '../../../utils/blog-i18n' export async function getStaticPaths() { - const allPosts = (await getCollection('blog')) + const allRawPosts = await getCollection('blog') + const allPosts = allRawPosts .filter(isBasePost) .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()) + const translatedPostIds = new Set( + allRawPosts.filter((post) => !isBasePost(post)).map((post) => post.id), + ) return locales .filter((l) => l !== defaultLocale) .flatMap((locale) => - allPosts.map((post) => ({ - params: { locale, slug: post.id }, - props: { post, allPosts }, - })), + allPosts + .filter((post) => translatedPostIds.has(`${locale}/${post.id}`)) + .map((post) => ({ + params: { locale, slug: post.id }, + props: { post, allPosts }, + })), ) } diff --git a/src/views/BlogPostPage.astro b/src/views/BlogPostPage.astro index 16fd52e..be2c64e 100644 --- a/src/views/BlogPostPage.astro +++ b/src/views/BlogPostPage.astro @@ -33,6 +33,7 @@ import { getDateParts, defaultLocale, htmlLangMap, + locales, type Locale, } from '../i18n' import { getCollection } from 'astro:content' @@ -64,6 +65,21 @@ const authors = await getAllAuthors() const tagsData = await getAllTags() const post = localizePost(basePost, allRawPosts, locale) const baseSlug = basePost.id +const isFallbackLocalePost = locale !== defaultLocale && post.id === basePost.id +const canonicalPostPath = isFallbackLocalePost + ? `/blog/${baseSlug}/` + : u(`/blog/${baseSlug}/`) +const canonicalPostUrl = new URL(canonicalPostPath, Astro.site).href +const alternateUrls = locales + .filter( + (l) => + l === defaultLocale || + allRawPosts.some((candidate) => candidate.id === `${l}/${baseSlug}`), + ) + .map((l) => ({ + locale: l, + url: new URL(getLocalizedUrl(`/blog/${baseSlug}/`, l), Astro.site).href, + })) const { Content, headings } = await render(post) const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3) @@ -145,7 +161,7 @@ const blogPostingLd = { }, mainEntityOfPage: { '@type': 'WebPage', - '@id': `https://acecore.net/blog/${baseSlug}/`, + '@id': canonicalPostUrl, }, inLanguage: htmlLangMap[locale], } @@ -229,9 +245,12 @@ const ctaHeading = isDefaultLocale jsonLd={jsonLdData} publishedTime={post.data.date} modifiedTime={post.data.lastUpdated} + robots={isFallbackLocalePost ? 'noindex, follow' : 'index, follow'} tags={post.data.tags} author={author?.name} locale={locale} + canonicalPath={canonicalPostPath} + alternateUrls={alternateUrls} preloadImage={postImage ? { src: optimizeImage(postImage), @@ -397,7 +416,7 @@ const ctaHeading = isDefaultLocale