ããã«ã¡ã¯ãåºå£ã§ãã
ã¿ã¤ãã«ã«ããéããæè¡ããã°ãã¯ã¦ãªããã°ã«ç§»è¡ãã¾ããã
ãã®è¨äºã§ã¯ããªã移è¡ãããã¨ã«ãªã£ãã®ããã©ããã£ã¦ç§»è¡ããã®ãã移è¡ã§è¦å´ããã¨ãããªã©ãã¾ã¨ãã¦ããããã¨æãã¾ãã
ããè±ã»ã«ããã¹ãããã°ãè±Contentfulããã¯ã¦ãªããã°ã¸ã®ç§»è¡ããèãã§ããã°åèã«ãªãã®ã§ã¯ãªããã¨æãã¾ãã
ãªã移è¡ããã®ã
ã¾ããããããªã移è¡ããã®ãã¨ãã話ããã
ã¿ã¤ãã«ã«ãããããã«ãNuxtã使ã£ã¦ç¬èªã«éçºãè¡ã£ã¦ãã¾ããããéçºããæ°å¹´çµã¡ãããã¤ãã®åé¡ãããã¾ããã
大ããåããã¨2ã¤ã§ãã
- Nuxt 3ã¸ã®ç§»è¡ã大å¤ããã
- Contentfulã¸ã®ä¸æºãåã£ã¦ãã
Nuxt 3ã¸ã®ç§»è¡ã大å¤ããã
Nuxt + Netlify + Contentfulã§æ§æãããããã°ã®éçºå½å2020å¹´4æããã¯ãæ¥æ¬ã ã¨Vueãæ¯è¼ç人æ°ã ã£ãããã§ãã
trends.google.co.jp
ãã®é ã®mofmofã¯Vueãæ¡ç¨ãããã¨ãå¤ã
ãã£ããã§ãããä»ã¯ãã¬ã³ãã®ç§»ãå¤ãããç¸ã¾ã£ã¦ãVueãããReactã®æ¹ãå¾æã¨ãã人ãå¢ããVueãNuxtãåãã人ãç¸å¯¾çã«å°ãªããªãã¾ããã
ãããªãã¨å°ãã®ãNuxt 2ãã3ã¸ã®ç§»è¡ã§ãã
Nuxt 2ãã3ã¸ã®ç§»è¡ã大å¤ãããã®ã¯æåãªè©±ã§ãããæè¡ããã°ã«ã¤ãã¦ãåæ§ã®åé¡ãæ±ãã¦ãã¾ããã
Contentfulã¸ã®ä¸æºãåã£ã¦ãã
2ã¤ããã¾ãã
1ã¤ç®ããæéä½ç³»ã§ãã
ä¸æ¨å¹´ã®4æããã¨ã³ã¸ãã¢ãªã³ã°ã³ã¼ãã¨ãã¦æºããããã«ãªãããã®ä¸ç°ã§ã¢ã¦ããããéãå¢ããããã«ã¡ã³ãã¼ã®ã¿ããªã«ããã°ãæ¸ãã¦ãããããã«è¨ç»ãããã¨ããã£ããã§ãããå
¨å¡ãæå¾
ããã¨æéã大å¤ãªãã¨ã«ãªãã¨è©±ãããã¨ãããã¾ããã確ã20人以ä¸ã«ãªãã¨â¦ã¿ãããªè©±ã ã£ãè¦ããããã¾ãã
ãã®è¨ç»èªä½ã¯å¥ã®çç±ã«ããä¸æããããªãã£ããã§ããããã®å¾ãããã°ãæ¸ãã¦æ¬²ããã¨ãé¡ãããã¨Contentfulã®æéã©ããã¾ããããã¨ãã話ãåºã¦ããäºæ
ãç¶ãã¦ãã¾ããã
ãã1ã¤ããContentfulã®UIçã®åé¡ã§ãã
ã¨ãã£ã¿ã使ãã«ããããã¬ãã¥ã¼ãã¿ã°ãç»åã®æ±ãããè¾ããªã©ã®ã使ãåæã®é¨åã¸ã®ä¸æºã§ãã
以ä¸ã®çç±ã«ãããå¥ã®ä½ãã«ç§»è¡ãããã¨ãã話ã«ãªãã¾ããããããä¸æ¨å¹´2022å¹´æ«ãã2023å¹´é ã«ããã¦ã®è©±ã§ãã
å½åã®è¨ç»
2023å¹´4ææç¹ã§ã¯ãNext + Notionã«ããæ¹åã§ã¾ã¨ã¾ã£ã¦ãã¾ããã
Nuxt â NextãContentful â Notion ã¨ããæãã§ãã
Nextã¯App Routerãåºå§ããããã§ãããã¯ä»å¾ã®Nextã¯ã©ããªããã ãããã¨ä¸å®ãªææã§ããããNextãªããã£ããã¢ããããã¦ããã®ã§Nuxtããã¯ãªãã¨ããªãã ããã¨ããå¤æããã¾ããã
Notionã¯æ®æ®µããå©ç¨ãã¦ããã®ã§ãã¡ã³ãã¼ã¯åºæ¬åå ãã¦ãã¾ãããè¨äºã®å·çãããããã ããã¨ããçç±ã§æ¡ç¨ãã¾ããã
ãã ããããä¸æãè¡ãã¾ããã§ãããæ¹åæ§ã¨ãã¦ã¯è¯ãããã ã£ããã§ããã主åãã¦ããã¡ã³ãã¼ã®æ¡ä»¶ãå¿ãããªã£ãããã¦ãã¦ãä¸æãé²ããããªããªã£ã¦ãã¾ãã¾ããã
æ¹ãã¦ç§»è¡ãèãã
ããã¦2023å¹´æ«ã«åã³è¨äºãæ¸ãã¦ãããããè¨ç»ãç«ã¡ä¸ãããã ã¨ããã¨ããã°ããªãã¨ããããããã¨ãªã£ã次第ã§ãã
ä»åã¯ååã®åçãæ´»ããã¦ãéçºãªã½ã¼ã¹ã¯ä½¿ããªãåæã§èãã¾ããã
åè£ã¯ããã¤ãããã¾ããããã¯ã¦ãªããã°ã«æ±ºå®ãã¾ããã
ãµããã£ã¬ã¯ããªãªãã·ã§ã³ã使ããã®ã決ãæã®1ã¤ã«ãªã£ã¦ãã¾ãã
移è¡ã«ã¤ãã¦
ãããªãããªã§ç§»è¡ãããã¨ã«ãªã£ãã®ã§ãã¯ã¦ãªããã°ã«ã¤ãã¦è©³ç´°ãè²ã
調æ»ãã¾ããã
以ä¸ãä¸é¨èª¿æ»çµæãæç²ãã¾ãã
- ä¸æ¬ã§ç§»è¡ããã«ã¯ãMovable TypeãWordPresså½¢å¼ï¼WXRï¼ã®ãã¼ã¿ãå¿
è¦
- è¨äºã®ã¤ã³ãã¼ãã¯ãªã¼ãã¼æ¨©éã§ããè¡ããªã
ã¤ã³ãã¼ãã§ç»é²ããè¨äºã¯èè
æ
å ±ï¼ä½æè
æ
å ±ï¼ãè¨å®ã§ããªã
ã¤ã³ãã¼ããããªã¼ãã¼æ¨©éã®ã¢ã«ã¦ã³ãã«èªåçã«ç´ä»ãããã¾ãããããç½ ã§ããã
æã
ã¨ãã¦ã¯æ¸ãã人ã®æ
å ±ã¯å¤§åã ã¨æã£ã¦ããã®ã§ãWXRã§è¨å®åºæ¥ãããã«ãã¦ããããã¨å¬ããã®ã§ãããåãåãããããã¨ãããã¤ã³ãã¼ãæã«ãã¤ã³ãã¼ãå¾ã«ãè¨å®åºæ¥ãªãã¨ã®ãã¨ã ã£ãã®ã§ãã¡ãã£ã¨å·¥å¤«ãã¦å¯¾å¿ãã¾ããã詳ããã¯å¾è¿°ãã¾ãã
è¨äºURLã®å½¢å¼ã¯
- æ¨æºï¼
/entry/2011/11/07/161845
- ãã¤ã¢ãªã¼ï¼
/entry/20111107/1320650325
- ã¿ã¤ãã«ï¼
/entry/2011/11/07/é±æ«ã¯å·ã«è¡ãã¾ãã
ã®ã©ãããåºæ¬
ããã¨ã¯å¥ã«è¨äºãã¨ã«ã«ã¹ã¿ã ï¼/entry/[èªç±å
¥å]
ãé¸ã¹ã¾ãã移è¡è¨äºã«ã¤ãã¦ã¯ã«ã¹ã¿ã ã使ãã¾ããã
ã¡ãªã¿ã«ã/entry/
ã®é¨åã¯å¤æ´åºæ¥ã¾ãã
staff.hatenablog.com
ã¤ã³ãã¼ãæã«ããåºæ¥ãªãã®ã§ã¡ãã£ã¨ç½ æãããã¾ãã
DevBlogã«ããã¨å
¬å¼ã®ã¾ã¨ããµã¤ãã«æ²è¼ããã
æµå
¥ãå¢ããã®ã¯è¯ããã¨ãªã®ã§ãã¡ãã£ã¨æå¾
ãã¦ãã¾ãã
hatena.blog
è¨äºç§»è¡
åè¿°ã®éããèè
æ
å ±ãã©ããã£ã¦ç§»è¡ãããã課é¡ã§ããã
ä»åã¯ä»¥ä¸ã®ãã¿ã¼ã³ã«åãã¦å¯¾å¿ãããã¨ã«ãã¾ããã
- å¨ç±ä¸ and å人å義ã§ç§»è¡ããã人 and è¨äºãå°ãªã人
- æåã§å¯¾å¿ãã¦ããã
- å¨ç±ä¸ and å人å義ã§ç§»è¡ããã人 and è¨äºãå¤ã人
- AtomPubã§åãè¾¼ãã§ããã
- å人å義ãããªãã¦ãã人 or éè·æ¸ã¿ã®äºº
- ã¤ã³ãã¼ãæ©è½ã使ã£ã¦WXRãåãè¾¼ã
èè
æ
å ±ãç¶æãã¤ã¤ãããããã«ç§»è¡ãããªãAtomPubã使ããããªãããªã¨æãã¾ãã
AtomPubã¯ãå人ã¢ã«ã¦ã³ãã®æ
å ±ãå¿
è¦ãªã®ã§ãã³ã¼ãã¯ãã¡ãã§ç¨æãã¦ãããããã¼ã«ã«ãã·ã³ã§å®è¡ãã¦ãããå½¢ãåãã¾ããã詳細ã¯å¾è¿°ã
ã¤ã³ãã¼ãæ©è½ã使ã£ã¦WXRãåãè¾¼ãå ´å
è¨äºç®¡çã«Contentfulã使ã£ã¦ããã®ã§ãCLIã使ã£ã¦JSONå½¢å¼ã§ãã¼ã¿ãåºåãã¾ãã
WXRå½¢å¼ã¸ã®å¤æã¯ã¹ã¯ãªãããä½ãã¾ããã2023å¹´12ææ«æç¹ã§åä½ç¢ºèªãã¦ãã¾ãã
import fs from "fs";
import { create } from "xmlbuilder2";
import { parseISO, format } from "date-fns";
import { formatInTimeZone } from "date-fns-tz";
function createWxrXml(entries, tags, excludedAuthorIds) {
const root = {
rss: {
"@version": "2.0",
"@xmlns:excerpt": "http://wordpress.org/export/1.2/excerpt/",
"@xmlns:content": "http://purl.org/rss/1.0/modules/content/",
"@xmlns:wfw": "http://wellformedweb.org/CommentAPI/",
"@xmlns:dc": "http://purl.org/dc/elements/1.1/",
"@xmlns:wp": "http://wordpress.org/export/1.2/",
channel: {
link: "https://tech.mof-mof.co.jp",
item: entries
.filter(
(entry) =>
!excludedAuthorIds.includes(entry.fields.author?.["en-US"].sys.id)
)
.map((entry) => {
return {
title: entry.fields.title["en-US"],
link: `https://tech.mof-mof.co.jp/${entry.fields.url["en-US"]}`,
description: entry.fields.summary?.["en-US"] || "",
"content:encoded": {
$: entry.fields.article["en-US"],
},
"wp:post_date": formatInTimeZone(
parseISO(entry.fields.publishedDate["en-US"]),
"Asia/Tokyo",
"yyyy-MM-dd HH:mm:ss"
),
"wp:status": "publish",
"wp:post_type": "post",
category: entry.fields.tags?.["en-US"].map((entryTag) => {
const tag = tags.find((t) => t.sys.id === entryTag.sys.id);
return {
"@domain": "category",
"@nicename": encodeURIComponent(tag.fields.slug["en-US"]),
$: tag.fields.name["en-US"],
};
}),
};
}),
},
},
};
const doc = create(root);
return doc.end({ prettyPrint: true });
}
const contentfulData = JSON.parse(fs.readFileSync("/path/to/something.json", "utf8"));
const entries = contentfulData.entries.filter(
(entry) => entry.sys.contentType.sys.id === "post"
);
const tags = contentfulData.entries.filter(
(entry) => entry.sys.contentType.sys.id === "tag"
);
const excludedAuthorIds = [
"hogehogehoge",
"fugafugafuga"
];
const wxrXml = createWxrXml(entries, tags, excludedAuthorIds);
fs.writeFileSync(
`output/wxr-${format(
new Date(),
"yyyy-MM-dd-HH-mm"
)}.xml`,
wxrXml
);
å®è¡ããã¨WXRãã¡ã¤ã«ãåºåãããã®ã§ããªã¼ãã¼æ¨©éã®ã¢ã«ã¦ã³ãã§ã¤ã³ãã¼ãããã°OKã§ãã
è¨äºå
ã®ç»åã«ã¤ãã¦ã¯ãä¸é¨ãé¤ãç»åãã¼ã¿ã®ç§»è¡ã¡ãã¥ã¼ãã移è¡åºæ¥ã¾ããã
移è¡åºæ¥ãªãã£ãç»åã«ã¤ãã¦ã¯ãæåã§å¯¾å¿ãã¦ãã¾ãã
AtomPubã使ã£ããã¿ã¼ã³ã®å ´å
ã¤ã³ãã¼ãæ©è½å©ç¨æã¨åæ§ã«Contentful CLIã§JSONãã¡ã¤ã«ãåºåãããã®ã使ãã¾ãã
ãã¡ããåè¿°ã®éããã¹ã¯ãªãããçµã¿ã¾ããã
import axios from "axios";
import { create } from "xmlbuilder2";
import { parseISO, format } from "date-fns";
function convertEntryToXml(entry, tags) {
const xmlObj = {
entry: {
"@xmlns": "http://www.w3.org/2005/Atom",
"@xmlns:app": "http://www.w3.org/2007/app",
title: entry.fields.title["en-US"],
content: {
"@type": "text/x-markdown",
"#": entry.fields.article["en-US"],
},
updated: parseISO(entry.fields.publishedDate["en-US"]).toISOString(),
category: entry.fields.tags?.["en-US"].map((entryTag) => {
const tag = tags.find((t) => t.sys.id === entryTag.sys.id);
return {
"@term": tag.fields.name["en-US"],
};
}),
"app:control": {
"app:draft": "no",
"app:preview": "no",
},
"hatenablog:custom-url": {
"@xmlns:hatenablog": "http://www.hatena.ne.jp/info/xmlns#hatenablog",
"#": entry.fields.url["en-US"],
},
},
};
const doc = create(xmlObj);
return doc.end({ prettyPrint: true });
}
const contentfulData = JSON.parse(fs.readFileSync("/path/to/something", "utf8"));
const entries = contentfulData.entries.filter(
(entry) => entry.sys.contentType.sys.id === "post"
);
const tags = contentfulData.entries.filter(
(entry) => entry.sys.contentType.sys.id === "tag"
);
const filteredEntries = contentfulData.entries.filter(
(entry) =>
entry.fields.author?.["en-US"].sys.id === process.env.CONTENTFUL_AUTHOR_ID
);
filteredEntries.forEach(async (entry) => {
const xmlData = convertEntryToXml(entry, tags);
const url = `https://blog.hatena.ne.jp/${process.env.HATENA_BLOG_OWNER_ID}/${process.env.HATENA_BLOG_ID}/atom/entry`;
try {
const response = await axios.post(url, xmlData, {
headers: {
"Content-Type": "application/xml",
Authorization: `Basic ${Buffer.from(
`${process.env.HATENA_ID}:${process.env.HATENA_API_KEY}`
).toString("base64")}`,
},
});
console.log(`Entry posted successfully: ${response.data}`);
} catch (error) {
console.error("Error posting entry:", error);
}
});
â»HATENA_IDãHATENA_API_KEYã¯å人ã§çºè¡ãããã®ãæå®ãã
â»HATENA_BLOG_OWNER_IDãHATENA_BLOG_IDã¯ããã®ããã°ã®å ´åã ã¨ããããmofmof-inc
ãmofmof-inc.hatenablog.com
ã¨ãªãã¾ã
AtomPubã使ã£ã移è¡ã ã¨ç»åããã¾ã移è¡åºæ¥ãªãã£ãã®ã§ãæåã§å¯¾å¿ãã¾ãããçµæ§å¤§å¤ã§ããã
ãµããã£ã¬ã¯ããªãªãã·ã§ã³
ãã¼ã¿ç§»è¡ãå®äºãã¦ãå
¬éã®æºåãåºæ¥ã¦ããå©ç¨ãã¾ããã
éç¨éå§å¾ã«è¨å®ãããã¨ãåºæ¥ããã§ããããã¾ãè¨å®åºæ¥ã¦ããªãã¨ã¢ã¯ã»ã¹åºæ¥ãªããªãæéãçºçããããã¦å¤§å¤ã ã¨æã£ãã®ã§ãå
¬éåã«å®æ½ãã¾ããã
ã³ã¼ãã¬ã¼ããµã¤ãã®ãã¡ã¤ã³é
ä¸ã®/tech-blog
ã«è¨å®ãã¦ãã¾ãã
Netfilyã®ãªãã¼ã¹ãããã·è¨å®
ã³ã¼ãã¬ã¼ããµã¤ããNetfilyã使ã£ã¦éç¨ãã¦ããã®ã§ãNetfilyã§ãªãã¼ã¹ãããã·ãè¨å®ããå¿
è¦ãããã¾ããã
Netlifyã®ããã¥ã¡ã³ãããã©ã¼ã©ã ã§ã®ããåããè¦ã¦ããæããå®éã«è¨å®ã§ãããå°ãæªããã£ããã§ããåé¡ãªãåãã¦ãã¾ãã
åºæ¥ããªãnginxãfastlyçã®ã¯ã¦ãªããã°ãæ¤è¨¼æ¸ã¿ã®ãµã¼ãã¹ã使ã£ã¦è¨å®ããæ¹ãè¯ãã¨æãã¾ãã
[[redirects]]
from = "/tech-blog/*"
to = "https://0123456789.hatenablog-oem.com/tech-blog/:splat"
status = 301
force = true
headers = { X-Forwarded-Host = "www.mof-mof.co.jp", X-Hatena-Blog-Subdirectory-Token = "1234567890abcdef" }
_redirect
ãã¡ã¤ã«ã§ãªãã¤ã¬ã¯ãã®è¨å®ã¯å¯è½ã§ãããã«ã¹ã¿ã ãããã¼ãå¿
è¦ãªãããnetlify.toml
ã®æ¹ã§è¨å®ãã¦ãã¾ãã
robots.txtãè¨ç½®
ãµããã£ã¬ã¯ããªãªãã·ã§ã³ã使ãã¨ãã®æ¨å¥¨è¨å®ãããã®ã§ãã³ã¼ãã¬ã¼ããµã¤ãã®robots.txtã«è¿½å ãã¾ããã
User-agent: *
Disallow: /tech-blog/api/
Disallow: /tech-blog/draft/
Disallow: /tech-blog/preview
Sitemap: https://www.example.com/tech-blog/sitemap_index.xml
User-agent: Mediapartners-Google
Disallow: /tech-blog/draft/
Disallow: /tech-blog/preview
Netlifyã®Prerenderingãªãã·ã§ã³è¨å®
Prerenderingãªãã·ã§ã³ï¼è¨äºå·çæç¹ã§ã¯ãã¼ã¿çï¼ã使ã£ã¦ããã¨ãµããã£ã¬ã¯ããªãªãã·ã§ã³ã®æ¤è¨¼ã«å¤±æãã¦ãã¾ãã¾ããã
ããããªãã«ãããã¨ã§ãæ¤è¨¼ãã¼ã«ã§ã®æ¤è¨¼ãOKã«ãªã£ãã®ã§ããã¡ã使ã£ã¦ããå ´åã¯ãªãã«ããæ¹ãè¯ãããã§ãã
æ¤è¨¼ãã¼ã«ã§1ã¤ã ãæ¤è¨¼å¤±æãã
ãã¯ã¦ãªããã°ã®ãµã¼ãã¼ãè¿ããLocationããããæ£ããã¯ã©ã¤ã¢ã³ãã«å°éãã¦ãããã¨ããæ¤è¨¼ã«å¤±æãã¦ãã¾ãã¾ãã
ãã£ã¨è¦ãã¨ããåé¡ã¯å¤§ãããªããããªã®ã§ç¡è¦ãã¦ä½¿ããã¨ã«ãã¾ããã
æå¾ã«ããµããã£ã¬ã¯ããªã§åãã®ã確èªåºæ¥ãããå
ã
ã®æè¡ããã°ãããªãã¤ã¬ã¯ãããããã«Netfilyã®ãªãã¤ã¬ã¯ãè¨å®ãè¡ã£ã¦å®äºã§ãã
ç´°ãããã¨ãããã¨ãç»åã®ç§»è¡ã¨ãã«ãã´ãªã¼ã®æ´çã¨ããã£ã¦ãããã¾ããå²æãã¾ãã
ã¾ã¨ã
以ä¸ã§ãã¯ã¦ãªããã°ã¸ã®ç§»è¡ãå®äºãã¾ããã
ãã®è¨äºãã¯ã¦ãªããã°ã§è¦ãã¦ããã®ã§ããã°ã移è¡ãæåãã¦ããã¨ãããã¨ã ã¨æãã¾ãã
é·ãè¨äºã«ãªãã¾ããããããã¾ã§ã覧ããã ããããã¨ããããã¾ããã
ä»å¾ã¯ãã¯ã¦ãªããã°ã§æè¡çãªçºä¿¡ããã¦ããããã¨æãã¾ãããããããé¡ããã¾ãã