// Generated by scripts/generate-manifest.mjs. Do not edit by hand. import type { AddOnCompiled } from '../../../../../types.js' type TemplateRecord = Record type TemplateAddOn = TemplateRecord & { integrations?: Array routes?: Array } type TemplateRenderContext = { [key: string]: any packageManager: any projectName: any typescript: any tailwind: any blank: any js: any jsx: any fileRouter: any codeRouter: any routerOnly: any includeExamples: any addOnEnabled: Record addOnOption: Record addOns: Array integrations: Array routes: Array getPackageManagerAddScript: (...args: Array) => string getPackageManagerRunScript: (...args: Array) => string getPackageManagerExecuteScript: (...args: Array) => string relativePath: (...args: Array) => string integrationImportContent: (...args: Array) => string integrationImportCode: (...args: Array) => string | undefined renderTemplate: (content: string) => string ignoreFile: () => never } type TemplateRenderer = (context: TemplateRenderContext) => string | undefined function __escapeXML(value: unknown) { if (value === undefined || value === null) { return '' } return String(value).replace(/[&<>'"]/g, (character) => { switch (character) { case '&': return '&' case '<': return '<' case '>': return '>' case '"': return '"' case "'": return ''' default: return character } }) } export function getManifestTemplateKey(template: string) { let hash = 0x811c9dc5 for (let i = 0; i < template.length; i++) { hash ^= template.charCodeAt(i) hash = Math.imul(hash, 0x01000193) >>> 0 } return `${hash.toString(16).padStart(8, '0')}:${template.length}` } const templateRenderers: Record = { } export function hasManifestTemplate(template: string) { return getManifestTemplateKey(template) in templateRenderers } export function renderManifestTemplate( template: string, context: TemplateRenderContext, ) { const key = getManifestTemplateKey(template) const renderer = templateRenderers[key] if (!renderer) { throw new Error(`Template ${key} was not precompiled into the manifest`) } return renderer(context) ?? '' } export const addOn = { "name": "Strapi", "description": "Headless CMS with admin UI (self-hosted, content models in TypeScript).", "link": "https://strapi.io/", "phase": "add-on", "type": "add-on", "category": "cms", "color": "#4945FF", "priority": 110, "modes": [ "file-router" ], "routes": [ { "url": "/demo/strapi", "name": "Strapi Articles", "path": "src/routes/demo/strapi.tsx", "jsName": "StrapiArticles" } ], "id": "strapi", "version": "0.0.0", "packageAdditions": { "dependencies": { "@strapi/client": "^1.6.1", "lucide-react": "^0.577.0", "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0", "use-debounce": "^10.1.0" } }, "readme": "## Strapi CMS Integration\n\nThis add-on integrates Strapi CMS with your TanStack Start application using the official Strapi Client SDK.\n\n### Features\n\n- Article listing with search and pagination\n- Article detail pages with dynamic block rendering\n- Rich text, quotes, media, and image slider blocks\n- Markdown content rendering with GitHub Flavored Markdown\n- Responsive image handling with error fallbacks\n- URL-based search and pagination (shareable/bookmarkable)\n- Graceful error handling with helpful setup instructions\n\n### Project Structure\n\n```\nparent/\n├── client/ # TanStack Start frontend (your project name)\n│ ├── src/\n│ │ ├── components/\n│ │ │ ├── blocks/ # Block rendering components\n│ │ │ ├── markdown-content.tsx\n│ │ │ ├── pagination.tsx\n│ │ │ ├── search.tsx\n│ │ │ └── strapi-image.tsx\n│ │ ├── data/\n│ │ │ ├── loaders/ # Server functions\n│ │ │ └── strapi-sdk.ts\n│ │ ├── lib/\n│ │ │ └── strapi-utils.ts\n│ │ ├── routes/demo/\n│ │ │ ├── strapi.tsx # Articles list\n│ │ │ └── strapi.$articleId.tsx # Article detail\n│ │ └── types/\n│ │ └── strapi.ts\n│ ├── .env.local\n│ └── package.json\n└── server/ # Strapi CMS backend (create manually or use hosted Strapi)\n ├── src/api/ # Content types\n ├── config/ # Strapi configuration\n └── package.json\n```\n\n### Quick Start\n\nCreate your Strapi project separately (or use an existing hosted Strapi instance), then point this app to it with `VITE_STRAPI_URL`.\n\n**1. Set up Strapi:**\n\nFollow the Strapi quick-start guide to create a local project, or use your existing Strapi deployment:\n\n- https://docs.strapi.io/dev-docs/quick-start\n\nIf you created a local Strapi project in a sibling `server` directory, continue with:\n\n```bash\ncd ../server\nnpm install # or pnpm install / yarn install\n```\n\n**2. Start the Strapi server:**\n\n```bash\nnpm run develop # Starts at http://localhost:1337\n```\n\n**3. Create an admin account:**\n\nOpen http://localhost:1337/admin and create your first admin user.\n\n**4. Create content:**\n\nIn the Strapi admin panel, go to Content Manager > Article and create some articles.\n\n**5. Start your TanStack app (in another terminal):**\n\n```bash\ncd ../client # or your project name\nnpm run dev # Starts at http://localhost:3000\n```\n\n**6. View the demo:**\n\nNavigate to http://localhost:3000/demo/strapi to see your articles.\n\n### Environment Variables\n\nThe following environment variable is pre-configured in `.env.local`:\n\n```bash\nVITE_STRAPI_URL=\"http://localhost:1337\"\n```\n\nFor production, update this to your deployed Strapi URL.\n\n### Demo Pages\n\n| URL | Description |\n|-----|-------------|\n| `/demo/strapi` | Articles list with search and pagination |\n| `/demo/strapi/:articleId` | Article detail with block rendering |\n\n### Search and Pagination\n\n- **Search**: Type in the search box to filter articles by title or description\n- **Pagination**: Navigate between pages using the pagination controls\n- **URL State**: Search and page are stored in the URL (`?query=term&page=2`)\n\n### Block Types Supported\n\n| Block | Component | Description |\n|-------|-----------|-------------|\n| `shared.rich-text` | RichText | Markdown content |\n| `shared.quote` | Quote | Blockquote with author |\n| `shared.media` | Media | Single image/video |\n| `shared.slider` | Slider | Image gallery grid |\n\n### Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `@strapi/client` | Official Strapi SDK |\n| `react-markdown` | Markdown rendering |\n| `remark-gfm` | GitHub Flavored Markdown |\n| `use-debounce` | Debounced search input |\n\n### Running Both Servers\n\nOpen two terminal windows from the parent directory:\n\n**Terminal 1 - Strapi:**\n```bash\ncd server && npm run develop\n```\n\n**Terminal 2 - TanStack Start:**\n```bash\ncd client && npm run dev # or your project name\n```\n\n### Customization\n\n**Change page size:**\nEdit `src/data/loaders/articles.ts` and modify `PAGE_SIZE`.\n\n**Add new block types:**\n1. Create component in `src/components/blocks/`\n2. Export from `src/components/blocks/index.ts`\n3. Add case to `block-renderer.tsx` switch statement\n4. Update populate in articles loader\n\n**Add new content types:**\n1. Add types to `src/types/strapi.ts`\n2. Create loader in `src/data/loaders/`\n3. Create route in `src/routes/demo/`\n\n### Learn More\n\n- [Strapi Documentation](https://docs.strapi.io/)\n- [Strapi Client SDK](https://www.npmjs.com/package/@strapi/client)\n- [Strapi Cloud Template Blog](https://github.com/strapi/strapi-cloud-template-blog)\n- [TanStack Start Documentation](https://tanstack.com/start/latest)\n- [TanStack Router Search Params](https://tanstack.com/router/latest/docs/framework/react/guide/search-params)\n", "readmeIsEjs": false, "files": { "_dot_env.local.append": "# Strapi configuration\nVITE_STRAPI_URL=\"http://localhost:1337\"", "src/components/blocks/block-renderer.tsx": "import { RichText } from \"./rich-text\";\nimport { Quote } from \"./quote\";\nimport { Media } from \"./media\";\nimport { Slider } from \"./slider\";\n\nimport type { IRichText } from \"./rich-text\";\nimport type { IQuote } from \"./quote\";\nimport type { IMedia } from \"./media\";\nimport type { ISlider } from \"./slider\";\n\n// Union type of all block types\nexport type Block = IRichText | IQuote | IMedia | ISlider;\n\ninterface BlockRendererProps {\n blocks: Array;\n}\n\n/**\n * BlockRenderer - Renders dynamic content blocks from Strapi\n *\n * Usage:\n * ```tsx\n * \n * ```\n */\nexport function BlockRenderer({ blocks }: Readonly) {\n if (!blocks || blocks.length === 0) return null;\n\n const renderBlock = (block: Block) => {\n switch (block.__component) {\n case \"shared.rich-text\":\n return ;\n case \"shared.quote\":\n return ;\n case \"shared.media\":\n return ;\n case \"shared.slider\":\n return ;\n default:\n // Log unknown block types in development\n console.warn(\"Unknown block type:\", (block as any).__component);\n return null;\n }\n };\n\n return (\n
\n {blocks.map((block, index) => (\n
\n {renderBlock(block)}\n
\n ))}\n
\n );\n}\n", "src/components/blocks/index.ts": "export { BlockRenderer } from \"./block-renderer\";\nexport type { Block } from \"./block-renderer\";\n\nexport { RichText } from \"./rich-text\";\nexport type { IRichText } from \"./rich-text\";\n\nexport { Quote } from \"./quote\";\nexport type { IQuote } from \"./quote\";\n\nexport { Media } from \"./media\";\nexport type { IMedia } from \"./media\";\n\nexport { Slider } from \"./slider\";\nexport type { ISlider } from \"./slider\";\n", "src/components/blocks/media.tsx": "import { StrapiImage } from '@/components/strapi-image'\nimport type { TImage } from '@/types/strapi'\n\nexport interface IMedia {\n __component: 'shared.media'\n id: number\n file?: TImage\n}\n\nexport function Media({ file }: Readonly) {\n if (!file) return null\n\n return (\n
\n \n {file.alternativeText && (\n
\n {file.alternativeText}\n
\n )}\n
\n )\n}\n", "src/components/blocks/quote.tsx": "export interface IQuote {\n __component: 'shared.quote'\n id: number\n body: string\n title?: string\n}\n\nexport function Quote({ body, title }: Readonly) {\n return (\n
\n

{body}

\n {title && (\n \n — {title}\n \n )}\n
\n )\n}\n", "src/components/blocks/rich-text.tsx": "import { MarkdownContent } from \"@/components/markdown-content\";\n\nexport interface IRichText {\n __component: \"shared.rich-text\";\n id: number;\n body: string;\n}\n\nexport function RichText({ body }: Readonly) {\n return ;\n}\n", "src/components/blocks/slider.tsx": "import { StrapiImage } from \"@/components/strapi-image\";\nimport type { TImage } from \"@/types/strapi\";\n\nexport interface ISlider {\n __component: \"shared.slider\";\n id: number;\n files?: Array;\n}\n\nexport function Slider({ files }: Readonly) {\n if (!files || files.length === 0) return null;\n\n return (\n
\n
\n {files.map((file, index) => (\n
\n \n
\n ))}\n
\n
\n );\n}\n", "src/components/markdown-content.tsx": "import Markdown from 'react-markdown'\nimport remarkGfm from 'remark-gfm'\n\ninterface MarkdownContentProps {\n content: string | undefined | null\n className?: string\n}\n\nconst styles = {\n h1: 'mb-6 text-3xl font-bold text-[var(--sea-ink)]',\n h2: 'mb-4 text-2xl font-bold text-[var(--sea-ink)]',\n h3: 'mb-3 text-xl font-bold text-[var(--sea-ink)]',\n p: 'demo-muted mb-4 leading-relaxed',\n a: 'hover:underline',\n ul: 'demo-muted mb-4 list-disc space-y-2 pl-6',\n ol: 'demo-muted mb-4 list-decimal space-y-2 pl-6',\n li: 'leading-relaxed',\n blockquote:\n 'demo-card my-4 border-l-4 border-l-[var(--lagoon-deep)] pl-4 italic',\n code: 'text-sm font-mono',\n pre: 'demo-code-block mb-4 overflow-x-auto',\n table: 'w-full border-collapse mb-4',\n th: 'border border-[var(--line)] bg-[var(--chip-bg)] p-2 text-left text-[var(--sea-ink)]',\n td: 'demo-muted border border-[var(--line)] p-2',\n img: 'max-w-full h-auto rounded-lg my-4',\n hr: 'my-8 border-[var(--line)]',\n strong: 'font-semibold text-[var(--sea-ink)]',\n}\n\nexport function MarkdownContent({\n content,\n className = '',\n}: MarkdownContentProps) {\n if (!content) return null\n\n return (\n
\n

{children}

,\n h2: ({ children }) =>

{children}

,\n h3: ({ children }) =>

{children}

,\n p: ({ children }) =>

{children}

,\n a: ({ href, children }) => (\n \n {children}\n \n ),\n ul: ({ children }) =>
    {children}
,\n ol: ({ children }) =>
    {children}
,\n li: ({ children }) =>
  • {children}
  • ,\n blockquote: ({ children }) => (\n
    {children}
    \n ),\n code: ({ className, children }) => {\n const isCodeBlock = className?.includes('language-')\n if (isCodeBlock) {\n return (\n
    \n                  {children}\n                
    \n )\n }\n return {children}\n },\n pre: ({ children }) => <>{children},\n table: ({ children }) => (\n {children}
    \n ),\n th: ({ children }) => {children},\n td: ({ children }) => {children},\n img: ({ src, alt }) => (\n {alt\n ),\n hr: () =>
    ,\n strong: ({ children }) => (\n {children}\n ),\n }}\n >\n {content}\n \n
    \n )\n}\n", "src/components/pagination.tsx": "import { useRouter, useSearch } from '@tanstack/react-router'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\n\ninterface PaginationProps {\n pageCount: number\n className?: string\n}\n\nexport function Pagination({ pageCount, className = '' }: PaginationProps) {\n const router = useRouter()\n const search = useSearch({ strict: false })\n const currentPage = Number((search as any)?.page) || 1\n\n const handlePageChange = (page: number) => {\n router.navigate({\n to: '.',\n search: (prev) => ({ ...prev, page }),\n replace: true,\n })\n }\n\n // Generate page numbers to display\n const getPageNumbers = () => {\n const pages: Array = []\n const showEllipsis = pageCount > 7\n\n if (showEllipsis) {\n pages.push(1)\n\n if (currentPage > 3) {\n pages.push('ellipsis')\n }\n\n const start = Math.max(2, currentPage - 1)\n const end = Math.min(pageCount - 1, currentPage + 1)\n\n for (let i = start; i <= end; i++) {\n pages.push(i)\n }\n\n if (currentPage < pageCount - 2) {\n pages.push('ellipsis')\n }\n\n if (pageCount > 1) {\n pages.push(pageCount)\n }\n } else {\n for (let i = 1; i <= pageCount; i++) {\n pages.push(i)\n }\n }\n\n return pages\n }\n\n const pageNumbers = getPageNumbers()\n\n if (pageCount <= 1) return null\n\n return (\n \n )\n}\n", "src/components/search.tsx": "import { useRouter, useSearch } from '@tanstack/react-router'\nimport { useDebouncedCallback } from 'use-debounce'\n\ninterface SearchProps {\n readonly className?: string\n}\n\nexport function Search({ className = '' }: SearchProps) {\n const search = useSearch({ strict: false })\n const router = useRouter()\n\n const handleSearch = useDebouncedCallback((term: string) => {\n router.navigate({\n to: '.',\n search: (prev) => ({\n ...prev,\n page: 1,\n query: term || undefined,\n }),\n replace: true,\n })\n }, 300)\n\n return (\n ) =>\n handleSearch(e.target.value)\n }\n defaultValue={(search as any)?.query || ''}\n className={`demo-input ${className}`}\n />\n )\n}\n", "src/components/strapi-image.tsx": "import { useState } from 'react'\nimport { getStrapiMedia } from '@/lib/strapi-utils'\n\ninterface StrapiImageProps {\n src: string | undefined | null\n alt?: string | null\n className?: string\n width?: number | string\n height?: number | string\n}\n\nexport function StrapiImage({\n src,\n alt,\n className = '',\n width,\n height,\n}: StrapiImageProps) {\n const [hasError, setHasError] = useState(false)\n\n if (!src) return null\n\n const imageUrl = getStrapiMedia(src)\n\n if (hasError) {\n return (\n \n Image not available\n \n )\n }\n\n return (\n setHasError(true)}\n />\n )\n}\n", "src/data/loaders/articles.ts": "import { createServerFn } from \"@tanstack/react-start\";\nimport { sdk } from \"@/data/strapi-sdk\";\nimport type { TArticle, TStrapiResponseCollection, TStrapiResponseSingle } from \"@/types/strapi\";\n\nconst PAGE_SIZE = 3;\n\nconst articles = sdk.collection(\"articles\");\n\n/**\n * Fetch articles with optional filtering, search, and pagination\n */\nconst getArticles = async (\n page?: number,\n category?: string,\n query?: string\n) => {\n const filterConditions: Array> = [];\n\n // Add search query filter\n if (query) {\n filterConditions.push({\n $or: [\n { title: { $containsi: query } },\n { description: { $containsi: query } },\n ],\n });\n }\n\n // Add category filter\n if (category) {\n filterConditions.push({\n category: {\n slug: { $eq: category },\n },\n });\n }\n\n const filters =\n filterConditions.length === 0\n ? undefined\n : filterConditions.length === 1\n ? filterConditions[0]\n : { $and: filterConditions };\n\n return articles.find({\n sort: [\"createdAt:desc\"],\n pagination: {\n page: page || 1,\n pageSize: PAGE_SIZE,\n },\n populate: [\"cover\", \"author\", \"category\"],\n filters,\n }) as Promise>;\n};\n\n/**\n * Fetch a single article by documentId\n */\nconst getArticleById = async (documentId: string) => {\n return articles.findOne(documentId, {\n populate: [\"cover\", \"author\", \"category\", \"blocks.file\", \"blocks.files\"],\n }) as Promise>;\n};\n\n/**\n * Fetch a single article by slug\n */\nconst getArticleBySlug = async (slug: string) => {\n return articles.find({\n filters: {\n slug: { $eq: slug },\n },\n populate: [\"cover\", \"author\", \"category\", \"blocks.file\", \"blocks.files\"],\n }) as Promise>;\n};\n\n// Server Functions - these run on the server and can be called from components\n\nexport const getArticlesData = createServerFn({\n method: \"GET\",\n})\n .inputValidator(\n (input?: { page?: number; category?: string; query?: string }) => input\n )\n .handler(async ({ data }): Promise> => {\n const response = await getArticles(data?.page, data?.category, data?.query);\n return response;\n });\n\nexport const getArticleByIdData = createServerFn({\n method: \"GET\",\n})\n .inputValidator((documentId: string) => documentId)\n .handler(async ({ data: documentId }): Promise> => {\n const response = await getArticleById(documentId);\n return response;\n });\n\nexport const getArticleBySlugData = createServerFn({\n method: \"GET\",\n})\n .inputValidator((slug: string) => slug)\n .handler(async ({ data: slug }): Promise> => {\n const response = await getArticleBySlug(slug);\n return response;\n });\n", "src/data/loaders/index.ts": "import {\n getArticlesData,\n getArticleByIdData,\n getArticleBySlugData,\n} from \"./articles\";\n\n/**\n * Strapi API - Server functions for fetching data from Strapi\n *\n * Usage in route loaders:\n * ```ts\n * import { strapiApi } from \"@/data/loaders\";\n *\n * export const Route = createFileRoute(\"/articles\")({\n * loader: async () => {\n * const { data, meta } = await strapiApi.articles.getArticlesData();\n * return data;\n * },\n * });\n * ```\n */\nexport const strapiApi = {\n articles: {\n getArticlesData,\n getArticleByIdData,\n getArticleBySlugData,\n },\n};\n", "src/data/strapi-sdk.ts": "import { strapi } from \"@strapi/client\";\n\n// Strapi base URL (without /api)\nconst STRAPI_BASE = import.meta.env.VITE_STRAPI_URL ?? \"http://localhost:1337\";\n\n// Initialize the Strapi SDK with /api endpoint\nconst sdk = strapi({ baseURL: new URL(\"/api\", STRAPI_BASE).href });\n\nexport { sdk };\n", "src/lib/strapi-utils.ts": "/**\n * Strapi URL helpers\n */\n\nconst DEFAULT_STRAPI_URL = \"http://localhost:1337\";\n\n// Base Strapi URL (without /api)\nexport function getStrapiURL(): string {\n // Handle SSR where import.meta.env might not be fully available\n if (typeof import.meta !== \"undefined\" && import.meta.env?.VITE_STRAPI_URL) {\n return import.meta.env.VITE_STRAPI_URL;\n }\n return DEFAULT_STRAPI_URL;\n}\n\n// Get full URL for media assets\nexport function getStrapiMedia(url: string | undefined | null): string {\n if (!url) return \"\";\n if (url.startsWith(\"data:\") || url.startsWith(\"http\") || url.startsWith(\"//\")) {\n return url;\n }\n // Ensure we always have a valid base URL\n const baseUrl = getStrapiURL() || DEFAULT_STRAPI_URL;\n return `${baseUrl}${url.startsWith(\"/\") ? \"\" : \"/\"}${url}`;\n}\n", "src/routes/demo/strapi.$articleId.tsx": "import { createFileRoute, Link } from '@tanstack/react-router'\nimport { strapiApi } from '@/data/loaders'\nimport { StrapiImage } from '@/components/strapi-image'\nimport { BlockRenderer } from '@/components/blocks'\nimport type { TArticle } from '@/types/strapi'\n\nexport const Route = createFileRoute('/demo/strapi/$articleId')({\n component: RouteComponent,\n errorComponent: ErrorComponent,\n loader: async ({ params }) => {\n try {\n const response = await strapiApi.articles.getArticleByIdData({\n data: params.articleId,\n })\n return { success: true, article: response.data }\n } catch (error) {\n return {\n success: false,\n error:\n error instanceof Error ? error.message : 'Failed to load article',\n article: null,\n }\n }\n },\n})\n\nfunction ErrorComponent({ error }: { error: Error }) {\n return (\n
    \n
    \n \n ← Back to Articles\n \n
    \n

    Error Loading Article

    \n

    {error.message}

    \n
    \n
    \n
    \n )\n}\n\nfunction RouteComponent() {\n const { success, article, error } = Route.useLoaderData() as {\n success: boolean\n article: TArticle | null\n error?: string\n }\n\n // Show error state\n if (!success || !article) {\n return (\n
    \n
    \n \n \n \n \n Back to Articles\n \n\n
    \n
    \n
    \n

    \n {error || 'Article Not Found'}\n

    \n

    \n Make sure the Strapi server is running and the article exists.\n

    \n
    \n
    \n
    \n
    \n
    \n )\n }\n\n return (\n
    \n
    \n \n \n \n \n Back to Articles\n \n\n
    \n \n\n
    \n

    {article.title || 'Untitled'}

    \n\n
    \n {article.author?.name && (\n \n By{' '}\n \n {article.author.name}\n \n \n )}\n {article.createdAt && (\n \n {new Date(article.createdAt).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })}\n \n )}\n
    \n\n {article.category?.name && (\n
    \n {article.category.name}\n
    \n )}\n\n {article.description && (\n
    \n

    \n {article.description}\n

    \n
    \n )}\n\n {article.blocks && article.blocks.length > 0 && (\n \n )}\n
    \n
    \n
    \n
    \n )\n}\n", "src/routes/demo/strapi.tsx": "import { createFileRoute, Link } from '@tanstack/react-router'\nimport { z } from 'zod'\nimport { strapiApi } from '@/data/loaders'\nimport { StrapiImage } from '@/components/strapi-image'\nimport { Search } from '@/components/search'\nimport { Pagination } from '@/components/pagination'\nimport type { TArticle } from '@/types/strapi'\n\ntype LoaderResult = {\n status: 'success' | 'empty' | 'error'\n articles: TArticle[]\n meta?: { pagination?: { page: number; pageCount: number; total: number } }\n error?: string\n query?: string\n}\n\nconst searchSchema = z.object({\n query: z.string().optional(),\n page: z.number().default(1),\n})\n\nexport const Route = createFileRoute('/demo/strapi')({\n component: RouteComponent,\n validateSearch: searchSchema,\n loaderDeps: ({ search }) => ({ search }),\n loader: async ({ deps }): Promise => {\n const { query, page } = deps.search\n try {\n const response = await strapiApi.articles.getArticlesData({\n data: { query, page },\n })\n\n // Check if we got data\n if (!response || !response.data) {\n return {\n status: 'empty',\n articles: [],\n meta: response?.meta,\n query,\n }\n }\n\n // Check if data array is empty\n if (response.data.length === 0) {\n return {\n status: 'empty',\n articles: [],\n meta: response.meta,\n query,\n }\n }\n\n return {\n status: 'success',\n articles: response.data,\n meta: response.meta,\n query,\n }\n } catch (error) {\n console.error('Strapi fetch error:', error)\n return {\n status: 'error',\n articles: [],\n error:\n error instanceof Error\n ? error.message\n : 'Failed to connect to Strapi',\n query,\n }\n }\n },\n})\n\nfunction StrapiServerInstructions() {\n return (\n
    \n

    Start the Strapi Server

    \n
    \n

    \n $ cd ../server\n

    \n

    \n $ npm install\n

    \n

    \n $ npm run develop\n

    \n
    \n

    \n Then create an admin at{' '}\n \n http://localhost:1337/admin\n \n

    \n
    \n )\n}\n\nfunction ConnectionError({ error }: { error?: string }) {\n return (\n
    \n
    \n
    \n

    \n Cannot Connect to Strapi\n

    \n

    \n Make sure your Strapi server is running at{' '}\n http://localhost:1337\n

    \n {error &&

    Error: {error}

    }\n \n
    \n
    \n
    \n )\n}\n\nfunction NoArticlesFound({ query }: { query?: string }) {\n if (query) {\n return (\n
    \n
    \n

    No Results Found

    \n

    \n No articles match your search for \"{query}\". Try adjusting your\n search terms.\n

    \n
    \n
    \n )\n }\n\n return (\n
    \n
    \n

    No Articles Yet

    \n

    \n Your Strapi server is running, but there are no published articles.\n Create and publish your first article to see it here.\n

    \n\n
    \n

    How to add articles:

    \n
      \n
    1. \n 1.\n \n Open{' '}\n \n Strapi Admin Panel\n \n \n
    2. \n
    3. \n 2.\n \n Go to{' '}\n \n Content Manager\n {' '}\n → Article\n \n
    4. \n
    5. \n 3.\n \n Click{' '}\n \n Create new entry\n \n \n
    6. \n
    7. \n 4.\n \n Fill in the details and click{' '}\n Publish\n \n
    8. \n
    \n
    \n
    \n
    \n )\n}\n\nfunction RouteComponent() {\n const { status, articles, meta, error, query } = Route.useLoaderData()\n\n return (\n
    \n
    \n

    CMS

    \n

    Strapi Articles

    \n\n
    \n \n
    \n\n {status === 'error' && }\n\n {status === 'empty' && }\n\n {status === 'success' && (\n <>\n
    \n {articles.map((article: TArticle) => (\n \n
    \n \n\n
    \n

    \n {article.title || 'Untitled'}\n

    \n\n {article.description && (\n

    \n {article.description}\n

    \n )}\n\n
    \n {article.author?.name && (\n \n By {article.author.name}\n \n )}\n {article.createdAt && (\n \n {new Date(article.createdAt).toLocaleDateString()}\n \n )}\n
    \n\n {article.category?.name && (\n
    \n \n {article.category.name}\n \n
    \n )}\n
    \n
    \n \n ))}\n
    \n\n {meta?.pagination && meta.pagination.pageCount > 1 && (\n
    \n \n
    \n )}\n \n )}\n
    \n
    \n )\n}\n", "src/types/strapi.ts": "/**\n * Strapi type definitions\n * These types match the Strapi Cloud Template Blog schema\n */\n\nimport type { Block } from \"@/components/blocks\";\n\n// Base image type from Strapi media library\nexport type TImage = {\n id: number;\n documentId: string;\n alternativeText: string | null;\n url: string;\n};\n\n// Author content type\nexport type TAuthor = {\n id: number;\n documentId: string;\n name: string;\n email?: string;\n createdAt: string;\n updatedAt: string;\n publishedAt: string;\n};\n\n// Category content type\nexport type TCategory = {\n id: number;\n documentId: string;\n name: string;\n slug: string;\n description?: string;\n createdAt: string;\n updatedAt: string;\n publishedAt: string;\n};\n\n// Article content type\nexport type TArticle = {\n id: number;\n documentId: string;\n title: string;\n description: string;\n slug: string;\n cover?: TImage;\n author?: TAuthor;\n category?: TCategory;\n blocks?: Array;\n createdAt: string;\n updatedAt: string;\n publishedAt: string;\n};\n\n// Strapi response wrappers\nexport type TStrapiResponseSingle = {\n data: T;\n meta?: {\n pagination?: TStrapiPagination;\n };\n};\n\nexport type TStrapiResponseCollection = {\n data: Array;\n meta?: {\n pagination?: TStrapiPagination;\n };\n};\n\nexport type TStrapiPagination = {\n page: number;\n pageSize: number;\n pageCount: number;\n total: number;\n};\n\nexport type TStrapiError = {\n status: number;\n name: string;\n message: string;\n details?: Record>;\n};\n\nexport type TStrapiResponse = {\n data?: T;\n error?: TStrapiError;\n meta?: {\n pagination?: TStrapiPagination;\n };\n};\n" }, "deletedFiles": [], "smallLogo": "\n\n\n\n\n\n\n\n" } satisfies AddOnCompiled