# Masutaka's ChangeLog Memo > This is a blog that records Masutaka's change history. -------------------------------------------------------------------------------- title: "Making this Hugo-built blog multilingual" date: "2025-09-23" -------------------------------------------------------------------------------- I've made this blog built with Hugo multilingual. For now, it's only available in English. - https://masutaka.net/ - https://masutaka.net/en/ :point_left: ## Why I implemented multilingual support As part of my OSS activities at work, I often cross-post translated articles to platforms like dev.to and Medium. Recently, I've been casually translating and cross-posting articles on my own as well. For example, the English translation of {{< post id="2025-08-15-1" lang="ja" >}} was cross-posted to these three sites: - [dev.to](https://dev.to/masutaka/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel-5f76) - This served as the original English version - [Hashnode](https://masutaka.hashnode.dev/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel) - Set dev.to's article as the canonical version - [Medium](https://medium.com/@masutaka/migration-from-pocket-and-hatena-bookmark-to-raindrop-io-and-creating-helm-raindrop-el-6e67e22a7f17) - Set dev.to's article as the canonical version However, since I'm maintaining this blog masutaka.net, I thought it would be better to store the original English articles here and cross-post them to the above three sites. Setting the canonical URL to my own blog seems like it would also be beneficial for SEO. ## Hugo's Multilingual Support Hugo includes built-in multilingual support with simple configuration. All you need to do is add the following settings in your `config.toml` file: ```toml defaultContentLanguage = "ja" [languages.ja] weight = 1 languageName = "日本語" title = "マスタカの ChangeLog メモ" [languages.en] weight = 2 languageName = "English" title = "Masutaka's ChangeLog Memo" ``` Articles with `.en.md` in their filenames will be the English versions. - Japanese version: `content/posts/2025-08-15-1.md` - English version: `content/posts/2025-08-15-1.en.md` With this configuration, articles can be accessed via the following URLs: - Japanese version: `https://masutaka.net/2025-08-15-1/` - English version: `https://masutaka.net/en/2025-08-15-1/` ## Multilingual Implementation Strategy When implementing multilingual support, I followed this approach: ### 1. Maintain Existing URLs Unchanged The most important policy was to absolutely preserve all existing URLs. Given that this blog has been operational for over 20 years, any changes to URLs would cause numerous broken links. Therefore, I kept the Japanese version as is and added an `/en/` prefix only for the English version. - `/index.html` - Almost unchanged, with `/en/index.html` added - `/index.xml` - Unchanged, with `/en/index.xml` added - `/sitemap.xml` - Changed to reference `/ja/sitemap.xml` and `/en/sitemap.xml` - `/llms.txt`, `/llms-full.txt` - Unchanged, with `/en/llms.txt` and `/en/llms-full.txt` added - See reference: {{< post "2025-05-18-1" >}} :point_right: The behavior described above is achieved through the configuration settings for `defaultContentLanguage`, `[languages.ja]`, and `[languages.en]`. ### 2. Keep English Menu Options in the Top Right Minimal The Device, History, and About pages contain content that is primarily Japanese-specific with limited value in translation, so I decided to skip them this time. - **Japanese Version**: [Archive]({{< relref path="archives.md" lang="ja" >}}), [Tags]({{< relref path="tags.md" lang="ja" >}}), [Search]({{< relref path="search.md" lang="ja" >}}), [Device]({{< relref path="device.md" lang="ja" >}}), [History]({{< relref path="history.md" lang="ja" >}}), [About]({{< relref path="about.md" lang="ja" >}}) - **English Version**: Only includes [Archive]({{< relref path="archives.md" lang="en" >}}), [Tags]({{< relref path="tags.md" lang="en" >}}), [Search]({{< relref path="search.md" lang="en" >}}) ## Implementation Details Here's an overview of the actual changes. ### 1. Modifications to config.toml Added language configuration settings and corresponding menu settings for each language. :point_right: The diff and the updated config.toml are attached in the [Appendix](#appendix). ### 2. Creating English versions of static pages - `content/archives.en.md` - Archives page - `content/search.en.md` - Search page - `content/privacy.en.md` - Privacy Policy :warning: Initially, I attempted to handle this by creating symbolic links from `*.en.md` to `*.md`, but Hugo did not recognize these links. ### 3. Modifying custom shortcodes Updated the custom `post` shortcode to accept an `lang` argument. **layouts/shortcodes/post.html:** ```html {{- $id := .Get "id" | default (.Get 0) -}} {{- $lang := .Get "lang" | default .Page.Lang -}} {{- $url := relref . (dict "path" $id "lang" $lang) -}} {{- $title := .Get "title" -}} {{ with $title }}{{ $title }}{{ else }}[{{ $id }}]{{ end }} ``` Example usage: ```go-html-template {{}} {{}} ``` ### 4. Translation Support for Custom Partials Since some translations were needed in `layouts/partials/*.html`, I created `i18n/ja.toml` and `i18n/en.toml` files. Usage example: ```html {{ i18n "sendMessage" }} ``` **i18n/ja.toml:** ```toml sendMessage = "メッセージ送信" ``` **i18n/en.toml:** ```toml sendMessage = "Send Message" ``` ## Conclusion I successfully added multilingual support to this Hugo-built blog. The implementation was cleaner than I expected :+1: - All existing URLs remained unchanged, limiting the impact to the `/en/` directory - Achieved using only Hugo's standard features with minimal customization - Reduced maintenance costs by keeping the English menu minimal - sitemap.xml and llms.txt were automatically multilingualized Moving forward, I'll translate new articles as needed and use https://masutaka.net/en/ as the original source while cross-posting to dev.to, Hashnode, and Medium. While I'm not sure if there's actually an English-speaking audience, I'm satisfied with the result. With recent AI translation being fairly accurate, it's not that difficult anyway :sunglasses: ## References - [Multilingual mode - Hugo](https://gohugo.io/content-management/multilingual/) ## Appendix ### Changes to config.toml
Show details ```diff diff --git a/config.toml b/config.toml index 0830be8b..a6bae12c 100644 --- a/config.toml +++ b/config.toml @@ -7,7 +7,6 @@ googleAnalytics = "G-K28CQCC064" hasCJKLanguage = true languageCode = "ja" theme = "papermod" -title = "マスタカの ChangeLog メモ" [permalinks] posts = "/:filename" @@ -27,14 +26,12 @@ title = "マスタカの ChangeLog メモ" isPlainText = true mediaType = "text/plain" rel = "alternate" - root = true [outputFormats.llmsfull] baseName = "llms-full" isPlainText = true mediaType = "text/plain" rel = "alternate" - root = true # # papermod configuration @@ -51,14 +48,9 @@ title = "マスタカの ChangeLog メモ" author = "masutaka" comments = true defaultTheme = "auto" - description = "マスタカの変更履歴が記録されていくブログです。" showtoc = true tocopen = true -[params.homeInfoParams] - Title = "マスタカネット" - Content = "マスタカの変更履歴が記録されていくブログです。" - [params.assets] theme_color = "#ffffff" msapplication_TileColor = "#da532c" @@ -79,46 +71,81 @@ title = "マスタカの ChangeLog メモ" [[params.socialIcons]] name = "GitHub" url = "https://github.com/masutaka" -[[params.socialIcons]] - name = "Dev" - url = "https://dev.to/masutaka" -[[params.socialIcons]] - name = "Hashnode" - url = "https://masutaka.hashnode.dev/" -[[params.socialIcons]] - name = "Medium" - url = "https://medium.com/@masutaka" [[params.socialIcons]] name = "RSS" url = "/index.xml" -[[menu.main]] +# +# Multilingual +# + +[languages.ja] + weight = 1 + languageName = "日本語" + title = "マスタカの ChangeLog メモ" + +[languages.ja.params] + description = "マスタカの変更履歴が記録されていくブログです。" + +[languages.ja.params.homeInfoParams] + Title = "マスタカネット" + Content = "マスタカの変更履歴が記録されていくブログです。" + +[[languages.ja.menu.main]] identifier = "archives" name = "Archive" url = "/archives/" weight = 1 -[[menu.main]] +[[languages.ja.menu.main]] identifier = "tags" name = "Tags" url = "/tags/" weight = 2 -[[menu.main]] +[[languages.ja.menu.main]] identifier = "search" name = "Search" url = "/search/" weight = 3 -[[menu.main]] +[[languages.ja.menu.main]] identifier = "device" name = "Device" url = "/device/" weight = 4 -[[menu.main]] +[[languages.ja.menu.main]] identifier = "history" name = "History" url = "/history/" weight = 5 -[[menu.main]] +[[languages.ja.menu.main]] identifier = "about" name = "About" url = "/about/" weight = 6 + +[languages.en] + weight = 2 + languageName = "English" + title = "Masutaka's ChangeLog Memo" + +[languages.en.params] + description = "This is a blog that records Masutaka's change history." + +[languages.en.params.homeInfoParams] + Title = "Masutaka Net" + Content = "This is a blog that records Masutaka's change history." + +[[languages.en.menu.main]] + identifier = "archives" + name = "Archive" + url = "/en/archives/" + weight = 1 +[[languages.en.menu.main]] + identifier = "tags" + name = "Tags" + url = "/en/tags/" + weight = 2 +[[languages.en.menu.main]] + identifier = "search" + name = "Search" + url = "/en/search/" + weight = 3 ```
### config.toml after multilingualization
Show details ```toml baseURL = "https://masutaka.net/" defaultContentLanguage = "ja" disablePathToLower = true enableEmoji = true enableRobotsTXT = true googleAnalytics = "G-K28CQCC064" hasCJKLanguage = true languageCode = "ja" theme = "papermod" [permalinks] posts = "/:filename" [taxonomies] tag = "tags" [markup.goldmark.renderer] hardWraps = true unsafe = true [outputs] home = ["html", "rss", "llms", "llmsfull"] [outputFormats.llms] baseName = "llms" isPlainText = true mediaType = "text/plain" rel = "alternate" [outputFormats.llmsfull] baseName = "llms-full" isPlainText = true mediaType = "text/plain" rel = "alternate" # # papermod configuration # [params] AmazonJpAffiliateID = "masutaka04-22" DateFormat = "2006-01-02 (Mon)" ShowAllPagesInArchive = true ShowCodeCopyButtons = true ShowFullTextinRSS = true ShowPageNums = true ShowPostNavLinks = true author = "masutaka" comments = true defaultTheme = "auto" showtoc = true tocopen = true [params.assets] theme_color = "#ffffff" msapplication_TileColor = "#da532c" [params.social] fediverse_creator = "@masutaka@mstdn.love" twitter = "@masutaka" [[params.socialIcons]] name = "Mastodon" url = "https://mstdn.love/@masutaka" [[params.socialIcons]] name = "Bluesky" url = "https://bsky.app/profile/masutaka.net" [[params.socialIcons]] name = "Twitter" url = "https://twitter.com/masutaka" [[params.socialIcons]] name = "GitHub" url = "https://github.com/masutaka" [[params.socialIcons]] name = "RSS" url = "/index.xml" # # Multilingual # [languages.ja] weight = 1 languageName = "日本語" title = "マスタカの ChangeLog メモ" [languages.ja.params] description = "マスタカの変更履歴が記録されていくブログです。" [languages.ja.params.homeInfoParams] Title = "マスタカネット" Content = "マスタカの変更履歴が記録されていくブログです。" [[languages.ja.menu.main]] identifier = "archives" name = "Archive" url = "/archives/" weight = 1 [[languages.ja.menu.main]] identifier = "tags" name = "Tags" url = "/tags/" weight = 2 [[languages.ja.menu.main]] identifier = "search" name = "Search" url = "/search/" weight = 3 [[languages.ja.menu.main]] identifier = "device" name = "Device" url = "/device/" weight = 4 [[languages.ja.menu.main]] identifier = "history" name = "History" url = "/history/" weight = 5 [[languages.ja.menu.main]] identifier = "about" name = "About" url = "/about/" weight = 6 [languages.en] weight = 2 languageName = "English" title = "Masutaka's ChangeLog Memo" [languages.en.params] description = "This is a blog that records Masutaka's change history." [languages.en.params.homeInfoParams] Title = "Masutaka Net" Content = "This is a blog that records Masutaka's change history." [[languages.en.menu.main]] identifier = "archives" name = "Archive" url = "/en/archives/" weight = 1 [[languages.en.menu.main]] identifier = "tags" name = "Tags" url = "/en/tags/" weight = 2 [[languages.en.menu.main]] identifier = "search" name = "Search" url = "/en/search/" weight = 3 ```
-------------------------------------------------------------------------------- title: "Migration from Pocket and Hatena Bookmark to Raindrop.io (and Creating helm-raindrop.el)" date: "2025-08-15" -------------------------------------------------------------------------------- I've migrated from [Pocket](https://getpocket.com/) and [Hatena Bookmark (Hatebu)](https://b.hatena.ne.jp/), services I had been using for over a decade, to a new bookmarking service: Raindrop.io. I've also created helm-raindrop.el so I can comfortably search from Emacs as before. ## What is Raindrop.io? :link: https://raindrop.io/ Raindrop.io is a bookmarking service with a modern design and rich features. - Organize bookmarks with collections - Tagging and smart filters - Full-text search (premium plan only) - Browser extensions, mobile apps - [Integrations with external services](https://raindrop.io/integrations) like IFTTT and Zapier - [REST API](https://developer.raindrop.io/) for developers The backend is proprietary, but everything else is open-source (OSS) and maintained by a single person, [Rustem Mussabekov](https://github.com/exentrich), who lives in Kazakhstan. {{< github_repo "raindropio/app" >}} {{< github_repo "raindropio/mobile" >}} {{< github_repo "raindropio/desktop" >}} ## Migration Pocket and Hatebu to Raindrop.io On July 8, 2025, Pocket was discontinued. I had used it as a "read-it-later" service for many years, so I had to find an alternative. I had also been using Hatebu for a full 16 years since 2009, but since I've been distancing myself from social media in recent years, I decided to switch to a unified service. Here's why I chose Raindrop.io: - The free plan is more than enough (unlimited bookmarks) - I can use it as a "read-it-later" service, just like Pocket - I can also use it as a persistent bookmarking service, like Hatebu - The API is public, making it easy to create my own tools - It has a data export feature, so there's less worry about vendor lock-in ## The good things about Raindrop.io After using it, I found several convenient features. ### The free plan is practical - **URLs can be edited** - You can correct the URL later even if it changes - **Collections feature** - You can organize bookmarks like folders and set them to be public or private individually - **Nested collections** - You can set up a hierarchical structure - **Highlighting feature** - You can mark important parts of an article - **Mobile app** - In addition to the web version, browser extensions, and desktop version, there are also apps for iOS and Android ### The paid plan's automated web archiving is convenient :link: https://help.raindrop.io/premium-features - **AI-powered suggestions** - It suggests appropriate collections and tags when saving and suggests merging or renaming similar tags - **Full-text search** - It performs a full-text search on the content of saved articles - **Automated web archiving feature** - It permanently saves content even if the site disappears - **Reminder feature** - It notifies you of a specific bookmark via the app or email at a specified time - **Annotation feature** - You can add notes to highlights - **Detection of duplicate and broken links** - Makes maintenance easier (?) - **Automated backup** - You can manage versions by linking with Google Drive - **Extended upload limit** - You can upload up to 10GB of images, videos, and PDFs per month I tried the paid plan and found the automated web archiving to be very convenient. I can't help but fix broken links when I find them. I know some might argue that if you can't find them, you don't have to maintain them... While fixing them, I realized that many of my past bookmarks were inaccessible. With Raindrop.io's automated web archiving, a new reason is added to "read-it-later" or "bookmark": "it'll be readable forever." The current paid plan is $33.04 per year (tax included), which is about $3.54 per month (tax included), so it's not a bad price. ## The bad things about Raindrop.io It's not a perfect service, so there are a few points that are difficult to use. - **The app icon is plain** - It's difficult to spot in my smartphone's share menu, and I often have to search for it - **The saving process is a bit sluggish** - It feels like it takes an extra 1-2 seconds to save a bookmark compared to Pocket - **No archiving feature** - If you're looking for a 'mark as read' feature like Pocket's, you'll need to create a dedicated collection and manually move bookmarks to it (which requires two taps) However, even with these points, it's still a good and very usable service. ## I created helm-raindrop.el For this migration, I created helm-raindrop.el, which allows you to search and browse Raindrop.io bookmarks from Emacs. {{< github_repo "masutaka/emacs-helm-raindrop" >}} I'm not following what's next after Helm, but since the main function of helm-raindrop.el is to create a cache file `~/.emacs.d/helm-raindrop` with the REST API, I don't think it will be difficult to migrate later. Actually, I had a similar tool for Hatebu, and its extreme convenience was one of the reasons I was hesitant to migrate. {{< github_repo "masutaka/emacs-helm-hatena-bookmark" >}} Being able to search bookmarks quickly from Emacs is quite comfortable. For example, if I think, "where was that article?", I just press `⌘-b` and enter a search word. That's what my [init.el](https://github.com/masutaka/dotfiles-public/blob/816650d2ea5412ddf9e72925661f65eaed6218f5/.emacs.d/init.el) does. {{< youtube id="te8OTzM4V3A" title="Emacs helm-raindrop.el demo" >}} I'm not sure if it's a good or bad thing that I can continue to search for bookmarks from Emacs, but for now, I've managed to maintain the status quo. ## Conclusion Following the discontinuation of Pocket, I migrated to Raindrop.io, including Hatebu, which I had used for a full 16 years. At first, it was a reluctant migration, but after using it, I found it to be a surprisingly good service. - The free plan is practical, and the paid plan is also reasonably priced at $33.04 per year (tax included) - The API is public, making it easy to integrate with self-made tools - It has modern features like AI and automated web archiving I also created helm-raindrop.el, so I've been able to maintain the ability to search from Emacs. I recommend it to anyone looking for a bookmarking service. The free plan is very usable, and migrating from Pocket or Hatebu is easy, so it's a good idea to give it a try. I'll continue to use it and will write another article if I discover anything new. -------------------------------------------------------------------------------- title: "Self-implemented IFTTT Pro's RSS feed notification feature with AWS serverless architecture" date: "2025-07-20" -------------------------------------------------------------------------------- For casual information gathering, I've been running a serverless application called masutaka-feed since 2020. {{< github_repo "masutaka/masutaka-feed" >}} - Post GitHub private feeds[^1] to Mastodon - Star and follow notifications are also sent to [Pushover](https://pushover.net/) - Post [Hatena Bookmark favorites](https://b.hatena.ne.jp/help/entry/favorite) feeds[^2] to Mastodon These are pieces of information that aren't worth subscribing to seriously with a feed reader, but I want to keep them in my field of view. ※ Mastodon posts are made to the private account [@masutakafeed@mstdn.love](https://mstdn.love/@masutakafeed) [^1]: Example: https://github.com/masutaka.private.atom?token=xxxx [^2]: Example: https://b.hatena.ne.jp/masutaka26/favorite.rss?key=xxxx ## Previous Architecture Diagram Previously, I used IFTTT's RSS Feed Integration to detect new items and call Lambda functions via API Gateway. However, starting in 2024, RSS Feed Integration became available only with paid IFTTT Pro, so I was reluctantly paying the annual fee of `$34.99`. **GitHub:** ![Previous GitHub private feed architecture diagram](/images/masutaka-feed-prev-github.png) **Hatena Bookmark:** ![Previous Hatena Bookmark favorites feed architecture diagram](/images/masutaka-feed-prev-hatebu.png) ## Current Architecture Diagram Since I wasn't using IFTTT anywhere else, I really didn't want to continue paying the annual `$34.99`, so I completely migrated to an AWS serverless architecture this time. The architecture uses EventBridge Scheduler to periodically check feeds and DynamoDB for read state management. Dependencies on external services have been eliminated, and processing is now completed entirely within AWS. **GitHub:** ![Current GitHub private feed architecture diagram](/images/masutaka-feed-curr-github.png) **Hatena Bookmark:** ![Current Hatena Bookmark favorites feed architecture diagram](/images/masutaka-feed-curr-hatebu.png) ## Current Architecture In the new architecture, the following components work together: ### Periodic execution with EventBridge Scheduler GitHub private feeds are checked every 5 minutes, and Hatena Bookmark favorites feeds every 15 minutes, executing the Lambda Subscriber functions shown below. ### Lambda function responsibility separation Each feed is separated into 2 Lambda functions: 1. Subscriber function: Feed retrieval and new item detection - Retrieve RSS/Atom feeds and parse with [rss-parser](https://www.npmjs.com/package/rss-parser) - Manage read state with DynamoDB - Call Notifier function if there are new items 2. Notifier function: Execute notification processing - Filtering processing (GitHub only notifies specific events) - Post to Mastodon - Send notifications to Pushover (GitHub only) ### Duplicate prevention with DynamoDB - Create tables for each feed - GitHub: `masutaka-feed-github-state` - Hatena Bookmark: `masutaka-feed-hatebu-state` - Set partition keys - GitHub: Atom feed, each `entry`'s `id` - Hatena Bookmark: RSS 1.0 feed, each `item`'s `rdf:about` - Set TTL to automatically delete old records after 30 days ### Configuration management with SAM continues As before, I use AWS SAM to manage the configuration of added components. - [samconfig.toml](https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/samconfig.toml) - [template.yaml](https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/template.yaml) Environment variables continue to be managed with GitHub Actions Secrets/Variables, and automatic deployment occurs when commits are added to the main branch. - [.github/workflows/deploy.yml](https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/.github/workflows/deploy.yml) ## After the migration - Despite increased responsibilities, the code became unexpectedly cleaner - rss-parser was more convenient than expected - by just defining feed types like [GitHubFeedItem](https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/github/subscriber/index.ts#L6-L20) and [HatebuFeedItem](https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/hatebu/subscriber/index.ts#L6C11-L25), parsing became simple - As a result, I was able to abandon [HTML extraction using regular expressions](https://github.com/masutaka/masutaka-feed/blob/da3954210815138d76e9bf413cc344f96c015f40/hatebu/index.js#L72-L87) - No longer need to pay IFTTT Pro's annual `$34.99` - AWS costs stay within the free tier - Running error-free for about 2 weeks ## Implemented with Claude Code After starting to pay for IFTTT Pro, I had the concept in my head, but since it was essentially refactoring, I couldn't get motivated and left it alone. Claude Code helped me overcome that lack of motivation, and I made great use of it. 1. Migration from JavaScript to TypeScript - https://github.com/masutaka/masutaka-feed/pull/74 2. Migration from IFTTT Pro to AWS self-implementation - https://github.com/masutaka/masutaka-feed/pull/77 :bulb: Each commit message in the pull requests contains Claude Code prompts. Resource definitions in template.yaml are tedious to research, but Claude Code wrote them in one shot. Of course, I verified them afterwards. - EventBridge Schedulers definition - DynamoDB Tables definition - IAM Role settings for each resource - CloudWatch Alarm monitoring definitions ## Conclusion By self-implementing IFTTT Pro's feed notification feature with AWS serverless architecture, I was able to gain the following benefits: - Cost reduction: Annual fixed cost of `$34.99` became essentially zero - Transparency gained: Full control over what items are added to feeds and what items have been marked as read - Enhanced monitoring: Can set up monitoring for feed subscriptions and detect failures immediately - Code optimization: Optimized feed parsing using rss-parser Additionally, by utilizing Claude Code, I was able to significantly lower the motivation barrier for tasks like migrating from JavaScript to TypeScript and creating SAM templates. I will continue to operate masutaka-feed at a low altitude going forward. -------------------------------------------------------------------------------- title: "Added llms.txt and llms-full.txt to My Hugo-built Website" date: "2025-05-18" -------------------------------------------------------------------------------- ## What is llms.txt? `llms.txt` is a Markdown-formatted text file proposed to address the limitation that LLMs have small context windows and cannot process entire websites. It was proposed on September 3, 2024, by Jeremy Howard of Answer.AI at https://llmstxt.org/. It is not defined in an RFC like robots.txt. The format includes certain specifications such as the website name in an H1 section, a brief summary, and a list of links in H2 sections. Example: https://llmstxt.org/llms.txt There's also `llms-full.txt`, which contains all website information. Example: https://developers.cloudflare.com/llms-full.txt There are directory sites for llms.txt as well: * https://llmstxt.site/ * https://directory.llmstxt.cloud/ * https://llmstxthub.com/ ## The llms.txt and llms-full.txt I Added I've added both: * https://masutaka.net/llms.txt * https://masutaka.net/llms-full.txt ## How to Configure in Hugo The necessary work involves modifying or creating just three files: * config.toml (modify) * layouts/index.llms.txt (create new) * layouts/index.llmsfull.txt (create new) ### config.toml I added the following to config.toml: Defining [outputFormats](https://gohugo.io/configuration/output-formats/) for `llms` and `llmsfull` while adding them to the default value of [outputs.home](https://gohugo.io/configuration/outputs/), which is `["html", "rss"]`. ```toml [outputs] home = ["html", "rss", "llms", "llmsfull"] [outputFormats.llms] baseName = "llms" isPlainText = true mediaType = "text/plain" rel = "alternate" root = true [outputFormats.llmsfull] baseName = "llms-full" isPlainText = true mediaType = "text/plain" rel = "alternate" root = true ``` ### layouts/index.llms.txt I created `layouts/index.llms.txt` as a template for `llms.txt`: ```go-text-template # {{ .Site.Title }} > {{ .Site.Params.Description }} ## Articles {{ $yearMonthGroups := slice -}} {{ range where (where (sort (.Site.GetPage "/posts/").Pages "Date" "desc") "Draft" "ne" true) "Sitemap.Disable" "ne" true -}} {{ $yearMonth := .Date.Format "2006/01" -}} {{ if not (in $yearMonthGroups $yearMonth) }} * {{ $yearMonth -}} {{ $yearMonthGroups = $yearMonthGroups | append $yearMonth -}} {{ end }} * [{{ .Title }}]({{ .Permalink }}) {{- end }} ## Others {{- $baseURL := .Site.BaseURL | strings.TrimSuffix "/" }} * [Device]({{ $baseURL }}/device/): 所有デバイス * [History]({{ $baseURL }}/history/): このサイトの歴史 * [About]({{ $baseURL }}/about/): 自己紹介 * [Privacy Policy]({{ $baseURL }}/privacy/): 当サイトの広告、アフィリエイト、プライバシーポリシー ``` Since Hugo site structures can vary widely, this is just one example. My blog has both blog posts and other articles, and since other articles don't change much, I hardcoded them in the Others section. ### layouts/index.llmsfull.txt I created `layouts/index.llmsfull.txt` as a template for `llms-full.txt`: ```go-text-template # {{ .Site.Title }} > {{ .Site.Params.Description }} {{/* Articles */}} {{ range where (where (sort (.Site.GetPage "/posts/").Pages "Date" "desc") "Draft" "ne" true) "Sitemap.Disable" "ne" true }} -------------------------------------------------------------------------------- title: "{{ .Title }}" date: "{{ .Date.Format "2006-01-02" }}" -------------------------------------------------------------------------------- {{ replaceRE "{{<\\s*comment\\s*>}}(.|\n)*?{{<\\s*/comment\\s*>}}" "" .RawContent -}} {{ end -}} {{/* Others */}} {{ range slice "device.md" "history.md" "about.md" "privacy.md" -}} {{ with site.GetPage . -}} -------------------------------------------------------------------------------- title: "{{ .Title }}" lastmod: "{{ .Date.Format "2006-01-02" }}" -------------------------------------------------------------------------------- {{ replaceRE "{{<\\s*comment\\s*>}}(.|\n)*?{{<\\s*/comment\\s*>}}" "" .RawContent -}} {{ end -}} {{ end -}} ``` I made the delimiter longer because some articles contained `---`. Also, I made sure comments like the following wouldn't be included in llms-full.txt: ```markdown {{<comment>}} This is a Hugo comment. It won't be output as an HTML comment either. {{</comment>}} ``` :bulb: Replace `<` and `>` in the code with `<` and `>` respectively. ## nginx Configuration Since llms.txt and llms-full.txt were showing character encoding issues, I added the following to the server directive: ```nginx location ~ "^/llms(-full)?\.txt" { root /usr/share/nginx/html; charset UTF-8; } ``` ## Conclusion I added llms.txt and llms-full.txt on my Hugo-built blog: * https://masutaka.net/llms.txt * https://masutaka.net/llms-full.txt Regardless of whether there's demand for it, I'm satisfied with the result. As a side note, I initially found a method on [Hugo Discourse](https://discourse.gohugo.io/t/support-for-llms-txt-standard-for-ai-crawlers/53782/3) that used `resources.ExecuteAsTemplate` in `layouts/robots.txt` to generate llms.txt. However, I decided not to use this approach since including llms.txt in robots.txt is not yet common practice. Another side note: in the past, my blog was created by converting a single ChangeLog-formatted file into HTML using a tool called [chalow](https://chalow.org/). Creating llms-full.txt reminded me of those days. It feels like I've come full circle. -------------------------------------------------------------------------------- title: "Created an Emacs Lisp function to insert the title of a GitHub Issue/PR/Discussion URL" date: "2025-04-15" -------------------------------------------------------------------------------- On GitHub, when you paste Issue/PR/Discussion URLs into comments or descriptions, GitHub automatically renders the status, title, and number. For example, if you write the following: ```markdown * https://github.com/masutaka/sandbox/issues/93 * https://github.com/masutaka/sandbox/issues/70 * https://github.com/masutaka/sandbox/pull/90 * https://github.com/masutaka/sandbox/discussions/91 ``` It renders like this: ![Rendering example](/images/github-rendered-example.png) However, when writing in a plain text area or text editor, the URLs aren't automatically expanded, so you can't see the titles or statuses at a glance. This isn't a problem with just a few URLs, but it can become confusing when dealing with many. Previously, I added comments manually like this, but it became tedious, so I created an Emacs Lisp function to automate the process. ```markdown * https://github.com/masutaka/sandbox/issues/93 (In Progress) * https://github.com/masutaka/sandbox/issues/70 (Done) * https://github.com/masutaka/sandbox/pull/90 * https://github.com/masutaka/sandbox/discussions/91 ``` ## The Function I Created :link: [~/.emacs.d/init.el#L109-L152](https://github.com/masutaka/dotfiles-public/blob/2ba4f3f9a53405e6de01aaede3ad9676ed346930/.emacs.d/init.el#L109-L152) ```elisp (require 'cl-lib) (require 'request) (defun github-expand-link () "Use the GitHub API to get the information and insert a comment at the end of the current line in the following format: - Issue URL (Done or In Progress) - PR/Discussion URL " (interactive) (let ((url (thing-at-point 'url 'no-properties))) (if (not url) (message "[github-expand-link] No URL at point") (let* ((parsed-url (url-generic-parse-url url)) (host (url-host parsed-url)) (parts (split-string (url-filename parsed-url) "/" t))) (if (and (string-match-p "github\\.com$" host) (>= (length parts) 4)) (let ((access-token (my-lisp-load "github-expand-link-token")) (org (nth 0 parts)) (repo (nth 1 parts)) (type (nth 2 parts)) (number (nth 3 parts)) (type-alist '(("issues" . "issue") ("pull" . "pullRequest") ("discussions" . "discussion")))) (request "https://api.github.com/graphql" :type "POST" :headers `(("Authorization" . ,(concat "Bearer " access-token))) :data (json-encode `(("query" . ,(format "query { repository(owner: \"%s\", name: \"%s\") { %s(number: %d) { %s } } }" (url-hexify-string org) (url-hexify-string repo) (cdr (assoc type type-alist)) (string-to-number number) (if (equal type "issues") "title state" "title"))))) :parser 'json-read :sync t :success (cl-function (lambda (&key data response &allow-other-keys) (let* ((body (car (alist-get 'repository (alist-get 'data data)))) (title (alist-get 'title body)) (state (alist-get 'state body))) (end-of-line) (insert (if state (format " (%s)" title (if (equal "CLOSED" state) "Done" "In Progress")) (format " " title)))))) :error (cl-function (lambda (&key error-thrown response &allow-other-keys) (message "[github-expand-link] Fail %S to POST %s" error-thrown (request-response-url response)))))) (message "[github-expand-link] Not a valid GitHub Issue/PR/Discussion URL")))))) ``` Since there doesn't seem to be a GitHub REST API for retrieving Discussion information, I'm using the GraphQL API. I try not to use many third-party packages, but I do like using [request.el](https://github.com/tkf/emacs-request). my-lisp-load is a function I introduced in {{< post id="2016-05-06-2" lang="ja" >}}. If you create a PAT at https://github.com/settings/tokens and save it to ~/.emacs.d/spec/github-expand-link-token, you can load it with `(my-lisp-load "github-expand-link-token")`. If you want to reference Issues/PRs/Discussions in private repositories, set the PAT scope to `repo`. I've assigned the keybinding `⌘-i` to this function. ```elisp (define-key global-map (kbd "s-i") 'github-expand-link) ``` ## Conclusion Being able to automatically insert titles, reminiscent of shell TAB completion, has proven more useful than I expected. While writing this article, I decided to add Issue status as well, which required substantial changes. I don't write Emacs Lisp frequently, so in the past, it was tedious to look things up via Google or check the official documentation. However, recently it's been convenient to learn about useful functions through ChatGPT and similar tools. This time, I learned about the alist-get function that way. -------------------------------------------------------------------------------- title: "Website Advertising, Affiliate Program, and Privacy Policy" lastmod: "2022-05-05" -------------------------------------------------------------------------------- ## Amazon Associates Program On this website, Takashi Masuda (masutaka) participates in the Amazon Associates Program by featuring product recommendations and earning commissions from qualifying sales. For detailed information about this program, please refer to the "[Amazon Associates Program Operating Agreement](https://affiliate.amazon.co.jp/help/operating/agreement)". For product-specific details and inquiries, please visit the respective product pages. ## Google Analytics This website uses Google Analytics, a web analytics service provided by Google. This tool utilizes cookies to collect usage data. The collected data is anonymized and does not identify individual users. You can opt-out of this data collection by disabling cookies. Please check your web browser settings as needed. For detailed information about these policies, please refer to the "[Google Analytics Terms of Service](https://marketingplatform.google.com/about/analytics/terms/us/)" and "[Google Privacy and Terms](https://policies.google.com/technologies/ads?hl=en)". *Last updated: 2023-12-11 (Mon)*