diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..33ddd94 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "website-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/package-lock.json b/package-lock.json index 34dc377..6aa9b6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "next-sitemap": "^4.2.3", "payload": "3.76.1", "payload-exif": "^1.1.1", + "payload-plugin-llms-txt": "^0.2.0", "prism-react-renderer": "^2.3.1", "react": "19.2.1", "react-dom": "19.2.1", @@ -12873,6 +12874,22 @@ "payload": "^3.37.0" } }, + "node_modules/payload-plugin-llms-txt": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/payload-plugin-llms-txt/-/payload-plugin-llms-txt-0.2.0.tgz", + "integrity": "sha512-sBxGKAScX1D5AK+Dtv7t+6n3ouSD8j0Nz3ojmQ9044Wu0iHagl8aNh8pDMaYcVy95XdF3iGVcnG77b4rn+4W6w==", + "license": "MIT", + "peerDependencies": { + "@payloadcms/richtext-lexical": "^3.0.0", + "next": "^15.0.0", + "payload": "^3.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + } + } + }, "node_modules/payload/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", diff --git a/package.json b/package.json index e3e269a..5f7d788 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "next-sitemap": "^4.2.3", "payload": "3.76.1", "payload-exif": "^1.1.1", + "payload-plugin-llms-txt": "^0.2.0", "prism-react-renderer": "^2.3.1", "react": "19.2.1", "react-dom": "19.2.1", diff --git a/src/app/(frontend)/llms-full.txt/route.ts b/src/app/(frontend)/llms-full.txt/route.ts new file mode 100644 index 0000000..2c9cfbd --- /dev/null +++ b/src/app/(frontend)/llms-full.txt/route.ts @@ -0,0 +1,23 @@ +import { unstable_cache } from 'next/cache' +import config from '@payload-config' +import { createLlmsFullTxtRouteHandler } from 'payload-plugin-llms-txt/next' +import { llmsTxtOptions } from '@/plugins/llmsTxt' + +const handler = createLlmsFullTxtRouteHandler(config, llmsTxtOptions) + +const getLlmsFullTxt = unstable_cache( + async () => { + const response = await handler() + return response.text() + }, + ['llms-full-txt'], + { + tags: ['llms-full-txt'], + }, +) + +export async function GET() { + const text = await getLlmsFullTxt() + + return new Response(text, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) +} diff --git a/src/app/(frontend)/llms.txt/route.ts b/src/app/(frontend)/llms.txt/route.ts new file mode 100644 index 0000000..03b0096 --- /dev/null +++ b/src/app/(frontend)/llms.txt/route.ts @@ -0,0 +1,23 @@ +import { unstable_cache } from 'next/cache' +import config from '@payload-config' +import { createLlmsTxtRouteHandler } from 'payload-plugin-llms-txt/next' +import { llmsTxtOptions } from '@/plugins/llmsTxt' + +const handler = createLlmsTxtRouteHandler(config, llmsTxtOptions) + +const getLlmsTxt = unstable_cache( + async () => { + const response = await handler() + return response.text() + }, + ['llms-txt'], + { + tags: ['llms-txt'], + }, +) + +export async function GET() { + const text = await getLlmsTxt() + + return new Response(text, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) +} diff --git a/src/payload-types.ts b/src/payload-types.ts index 1d03a71..2dc1add 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -123,11 +123,13 @@ export interface Config { header: Header; footer: Footer; site: Site; + 'llms-txt-settings': LlmsTxtSetting; }; globalsSelect: { header: HeaderSelect | HeaderSelect; footer: FooterSelect | FooterSelect; site: SiteSelect | SiteSelect; + 'llms-txt-settings': LlmsTxtSettingsSelect | LlmsTxtSettingsSelect; }; locale: null; user: User; @@ -2385,6 +2387,22 @@ export interface Site { updatedAt?: string | null; createdAt?: string | null; } +/** + * Controls the content of /llms.txt and /llms-full.txt. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "llms-txt-settings". + */ +export interface LlmsTxtSetting { + id: string; + enabled?: boolean | null; + /** + * Shown at the top of llms.txt instead of the plugin siteDescription option, when set. + */ + introOverride?: string | null; + updatedAt?: string | null; + createdAt?: string | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "header_select". @@ -2441,6 +2459,17 @@ export interface SiteSelect { createdAt?: T; globalType?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "llms-txt-settings_select". + */ +export interface LlmsTxtSettingsSelect { + enabled?: T; + introOverride?: T; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "TaskSchedulePublish". diff --git a/src/plugins/index.ts b/src/plugins/index.ts index fecd615..91c5c17 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -5,12 +5,14 @@ import { redirectsPlugin } from '@payloadcms/plugin-redirects' import { seoPlugin } from '@payloadcms/plugin-seo' import { searchPlugin } from '@payloadcms/plugin-search' import { s3Storage } from '@payloadcms/storage-s3' +import { llmsTxtPlugin } from 'payload-plugin-llms-txt' import { Plugin } from 'payload' import { revalidateRedirects } from '@/hooks/revalidateRedirects' import { GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' import { FixedToolbarFeature, HeadingFeature, lexicalEditor } from '@payloadcms/richtext-lexical' import { searchFields } from '@/search/fieldOverrides' import { beforeSyncWithSearch } from '@/search/beforeSync' +import { llmsTxtOptions } from './llmsTxt' import { Page, Post } from '@/payload-types' import { getServerSideURL } from '@/utilities/getURL' @@ -122,4 +124,5 @@ export const plugins: Plugin[] = [ }, }, }), + llmsTxtPlugin(llmsTxtOptions), ] diff --git a/src/plugins/llmsTxt.ts b/src/plugins/llmsTxt.ts new file mode 100644 index 0000000..77d3f99 --- /dev/null +++ b/src/plugins/llmsTxt.ts @@ -0,0 +1,31 @@ +import type { LlmsTxtPluginOptions } from 'payload-plugin-llms-txt' +import { getServerSideURL } from '@/utilities/getURL' + +export const llmsTxtOptions: LlmsTxtPluginOptions = { + siteName: 'Logan Kuzyk', + siteDescription: + 'Personal website for Logan Kuzyk — projects, blog posts, career history, and photography.', + siteURL: getServerSideURL(), + enableFullText: true, + addSettingsGlobal: true, + collections: [ + { + slug: 'posts', + descriptionField: 'meta.description', + contentField: 'content', + urlPath: (doc) => `/posts/${doc.slug}`, + }, + { + slug: 'projects', + descriptionField: 'description', + contentField: 'content', + urlPath: (doc) => `/projects/${doc.slug}`, + }, + { + slug: 'pages', + descriptionField: 'meta.description', + label: 'Pages', + urlPath: (doc) => (doc.slug === 'home' ? '/' : `/${doc.slug}`), + }, + ], +}