diff --git a/.github/workflows/landing-publish.yml b/.github/workflows/landing-publish.yml index 14b014344..152391b5d 100644 --- a/.github/workflows/landing-publish.yml +++ b/.github/workflows/landing-publish.yml @@ -32,6 +32,19 @@ jobs: YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY || '' }} run: npm run build + - name: Check docs llms.txt + run: | + file=landing/dist/llms.txt + if [ ! -s "$file" ]; then + echo "::error::$file is missing or empty" + exit 1 + fi + count=$(grep -cE '^- \[[^]]+\]\(https://docs\.swmansion\.com/live-debugger/[^)]+\)' "$file" || true) + if [ "$count" -eq 0 ]; then + echo "::error::$file lists no pages" + exit 1 + fi + echo "llms.txt lists $count pages" - name: Publish generated content to GitHub Pages uses: JamesIves/github-pages-deploy-action@releases/v3 with: diff --git a/landing/astro.config.mjs b/landing/astro.config.mjs index 79e648f78..f8415495c 100644 --- a/landing/astro.config.mjs +++ b/landing/astro.config.mjs @@ -1,11 +1,13 @@ // @ts-check import { defineConfig, envField, fontProviders } from "astro/config"; +import swmGeo from "./swm-geo.mjs"; import react from "@astrojs/react"; import tailwindcss from "@tailwindcss/vite"; // https://astro.build/config export default defineConfig({ - integrations: [react()], + integrations: [ + swmGeo({ name: "LiveDebugger", description: "LiveView debugging made simple", repository: "live-debugger" }),react()], env: { schema: { ENABLE_ANALYTICS: envField.string({ diff --git a/landing/src/layouts/Layout.astro b/landing/src/layouts/Layout.astro index 290f87b23..a8cced5ac 100644 --- a/landing/src/layouts/Layout.astro +++ b/landing/src/layouts/Layout.astro @@ -1,4 +1,5 @@ --- +import { structuredData } from '../../swm-geo.mjs'; import "../styles/global.css"; import { Font } from "astro:assets"; import { Header } from "@/components/ui/Header"; @@ -64,6 +65,7 @@ const enableAnalytics = > ) } +
diff --git a/landing/swm-geo.mjs b/landing/swm-geo.mjs new file mode 100644 index 000000000..9ea517bf6 --- /dev/null +++ b/landing/swm-geo.mjs @@ -0,0 +1,129 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ORGANIZATION_ID = "https://swmansion.com/#organization"; + +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const decode = (value) => + value + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/(?:39|x27);/g, "'") + .trim(); + +// Same @id as swmansion.com, so engines read one company across both domains. +export function structuredData({ description, name, repository }) { + return { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "Organization", + "@id": ORGANIZATION_ID, + name: "Software Mansion", + url: "https://swmansion.com", + sameAs: [ + "https://github.com/software-mansion", + "https://www.linkedin.com/company/software-mansion/", + "https://twitter.com/swmansion", + "https://www.youtube.com/c/SoftwareMansion", + ], + }, + { + "@type": "SoftwareSourceCode", + name, + ...(description ? { description } : {}), + ...(repository + ? { + codeRepository: `https://github.com/software-mansion/${repository}`, + } + : {}), + author: { "@id": ORGANIZATION_ID }, + maintainer: { "@id": ORGANIZATION_ID }, + }, + ], + }; +} + +function collect(dir, root = dir, found = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) collect(full, root, found); + else if (entry.name.endsWith(".html") && entry.name !== "404.html") + found.push(path.relative(root, full)); + } + return found; +} + +export function buildLlmsTxt({ description, files, name, prefix, readFile }) { + const entries = []; + + for (const file of files) { + const html = readFile(file); + const raw = /